From 5652bb0016c65155ad6a57bebcb46264919cfb69 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Thu, 19 Mar 2026 18:12:37 -0700 Subject: [PATCH] axi: Add AXI RAM interface and AXI dual port RAM Signed-off-by: Alex Forencich --- README.md | 2 + src/axi/rtl/taxi_axi_dp_ram.f | 2 + src/axi/rtl/taxi_axi_dp_ram.sv | 244 +++++++++++++++ src/axi/rtl/taxi_axi_ram_if_rd.sv | 237 ++++++++++++++ src/axi/rtl/taxi_axi_ram_if_rdwr.f | 4 + src/axi/rtl/taxi_axi_ram_if_rdwr.sv | 238 ++++++++++++++ src/axi/rtl/taxi_axi_ram_if_wr.sv | 251 +++++++++++++++ src/axi/tb/taxi_axi_dp_ram/Makefile | 57 ++++ .../taxi_axi_dp_ram/test_taxi_axi_dp_ram.py | 292 ++++++++++++++++++ .../taxi_axi_dp_ram/test_taxi_axi_dp_ram.sv | 72 +++++ 10 files changed, 1399 insertions(+) create mode 100644 src/axi/rtl/taxi_axi_dp_ram.f create mode 100644 src/axi/rtl/taxi_axi_dp_ram.sv create mode 100644 src/axi/rtl/taxi_axi_ram_if_rd.sv create mode 100644 src/axi/rtl/taxi_axi_ram_if_rdwr.f create mode 100644 src/axi/rtl/taxi_axi_ram_if_rdwr.sv create mode 100644 src/axi/rtl/taxi_axi_ram_if_wr.sv create mode 100644 src/axi/tb/taxi_axi_dp_ram/Makefile create mode 100644 src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.py create mode 100644 src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.sv diff --git a/README.md b/README.md index f2b9c86..b552d45 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ The Taxi transport library contains many smaller components that can be composed * Width converter * Synchronous FIFO * Single-port RAM + * Dual-port RAM + * RAM interface * AXI lite * SV interface for AXI lite * AXI lite to AXI adapter diff --git a/src/axi/rtl/taxi_axi_dp_ram.f b/src/axi/rtl/taxi_axi_dp_ram.f new file mode 100644 index 0000000..2f9c8c0 --- /dev/null +++ b/src/axi/rtl/taxi_axi_dp_ram.f @@ -0,0 +1,2 @@ +taxi_axi_dp_ram.sv +taxi_axi_ram_if_rdwr.f diff --git a/src/axi/rtl/taxi_axi_dp_ram.sv b/src/axi/rtl/taxi_axi_dp_ram.sv new file mode 100644 index 0000000..1e8b16e --- /dev/null +++ b/src/axi/rtl/taxi_axi_dp_ram.sv @@ -0,0 +1,244 @@ +// 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 + +/* + * AXI4 dual-port RAM + */ +module taxi_axi_dp_ram # +( + // Width of address bus in bits + parameter ADDR_W = 16, + // Extra pipeline register on output port A + parameter logic A_PIPELINE_OUTPUT = 1'b0, + // Extra pipeline register on output port B + parameter logic B_PIPELINE_OUTPUT = 1'b0, + // Interleave read and write burst cycles on port A + parameter logic A_INTERLEAVE = 1'b0, + // Interleave read and write burst cycles on port B + parameter logic B_INTERLEAVE = 1'b0 +) +( + /* + * Port A + */ + input wire logic a_clk, + input wire logic a_rst, + taxi_axi_if.wr_slv s_axi_wr_a, + taxi_axi_if.rd_slv s_axi_rd_a, + + /* + * Port B + */ + input wire logic b_clk, + input wire logic b_rst, + taxi_axi_if.wr_slv s_axi_wr_b, + taxi_axi_if.rd_slv s_axi_rd_b +); + +// extract parameters +localparam DATA_W = s_axi_wr_a.DATA_W; +localparam STRB_W = s_axi_wr_a.STRB_W; +localparam A_ID_W = s_axi_wr_a.ID_W; +localparam B_ID_W = s_axi_wr_b.ID_W; + +localparam VALID_ADDR_W = ADDR_W - $clog2(STRB_W); +localparam BYTE_LANES = STRB_W; +localparam BYTE_W = DATA_W/BYTE_LANES; + +// check configuration +if (BYTE_W * STRB_W != 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)"); + +wire [A_ID_W-1:0] ram_a_cmd_id; +wire [ADDR_W-1:0] ram_a_cmd_addr; +wire [DATA_W-1:0] ram_a_cmd_wr_data; +wire [STRB_W-1:0] ram_a_cmd_wr_strb; +wire ram_a_cmd_wr_en; +wire ram_a_cmd_rd_en; +wire ram_a_cmd_last; +wire ram_a_cmd_ready; +logic [A_ID_W-1:0] ram_a_rd_resp_id_reg = 'd0; +logic [DATA_W-1:0] ram_a_rd_resp_data_reg = 'd0; +logic ram_a_rd_resp_last_reg = 1'b0; +logic ram_a_rd_resp_valid_reg = 1'b0; +wire ram_a_rd_resp_ready; + +wire [B_ID_W-1:0] ram_b_cmd_id; +wire [ADDR_W-1:0] ram_b_cmd_addr; +wire [DATA_W-1:0] ram_b_cmd_wr_data; +wire [STRB_W-1:0] ram_b_cmd_wr_strb; +wire ram_b_cmd_wr_en; +wire ram_b_cmd_rd_en; +wire ram_b_cmd_last; +wire ram_b_cmd_ready; +logic [B_ID_W-1:0] ram_b_rd_resp_id_reg = 'd0; +logic [DATA_W-1:0] ram_b_rd_resp_data_reg = 'd0; +logic ram_b_rd_resp_last_reg = 1'b0; +logic ram_b_rd_resp_valid_reg = 1'b0; +wire ram_b_rd_resp_ready; + +taxi_axi_ram_if_rdwr #( + .DATA_W(DATA_W), + .ADDR_W(ADDR_W), + .STRB_W(STRB_W), + .ID_W(A_ID_W), + .AUSER_W(s_axi_wr_a.AWUSER_W > s_axi_rd_a.ARUSER_W ? s_axi_wr_a.AWUSER_W : s_axi_rd_a.ARUSER_W), + .WUSER_W(s_axi_wr_a.WUSER_W), + .RUSER_W(s_axi_rd_a.RUSER_W), + .PIPELINE_OUTPUT(A_PIPELINE_OUTPUT), + .INTERLEAVE(A_INTERLEAVE) +) +a_if ( + .clk(a_clk), + .rst(a_rst), + + /* + * AXI4 slave interface + */ + .s_axi_wr(s_axi_wr_a), + .s_axi_rd(s_axi_rd_a), + + /* + * RAM interface + */ + .ram_cmd_id(ram_a_cmd_id), + .ram_cmd_addr(ram_a_cmd_addr), + .ram_cmd_lock(), + .ram_cmd_cache(), + .ram_cmd_prot(), + .ram_cmd_qos(), + .ram_cmd_region(), + .ram_cmd_auser(), + .ram_cmd_wr_data(ram_a_cmd_wr_data), + .ram_cmd_wr_strb(ram_a_cmd_wr_strb), + .ram_cmd_wr_user(), + .ram_cmd_wr_en(ram_a_cmd_wr_en), + .ram_cmd_rd_en(ram_a_cmd_rd_en), + .ram_cmd_last(ram_a_cmd_last), + .ram_cmd_ready(ram_a_cmd_ready), + .ram_rd_resp_id(ram_a_rd_resp_id_reg), + .ram_rd_resp_data(ram_a_rd_resp_data_reg), + .ram_rd_resp_last(ram_a_rd_resp_last_reg), + .ram_rd_resp_user('0), + .ram_rd_resp_valid(ram_a_rd_resp_valid_reg), + .ram_rd_resp_ready(ram_a_rd_resp_ready) +); + +taxi_axi_ram_if_rdwr #( + .DATA_W(DATA_W), + .ADDR_W(ADDR_W), + .STRB_W(STRB_W), + .ID_W(B_ID_W), + .AUSER_W(s_axi_wr_b.AWUSER_W > s_axi_rd_b.ARUSER_W ? s_axi_wr_b.AWUSER_W : s_axi_rd_b.ARUSER_W), + .WUSER_W(s_axi_wr_b.WUSER_W), + .RUSER_W(s_axi_rd_b.RUSER_W), + .PIPELINE_OUTPUT(B_PIPELINE_OUTPUT), + .INTERLEAVE(B_INTERLEAVE) +) +b_if ( + .clk(b_clk), + .rst(b_rst), + + /* + * AXI4 slave interface + */ + .s_axi_wr(s_axi_wr_b), + .s_axi_rd(s_axi_rd_b), + + /* + * RAM interface + */ + .ram_cmd_id(ram_b_cmd_id), + .ram_cmd_addr(ram_b_cmd_addr), + .ram_cmd_lock(), + .ram_cmd_cache(), + .ram_cmd_prot(), + .ram_cmd_qos(), + .ram_cmd_region(), + .ram_cmd_auser(), + .ram_cmd_wr_data(ram_b_cmd_wr_data), + .ram_cmd_wr_strb(ram_b_cmd_wr_strb), + .ram_cmd_wr_user(), + .ram_cmd_wr_en(ram_b_cmd_wr_en), + .ram_cmd_rd_en(ram_b_cmd_rd_en), + .ram_cmd_last(ram_b_cmd_last), + .ram_cmd_ready(ram_b_cmd_ready), + .ram_rd_resp_id(ram_b_rd_resp_id_reg), + .ram_rd_resp_data(ram_b_rd_resp_data_reg), + .ram_rd_resp_last(ram_b_rd_resp_last_reg), + .ram_rd_resp_user('0), + .ram_rd_resp_valid(ram_b_rd_resp_valid_reg), + .ram_rd_resp_ready(ram_b_rd_resp_ready) +); + +// verilator lint_off MULTIDRIVEN +// (* RAM_STYLE="BLOCK" *) +logic [DATA_W-1:0] mem[2**VALID_ADDR_W] = '{default: '0}; +// verilator lint_on MULTIDRIVEN + +wire [VALID_ADDR_W-1:0] addr_a_valid = VALID_ADDR_W'(ram_a_cmd_addr >> (ADDR_W - VALID_ADDR_W)); +wire [VALID_ADDR_W-1:0] addr_b_valid = VALID_ADDR_W'(ram_b_cmd_addr >> (ADDR_W - VALID_ADDR_W)); + +assign ram_a_cmd_ready = !ram_a_rd_resp_valid_reg || ram_a_rd_resp_ready; + +always_ff @(posedge a_clk) begin + ram_a_rd_resp_valid_reg <= ram_a_rd_resp_valid_reg && !ram_a_rd_resp_ready; + + if (ram_a_cmd_rd_en && ram_a_cmd_ready) begin + ram_a_rd_resp_id_reg <= ram_a_cmd_id; + ram_a_rd_resp_data_reg <= mem[addr_a_valid]; + ram_a_rd_resp_last_reg <= ram_a_cmd_last; + ram_a_rd_resp_valid_reg <= 1'b1; + end else if (ram_a_cmd_wr_en && ram_a_cmd_ready) begin + for (integer i = 0; i < BYTE_LANES; i = i + 1) begin + if (ram_a_cmd_wr_strb[i]) begin + mem[addr_a_valid][BYTE_W*i +: BYTE_W] <= ram_a_cmd_wr_data[BYTE_W*i +: BYTE_W]; + end + end + end + + if (a_rst) begin + ram_a_rd_resp_valid_reg <= 1'b0; + end +end + +assign ram_b_cmd_ready = !ram_b_rd_resp_valid_reg || ram_b_rd_resp_ready; + +always_ff @(posedge b_clk) begin + ram_b_rd_resp_valid_reg <= ram_b_rd_resp_valid_reg && !ram_b_rd_resp_ready; + + if (ram_b_cmd_rd_en && ram_b_cmd_ready) begin + ram_b_rd_resp_id_reg <= ram_b_cmd_id; + ram_b_rd_resp_data_reg <= mem[addr_b_valid]; + ram_b_rd_resp_last_reg <= ram_b_cmd_last; + ram_b_rd_resp_valid_reg <= 1'b1; + end else if (ram_b_cmd_wr_en && ram_b_cmd_ready) begin + for (integer i = 0; i < BYTE_LANES; i = i + 1) begin + if (ram_b_cmd_wr_strb[i]) begin + mem[addr_b_valid][BYTE_W*i +: BYTE_W] <= ram_b_cmd_wr_data[BYTE_W*i +: BYTE_W]; + end + end + end + + if (b_rst) begin + ram_b_rd_resp_valid_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/axi/rtl/taxi_axi_ram_if_rd.sv b/src/axi/rtl/taxi_axi_ram_if_rd.sv new file mode 100644 index 0000000..3744bb6 --- /dev/null +++ b/src/axi/rtl/taxi_axi_ram_if_rd.sv @@ -0,0 +1,237 @@ +// 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 + +/* + * AXI4 RAM read interface + */ +module taxi_axi_ram_if_rd # +( + // Width of data bus in bits + parameter DATA_W = 32, + // Width of address bus in bits + parameter ADDR_W = 16, + // Width of wstrb (width of data bus in words) + parameter STRB_W = (DATA_W/8), + // Width of ID signal + parameter ID_W = 8, + // Width of auser signal + parameter AUSER_W = 1, + // Width of ruser signal + parameter RUSER_W = 1, + // Extra pipeline register on output + parameter logic PIPELINE_OUTPUT = 1'b0 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4 slave interface + */ + taxi_axi_if.rd_slv s_axi_rd, + + /* + * RAM interface + */ + output wire logic [ID_W-1:0] ram_rd_cmd_id, + output wire logic [ADDR_W-1:0] ram_rd_cmd_addr, + output wire logic ram_rd_cmd_lock, + output wire logic [3:0] ram_rd_cmd_cache, + output wire logic [2:0] ram_rd_cmd_prot, + output wire logic [3:0] ram_rd_cmd_qos, + output wire logic [3:0] ram_rd_cmd_region, + output wire logic [AUSER_W-1:0] ram_rd_cmd_auser, + output wire logic ram_rd_cmd_en, + output wire logic ram_rd_cmd_last, + input wire logic ram_rd_cmd_ready, + input wire logic [ID_W-1:0] ram_rd_resp_id, + input wire logic [DATA_W-1:0] ram_rd_resp_data, + input wire logic ram_rd_resp_last, + input wire logic [RUSER_W-1:0] ram_rd_resp_user, + input wire logic ram_rd_resp_valid, + output wire logic ram_rd_resp_ready +); + +// extract parameters +localparam logic AUSER_EN = s_axi_rd.ARUSER_EN; +localparam logic RUSER_EN = s_axi_rd.RUSER_EN; + +localparam VALID_ADDR_W = ADDR_W - $clog2(STRB_W); +localparam BYTE_LANES = STRB_W; +localparam BYTE_W = DATA_W/BYTE_LANES; + +// check configuration +if (BYTE_W * STRB_W != 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 (s_axi_rd.ADDR_W < ADDR_W) + $fatal(0, "Error: AXI address width is insufficient (instance %m)"); + +typedef enum logic [0:0] { + STATE_IDLE, + STATE_BURST +} state_t; + +state_t state_reg = STATE_IDLE, state_next; + +logic [ID_W-1:0] read_id_reg = '0, read_id_next; +logic [ADDR_W-1:0] read_addr_reg = '0, read_addr_next; +logic read_lock_reg = 1'b0, read_lock_next; +logic [3:0] read_cache_reg = 4'd0, read_cache_next; +logic [2:0] read_prot_reg = 3'd0, read_prot_next; +logic [3:0] read_qos_reg = 4'd0, read_qos_next; +logic [3:0] read_region_reg = 4'd0, read_region_next; +logic [AUSER_W-1:0] read_auser_reg = '0, read_auser_next; +logic read_addr_valid_reg = 1'b0, read_addr_valid_next; +logic read_last_reg = 1'b0, read_last_next; +logic [7:0] read_count_reg = 8'd0, read_count_next; +logic [2:0] read_size_reg = 3'd0, read_size_next; +logic [1:0] read_burst_reg = 2'd0, read_burst_next; + +logic s_axi_arready_reg = 1'b0, s_axi_arready_next; +logic [ID_W-1:0] s_axi_rid_pipe_reg = '0; +logic [DATA_W-1:0] s_axi_rdata_pipe_reg = '0; +logic s_axi_rlast_pipe_reg = 1'b0; +logic [RUSER_W-1:0] s_axi_ruser_pipe_reg = '0; +logic s_axi_rvalid_pipe_reg = 1'b0; + +assign s_axi_rd.arready = s_axi_arready_reg; +assign s_axi_rd.rid = PIPELINE_OUTPUT ? s_axi_rid_pipe_reg : ram_rd_resp_id; +assign s_axi_rd.rdata = PIPELINE_OUTPUT ? s_axi_rdata_pipe_reg : ram_rd_resp_data; +assign s_axi_rd.rresp = 2'b00; +assign s_axi_rd.rlast = PIPELINE_OUTPUT ? s_axi_rlast_pipe_reg : ram_rd_resp_last; +assign s_axi_rd.ruser = PIPELINE_OUTPUT ? s_axi_ruser_pipe_reg : ram_rd_resp_user; +assign s_axi_rd.rvalid = PIPELINE_OUTPUT ? s_axi_rvalid_pipe_reg : ram_rd_resp_valid; + +assign ram_rd_cmd_id = read_id_reg; +assign ram_rd_cmd_addr = read_addr_reg; +assign ram_rd_cmd_lock = read_lock_reg; +assign ram_rd_cmd_cache = read_cache_reg; +assign ram_rd_cmd_prot = read_prot_reg; +assign ram_rd_cmd_qos = read_qos_reg; +assign ram_rd_cmd_region = read_region_reg; +assign ram_rd_cmd_auser = AUSER_EN ? read_auser_reg : '0; +assign ram_rd_cmd_en = read_addr_valid_reg; +assign ram_rd_cmd_last = read_last_reg; + +assign ram_rd_resp_ready = s_axi_rd.rready || (PIPELINE_OUTPUT && !s_axi_rvalid_pipe_reg); + +always_comb begin + state_next = STATE_IDLE; + + read_id_next = read_id_reg; + read_addr_next = read_addr_reg; + read_lock_next = read_lock_reg; + read_cache_next = read_cache_reg; + read_prot_next = read_prot_reg; + read_qos_next = read_qos_reg; + read_region_next = read_region_reg; + read_auser_next = read_auser_reg; + read_addr_valid_next = read_addr_valid_reg && !ram_rd_cmd_ready; + read_last_next = read_last_reg; + read_count_next = read_count_reg; + read_size_next = read_size_reg; + read_burst_next = read_burst_reg; + + s_axi_arready_next = 1'b0; + + case (state_reg) + STATE_IDLE: begin + s_axi_arready_next = 1'b1; + + if (s_axi_rd.arready && s_axi_rd.arvalid) begin + read_id_next = s_axi_rd.arid; + read_addr_next = ADDR_W'(s_axi_rd.araddr); + read_lock_next = s_axi_rd.arlock; + read_cache_next = s_axi_rd.arcache; + read_prot_next = s_axi_rd.arprot; + read_qos_next = s_axi_rd.arqos; + read_region_next = s_axi_rd.arregion; + read_auser_next = s_axi_rd.aruser; + read_count_next = s_axi_rd.arlen; + read_size_next = s_axi_rd.arsize <= 3'($clog2(STRB_W)) ? s_axi_rd.arsize : 3'($clog2(STRB_W)); + read_burst_next = s_axi_rd.arburst; + + s_axi_arready_next = 1'b0; + read_last_next = read_count_next == 0; + read_addr_valid_next = 1'b1; + state_next = STATE_BURST; + end else begin + state_next = STATE_IDLE; + end + end + STATE_BURST: begin + if (ram_rd_cmd_ready && ram_rd_cmd_en) begin + if (read_burst_reg != 2'b00) begin + read_addr_next = read_addr_reg + (1 << read_size_reg); + end + read_count_next = read_count_reg - 1; + read_last_next = read_count_next == 0; + if (read_count_reg > 0) begin + read_addr_valid_next = 1'b1; + state_next = STATE_BURST; + end else begin + s_axi_arready_next = 1'b1; + state_next = STATE_IDLE; + end + end else begin + state_next = STATE_BURST; + end + end + endcase +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + read_id_reg <= read_id_next; + read_addr_reg <= read_addr_next; + read_lock_reg <= read_lock_next; + read_cache_reg <= read_cache_next; + read_prot_reg <= read_prot_next; + read_qos_reg <= read_qos_next; + read_region_reg <= read_region_next; + read_auser_reg <= read_auser_next; + read_addr_valid_reg <= read_addr_valid_next; + read_last_reg <= read_last_next; + read_count_reg <= read_count_next; + read_size_reg <= read_size_next; + read_burst_reg <= read_burst_next; + + s_axi_arready_reg <= s_axi_arready_next; + + if (!s_axi_rvalid_pipe_reg || s_axi_rd.rready) begin + s_axi_rid_pipe_reg <= ram_rd_resp_id; + s_axi_rdata_pipe_reg <= ram_rd_resp_data; + s_axi_rlast_pipe_reg <= ram_rd_resp_last; + s_axi_ruser_pipe_reg <= ram_rd_resp_user; + s_axi_rvalid_pipe_reg <= ram_rd_resp_valid; + end + + if (rst) begin + state_reg <= STATE_IDLE; + + read_addr_valid_reg <= 1'b0; + + s_axi_arready_reg <= 1'b0; + s_axi_rvalid_pipe_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/axi/rtl/taxi_axi_ram_if_rdwr.f b/src/axi/rtl/taxi_axi_ram_if_rdwr.f new file mode 100644 index 0000000..5c9f9b5 --- /dev/null +++ b/src/axi/rtl/taxi_axi_ram_if_rdwr.f @@ -0,0 +1,4 @@ +taxi_axi_ram_if_wr.sv +taxi_axi_ram_if_rd.sv +taxi_axi_ram_if_rdwr.sv +taxi_axi_if.sv diff --git a/src/axi/rtl/taxi_axi_ram_if_rdwr.sv b/src/axi/rtl/taxi_axi_ram_if_rdwr.sv new file mode 100644 index 0000000..9d96243 --- /dev/null +++ b/src/axi/rtl/taxi_axi_ram_if_rdwr.sv @@ -0,0 +1,238 @@ +// 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 + +/* + * AXI4 RAM read/write interface + */ +module taxi_axi_ram_if_rdwr # +( + // Width of data bus in bits + parameter DATA_W = 32, + // Width of address bus in bits + parameter ADDR_W = 16, + // Width of wstrb (width of data bus in words) + parameter STRB_W = (DATA_W/8), + // Width of ID signal + parameter ID_W = 8, + // Width of auser output + parameter AUSER_W = 1, + // Width of wuser signal + parameter WUSER_W = 1, + // Width of ruser signal + parameter RUSER_W = 1, + // Width of auser output + // parameter AUSER_W = (ARUSER_EN && (!AWUSER_EN || ARUSER_W > AWUSER_W)) ? ARUSER_W : AWUSER_W, + // Extra pipeline register on output + parameter logic PIPELINE_OUTPUT = 1'b0, + // Interleave read and write burst cycles + parameter logic INTERLEAVE = 1'b0 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4 slave interface + */ + taxi_axi_if.wr_slv s_axi_wr, + taxi_axi_if.rd_slv s_axi_rd, + + /* + * RAM interface + */ + output wire [ID_W-1:0] ram_cmd_id, + output wire [ADDR_W-1:0] ram_cmd_addr, + output wire ram_cmd_lock, + output wire [3:0] ram_cmd_cache, + output wire [2:0] ram_cmd_prot, + output wire [3:0] ram_cmd_qos, + output wire [3:0] ram_cmd_region, + output wire [AUSER_W-1:0] ram_cmd_auser, + output wire [DATA_W-1:0] ram_cmd_wr_data, + output wire [STRB_W-1:0] ram_cmd_wr_strb, + output wire [WUSER_W-1:0] ram_cmd_wr_user, + output wire ram_cmd_wr_en, + output wire ram_cmd_rd_en, + output wire ram_cmd_last, + input wire ram_cmd_ready, + input wire [ID_W-1:0] ram_rd_resp_id, + input wire [DATA_W-1:0] ram_rd_resp_data, + input wire ram_rd_resp_last, + input wire [RUSER_W-1:0] ram_rd_resp_user, + input wire ram_rd_resp_valid, + output wire ram_rd_resp_ready +); + +wire [ID_W-1:0] ram_wr_cmd_id; +wire [ADDR_W-1:0] ram_wr_cmd_addr; +wire ram_wr_cmd_lock; +wire [3:0] ram_wr_cmd_cache; +wire [2:0] ram_wr_cmd_prot; +wire [3:0] ram_wr_cmd_qos; +wire [3:0] ram_wr_cmd_region; +wire [AUSER_W-1:0] ram_wr_cmd_auser; +wire ram_wr_cmd_en; +wire ram_wr_cmd_last; +wire ram_wr_cmd_ready; + +wire [ID_W-1:0] ram_rd_cmd_id; +wire [ADDR_W-1:0] ram_rd_cmd_addr; +wire ram_rd_cmd_lock; +wire [3:0] ram_rd_cmd_cache; +wire [2:0] ram_rd_cmd_prot; +wire [3:0] ram_rd_cmd_qos; +wire [3:0] ram_rd_cmd_region; +wire [AUSER_W-1:0] ram_rd_cmd_auser; +wire ram_rd_cmd_en; +wire ram_rd_cmd_last; +wire ram_rd_cmd_ready; + +taxi_axi_ram_if_wr #( + .DATA_W(DATA_W), + .ADDR_W(ADDR_W), + .STRB_W(STRB_W), + .ID_W(ID_W), + .AUSER_W(AUSER_W), + .WUSER_W(WUSER_W) +) +wr_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4 slave interface + */ + .s_axi_wr(s_axi_wr), + + /* + * RAM interface + */ + .ram_wr_cmd_id(ram_wr_cmd_id), + .ram_wr_cmd_addr(ram_wr_cmd_addr), + .ram_wr_cmd_lock(ram_wr_cmd_lock), + .ram_wr_cmd_cache(ram_wr_cmd_cache), + .ram_wr_cmd_prot(ram_wr_cmd_prot), + .ram_wr_cmd_qos(ram_wr_cmd_qos), + .ram_wr_cmd_region(ram_wr_cmd_region), + .ram_wr_cmd_auser(ram_wr_cmd_auser), + .ram_wr_cmd_data(ram_cmd_wr_data), + .ram_wr_cmd_strb(ram_cmd_wr_strb), + .ram_wr_cmd_user(ram_cmd_wr_user), + .ram_wr_cmd_en(ram_wr_cmd_en), + .ram_wr_cmd_last(ram_wr_cmd_last), + .ram_wr_cmd_ready(ram_wr_cmd_ready) +); + +taxi_axi_ram_if_rd #( + .DATA_W(DATA_W), + .ADDR_W(ADDR_W), + .STRB_W(STRB_W), + .ID_W(ID_W), + .AUSER_W(AUSER_W), + .RUSER_W(RUSER_W), + .PIPELINE_OUTPUT(PIPELINE_OUTPUT) +) +rd_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4 slave interface + */ + .s_axi_rd(s_axi_rd), + + /* + * RAM interface + */ + .ram_rd_cmd_id(ram_rd_cmd_id), + .ram_rd_cmd_addr(ram_rd_cmd_addr), + .ram_rd_cmd_lock(ram_rd_cmd_lock), + .ram_rd_cmd_cache(ram_rd_cmd_cache), + .ram_rd_cmd_prot(ram_rd_cmd_prot), + .ram_rd_cmd_qos(ram_rd_cmd_qos), + .ram_rd_cmd_region(ram_rd_cmd_region), + .ram_rd_cmd_auser(ram_rd_cmd_auser), + .ram_rd_cmd_en(ram_rd_cmd_en), + .ram_rd_cmd_last(ram_rd_cmd_last), + .ram_rd_cmd_ready(ram_rd_cmd_ready), + .ram_rd_resp_id(ram_rd_resp_id), + .ram_rd_resp_data(ram_rd_resp_data), + .ram_rd_resp_last(ram_rd_resp_last), + .ram_rd_resp_user(ram_rd_resp_user), + .ram_rd_resp_valid(ram_rd_resp_valid), + .ram_rd_resp_ready(ram_rd_resp_ready) +); + +// arbitration +logic read_eligible; +logic write_eligible; + +logic write_en; +logic read_en; + +logic last_read_reg = 1'b0, last_read_next; +logic transaction_reg = 1'b0, transaction_next; + +assign ram_cmd_wr_en = write_en; +assign ram_cmd_rd_en = read_en; + +assign ram_cmd_id = ram_cmd_rd_en ? ram_rd_cmd_id : ram_wr_cmd_id; +assign ram_cmd_addr = ram_cmd_rd_en ? ram_rd_cmd_addr : ram_wr_cmd_addr; +assign ram_cmd_lock = ram_cmd_rd_en ? ram_rd_cmd_lock : ram_wr_cmd_lock; +assign ram_cmd_cache = ram_cmd_rd_en ? ram_rd_cmd_cache : ram_wr_cmd_cache; +assign ram_cmd_prot = ram_cmd_rd_en ? ram_rd_cmd_prot : ram_wr_cmd_prot; +assign ram_cmd_qos = ram_cmd_rd_en ? ram_rd_cmd_qos : ram_wr_cmd_qos; +assign ram_cmd_region = ram_cmd_rd_en ? ram_rd_cmd_region : ram_wr_cmd_region; +assign ram_cmd_auser = ram_cmd_rd_en ? ram_rd_cmd_auser : ram_wr_cmd_auser; +assign ram_cmd_last = ram_cmd_rd_en ? ram_rd_cmd_last : ram_wr_cmd_last; + +assign ram_wr_cmd_ready = ram_cmd_ready && write_en; +assign ram_rd_cmd_ready = ram_cmd_ready && read_en; + +always_comb begin + write_en = 1'b0; + read_en = 1'b0; + + last_read_next = last_read_reg; + transaction_next = transaction_reg; + + write_eligible = ram_wr_cmd_en && ram_cmd_ready; + read_eligible = ram_rd_cmd_en && ram_cmd_ready; + + if (write_eligible && (!read_eligible || last_read_reg || (!INTERLEAVE && transaction_reg)) && (INTERLEAVE || !transaction_reg || !last_read_reg)) begin + last_read_next = 1'b0; + transaction_next = !ram_wr_cmd_last; + + write_en = 1'b1; + end else if (read_eligible && (INTERLEAVE || !transaction_reg || last_read_reg)) begin + last_read_next = 1'b1; + transaction_next = !ram_rd_cmd_last; + + read_en = 1'b1; + end +end + +always_ff @(posedge clk) begin + last_read_reg <= last_read_next; + transaction_reg <= transaction_next; + + if (rst) begin + last_read_reg <= 1'b0; + transaction_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/axi/rtl/taxi_axi_ram_if_wr.sv b/src/axi/rtl/taxi_axi_ram_if_wr.sv new file mode 100644 index 0000000..506ff9e --- /dev/null +++ b/src/axi/rtl/taxi_axi_ram_if_wr.sv @@ -0,0 +1,251 @@ +// 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 + +/* + * AXI4 RAM write interface + */ +module taxi_axi_ram_if_wr # +( + // Width of data bus in bits + parameter DATA_W = 32, + // Width of address bus in bits + parameter ADDR_W = 16, + // Width of wstrb (width of data bus in words) + parameter STRB_W = (DATA_W/8), + // Width of ID signal + parameter ID_W = 8, + // Width of auser signal + parameter AUSER_W = 1, + // Width of wuser signal + parameter WUSER_W = 1 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4 slave interface + */ + taxi_axi_if.wr_slv s_axi_wr, + + /* + * RAM interface + */ + output wire logic [ID_W-1:0] ram_wr_cmd_id, + output wire logic [ADDR_W-1:0] ram_wr_cmd_addr, + output wire logic ram_wr_cmd_lock, + output wire logic [3:0] ram_wr_cmd_cache, + output wire logic [2:0] ram_wr_cmd_prot, + output wire logic [3:0] ram_wr_cmd_qos, + output wire logic [3:0] ram_wr_cmd_region, + output wire logic [AUSER_W-1:0] ram_wr_cmd_auser, + output wire logic [DATA_W-1:0] ram_wr_cmd_data, + output wire logic [STRB_W-1:0] ram_wr_cmd_strb, + output wire logic [WUSER_W-1:0] ram_wr_cmd_user, + output wire logic ram_wr_cmd_en, + output wire logic ram_wr_cmd_last, + input wire logic ram_wr_cmd_ready +); + +// extract parameters +localparam logic AUSER_EN = s_axi_wr.AWUSER_EN; +localparam logic WUSER_EN = s_axi_wr.WUSER_EN; + +localparam VALID_ADDR_W = ADDR_W - $clog2(STRB_W); +localparam BYTE_LANES = STRB_W; +localparam BYTE_W = DATA_W/BYTE_LANES; + +// check configuration +if (BYTE_W * STRB_W != 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 (s_axi_wr.ADDR_W < ADDR_W) + $fatal(0, "Error: AXI address width is insufficient (instance %m)"); + +typedef enum logic [1:0] { + STATE_IDLE, + STATE_BURST, + STATE_RESP +} state_t; + +state_t state_reg = STATE_IDLE, state_next; + +logic [ID_W-1:0] write_id_reg = '0, write_id_next; +logic [ADDR_W-1:0] write_addr_reg = '0, write_addr_next; +logic write_lock_reg = 1'b0, write_lock_next; +logic [3:0] write_cache_reg = 4'd0, write_cache_next; +logic [2:0] write_prot_reg = 3'd0, write_prot_next; +logic [3:0] write_qos_reg = 4'd0, write_qos_next; +logic [3:0] write_region_reg = 4'd0, write_region_next; +logic [AUSER_W-1:0] write_auser_reg = '0, write_auser_next; +logic write_addr_valid_reg = 1'b0, write_addr_valid_next; +logic write_last_reg = 1'b0, write_last_next; +logic [7:0] write_count_reg = 8'd0, write_count_next; +logic [2:0] write_size_reg = 3'd0, write_size_next; +logic [1:0] write_burst_reg = 2'd0, write_burst_next; + +logic s_axi_awready_reg = 1'b0, s_axi_awready_next; +logic [ID_W-1:0] s_axi_bid_reg = '0, s_axi_bid_next; +logic s_axi_bvalid_reg = 1'b0, s_axi_bvalid_next; + +assign s_axi_wr.awready = s_axi_awready_reg; +assign s_axi_wr.wready = write_addr_valid_reg && ram_wr_cmd_ready; +assign s_axi_wr.bid = s_axi_bid_reg; +assign s_axi_wr.bresp = 2'b00; +assign s_axi_wr.buser = '0; +assign s_axi_wr.bvalid = s_axi_bvalid_reg; + +assign ram_wr_cmd_id = write_id_reg; +assign ram_wr_cmd_addr = write_addr_reg; +assign ram_wr_cmd_lock = write_lock_reg; +assign ram_wr_cmd_cache = write_cache_reg; +assign ram_wr_cmd_prot = write_prot_reg; +assign ram_wr_cmd_qos = write_qos_reg; +assign ram_wr_cmd_region = write_region_reg; +assign ram_wr_cmd_auser = AUSER_EN ? write_auser_reg : '0; +assign ram_wr_cmd_data = s_axi_wr.wdata; +assign ram_wr_cmd_strb = s_axi_wr.wstrb; +assign ram_wr_cmd_user = WUSER_EN ? s_axi_wr.wuser : '0; +assign ram_wr_cmd_en = write_addr_valid_reg && s_axi_wr.wvalid; +assign ram_wr_cmd_last = write_last_reg; + +always_comb begin + state_next = STATE_IDLE; + + write_id_next = write_id_reg; + write_addr_next = write_addr_reg; + write_lock_next = write_lock_reg; + write_cache_next = write_cache_reg; + write_prot_next = write_prot_reg; + write_qos_next = write_qos_reg; + write_region_next = write_region_reg; + write_auser_next = write_auser_reg; + write_addr_valid_next = write_addr_valid_reg; + write_last_next = write_last_reg; + write_count_next = write_count_reg; + write_size_next = write_size_reg; + write_burst_next = write_burst_reg; + + s_axi_awready_next = 1'b0; + s_axi_bid_next = s_axi_bid_reg; + s_axi_bvalid_next = s_axi_bvalid_reg && !s_axi_wr.bready; + + case (state_reg) + STATE_IDLE: begin + s_axi_awready_next = 1'b1; + + if (s_axi_wr.awready && s_axi_wr.awvalid) begin + write_id_next = s_axi_wr.awid; + write_addr_next = ADDR_W'(s_axi_wr.awaddr); + write_lock_next = s_axi_wr.awlock; + write_cache_next = s_axi_wr.awcache; + write_prot_next = s_axi_wr.awprot; + write_qos_next = s_axi_wr.awqos; + write_region_next = s_axi_wr.awregion; + write_auser_next = s_axi_wr.awuser; + write_count_next = s_axi_wr.awlen; + write_size_next = s_axi_wr.awsize <= 3'($clog2(STRB_W)) ? s_axi_wr.awsize : 3'($clog2(STRB_W)); + write_burst_next = s_axi_wr.awburst; + + write_addr_valid_next = 1'b1; + s_axi_awready_next = 1'b0; + if (s_axi_wr.awlen > 0) begin + write_last_next = 1'b0; + end else begin + write_last_next = 1'b1; + end + state_next = STATE_BURST; + end else begin + state_next = STATE_IDLE; + end + end + STATE_BURST: begin + if (s_axi_wr.wready && s_axi_wr.wvalid) begin + if (write_burst_reg != 2'b00) begin + write_addr_next = write_addr_reg + (1 << write_size_reg); + end + write_count_next = write_count_reg - 1; + write_last_next = write_count_next == 0; + if (write_count_reg > 0) begin + write_addr_valid_next = 1'b1; + state_next = STATE_BURST; + end else begin + write_addr_valid_next = 1'b0; + if (s_axi_wr.bready || !s_axi_wr.bvalid) begin + s_axi_bid_next = write_id_reg; + s_axi_bvalid_next = 1'b1; + s_axi_awready_next = 1'b1; + state_next = STATE_IDLE; + end else begin + state_next = STATE_RESP; + end + end + end else begin + state_next = STATE_BURST; + end + end + STATE_RESP: begin + if (s_axi_wr.bready || !s_axi_wr.bvalid) begin + s_axi_bid_next = write_id_reg; + s_axi_bvalid_next = 1'b1; + s_axi_awready_next = 1'b1; + state_next = STATE_IDLE; + end else begin + state_next = STATE_RESP; + end + end + default: begin + // unknown state + state_next = STATE_IDLE; + end + endcase +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + write_id_reg <= write_id_next; + write_addr_reg <= write_addr_next; + write_lock_reg <= write_lock_next; + write_cache_reg <= write_cache_next; + write_prot_reg <= write_prot_next; + write_qos_reg <= write_qos_next; + write_region_reg <= write_region_next; + write_auser_reg <= write_auser_next; + write_addr_valid_reg <= write_addr_valid_next; + write_last_reg <= write_last_next; + write_count_reg <= write_count_next; + write_size_reg <= write_size_next; + write_burst_reg <= write_burst_next; + + s_axi_awready_reg <= s_axi_awready_next; + s_axi_bid_reg <= s_axi_bid_next; + s_axi_bvalid_reg <= s_axi_bvalid_next; + + if (rst) begin + state_reg <= STATE_IDLE; + + write_addr_valid_reg <= 1'b0; + + s_axi_awready_reg <= 1'b0; + s_axi_bvalid_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/axi/tb/taxi_axi_dp_ram/Makefile b/src/axi/tb/taxi_axi_dp_ram/Makefile new file mode 100644 index 0000000..3916d76 --- /dev/null +++ b/src/axi/tb/taxi_axi_dp_ram/Makefile @@ -0,0 +1,57 @@ +# 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 = 1ps + +RTL_DIR = ../../rtl +LIB_DIR = ../../lib +TAXI_SRC_DIR = $(LIB_DIR)/taxi/src + +DUT = taxi_axi_dp_ram +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_DATA_W := 32 +export PARAM_ADDR_W := 16 +export PARAM_STRB_W := $(shell expr $(PARAM_DATA_W) / 8 ) +export PARAM_ID_W := 8 +export PARAM_A_PIPELINE_OUTPUT := 0 +export PARAM_B_PIPELINE_OUTPUT := 0 +export PARAM_A_INTERLEAVE := 0 +export PARAM_B_INTERLEAVE := 0 + +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 diff --git a/src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.py b/src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.py new file mode 100644 index 0000000..1bf4a74 --- /dev/null +++ b/src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.py @@ -0,0 +1,292 @@ +#!/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 random + +import cocotb_test.simulator +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiBus, AxiMaster + + +class TB(object): + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.a_clk, 8, units="ns").start()) + cocotb.start_soon(Clock(dut.b_clk, 10, units="ns").start()) + + self.axi_master = [] + + self.axi_master.append(AxiMaster(AxiBus.from_entity(dut.s_axi_a), dut.a_clk, dut.a_rst)) + self.axi_master.append(AxiMaster(AxiBus.from_entity(dut.s_axi_b), dut.b_clk, dut.b_rst)) + + def set_idle_generator(self, generator=None): + if generator: + for axi_master in self.axi_master: + axi_master.write_if.aw_channel.set_pause_generator(generator()) + axi_master.write_if.w_channel.set_pause_generator(generator()) + axi_master.read_if.ar_channel.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + for axi_master in self.axi_master: + axi_master.write_if.b_channel.set_pause_generator(generator()) + axi_master.read_if.r_channel.set_pause_generator(generator()) + + async def cycle_reset(self): + self.dut.a_rst.setimmediatevalue(0) + self.dut.b_rst.setimmediatevalue(0) + await RisingEdge(self.dut.a_clk) + await RisingEdge(self.dut.a_clk) + self.dut.a_rst.value = 1 + self.dut.b_rst.value = 1 + await RisingEdge(self.dut.a_clk) + await RisingEdge(self.dut.a_clk) + self.dut.a_rst.value = 0 + await RisingEdge(self.dut.b_clk) + self.dut.b_rst.value = 0 + await RisingEdge(self.dut.a_clk) + await RisingEdge(self.dut.a_clk) + + +async def run_test_write(dut, port=0, data_in=None, idle_inserter=None, backpressure_inserter=None, size=None): + + tb = TB(dut) + + axi_master = tb.axi_master[port] + byte_lanes = axi_master.write_if.byte_lanes + max_burst_size = axi_master.write_if.max_burst_size + + if size is None: + size = max_burst_size + + await tb.cycle_reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + for length in list(range(1, byte_lanes*2))+[1024]: + for offset in list(range(byte_lanes, byte_lanes*2))+list(range(4096-byte_lanes, 4096)): + tb.log.info("length %d, offset %d, size %d", length, offset, size) + addr = offset+0x1000 + test_data = bytearray([x % 256 for x in range(length)]) + + await axi_master.write(addr-4, b'\xaa'*(length+8)) + + await axi_master.write(addr, test_data, size=size) + + data = await axi_master.read(addr-1, length+2) + + assert data.data == b'\xaa'+test_data+b'\xaa' + + await RisingEdge(dut.a_clk) + await RisingEdge(dut.a_clk) + + +async def run_test_read(dut, port=0, data_in=None, idle_inserter=None, backpressure_inserter=None, size=None): + + tb = TB(dut) + + axi_master = tb.axi_master[port] + byte_lanes = axi_master.write_if.byte_lanes + max_burst_size = axi_master.write_if.max_burst_size + + if size is None: + size = max_burst_size + + await tb.cycle_reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + for length in list(range(1, byte_lanes*2))+[1024]: + for offset in list(range(byte_lanes, byte_lanes*2))+list(range(4096-byte_lanes, 4096)): + tb.log.info("length %d, offset %d, size %d", length, offset, size) + addr = offset+0x1000 + test_data = bytearray([x % 256 for x in range(length)]) + + await axi_master.write(addr, test_data) + + data = await axi_master.read(addr, length, size=size) + + assert data.data == test_data + + await RisingEdge(dut.a_clk) + await RisingEdge(dut.a_clk) + + +async def run_test_arb(dut, data_in=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + await tb.cycle_reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + async def worker(master, offset): + wr_op = master.init_write(offset, b'\x11\x22\x33\x44') + rd_op = master.init_read(offset, 4) + + await wr_op.wait() + await rd_op.wait() + + workers = [] + + for k in range(10): + workers.append(cocotb.start_soon(worker(tb.axi_master[0], k*256))) + workers.append(cocotb.start_soon(worker(tb.axi_master[1], k*256))) + + while workers: + await workers.pop(0).join() + + await RisingEdge(dut.a_clk) + await RisingEdge(dut.a_clk) + + +async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + await tb.cycle_reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + async def worker(master, offset, aperture, count=16): + for k in range(count): + length = random.randint(1, min(512, aperture)) + addr = offset+random.randint(0, aperture-length) + test_data = bytearray([x % 256 for x in range(length)]) + + await Timer(random.randint(1, 100), 'ns') + + await master.write(addr, test_data) + + await Timer(random.randint(1, 100), 'ns') + + data = await master.read(addr, length) + assert data.data == test_data + + workers = [] + + for k in range(16): + workers.append(cocotb.start_soon(worker(tb.axi_master[k % len(tb.axi_master)], k*0x1000, 0x1000, count=16))) + + while workers: + await workers.pop(0).join() + + await RisingEdge(dut.a_clk) + await RisingEdge(dut.a_clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +if getattr(cocotb, 'top', None) is not None: + + data_width = len(cocotb.top.s_axi_a.wdata) + byte_lanes = data_width // 8 + max_burst_size = (byte_lanes-1).bit_length() + + for test in [run_test_write, run_test_read]: + + factory = TestFactory(test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.add_option("size", [None]+list(range(max_burst_size))) + factory.add_option("port", [0, 1]) + factory.generate_tests() + + factory = TestFactory(run_test_arb) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_stress_test) + factory.add_option("idle_inserter", [None, cycle_pause]) + 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()) + + +@pytest.mark.parametrize("data_w", [8, 16, 32]) +def test_taxi_axi_dp_ram(request, data_w): + dut = "taxi_axi_dp_ram" + 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['DATA_W'] = data_w + parameters['ADDR_W'] = 16 + parameters['STRB_W'] = parameters['DATA_W'] // 8 + parameters['ID_W'] = 8 + parameters['A_PIPELINE_OUTPUT'] = 0 + parameters['B_PIPELINE_OUTPUT'] = 0 + parameters['A_INTERLEAVE'] = 0 + parameters['B_INTERLEAVE'] = 0 + + 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, + ) diff --git a/src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.sv b/src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.sv new file mode 100644 index 0000000..68dcbe6 --- /dev/null +++ b/src/axi/tb/taxi_axi_dp_ram/test_taxi_axi_dp_ram.sv @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 dual-port RAM testbench + */ +module test_taxi_axi_dp_ram # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 32, + parameter ADDR_W = 16, + parameter STRB_W = (DATA_W/8), + parameter ID_W = 8, + parameter logic A_PIPELINE_OUTPUT = 1'b0, + parameter logic B_PIPELINE_OUTPUT = 1'b0, + parameter logic A_INTERLEAVE = 1'b0, + parameter logic B_INTERLEAVE = 1'b0 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +logic a_clk; +logic a_rst; +logic b_clk; +logic b_rst; + +taxi_axi_if #( + .DATA_W(DATA_W), + .ADDR_W(ADDR_W+16), + .STRB_W(STRB_W), + .ID_W(ID_W) +) s_axi_a(), s_axi_b(); + +taxi_axi_dp_ram #( + .ADDR_W(ADDR_W), + .A_PIPELINE_OUTPUT(A_PIPELINE_OUTPUT), + .B_PIPELINE_OUTPUT(B_PIPELINE_OUTPUT), + .A_INTERLEAVE(A_INTERLEAVE), + .B_INTERLEAVE(B_INTERLEAVE) +) +uut ( + /* + * Port A + */ + .a_clk(a_clk), + .a_rst(a_rst), + .s_axi_wr_a(s_axi_a), + .s_axi_rd_a(s_axi_a), + + /* + * Port B + */ + .b_clk(b_clk), + .b_rst(b_rst), + .s_axi_wr_b(s_axi_b), + .s_axi_rd_b(s_axi_b) +); + +endmodule + +`resetall