mirror of
https://github.com/fpganinja/taxi.git
synced 2026-01-18 01:30:36 -08:00
lss: Add I2C slave APB master module and testbench
Signed-off-by: Alex Forencich <alex@alexforencich.com>
This commit is contained in:
@@ -172,6 +172,7 @@ The Taxi transport library contains many smaller components that can be composed
|
||||
* I2C master
|
||||
* I2C single register
|
||||
* I2C slave
|
||||
* I2C slave APB master
|
||||
* I2C slave AXI lite master
|
||||
* MDIO master
|
||||
* UART
|
||||
|
||||
4
src/lss/rtl/taxi_i2c_slave_apb_master.f
Normal file
4
src/lss/rtl/taxi_i2c_slave_apb_master.f
Normal file
@@ -0,0 +1,4 @@
|
||||
taxi_i2c_slave_apb_master.sv
|
||||
taxi_i2c_slave.sv
|
||||
../lib/taxi/src/axis/rtl/taxi_axis_if.sv
|
||||
../lib/taxi/src/apb/rtl/taxi_apb_if.sv
|
||||
457
src/lss/rtl/taxi_i2c_slave_apb_master.sv
Normal file
457
src/lss/rtl/taxi_i2c_slave_apb_master.sv
Normal file
@@ -0,0 +1,457 @@
|
||||
// SPDX-License-Identifier: CERN-OHL-S-2.0
|
||||
/*
|
||||
|
||||
Copyright (c) 2019-2026 FPGA Ninja, LLC
|
||||
|
||||
Authors:
|
||||
- Alex Forencich
|
||||
|
||||
*/
|
||||
|
||||
`resetall
|
||||
`timescale 1ns / 1ps
|
||||
`default_nettype none
|
||||
|
||||
/*
|
||||
* I2C slave APB master wrapper
|
||||
*/
|
||||
module taxi_i2c_slave_apb_master #
|
||||
(
|
||||
parameter FILTER_LEN = 4
|
||||
)
|
||||
(
|
||||
input wire logic clk,
|
||||
input wire logic rst,
|
||||
|
||||
/*
|
||||
* I2C interface
|
||||
*/
|
||||
input wire logic i2c_scl_i,
|
||||
output wire logic i2c_scl_o,
|
||||
input wire logic i2c_sda_i,
|
||||
output wire logic i2c_sda_o,
|
||||
|
||||
/*
|
||||
* APB master interface
|
||||
*/
|
||||
taxi_apb_if.mst m_apb,
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
output wire logic busy,
|
||||
output wire logic [6:0] bus_address,
|
||||
output wire logic bus_addressed,
|
||||
output wire logic bus_active,
|
||||
|
||||
/*
|
||||
* Configuration
|
||||
*/
|
||||
input wire logic enable,
|
||||
input wire logic [6:0] device_address
|
||||
);
|
||||
/*
|
||||
|
||||
I2C
|
||||
|
||||
Read
|
||||
__ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ __
|
||||
sda \__/_6_X_5_X_4_X_3_X_2_X_1_X_0_/ R \_A_/_7_X_6_X_5_X_4_X_3_X_2_X_1_X_0_\_A_/_7_X_6_X_5_X_4_X_3_X_2_X_1_X_0_/ N \__/
|
||||
____ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ____
|
||||
scl ST \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ SP
|
||||
|
||||
Write
|
||||
__ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ __
|
||||
sda \__/_6_X_5_X_4_X_3_X_2_X_1_X_0_\_W___A_/_7_X_6_X_5_X_4_X_3_X_2_X_1_X_0_\_A_/_7_X_6_X_5_X_4_X_3_X_2_X_1_X_0_\_A____/
|
||||
____ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ____
|
||||
scl ST \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ \_/ SP
|
||||
|
||||
Operation:
|
||||
|
||||
This module enables I2C control over an APB bus, useful for enabling a design
|
||||
to operate as a peripheral to an external microcontroller or similar. The APB
|
||||
interface are fully parametrizable, with the restriction that the bus must be
|
||||
divided into 2**m words of 8*2**n bits.
|
||||
|
||||
Writing via I2C first accesses an internal address register, followed by the
|
||||
actual APB bus. The first k bytes go to the address register, where
|
||||
|
||||
k = ceil(log2(ADDR_W+log2(DATA_W/SELECT_W))/8)
|
||||
|
||||
. The address pointer will automatically increment with reads and writes. For
|
||||
buses with word size > 8 bits, the address register is in bytes and unaligned
|
||||
writes will be padded with zeros. Writes to the same bus address in the same
|
||||
I2C transaction are coalesced and written either once a complete word is ready
|
||||
or when the I2C transaction terminates with a stop or repeated start.
|
||||
|
||||
Reading via the I2C interface immediately starts reading from the APB interface
|
||||
starting from the current value of the internal address register. Like writes,
|
||||
reads are also coalesced when possible. One APB read is performed on the first
|
||||
I2C read. Once that has been completely transferred out, another read will be
|
||||
performed on the start of the next I2C read operation.
|
||||
|
||||
Read
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|_|_|_|_|_|_|_|_| |_|_|_|_|_|_|_|_|_|_ ... _|_|_|_|_|_|_|_|_|_| |_|_|_|_|_|_|_|_|___|_|_|_|_|_|_|_|_|_ ... _|_|_|_|_|_|_|_|_| |_|
|
||||
|
||||
ST Device Addr W A Address MSB A Address LSB A RS Device Addr R A Data byte 0 A Data byte N N SP
|
||||
|
||||
Write
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|_|_|_|_|_|_|_|_| |_|_|_|_|_|_|_|_|_|_ ... _|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_ ... _|_|_|_|_|_|_|_|_|___|
|
||||
|
||||
ST Device Addr W A Address MSB A Address LSB A Data byte 0 A Data byte N A SP
|
||||
|
||||
Status:
|
||||
|
||||
busy
|
||||
module is communicating over the bus
|
||||
|
||||
bus_control
|
||||
module has control of bus in active state
|
||||
|
||||
bus_active
|
||||
bus is active, not necessarily controlled by this module
|
||||
|
||||
Parameters:
|
||||
|
||||
device_address
|
||||
address of slave device
|
||||
|
||||
Example of interfacing with tristate pins:
|
||||
|
||||
assign scl_i = scl_pin;
|
||||
assign scl_pin = scl_o ? 1'bz : 1'b0;
|
||||
assign sda_i = sda_pin;
|
||||
assign sda_pin = sda_o ? 1'bz : 1'b0;
|
||||
|
||||
Example of two interconnected internal I2C devices:
|
||||
|
||||
assign scl_1_i = scl_1_o & scl_2_o;
|
||||
assign scl_2_i = scl_1_o & scl_2_o;
|
||||
assign sda_1_i = sda_1_o & sda_2_o;
|
||||
assign sda_2_i = sda_1_o & sda_2_o;
|
||||
|
||||
Example of two I2C devices sharing the same pins:
|
||||
|
||||
assign scl_1_i = scl_pin;
|
||||
assign scl_2_i = scl_pin;
|
||||
assign scl_pin = (scl_1_o & scl_2_o) ? 1'bz : 1'b0;
|
||||
assign sda_1_i = sda_pin;
|
||||
assign sda_2_i = sda_pin;
|
||||
assign sda_pin = (sda_1_o & sda_2_o) ? 1'bz : 1'b0;
|
||||
|
||||
Notes:
|
||||
|
||||
scl_o should not be connected directly to scl_i, only via AND logic or a tristate
|
||||
I/O pin. This would prevent devices from stretching the clock period.
|
||||
|
||||
*/
|
||||
|
||||
// TODO REMOVE THIS
|
||||
/* verilator lint_off WIDTHTRUNC */
|
||||
|
||||
localparam DATA_W = m_apb.DATA_W;
|
||||
localparam ADDR_W = m_apb.ADDR_W;
|
||||
localparam STRB_W = m_apb.STRB_W;
|
||||
|
||||
// for interfaces that are more than one word wide, disable address lines
|
||||
localparam VALID_ADDR_W = ADDR_W - $clog2(STRB_W);
|
||||
// width of data port in words
|
||||
localparam BYTE_LANES = STRB_W;
|
||||
// size of words
|
||||
localparam BYTE_SIZE = DATA_W/BYTE_LANES;
|
||||
|
||||
localparam WORD_PART_ADDR_W = $clog2(BYTE_SIZE/8);
|
||||
|
||||
localparam ADDR_W_ADJ = ADDR_W+WORD_PART_ADDR_W;
|
||||
|
||||
localparam ADDR_WORD_W = (ADDR_W_ADJ+7)/8;
|
||||
|
||||
// check configuration
|
||||
if (BYTE_LANES * BYTE_SIZE != DATA_W)
|
||||
$fatal(0, "Error: AXI data width not evenly divisible (instance %m)");
|
||||
|
||||
if (2**$clog2(BYTE_LANES) != BYTE_LANES)
|
||||
$fatal(0, "Error: AXI word width must be even power of two (instance %m)");
|
||||
|
||||
if (8*2**$clog2(BYTE_SIZE/8) != BYTE_SIZE)
|
||||
$fatal(0, "Error: AXI word size must be a power of two multiple of 8 bits (instance %m)");
|
||||
|
||||
localparam [2:0]
|
||||
STATE_IDLE = 3'd0,
|
||||
STATE_ADDRESS = 3'd1,
|
||||
STATE_READ_1 = 3'd2,
|
||||
STATE_READ_2 = 3'd3,
|
||||
STATE_WRITE_1 = 3'd4,
|
||||
STATE_WRITE_2 = 3'd5;
|
||||
|
||||
logic [2:0] state_reg = STATE_IDLE, state_next;
|
||||
|
||||
logic [7:0] count_reg = '0, count_next;
|
||||
logic last_cycle_reg = 1'b0;
|
||||
|
||||
logic [ADDR_W_ADJ-1:0] addr_reg = '0, addr_next;
|
||||
logic [DATA_W-1:0] data_reg = '0, data_next;
|
||||
|
||||
logic m_apb_psel_reg = 1'b0, m_apb_psel_next;
|
||||
logic m_apb_penable_reg = 1'b0, m_apb_penable_next;
|
||||
logic m_apb_pwrite_reg = 1'b0, m_apb_pwrite_next;
|
||||
logic [STRB_W-1:0] m_apb_pstrb_reg = '0, m_apb_pstrb_next;
|
||||
|
||||
logic busy_reg = 1'b0;
|
||||
|
||||
taxi_axis_if #(.DATA_W(8)) axis_tx();
|
||||
taxi_axis_if #(.DATA_W(8)) axis_rx();
|
||||
|
||||
logic [7:0] axis_tx_reg = '0, axis_tx_next;
|
||||
logic axis_tx_valid_reg = 1'b0, axis_tx_valid_next;
|
||||
assign axis_tx.tdata = axis_tx_reg;
|
||||
assign axis_tx.tvalid = axis_tx_valid_reg;
|
||||
assign axis_tx.tlast = 1'b1;
|
||||
assign axis_tx.tid = '0;
|
||||
assign axis_tx.tdest = '0;
|
||||
assign axis_tx.tuser = '0;
|
||||
|
||||
logic axis_rx_ready_reg = 1'b0, axis_rx_ready_next;
|
||||
assign axis_rx.tready = axis_rx_ready_reg;
|
||||
|
||||
assign m_apb.paddr = addr_reg;
|
||||
assign m_apb.pprot = 3'b010;
|
||||
assign m_apb.psel = m_apb_psel_reg;
|
||||
assign m_apb.penable = m_apb_penable_reg;
|
||||
assign m_apb.pwrite = m_apb_pwrite_reg;
|
||||
assign m_apb.pwdata = data_reg;
|
||||
assign m_apb.pstrb = m_apb_pstrb_reg;
|
||||
assign m_apb.pauser = '0;
|
||||
assign m_apb.pwuser = '0;
|
||||
|
||||
assign busy = busy_reg;
|
||||
|
||||
always_comb begin
|
||||
state_next = STATE_IDLE;
|
||||
|
||||
count_next = count_reg;
|
||||
|
||||
axis_tx_next = axis_tx_reg;
|
||||
axis_tx_valid_next = axis_tx_valid_reg && !axis_tx.tready;
|
||||
|
||||
axis_rx_ready_next = 1'b0;
|
||||
|
||||
addr_next = addr_reg;
|
||||
data_next = data_reg;
|
||||
|
||||
m_apb_psel_next = 1'b0;
|
||||
m_apb_penable_next = 1'b0;
|
||||
m_apb_pwrite_next = m_apb_pwrite_reg;
|
||||
m_apb_pstrb_next = m_apb_pstrb_reg;
|
||||
|
||||
case (state_reg)
|
||||
STATE_IDLE: begin
|
||||
// idle, wait for I2C interface
|
||||
m_apb_pwrite_next = 1'b0;
|
||||
|
||||
if (axis_rx.tvalid) begin
|
||||
// store address and write
|
||||
count_next = 8'(ADDR_WORD_W-1);
|
||||
state_next = STATE_ADDRESS;
|
||||
end else if (axis_tx.tready && !axis_tx_valid_reg) begin
|
||||
// read
|
||||
m_apb_psel_next = 1'b1;
|
||||
state_next = STATE_READ_1;
|
||||
end
|
||||
end
|
||||
STATE_ADDRESS: begin
|
||||
// store address
|
||||
axis_rx_ready_next = 1'b1;
|
||||
|
||||
if (axis_rx_ready_reg && axis_rx.tvalid) begin
|
||||
// store pointers
|
||||
addr_next[8*count_reg +: 8] = axis_rx.tdata;
|
||||
count_next = count_reg - 1;
|
||||
if (count_reg == 0) begin
|
||||
// end of header
|
||||
// set initial word offset
|
||||
if (ADDR_W == VALID_ADDR_W && WORD_PART_ADDR_W == 0) begin
|
||||
count_next = '0;
|
||||
end else begin
|
||||
count_next = 8'(addr_next[ADDR_W_ADJ-VALID_ADDR_W-1:0]);
|
||||
end
|
||||
m_apb_pstrb_next = '0;
|
||||
data_next = '0;
|
||||
if (axis_rx.tlast) begin
|
||||
// end of transaction
|
||||
state_next = STATE_IDLE;
|
||||
end else begin
|
||||
// start writing
|
||||
state_next = STATE_WRITE_1;
|
||||
end
|
||||
end else begin
|
||||
if (axis_rx.tlast) begin
|
||||
// end of transaction
|
||||
state_next = STATE_IDLE;
|
||||
end else begin
|
||||
state_next = STATE_ADDRESS;
|
||||
end
|
||||
end
|
||||
end else begin
|
||||
state_next = STATE_ADDRESS;
|
||||
end
|
||||
end
|
||||
STATE_READ_1: begin
|
||||
// wait for data
|
||||
m_apb_psel_next = 1'b1;
|
||||
m_apb_penable_next = 1'b1;
|
||||
m_apb_pwrite_next = 1'b0;
|
||||
|
||||
if (m_apb.psel && m_apb.penable && m_apb.pready) begin
|
||||
// read cycle complete, store result
|
||||
m_apb_psel_next = 1'b0;
|
||||
m_apb_penable_next = 1'b0;
|
||||
data_next = m_apb.prdata;
|
||||
addr_next = addr_reg + (1 << (ADDR_W-VALID_ADDR_W+WORD_PART_ADDR_W));
|
||||
state_next = STATE_READ_2;
|
||||
end else begin
|
||||
state_next = STATE_READ_1;
|
||||
end
|
||||
end
|
||||
STATE_READ_2: begin
|
||||
// send data
|
||||
if (axis_rx.tvalid || !bus_addressed) begin
|
||||
// no longer addressed or now addressed for write, return to idle
|
||||
state_next = STATE_IDLE;
|
||||
end else if (axis_tx.tready && !axis_tx_valid_reg) begin
|
||||
// transfer word and update pointers
|
||||
axis_tx_next = data_reg[8*count_reg +: 8];
|
||||
axis_tx_valid_next = 1'b1;
|
||||
count_next = count_reg + 1;
|
||||
if (count_reg == 8'((STRB_W*BYTE_SIZE/8)-1)) begin
|
||||
// end of stored data word; return to idle
|
||||
count_next = 0;
|
||||
state_next = STATE_IDLE;
|
||||
end else begin
|
||||
state_next = STATE_READ_2;
|
||||
end
|
||||
end else begin
|
||||
state_next = STATE_READ_2;
|
||||
end
|
||||
end
|
||||
STATE_WRITE_1: begin
|
||||
// write data
|
||||
axis_rx_ready_next = 1'b1;
|
||||
m_apb_pwrite_next = 1'b1;
|
||||
|
||||
if (axis_rx_ready_reg && axis_rx.tvalid) begin
|
||||
// store word
|
||||
data_next[8*count_reg +: 8] = axis_rx.tdata;
|
||||
count_next = count_reg + 1;
|
||||
m_apb_pstrb_next[count_reg >> ((BYTE_SIZE/8)-1)] = 1'b1;
|
||||
if (count_reg == 8'((STRB_W*BYTE_SIZE/8)-1) || axis_rx.tlast) begin
|
||||
// have full word or at end of block, start write operation
|
||||
count_next = 0;
|
||||
m_apb_psel_next = 1'b1;
|
||||
state_next = STATE_WRITE_2;
|
||||
end else begin
|
||||
state_next = STATE_WRITE_1;
|
||||
end
|
||||
end else begin
|
||||
state_next = STATE_WRITE_1;
|
||||
end
|
||||
end
|
||||
STATE_WRITE_2: begin
|
||||
// wait for write completion
|
||||
m_apb_psel_next = 1'b1;
|
||||
m_apb_penable_next = 1'b1;
|
||||
m_apb_pwrite_next = 1'b1;
|
||||
|
||||
if (m_apb.psel && m_apb.penable && m_apb.pready) begin
|
||||
// end of write operation
|
||||
data_next = '0;
|
||||
addr_next = addr_reg + (1 << (ADDR_W-VALID_ADDR_W+WORD_PART_ADDR_W));
|
||||
m_apb_psel_next = 1'b0;
|
||||
m_apb_penable_next = 1'b0;
|
||||
m_apb_pstrb_next = '0;
|
||||
if (last_cycle_reg) begin
|
||||
// end of transaction
|
||||
state_next = STATE_IDLE;
|
||||
end else begin
|
||||
state_next = STATE_WRITE_1;
|
||||
end
|
||||
end else begin
|
||||
state_next = STATE_WRITE_2;
|
||||
end
|
||||
end
|
||||
default: begin
|
||||
// invalid state - return to idle
|
||||
state_next = STATE_IDLE;
|
||||
end
|
||||
endcase
|
||||
end
|
||||
|
||||
always_ff @(posedge clk) begin
|
||||
state_reg <= state_next;
|
||||
|
||||
count_reg <= count_next;
|
||||
|
||||
if (axis_rx_ready_reg & axis_rx.tvalid) begin
|
||||
last_cycle_reg <= axis_rx.tlast;
|
||||
end
|
||||
|
||||
addr_reg <= addr_next;
|
||||
data_reg <= data_next;
|
||||
|
||||
m_apb_psel_reg <= m_apb_psel_next;
|
||||
m_apb_penable_reg <= m_apb_penable_next;
|
||||
m_apb_pwrite_reg <= m_apb_pwrite_next;
|
||||
m_apb_pstrb_reg <= m_apb_pstrb_next;
|
||||
|
||||
busy_reg <= state_next != STATE_IDLE;
|
||||
|
||||
axis_tx_reg <= axis_tx_next;
|
||||
axis_tx_valid_reg <= axis_tx_valid_next;
|
||||
|
||||
axis_rx_ready_reg <= axis_rx_ready_next;
|
||||
|
||||
if (rst) begin
|
||||
state_reg <= STATE_IDLE;
|
||||
axis_tx_valid_reg <= 1'b0;
|
||||
axis_rx_ready_reg <= 1'b0;
|
||||
m_apb_psel_reg <= 1'b0;
|
||||
m_apb_penable_reg <= 1'b0;
|
||||
busy_reg <= 1'b0;
|
||||
end
|
||||
end
|
||||
|
||||
taxi_i2c_slave #(
|
||||
.FILTER_LEN(FILTER_LEN)
|
||||
)
|
||||
i2c_slave_inst (
|
||||
.clk(clk),
|
||||
.rst(rst),
|
||||
|
||||
// Host interface
|
||||
.release_bus(1'b0),
|
||||
.s_axis_tx(axis_tx),
|
||||
.m_axis_rx(axis_rx),
|
||||
|
||||
// I2C Interface
|
||||
.scl_i(i2c_scl_i),
|
||||
.scl_o(i2c_scl_o),
|
||||
.sda_i(i2c_sda_i),
|
||||
.sda_o(i2c_sda_o),
|
||||
|
||||
// Status
|
||||
.busy(),
|
||||
.bus_address(bus_address),
|
||||
.bus_addressed(bus_addressed),
|
||||
.bus_active(bus_active),
|
||||
|
||||
// Configuration
|
||||
.enable(enable),
|
||||
.device_address(device_address),
|
||||
.device_address_mask(7'h7f)
|
||||
);
|
||||
|
||||
endmodule
|
||||
|
||||
`resetall
|
||||
52
src/lss/tb/taxi_i2c_slave_apb_master/Makefile
Normal file
52
src/lss/tb/taxi_i2c_slave_apb_master/Makefile
Normal file
@@ -0,0 +1,52 @@
|
||||
# SPDX-License-Identifier: CERN-OHL-S-2.0
|
||||
#
|
||||
# Copyright (c) 2020-2026 FPGA Ninja, LLC
|
||||
#
|
||||
# Authors:
|
||||
# - Alex Forencich
|
||||
|
||||
TOPLEVEL_LANG = verilog
|
||||
|
||||
SIM ?= verilator
|
||||
WAVES ?= 0
|
||||
|
||||
COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
RTL_DIR = ../../rtl
|
||||
LIB_DIR = ../../lib
|
||||
TAXI_SRC_DIR = $(LIB_DIR)/taxi/src
|
||||
|
||||
DUT = taxi_i2c_slave_apb_master
|
||||
COCOTB_TEST_MODULES = test_$(DUT)
|
||||
COCOTB_TOPLEVEL = test_$(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv
|
||||
VERILOG_SOURCES += $(RTL_DIR)/$(DUT).f
|
||||
|
||||
# handle file list files
|
||||
process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1)))
|
||||
process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f))
|
||||
uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1))
|
||||
VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES)))
|
||||
|
||||
# module parameters
|
||||
export PARAM_FILTER_LEN := 4
|
||||
export PARAM_APB_DATA_W := 32
|
||||
export PARAM_APB_ADDR_W := 16
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python
|
||||
# SPDX-License-Identifier: CERN-OHL-S-2.0
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-2026 FPGA Ninja, LLC
|
||||
|
||||
Authors:
|
||||
- Alex Forencich
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
import cocotb_test.simulator
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.i2c import I2cMaster
|
||||
from cocotbext.axi import ApbBus, ApbRam
|
||||
|
||||
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
cocotb.start_soon(Clock(dut.clk, 8, units="ns").start())
|
||||
|
||||
self.i2c_master = I2cMaster(sda=dut.i2c_sda_o, sda_o=dut.i2c_sda_i,
|
||||
scl=dut.i2c_scl_o, scl_o=dut.i2c_scl_i, speed=4000e3)
|
||||
|
||||
self.apb_ram = ApbRam(ApbBus.from_entity(dut.m_apb), dut.clk, dut.rst, size=2**16)
|
||||
|
||||
dut.enable.setimmediatevalue(1)
|
||||
dut.device_address.setimmediatevalue(0x50)
|
||||
|
||||
def set_backpressure_generator(self, generator=None):
|
||||
if generator:
|
||||
self.apb_ram.set_pause_generator(generator())
|
||||
|
||||
async def cycle_reset(self):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst.value = 1
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
|
||||
async def run_test_write(dut, data_in=None, backpressure_inserter=None):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_lanes = tb.apb_ram.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
tb.set_backpressure_generator(backpressure_inserter)
|
||||
|
||||
for length in range(1, byte_lanes*2):
|
||||
for offset in range(byte_lanes):
|
||||
tb.log.info("length %d, offset %d", length, offset)
|
||||
addr = offset+0x1000
|
||||
test_data = bytearray([x % 256 for x in range(length)])
|
||||
|
||||
tb.apb_ram.write(addr-128, b'\xaa'*(length+256))
|
||||
|
||||
await tb.i2c_master.write(0x50, struct.pack('>H', addr)+test_data)
|
||||
await tb.i2c_master.send_stop()
|
||||
|
||||
tb.log.debug("%s", tb.apb_ram.hexdump_str((addr & ~0xf)-16, (((addr & 0xf)+length-1) & ~0xf)+48))
|
||||
|
||||
assert tb.apb_ram.read(addr, length) == test_data
|
||||
assert tb.apb_ram.read(addr-1, 1) == b'\xaa'
|
||||
assert tb.apb_ram.read(addr+length, 1) == b'\xaa'
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
|
||||
async def run_test_read(dut, data_in=None, backpressure_inserter=None):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_lanes = tb.apb_ram.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
tb.set_backpressure_generator(backpressure_inserter)
|
||||
|
||||
for length in range(1, byte_lanes*2):
|
||||
for offset in range(byte_lanes):
|
||||
tb.log.info("length %d, offset %d", length, offset)
|
||||
addr = offset+0x1000
|
||||
test_data = bytearray([x % 256 for x in range(length)])
|
||||
|
||||
tb.apb_ram.write(addr, test_data)
|
||||
|
||||
await tb.i2c_master.write(0x50, struct.pack('>H', addr))
|
||||
data = await tb.i2c_master.read(0x50, length)
|
||||
await tb.i2c_master.send_stop()
|
||||
|
||||
assert data == test_data
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
|
||||
def cycle_pause():
|
||||
return itertools.cycle([1, 1, 1, 0])
|
||||
|
||||
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
for test in [run_test_write, run_test_read]:
|
||||
|
||||
factory = TestFactory(test)
|
||||
factory.add_option("backpressure_inserter", [None, cycle_pause])
|
||||
factory.generate_tests()
|
||||
|
||||
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib'))
|
||||
taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src'))
|
||||
|
||||
|
||||
def process_f_files(files):
|
||||
lst = {}
|
||||
for f in files:
|
||||
if f[-2:].lower() == '.f':
|
||||
with open(f, 'r') as fp:
|
||||
l = fp.read().split()
|
||||
for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]):
|
||||
lst[os.path.basename(f)] = f
|
||||
else:
|
||||
lst[os.path.basename(f)] = f
|
||||
return list(lst.values())
|
||||
|
||||
|
||||
def test_taxi_i2c_slave_apb_master(request):
|
||||
dut = "taxi_i2c_slave_apb_master"
|
||||
module = os.path.splitext(os.path.basename(__file__))[0]
|
||||
toplevel = module
|
||||
|
||||
verilog_sources = [
|
||||
os.path.join(tests_dir, f"{toplevel}.sv"),
|
||||
os.path.join(rtl_dir, f"{dut}.f"),
|
||||
]
|
||||
|
||||
verilog_sources = process_f_files(verilog_sources)
|
||||
|
||||
parameters = {}
|
||||
|
||||
parameters['FILTER_LEN'] = 4
|
||||
parameters['APB_DATA_W'] = 32
|
||||
parameters['APB_ADDR_W'] = 16
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
simulator="verilator",
|
||||
python_search=[tests_dir],
|
||||
verilog_sources=verilog_sources,
|
||||
toplevel=toplevel,
|
||||
module=module,
|
||||
parameters=parameters,
|
||||
sim_build=sim_build,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: CERN-OHL-S-2.0
|
||||
/*
|
||||
|
||||
Copyright (c) 2025-2026 FPGA Ninja, LLC
|
||||
|
||||
Authors:
|
||||
- Alex Forencich
|
||||
|
||||
*/
|
||||
|
||||
`resetall
|
||||
`timescale 1ns / 1ps
|
||||
`default_nettype none
|
||||
|
||||
/*
|
||||
* I2C slave APB master testbench
|
||||
*/
|
||||
module test_taxi_i2c_slave_apb_master #
|
||||
(
|
||||
/* verilator lint_off WIDTHTRUNC */
|
||||
parameter FILTER_LEN = 4,
|
||||
parameter APB_DATA_W = 32,
|
||||
parameter APB_ADDR_W = 16
|
||||
/* verilator lint_on WIDTHTRUNC */
|
||||
)
|
||||
();
|
||||
|
||||
logic clk;
|
||||
logic rst;
|
||||
|
||||
logic i2c_scl_i;
|
||||
logic i2c_scl_o;
|
||||
logic i2c_sda_i;
|
||||
logic i2c_sda_o;
|
||||
|
||||
taxi_apb_if #(
|
||||
.DATA_W(APB_DATA_W),
|
||||
.ADDR_W(APB_ADDR_W),
|
||||
.PAUSER_EN(0),
|
||||
.PWUSER_EN(0),
|
||||
.PRUSER_EN(0),
|
||||
.PBUSER_EN(0)
|
||||
) m_apb();
|
||||
|
||||
logic busy;
|
||||
logic [6:0] bus_address;
|
||||
logic bus_addressed;
|
||||
logic bus_active;
|
||||
|
||||
logic [15:0] prescale;
|
||||
logic stop_on_idle;
|
||||
|
||||
logic enable;
|
||||
logic [6:0] device_address;
|
||||
|
||||
taxi_i2c_slave_apb_master #(
|
||||
.FILTER_LEN(FILTER_LEN)
|
||||
)
|
||||
uut (
|
||||
.clk(clk),
|
||||
.rst(rst),
|
||||
|
||||
/*
|
||||
* I2C interface
|
||||
*/
|
||||
.i2c_scl_i(i2c_scl_i),
|
||||
.i2c_scl_o(i2c_scl_o),
|
||||
.i2c_sda_i(i2c_sda_i),
|
||||
.i2c_sda_o(i2c_sda_o),
|
||||
|
||||
/*
|
||||
* APB master interface
|
||||
*/
|
||||
.m_apb(m_apb),
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
.busy(busy),
|
||||
.bus_address(bus_address),
|
||||
.bus_addressed(bus_addressed),
|
||||
.bus_active(bus_active),
|
||||
|
||||
/*
|
||||
* Configuration
|
||||
*/
|
||||
.enable(enable),
|
||||
.device_address(device_address)
|
||||
);
|
||||
|
||||
endmodule
|
||||
|
||||
`resetall
|
||||
Reference in New Issue
Block a user