mirror of
https://github.com/fpganinja/taxi.git
synced 2025-12-10 17:28:40 -08:00
lss: Add I2C slave AXI lite master module and testbench
Signed-off-by: Alex Forencich <alex@alexforencich.com>
This commit is contained in:
467
src/lss/rtl/taxi_i2c_slave_axil_master.sv
Normal file
467
src/lss/rtl/taxi_i2c_slave_axil_master.sv
Normal file
@@ -0,0 +1,467 @@
|
||||
// SPDX-License-Identifier: CERN-OHL-S-2.0
|
||||
/*
|
||||
|
||||
Copyright (c) 2019-2025 FPGA Ninja, LLC
|
||||
|
||||
Authors:
|
||||
- Alex Forencich
|
||||
|
||||
*/
|
||||
|
||||
`resetall
|
||||
`timescale 1ns / 1ps
|
||||
`default_nettype none
|
||||
|
||||
/*
|
||||
* I2C slave AXI lite master wrapper
|
||||
*/
|
||||
module taxi_i2c_slave_axil_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,
|
||||
|
||||
/*
|
||||
* AXI4-Lite master interface
|
||||
*/
|
||||
taxi_axil_if.wr_mst m_axil_wr,
|
||||
taxi_axil_if.rd_mst m_axil_rd,
|
||||
|
||||
/*
|
||||
* 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 AXI lite bus, useful for enabling a
|
||||
design to operate as a peripheral to an external microcontroller or similar.
|
||||
The AXI lite 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 AXI lite 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 AXI lite
|
||||
interface starting from the current value of the internal address register.
|
||||
Like writes, reads are also coalesced when possible. One AXI lite 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_axil_wr.DATA_W;
|
||||
localparam ADDR_W = m_axil_wr.ADDR_W;
|
||||
localparam STRB_W = m_axil_wr.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_axil_awvalid_reg = 1'b0, m_axil_awvalid_next;
|
||||
logic [STRB_W-1:0] m_axil_wstrb_reg = '0, m_axil_wstrb_next;
|
||||
logic m_axil_wvalid_reg = 1'b0, m_axil_wvalid_next;
|
||||
logic m_axil_bready_reg = 1'b0, m_axil_bready_next;
|
||||
logic m_axil_arvalid_reg = 1'b0, m_axil_arvalid_next;
|
||||
logic m_axil_rready_reg = 1'b0, m_axil_rready_next;
|
||||
|
||||
logic busy_reg = 1'b0;
|
||||
|
||||
taxi_axis_if #(.DATA_W(8)) data_in();
|
||||
taxi_axis_if #(.DATA_W(8)) data_out();
|
||||
|
||||
logic [7:0] data_in_reg = '0, data_in_next;
|
||||
logic data_in_valid_reg = 1'b0, data_in_valid_next;
|
||||
assign data_in.tdata = data_in_reg;
|
||||
assign data_in.tvalid = data_in_valid_reg;
|
||||
assign data_in.tlast = 1'b1;
|
||||
assign data_in.tid = '0;
|
||||
assign data_in.tdest = '0;
|
||||
assign data_in.tuser = '0;
|
||||
|
||||
logic data_out_ready_reg = 1'b0, data_out_ready_next;
|
||||
assign data_out.tready = data_out_ready_reg;
|
||||
|
||||
assign m_axil_wr.awaddr = addr_reg;
|
||||
assign m_axil_wr.awprot = 3'b010;
|
||||
assign m_axil_wr.awvalid = m_axil_awvalid_reg;
|
||||
assign m_axil_wr.wdata = data_reg;
|
||||
assign m_axil_wr.wstrb = m_axil_wstrb_reg;
|
||||
assign m_axil_wr.wvalid = m_axil_wvalid_reg;
|
||||
assign m_axil_wr.bready = m_axil_bready_reg;
|
||||
|
||||
assign m_axil_rd.araddr = addr_reg;
|
||||
assign m_axil_rd.arprot = 3'b010;
|
||||
assign m_axil_rd.arvalid = m_axil_arvalid_reg;
|
||||
assign m_axil_rd.rready = m_axil_rready_reg;
|
||||
|
||||
assign busy = busy_reg;
|
||||
|
||||
always_comb begin
|
||||
state_next = STATE_IDLE;
|
||||
|
||||
count_next = count_reg;
|
||||
|
||||
data_in_next = data_in_reg;
|
||||
data_in_valid_next = data_in_valid_reg && !data_in.tready;
|
||||
|
||||
data_out_ready_next = 1'b0;
|
||||
|
||||
addr_next = addr_reg;
|
||||
data_next = data_reg;
|
||||
|
||||
m_axil_awvalid_next = m_axil_awvalid_reg && !m_axil_wr.awready;
|
||||
m_axil_wstrb_next = m_axil_wstrb_reg;
|
||||
m_axil_wvalid_next = m_axil_wvalid_reg && !m_axil_wr.wready;
|
||||
m_axil_bready_next = 1'b0;
|
||||
m_axil_arvalid_next = m_axil_arvalid_reg && !m_axil_rd.arready;
|
||||
m_axil_rready_next = 1'b0;
|
||||
|
||||
case (state_reg)
|
||||
STATE_IDLE: begin
|
||||
// idle, wait for I2C interface
|
||||
|
||||
if (data_out.tvalid) begin
|
||||
// store address and write
|
||||
count_next = 8'(ADDR_WORD_W-1);
|
||||
state_next = STATE_ADDRESS;
|
||||
end else if (data_in.tready && !data_in_valid_reg) begin
|
||||
// read
|
||||
m_axil_arvalid_next = 1'b1;
|
||||
m_axil_rready_next = 1'b1;
|
||||
state_next = STATE_READ_1;
|
||||
end
|
||||
end
|
||||
STATE_ADDRESS: begin
|
||||
// store address
|
||||
data_out_ready_next = 1'b1;
|
||||
|
||||
if (data_out_ready_reg && data_out.tvalid) begin
|
||||
// store pointers
|
||||
addr_next[8*count_reg +: 8] = data_out.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_axil_wstrb_next = 'd0;
|
||||
data_next = 'd0;
|
||||
if (data_out.tlast) begin
|
||||
// end of transaction
|
||||
state_next = STATE_IDLE;
|
||||
end else begin
|
||||
// start writing
|
||||
state_next = STATE_WRITE_1;
|
||||
end
|
||||
end else begin
|
||||
if (data_out.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_axil_rready_next = 1'b1;
|
||||
|
||||
if (m_axil_rd.rready && m_axil_rd.rvalid) begin
|
||||
// read cycle complete, store result
|
||||
m_axil_rready_next = 1'b0;
|
||||
data_next = m_axil_rd.rdata;
|
||||
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 (data_out.tvalid || !bus_addressed) begin
|
||||
// no longer addressed or now addressed for write, return to idle
|
||||
state_next = STATE_IDLE;
|
||||
end else if (data_in.tready && !data_in_valid_reg) begin
|
||||
// transfer word and update pointers
|
||||
data_in_next = data_reg[8*count_reg +: 8];
|
||||
data_in_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
|
||||
data_out_ready_next = 1'b1;
|
||||
|
||||
if (data_out_ready_reg && data_out.tvalid) begin
|
||||
// store word
|
||||
data_next[8*count_reg +: 8] = data_out.tdata;
|
||||
count_next = count_reg + 1;
|
||||
m_axil_wstrb_next[count_reg >> ((BYTE_SIZE/8)-1)] = 1'b1;
|
||||
if (count_reg == 8'((STRB_W*BYTE_SIZE/8)-1) || data_out.tlast) begin
|
||||
// have full word or at end of block, start write operation
|
||||
count_next = 0;
|
||||
m_axil_awvalid_next = 1'b1;
|
||||
m_axil_wvalid_next = 1'b1;
|
||||
m_axil_bready_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_axil_bready_next = 1'b1;
|
||||
|
||||
if (m_axil_wr.bready && m_axil_wr.bvalid) begin
|
||||
// end of write operation
|
||||
data_next = 'd0;
|
||||
addr_next = addr_reg + (1 << (ADDR_W-VALID_ADDR_W+WORD_PART_ADDR_W));
|
||||
m_axil_bready_next = 1'b0;
|
||||
m_axil_wstrb_next = 'd0;
|
||||
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 (data_out_ready_reg & data_out.tvalid) begin
|
||||
last_cycle_reg <= data_out.tlast;
|
||||
end
|
||||
|
||||
addr_reg <= addr_next;
|
||||
data_reg <= data_next;
|
||||
|
||||
m_axil_awvalid_reg <= m_axil_awvalid_next;
|
||||
m_axil_wstrb_reg <= m_axil_wstrb_next;
|
||||
m_axil_wvalid_reg <= m_axil_wvalid_next;
|
||||
m_axil_bready_reg <= m_axil_bready_next;
|
||||
m_axil_arvalid_reg <= m_axil_arvalid_next;
|
||||
m_axil_rready_reg <= m_axil_rready_next;
|
||||
|
||||
busy_reg <= state_next != STATE_IDLE;
|
||||
|
||||
data_in_reg <= data_in_next;
|
||||
data_in_valid_reg <= data_in_valid_next;
|
||||
|
||||
data_out_ready_reg <= data_out_ready_next;
|
||||
|
||||
if (rst) begin
|
||||
state_reg <= STATE_IDLE;
|
||||
data_in_valid_reg <= 1'b0;
|
||||
data_out_ready_reg <= 1'b0;
|
||||
m_axil_awvalid_reg <= 1'b0;
|
||||
m_axil_wvalid_reg <= 1'b0;
|
||||
m_axil_bready_reg <= 1'b0;
|
||||
m_axil_arvalid_reg <= 1'b0;
|
||||
m_axil_rready_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_data(data_in),
|
||||
.m_axis_data(data_out),
|
||||
|
||||
// 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
|
||||
Reference in New Issue
Block a user