fpga: lib: create new URAM and large AXI FIFO

This commit is contained in:
Max Köhler 2026-04-24 21:12:31 +02:00 committed by Martin Braun
parent 3d93412dec
commit 2eb2ac20d3
9 changed files with 1026 additions and 36 deletions

View File

@ -18,6 +18,8 @@ axi_fifo64_to_fifo32.v \
axi_fifo32_to_fifo16.v \
axi_fifo16_to_fifo32.v \
axi_fifo_bram.v \
axi_fifo_uram.sv \
axi_fifo_large.sv \
axi_fifo_cascade.v \
axi_fifo_flop2.v \
axi_fifo_flop.v \

View File

@ -0,0 +1,242 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axi_fifo_large
//
// Description:
//
// Buffers incoming AXI-Stream data using a cascade of FIFOs whose depth is
// controlled by the DEPTH parameter (number of words). For small depths the
// module instantiates a single axi_fifo with input and output flop2 stages.
// On UltraScale devices, when the requested depth exceeds a threshold,
// URAM-based storage is used for the bulk of the capacity with an BRAM
// cascade for the remaining depth. On other devices a pure BRAM cascade is
// generated. A register-based pipeline flop is placed at the input and output
// to break timing paths.
//
// Parameters:
//
// WIDTH : Data bus width in bits.
// DEPTH : FIFO depth in number of words. Actual depth is rounded
// up to the next multiple of 512 (min BRAM depth).
// DEVICE : Target device string ("ULTRASCALE", "7SERIES",
// "VIRTEX6", "SPARTAN6").
// MAX_NUM_URAM_BLOCKS: Maximum number of URAM primitives to use.
// Set to -1 (default) for no limit.
//
module axi_fifo_large #(
int WIDTH = 64,
int DEPTH = 4096,
string DEVICE = "7SERIES",
int MAX_NUM_URAM_BLOCKS = -1
) (
input logic clk,
input logic reset,
input logic clear,
input logic [WIDTH-1:0] i_tdata,
input logic i_tvalid,
output logic i_tready,
output logic [WIDTH-1:0] o_tdata,
output logic o_tvalid,
input logic o_tready,
output logic [19:0] space,
output logic [19:0] occupied
);
// ---------------------------------------------------------------------------
// Parameter checks
// ---------------------------------------------------------------------------
// DEPTH limited by space and occupied output width
if (DEPTH >= 2**20) begin
$error("axi_fifo_large: DEPTH (%0d) cannot exceed 20 bits of address space.", DEPTH);
end
// ---------------------------------------------------------------------------
// Size threshold for single FIFO vs cascade.
// ---------------------------------------------------------------------------
localparam int SIZE_THRESHOLD = (
(DEVICE == "ULTRASCALE") ? 12 : // Set to 12 to allow a single URAM block
(DEVICE == "7SERIES") ? 14 : (
(DEVICE == "VIRTEX6") ? 14 : (
(DEVICE == "SPARTAN6") ? 12 : (
12
))));
// create a single FIFO instance with input and output flop2 stage
if (DEPTH <= 2**SIZE_THRESHOLD) begin
axi_fifo_cascade #(.WIDTH(WIDTH), .SIZE($clog2(DEPTH))) single_fifo (
.clk(clk), .reset(reset), .clear(clear),
.i_tdata(i_tdata), .i_tvalid(i_tvalid), .i_tready(i_tready),
.o_tdata(o_tdata), .o_tvalid(o_tvalid), .o_tready(o_tready),
.space(space[15:0]), .occupied(occupied[15:0])
);
assign space[19:16] = '0;
assign occupied[19:16] = '0;
// create a FIFO cascade
end else begin
// -------------------------------------------------------------------------
// Register based 2 element FIFO to break timing paths.
// -------------------------------------------------------------------------
logic [WIDTH-1:0] i_tdata_pre;
logic i_tvalid_pre, i_tready_pre;
axi_fifo_flop2 #(.WIDTH(WIDTH)) pre_fifo (
.clk(clk), .reset(reset), .clear(clear),
.i_tdata(i_tdata), .i_tvalid(i_tvalid), .i_tready(i_tready),
.o_tdata(i_tdata_pre), .o_tvalid(i_tvalid_pre), .o_tready(i_tready_pre),
.space(), .occupied()
);
// -------------------------------------------------------------------------
// URAM stage
// -------------------------------------------------------------------------
// Static information about URAM primitives
localparam int URAM_BLOCK_WIDTH = 72; // Number of bits storable in a single URAM block
localparam int URAM_BLOCK_DEPTH = 4096; // Number of words storable in a single URAM block
// Number of URAM primitives needed per storage line (72 bits wide each).
localparam int URAM_PER_LINE = (WIDTH + URAM_BLOCK_WIDTH - 1) / URAM_BLOCK_WIDTH;
// Determine whether to use URAMs at all. URAMs are only available on
// ULTRASCALE, and we need at least URAM_PER_LINE blocks to store a
// single line. When MAX_NUM_URAM_BLOCKS is positive but too small to
// fit even one line, we skip the URAM path entirely and fall through
// to the BRAM cascade.
localparam bit USE_URAM = (DEVICE == "ULTRASCALE") &&
(MAX_NUM_URAM_BLOCKS < 0 || MAX_NUM_URAM_BLOCKS >= URAM_PER_LINE);
// Compute how much of the FIFO depth can be covered by URAMs
// This will always return at least 1 as smaller FIFOs are handled by the single FIFO case above.
localparam int URAM_COUNT_VAL1 = DEPTH / URAM_BLOCK_DEPTH;
// Cap the URAM depth by the maximum number of URAM blocks if specified.
localparam int URAM_COUNT_VAL2 = (MAX_NUM_URAM_BLOCKS < 0) ? URAM_COUNT_VAL1 : (MAX_NUM_URAM_BLOCKS / URAM_PER_LINE);
// return the minimum of the two URAM depth calculations to determine the actual URAM depth to use.
localparam int URAM_COUNT = (URAM_COUNT_VAL1 < URAM_COUNT_VAL2) ? URAM_COUNT_VAL1 : URAM_COUNT_VAL2;
localparam int URAM_DEPTH = URAM_COUNT * URAM_BLOCK_DEPTH;
// --- URAM stage ---
logic [WIDTH-1:0] uram_out_tdata;
logic uram_out_tvalid, uram_out_tready;
if (USE_URAM) begin : gen_uram_fifo
axi_fifo_uram #(.WIDTH(WIDTH), .DEPTH(URAM_DEPTH)) uram_fifo (
.clk(clk), .reset(reset), .clear(clear),
.i_tdata(i_tdata_pre),.i_tvalid(i_tvalid_pre), .i_tready(i_tready_pre),
.o_tdata(uram_out_tdata), .o_tvalid(uram_out_tvalid), .o_tready(uram_out_tready),
.space(), .occupied()
);
end else begin
// If not using URAM, connect the URAM interface signals to the pre_fifo output.
assign uram_out_tdata = i_tdata_pre;
assign uram_out_tvalid = i_tvalid_pre;
assign i_tready_pre = uram_out_tready;
end
// -------------------------------------------------------------------------
// BRAM stage
// -------------------------------------------------------------------------
// BRAM stages in the URAM path reuse the DEPTH of 13 as with a maximum BRAM
// size of 1024 words the default max cascade height of 8 is reached at a
// size of 2**13.
localparam int BRAM_MAX_SIZE = (DEVICE == "ULTRASCALE") ? 13 : SIZE_THRESHOLD;
localparam int BRAM_MAX_DEPTH = 2**BRAM_MAX_SIZE;
// Creating a BRAM instance will at least use 512 words for the supported architectures
localparam int BRAM_MIN_SIZE = 9;
localparam int BRAM_MIN_DEPTH = 2**BRAM_MIN_SIZE;
// calculate remaining depth after URAM stage
localparam int REMAINING_DEPTH = (USE_URAM) ? (DEPTH - URAM_DEPTH) : DEPTH;
// calculate the number of BRAM stages needed to cover the remaining depth
// determine depth rounded up to the next multiple of the minimum BRAM depth
localparam int REMAINING_DEPTH_CEIL = REMAINING_DEPTH + BRAM_MIN_DEPTH - 1;
localparam int BRAM_DEPTH = (REMAINING_DEPTH_CEIL / BRAM_MIN_DEPTH) * BRAM_MIN_DEPTH;
// determine the number of max size BRAM stages
localparam int NUM_MAX_BRAM = REMAINING_DEPTH_CEIL / BRAM_MAX_DEPTH;
// generate the BRAM stages for the remaining depth
localparam int BRAM_COUNT = NUM_MAX_BRAM + BRAM_MAX_SIZE - BRAM_MIN_SIZE;
logic [WIDTH-1:0] c_tdata [BRAM_COUNT:0];
logic c_tvalid [BRAM_COUNT:0], c_tready [BRAM_COUNT:0];
// connect the URAM output to the first BRAM stage (or to the output if no BRAM stages)
assign c_tdata[0] = uram_out_tdata;
assign c_tvalid[0] = uram_out_tvalid;
assign uram_out_tready = c_tready[0];
// generate the max sized BRAM stages
for (genvar j = 0; j < NUM_MAX_BRAM; j++) begin
axi_fifo #(.WIDTH(WIDTH), .SIZE(BRAM_MAX_SIZE)) max_size_fifo (
.clk(clk), .reset(reset), .clear(clear),
.i_tdata(c_tdata[j]), .i_tvalid(c_tvalid[j]), .i_tready(c_tready[j]),
.o_tdata(c_tdata[j+1]), .o_tvalid(c_tvalid[j+1]), .o_tready(c_tready[j+1]),
.space(), .occupied()
);
end
// generate the smaller BRAM stages if needed (use SIZE of -1 to generate a bypass)
localparam int NUM_SMALL_BRAM = BRAM_MAX_SIZE - BRAM_MIN_SIZE;
// use bits in REMAINING_DEPTH_CEIL to determine which FIFOs to use and
// which to bypass
localparam logic [NUM_SMALL_BRAM-1:0] BRAM_STAGE_SELECT = REMAINING_DEPTH_CEIL[BRAM_MAX_SIZE-1:BRAM_MIN_SIZE];
for (genvar k = 0; k < NUM_SMALL_BRAM; k++) begin
localparam int CURRENT_SIZE = BRAM_MAX_SIZE - k - 1;
localparam int BRAM_SIZE = (BRAM_STAGE_SELECT[NUM_SMALL_BRAM - k - 1]) ? CURRENT_SIZE : -1;
localparam int IDX = NUM_MAX_BRAM + k;
axi_fifo #(.WIDTH(WIDTH), .SIZE(BRAM_SIZE)) smaller_fifo (
.clk(clk), .reset(reset), .clear(clear),
.i_tdata(c_tdata[IDX]), .i_tvalid(c_tvalid[IDX]), .i_tready(c_tready[IDX]),
.o_tdata(c_tdata[IDX + 1]), .o_tvalid(c_tvalid[IDX + 1]), .o_tready(c_tready[IDX + 1]),
.space(), .occupied()
);
end
// -------------------------------------------------------------------------
// Flop 2 output stage
// -------------------------------------------------------------------------
axi_fifo_flop2 #(.WIDTH(WIDTH)) post_fifo (
.clk(clk), .reset(reset), .clear(clear),
.i_tdata(c_tdata[BRAM_COUNT]), .i_tvalid(c_tvalid[BRAM_COUNT]), .i_tready(c_tready[BRAM_COUNT]),
.o_tdata(o_tdata), .o_tvalid(o_tvalid), .o_tready(o_tready),
.space(), .occupied()
);
// -------------------------------------------------------------------------
// Space and occupied (diagnostics, not exact)
// -------------------------------------------------------------------------
localparam int TOTAL_DEPTH = (USE_URAM ? URAM_DEPTH : 0) + BRAM_DEPTH;
logic read, write;
always_comb begin
write = i_tvalid & i_tready;
read = o_tvalid & o_tready;
end
always_ff @(posedge clk) begin
if (reset | clear)
space <= TOTAL_DEPTH;
else if (read & ~write)
space <= space + 1'd1;
else if (write & ~read)
space <= space - 1'd1;
end
always_ff @(posedge clk) begin
if (reset | clear)
occupied <= '0;
else if (read & ~write)
occupied <= occupied - 1'd1;
else if (write & ~read)
occupied <= occupied + 1'd1;
end
end
endmodule

View File

@ -0,0 +1,253 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axi_fifo_uram
//
// Description:
//
// UltraRAM-based AXI-Stream FIFO for Xilinx UltraScale+ devices.
// Since UltraRAM requires multiple pipeline stages on the read path,
// an axi_fifo_short is placed on the output to buffer data and
// provide standard AXI-Stream backpressure. Reads from the UltraRAM
// are only issued when the output FIFO has enough space to absorb all
// in-flight data through the pipeline.
//
// Simple Dual Port: Port A writes, Port B reads. Follows Xilinx UG901
// coding style for UltraRAM pipeline inference.
//
// Parameters:
//
// WIDTH : Data width in bits. UltraRAM native width is 72 bits; Vivado
// handles wider or narrower widths automatically.
// DEPTH : FIFO depth (number of entries). Native UltraRAM depth is
// 4096 entries. The actual depth is rounded up to next multiple of
// 4096.
//
module axi_fifo_uram #(
int WIDTH = 72,
int DEPTH = 4096
) (
input logic clk,
input logic reset,
input logic clear,
input logic [WIDTH-1:0] i_tdata,
input logic i_tvalid,
output logic i_tready,
output logic [WIDTH-1:0] o_tdata,
output logic o_tvalid,
input logic o_tready,
output logic [15:0] space,
output logic [15:0] occupied
);
// Number of pipeline registers absorbable into the URAM cascade.
// Each URAM block in the cascade contributes one pipeline stage.
// cascade_depth = 2^max(0, SIZE-12), with a minimum of 1.
localparam int URAM_BLOCK_DEPTH = 4096;
localparam int CASCADE_DEPTH = (DEPTH + URAM_BLOCK_DEPTH - 1) / URAM_BLOCK_DEPTH;
localparam int ACTUAL_DEPTH = CASCADE_DEPTH * URAM_BLOCK_DEPTH;
localparam int SIZE = $clog2(ACTUAL_DEPTH);
// Total read latency through UltraRAM pipeline:
// 1 (memreg) + NBPIPE (mem_pipe_reg stages) + 1 (dout)
// = NBPIPE + 2 clock cycles
localparam int NBPIPE = CASCADE_DEPTH;
localparam int READ_LATENCY = NBPIPE + 2;
if (READ_LATENCY >= 32) begin
$error("axi_fifo_uram: READ_LATENCY (%0d) must be less than 32 to fit in space check.", READ_LATENCY);
end
//-------------------------------------------------
// Write / Read address pointers
//-------------------------------------------------
// Extra MSB for full/empty disambiguation
logic unsigned [SIZE:0] wr_addr, rd_addr;
logic unsigned [SIZE-1:0] wr_ptr, rd_ptr;
always_comb begin
wr_ptr = wr_addr[SIZE-1:0];
rd_ptr = rd_addr[SIZE-1:0];
end
logic empty, full, write, read;
always_comb begin
empty = (wr_addr == rd_addr);
full = (wr_addr[SIZE] != rd_addr[SIZE]) && (wr_ptr == rd_ptr);
write = i_tvalid & i_tready;
read = o_tready & o_tvalid;
end
assign i_tready = ~full;
// Issue a URAM read when data is available and the output FIFO has
// enough free space to absorb all in-flight data in the read pipeline.
logic [5:0] out_space;
logic can_read;
always_comb begin
can_read = ~empty & (out_space > READ_LATENCY[5:0]);
end
always_ff @(posedge clk) begin
if (reset | clear) begin
wr_addr <= '0;
end else if (write) begin
wr_addr <= wr_addr + 1'b1;
if (wr_ptr == ACTUAL_DEPTH - 1) begin
wr_addr <= '0;
wr_addr[SIZE] <= ~wr_addr[SIZE];
end
end
end
always_ff @(posedge clk) begin
if (reset | clear) begin
rd_addr <= '0;
end else if (can_read) begin
rd_addr <= rd_addr + 1'b1;
if (rd_ptr == ACTUAL_DEPTH - 1) begin
rd_addr <= '0;
rd_addr[SIZE] <= ~rd_addr[SIZE];
end
end
end
//-------------------------------------------------
// UltraRAM with pipelined read output
//
// Coding style matches the Xilinx UG901 SDP UltraRAM template exactly
// so that Vivado can absorb mem_pipe_reg[] and memreg into the URAM
// primitive's built-in pipeline registers (OREG). Key requirements:
// - Single always block for read+write under a common mem_en
// - No reset on the enable pipeline or data pipeline registers
// - Pipeline enables derived from mem_en (maps to OREG_CE)
//-------------------------------------------------
logic mem_en;
always_comb begin
mem_en = write | can_read;
end
// the code below this line follows the UG901 template for SDP UltraRAM
// -------------- start of template code --------------
(* ram_style = "ultra" *)
logic [WIDTH-1:0] mem [ACTUAL_DEPTH-1:0];
logic [WIDTH-1:0] memreg;
logic [WIDTH-1:0] mem_pipe_reg [NBPIPE-1:0];
logic mem_en_pipe_reg [NBPIPE:0];
logic [WIDTH-1:0] dout;
// RAM : Simple Dual Port - Port A writes, Port B reads
// Both under common mem_en to match URAM inference template
always_ff @(posedge clk) begin
if (mem_en) begin
if (write) begin
mem[wr_ptr] <= i_tdata;
end
memreg <= mem[rd_ptr];
end
end
// The enable of the RAM goes through a pipeline to produce a
// series of pipelined enable signals required to control the data
// pipeline. No reset - must match template for URAM register absorption.
always_ff @(posedge clk) begin
mem_en_pipe_reg[0] <= mem_en;
for (int i = 0; i < NBPIPE; i = i + 1) begin
mem_en_pipe_reg[i+1] <= mem_en_pipe_reg[i];
end
end
// RAM output data goes through a pipeline.
always_ff @(posedge clk) begin
if (mem_en_pipe_reg[0]) begin
mem_pipe_reg[0] <= memreg;
end
for (int i = 0; i < NBPIPE-1; i = i + 1) begin
if (mem_en_pipe_reg[i+1]) begin
mem_pipe_reg[i+1] <= mem_pipe_reg[i];
end
end
if (mem_en_pipe_reg[NBPIPE]) begin
dout <= mem_pipe_reg[NBPIPE-1];
end
end
// -------------- end of template code --------------
// Read-validity pipeline: tracks which pipeline slots contain actual
// new read data vs stale data from write-only mem_en cycles. This is
// separate from mem_en_pipe_reg to avoid interfering with URAM absorption.
logic [NBPIPE+1:0] rd_valid_pipe;
always_ff @(posedge clk) begin
if (reset | clear)
rd_valid_pipe <= '0;
else
rd_valid_pipe <= {rd_valid_pipe[NBPIPE:0], can_read};
end
logic dout_valid;
always_comb begin
dout_valid = rd_valid_pipe[NBPIPE+1];
end
//-------------------------------------------------
// Output FIFO
//
// An axi_fifo_short (32 entries) buffers the pipelined
// URAM output and provides AXI-Stream backpressure.
// Overflow is prevented by the can_read space check.
//-------------------------------------------------
axi_fifo_short #(.WIDTH(WIDTH)) out_fifo (
.clk (clk),
.reset (reset),
.clear (clear),
.i_tdata (dout),
.i_tvalid (dout_valid),
.i_tready (),
.o_tdata (o_tdata),
.o_tvalid (o_tvalid),
.o_tready (o_tready),
.space (out_space),
.occupied ()
);
//-------------------------------------------------
// Space and occupied (diagnostics, not exact)
//-------------------------------------------------
// Based on the read pattern the short FIFO may not be used to full capacity.
// The overall useable space in all conditions is the URAM depth and the short
// FIFO depth combined minus the number of elements needed for compensating
// the read latency pipeline. When the short FIFO is filled to a level where
// can_read deasserts, no more reads will be issued even though the elements
// in the short FIFO may be free.
localparam logic [15:0] NUMLINES = CASCADE_DEPTH * URAM_BLOCK_DEPTH + 32 - READ_LATENCY;
always_ff @(posedge clk) begin
if (reset | clear)
space <= NUMLINES;
else if (read & ~write)
space <= space + 1'd1;
else if (write & ~read)
space <= space - 1'd1;
end
always_ff @(posedge clk) begin
if (reset | clear)
occupied <= '0;
else if (read & ~write)
occupied <= occupied - 1'd1;
else if (write & ~read)
occupied <= occupied + 1'd1;
end
endmodule // axi_fifo_uram

View File

@ -1,29 +1,39 @@
#
# Copyright 2016 Ettus Research
# Copyright 2026 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: LGPL-3.0-or-later
#
#-------------------------------------------------
# Top-of-Makefile
#-------------------------------------------------
# Define BASE_DIR to point to the "top" dir
BASE_DIR = $(abspath ../../../../top)
BASE_DIR = $(abspath ../../../top)
# Include viv_sim_preamble after defining BASE_DIR
include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak
#-------------------------------------------------
# Design Specific
#-------------------------------------------------
# Include makefiles and sources for the DUT and its dependencies
include $(BASE_DIR)/../lib/control/Makefile.srcs
include $(BASE_DIR)/../lib/fifo/Makefile.srcs
DESIGN_SRCS = $(abspath \
$(CONTROL_LIB_SRCS) \
$(FIFO_SRCS) \
)
#-------------------------------------------------
# Testbench Specific
#-------------------------------------------------
# Define only one toplevel module
SIM_TOP = axi_fifo_tb
SIM_TOP = axi_fifo_tb_all
SIM_SRCS = $(abspath \
axi_fifo_tb.sv \
$(LIB_DIR)/fifo/axi_fifo.v \
$(LIB_DIR)/fifo/axi_fifo_flop.v \
$(LIB_DIR)/fifo/axi_fifo_flop2.v \
$(LIB_DIR)/fifo/axi_fifo_short.v \
$(LIB_DIR)/fifo/axi_fifo_bram.v \
)
SIM_SRCS = \
$(abspath axi_fifo/axi_fifo_tb.sv) \
$(abspath axi_fifo_uram/axi_fifo_uram_tb.sv) \
$(abspath axi_fifo_large/axi_fifo_large_tb.sv) \
$(abspath axi_fifo_tb_all.sv) \
#-------------------------------------------------
# Bottom-of-Makefile

View File

@ -4,23 +4,23 @@
`timescale 1ns/1ps
`define SIM_TIMEOUT_US 1000
`define NS_PER_TICK 1
`define NUM_TEST_CASES 4
`include "sim_exec_report.vh"
`include "sim_clks_rsts.vh"
`include "test_exec.svh"
`include "sim_cvita_lib.svh"
`include "sim_axis_lib.svh"
`include "sim_set_rb_lib.svh"
module axi_fifo_tb();
import PkgTestExec::*;
/*********************************************
** Setup Testbench
*********************************************/
`TEST_BENCH_INIT("axi_fifo_tb",`NUM_TEST_CASES,`NS_PER_TICK);
localparam CLK_PERIOD = $ceil(1e9/166.67e6);
`DEFINE_CLK(clk, CLK_PERIOD, 50);
`DEFINE_RESET(reset, 0, 100);
localparam CLK_PERIOD = 10; // 100 MHz
bit clk, reset;
sim_clock_gen #(.PERIOD(CLK_PERIOD), .AUTOSTART(0))
clk_gen (.clk(clk), .rst(reset));
// 4 variants: axi_fifo_flop, axi_fifo_flop2, axi_fifo_short, axi_fifo_bram
localparam NUM_FIFOS = 4;
@ -60,52 +60,63 @@ module axi_fifo_tb();
initial begin
clear = 'd0;
// Stop all clock events for simulation performance until setup is done.
clk_gen.kill();
test.start_tb("axi_fifo_tb", `SIM_TIMEOUT_US * 1us);
clk_gen.revive();
// Drive reset sequence.
clk_gen.start();
clk_gen.reset(20);
/********************************************************
** Test 1 -- Reset
********************************************************/
`TEST_CASE_START("Wait for Reset");
test.start_test("Wait for Reset");
m_axis.reset();
s_axis.reset();
while (reset) @(posedge clk);
`TEST_CASE_DONE(~reset);
@(negedge reset);
test.end_test(~reset);
/********************************************************
** Test 2 -- Check filling FIFOs
********************************************************/
`TEST_CASE_START("Check filling FIFOs");
test.start_test("Check filling FIFOs");
for (int i = 0; i < NUM_FIFOS; i++) begin
$display("Testing FIFO %0d, SIZE %0d",i,2**FIFO_SIZES[i]);
for (int k = 0; k < 2**FIFO_SIZES[i]; k++) begin
$sformat(s,"FIFO size should be %0d entries, but detected %0d!",2**FIFO_SIZES[i],k);
`ASSERT_FATAL(m_axis.axis.tready[i],s);
test.assert_fatal(m_axis.axis.tready[i], s);
m_axis.push_word(k,0,i);
end
$sformat(s,"FIFO depth appears to be greater than %0d entries! Might be due to output registering.",2**FIFO_SIZES[i]);
`ASSERT_WARN(~m_axis.axis.tready[i],s);
test.assert_warning(~m_axis.axis.tready[i], s);
end
`TEST_CASE_DONE(1);
test.end_test(1);
/********************************************************
** Test 3 -- Check emptying FIFOs
********************************************************/
`TEST_CASE_START("Check emptying FIFOs");
test.start_test("Check emptying FIFOs");
for (int i = 0; i < NUM_FIFOS; i++) begin
$display("Testing FIFO %0d, SIZE %0d",i,2**FIFO_SIZES[i]);
for (int k = 0; k < 2**FIFO_SIZES[i]; k = k + 1) begin
$sformat(s,"FIFO prematurely empty! Occured after %0d reads!",k);
`ASSERT_FATAL(s_axis.axis.tvalid[i],s);
test.assert_fatal(s_axis.axis.tvalid[i], s);
s_axis.pull_word(read_word,last,i);
$sformat(s,"Read invalid FIFO word! Expected: %0d, Actual: %0d",k,read_word);
`ASSERT_FATAL(read_word == k,s);
test.assert_fatal(read_word == k, s);
end
`ASSERT_FATAL(~s_axis.axis.tvalid[i],"FIFO not empty after reading all entries!");
test.assert_fatal(~s_axis.axis.tvalid[i], "FIFO not empty after reading all entries!");
end
`TEST_CASE_DONE(1);
test.end_test(1);
/********************************************************
** Test 4 -- Randomized Write / Read Timing
********************************************************/
`TEST_CASE_START("Randomized Write / Read");
test.start_test("Randomized Write / Read");
for (int i = 0; i < NUM_FIFOS; i++) begin
$display("Testing FIFO %0d, SIZE %0d",i,2**FIFO_SIZES[i]);
fork
@ -122,13 +133,14 @@ module axi_fifo_tb();
while ($signed($random()) > 0) @(posedge clk);
s_axis.pull_word(read_word,last,i);
$sformat(s,"Read invalid FIFO word! Expected: %0d, Actual: %0d",read_word,k);
`ASSERT_FATAL(read_word == k,s);
test.assert_fatal(read_word == k, s);
end
end
join
end
`TEST_CASE_DONE(1);
`TEST_BENCH_DONE;
test.end_test();
test.end_tb(0);
clk_gen.kill();
end
endmodule

View File

@ -0,0 +1,192 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axi_fifo_large_tb
//
// Description:
//
// Testbench for axi_fifo_large.
//
// Ensures correct flow of data and accurate reporting of space/occupied
// counts at start and end of test. Will not fill up the FIFO since this would
// be very time consuming for larger sizes.
//
`default_nettype none
module axi_fifo_large_tb #(
int WIDTH = 72,
int DEPTH = 4096,
string DEVICE = "ULTRASCALE",
int MAX_NUM_URAM_BLOCKS = -1
);
`include "test_exec.svh"
import PkgTestExec::*;
import PkgAxiStreamBfm::*;
import PkgRandom::*;
//---------------------------------------------------------------------------
// Local Parameters
//---------------------------------------------------------------------------
localparam real CLK_PERIOD = 10.0; // 100 MHz
localparam time TIMEOUT = 50ms;
//---------------------------------------------------------------------------
// Clocks and Resets
//---------------------------------------------------------------------------
bit clk, rst;
sim_clock_gen #(.PERIOD(CLK_PERIOD), .AUTOSTART(0))
clk_gen (.clk(clk), .rst(rst));
//---------------------------------------------------------------------------
// AXI-Stream Interfaces and BFM
//---------------------------------------------------------------------------
AxiStreamIf #(.DATA_WIDTH(WIDTH), .TKEEP(0), .TUSER(0)) i_axis (.clk(clk), .rst(rst));
AxiStreamIf #(.DATA_WIDTH(WIDTH), .TKEEP(0), .TUSER(0)) o_axis (.clk(clk), .rst(rst));
AxiStreamBfm #(.DATA_WIDTH(WIDTH), .TKEEP(0), .TUSER(0)) axis_bfm;
//---------------------------------------------------------------------------
// DUT
//---------------------------------------------------------------------------
wire [19:0] space;
wire [19:0] occupied;
// For the single-FIFO path (DEPTH <= 4096) the DUT instantiates axi_fifo whose
// capacity is the next power of 2 >= DEPTH, plus 1 for the output register.
// For the cascade path (DEPTH > 4096) capacity is rounded up to the next
// multiple of 512 (BRAM_MIN_DEPTH).
localparam int EXPECTED_SPACE = (DEPTH <= 4096) ?
(1 << $clog2(DEPTH)) + 1 :
((DEPTH + 511) / 512) * 512;
axi_fifo_large #(
.WIDTH (WIDTH),
.DEPTH (DEPTH),
.DEVICE (DEVICE),
.MAX_NUM_URAM_BLOCKS (MAX_NUM_URAM_BLOCKS)
) dut (
.clk (clk),
.reset (rst),
.clear (1'b0),
.i_tdata (i_axis.tdata),
.i_tvalid (i_axis.tvalid),
.i_tready (i_axis.tready),
.o_tdata (o_axis.tdata),
.o_tvalid (o_axis.tvalid),
.o_tready (o_axis.tready),
.space (space),
.occupied (occupied)
);
// The FIFO has no tlast port. Tie o_axis.tlast high so the BFM slave
// treats every received word as a complete single-word packet.
assign o_axis.tlast = 1'b1;
//---------------------------------------------------------------------------
// Helper Tasks
//---------------------------------------------------------------------------
typedef AxiStreamPacket #(WIDTH, 1) AxisPacket_t;
// Total number of words to send per test
// Just a few to test the functionality. Assume each FIFO in there is working
// fine in itself.
localparam int NUM_WORDS = 100;
// Send NUM_WORDS random single-word packets and verify received data matches.
task automatic run_traffic_test(string test_name, int master_stall, int slave_stall);
AxisPacket_t tx_pkts[$];
AxisPacket_t rx_pkt;
test.start_test(test_name, TIMEOUT);
axis_bfm.set_master_stall_prob(master_stall);
axis_bfm.set_slave_stall_prob(slave_stall);
// Generate and enqueue all TX words
for (int p = 0; p < NUM_WORDS; p++) begin
AxisPacket_t pkt = new();
pkt.data.push_back(Rand#(WIDTH)::rand_bit());
tx_pkts.push_back(pkt);
axis_bfm.put(pkt.copy());
end
// Receive and compare
for (int p = 0; p < NUM_WORDS; p++) begin
axis_bfm.get(rx_pkt);
`ASSERT_ERROR(
tx_pkts[p].data[0] === rx_pkt.data[0],
$sformatf("Word %0d data mismatch! TX=%0h RX=%0h",
p, tx_pkts[p].data[0], rx_pkt.data[0])
)
end
// Verify FIFO is empty
repeat (20) @(posedge clk);
`ASSERT_ERROR(occupied == 0,
$sformatf("FIFO not empty after test, occupied = %0d", occupied))
`ASSERT_ERROR(EXPECTED_SPACE == space,
$sformatf("FIFO space mismatch after drain, expected %0d got %0d",
EXPECTED_SPACE, space))
test.end_test();
endtask
//---------------------------------------------------------------------------
// Main Test Process
//---------------------------------------------------------------------------
initial begin : main
// stop all clock events for simulation performance
clk_gen.kill();
begin
string s;
$sformat(s, "axi_fifo_large_tb - DEPTH=%0d MAX_NUM_URAM_BLOCKS=%0d WIDTH=%0d", DEPTH, MAX_NUM_URAM_BLOCKS, WIDTH);
test.start_tb(s);
end
clk_gen.revive();
// Setup BFM
axis_bfm = new(i_axis.master, o_axis.slave);
axis_bfm.run();
// Reset
clk_gen.start();
clk_gen.reset(20);
@(negedge rst); // Wait for reset to deassert
repeat (10) @(posedge clk);
`ASSERT_ERROR(EXPECTED_SPACE == space,
$sformatf("FIFO space mismatch after reset, expected %0d got %0d",
EXPECTED_SPACE, space))
run_traffic_test("Slow master / slow slave", 50, 50);
//---------------------------------------
// Done
//---------------------------------------
test.end_tb(0);
clk_gen.kill();
end : main
endmodule : axi_fifo_large_tb
`default_nettype wire

View File

@ -0,0 +1,70 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axi_fifo_tb_all
//
// Description:
//
// Wrapper testbench for axi_fifo testing.
//
module axi_fifo_tb_all;
// --------------------------------------------------------------
// Standard FIFO testing
// --------------------------------------------------------------
axi_fifo_tb #() axi_fifo_tb_i();
// --------------------------------------------------------------
// URAM testing
// --------------------------------------------------------------
localparam int URAM_TEST_DEPTH[3] = '{4096, 4096*3, 4096*5};
for (genvar i = 0; i < $size(URAM_TEST_DEPTH); i++) begin : gen_uram_tb
axi_fifo_uram_tb #(
.WIDTH(72),
.DEPTH(URAM_TEST_DEPTH[i])
) axi_fifo_uram_tb_i ();
end : gen_uram_tb
// --------------------------------------------------------------
// Large FIFO testing
// --------------------------------------------------------------
localparam int NUM_URAM_BLOCKS[5] = '{-1, 0, 2, 8, 20};
localparam int WIDTHS[3] = '{64, 144, 256};
// Depth subset chosen to exercise every structural code path in axi_fifo_large:
// - DEPTH <= 4096 (ULTRASCALE SIZE_THRESHOLD): single axi_fifo path
// - DEPTH > 4096: URAM + BRAM cascade
// * BRAM stages are driven by bits [13:9] of REMAINING_DEPTH_CEIL where
// REMAINING = DEPTH mod 4096. The 8 unique bit patterns occur at
// REMAINING in {0, 512, 1024, 1536, 2048, 2560, 3072, 3584}.
// * URAM count = DEPTH / 4096; tested at 1, 2, 4, 8, 16 blocks.
localparam int DEPTHS[24] = '{
// Single-FIFO path (DEPTH <= ULTRASCALE threshold of 4096)
512, 1024, 2048, 4096,
// 1 URAM block + all 8 BRAM remainder patterns (REMAINING = 0..3584 step 512)
// REMAINING=0 is the pure-URAM case; the rest add 1-3 small BRAM stages
4096, 4608, 5120, 5632, 6144, 6656, 7168, 7680,
// Higher URAM counts, pure-URAM (no BRAM remainder)
8192, 16384, 32768,
// Non-512-aligned depths: verify EXPECTED_SPACE rounds up to next 512 boundary
100, 511, 513, 1000, 4097, 6000, 8456, 9103,
// Large number
2**16+512
};
for (genvar i = 0; i < $size(NUM_URAM_BLOCKS); i++) begin : gen_uram
for (genvar j = 0; j < $size(WIDTHS); j++) begin : gen_width
for (genvar k = 0; k < $size(DEPTHS); k++) begin : gen_depth
axi_fifo_large_tb #(
.WIDTH(WIDTHS[j]),
.DEPTH(DEPTHS[k]),
.DEVICE("ULTRASCALE"),
.MAX_NUM_URAM_BLOCKS(NUM_URAM_BLOCKS[i])
) axi_fifo_large_tb_i();
end : gen_depth
end : gen_width
end : gen_uram
endmodule

View File

@ -0,0 +1,208 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axi_fifo_uram_tb
//
// Description:
//
// Testbench for axi_fifo_uram.
//
// Verifies data integrity across the FIFO under three traffic scenarios:
// 1. Full rate (no stalls on either side)
// 2. Slow slave (fast master, slave stalls frequently)
// 3. Slow master (master stalls frequently, fast slave)
//
// Parameters:
// WIDTH : FIFO data width in bits
// DEPTH : FIFO depth in entries
//
`default_nettype none
module axi_fifo_uram_tb #(
parameter WIDTH = 72,
parameter DEPTH = 4096
);
`include "test_exec.svh"
`include "usrp_utils.svh"
import PkgTestExec::*;
import PkgAxiStreamBfm::*;
import PkgRandom::*;
//---------------------------------------------------------------------------
// Local Parameters
//---------------------------------------------------------------------------
localparam real CLK_PERIOD = 10.0; // 100 MHz
localparam time TIMEOUT = 100ms;
localparam int FIFO_SIZE = (DEPTH > 1) ? $clog2(DEPTH) : 0;
//---------------------------------------------------------------------------
// Clocks and Resets
//---------------------------------------------------------------------------
bit clk, rst;
sim_clock_gen #(.PERIOD(CLK_PERIOD), .AUTOSTART(0))
clk_gen (.clk(clk), .rst(rst));
//---------------------------------------------------------------------------
// AXI-Stream Interfaces and BFM
//---------------------------------------------------------------------------
AxiStreamIf #(.DATA_WIDTH(WIDTH), .TKEEP(0), .TUSER(0)) i_axis (.clk(clk), .rst(rst));
AxiStreamIf #(.DATA_WIDTH(WIDTH), .TKEEP(0), .TUSER(0)) o_axis (.clk(clk), .rst(rst));
AxiStreamBfm #(.DATA_WIDTH(WIDTH), .TKEEP(0), .TUSER(0)) axis_bfm;
//---------------------------------------------------------------------------
// DUT
//---------------------------------------------------------------------------
wire [15:0] space;
wire [15:0] occupied;
// Expected space is a multiple of 4096 (URAM block depth) plus overhead (0-32 words)
// for input/output flop2 stages.
localparam int EXPECTED_SPACE_MIN = ((DEPTH + 4095) / 4096) * 4096;
localparam int EXPECTED_SPACE_MAX = EXPECTED_SPACE_MIN + 32;
axi_fifo_uram #(
.WIDTH (WIDTH),
.DEPTH (DEPTH)
) dut (
.clk (clk),
.reset (rst),
.clear (1'b0),
.i_tdata (i_axis.tdata),
.i_tvalid (i_axis.tvalid),
.i_tready (i_axis.tready),
.o_tdata (o_axis.tdata),
.o_tvalid (o_axis.tvalid),
.o_tready (o_axis.tready),
.space (space),
.occupied (occupied)
);
// The FIFO has no tlast port. Tie o_axis.tlast high so the BFM slave
// treats every received word as a complete single-word packet.
assign o_axis.tlast = 1'b1;
//---------------------------------------------------------------------------
// Helper Tasks
//---------------------------------------------------------------------------
typedef AxiStreamPacket #(WIDTH, 1) AxisPacket_t;
// Total number of words to send per test.
// Enforce a minimum of 100 words while still stressing deeper FIFOs.
localparam int NUM_WORDS = `MAX((DEPTH * 2), 100);
// Send NUM_WORDS random single-word packets and verify received data matches.
task automatic run_traffic_test(string test_name, int master_stall, int slave_stall);
AxisPacket_t tx_pkts[$];
AxisPacket_t rx_pkt;
test.start_test(test_name, TIMEOUT);
axis_bfm.set_master_stall_prob(master_stall);
axis_bfm.set_slave_stall_prob(slave_stall);
// Generate and enqueue all TX words
for (int p = 0; p < NUM_WORDS; p++) begin
AxisPacket_t pkt = new();
pkt.data.push_back(Rand#(WIDTH)::rand_bit());
tx_pkts.push_back(pkt);
axis_bfm.put(pkt.copy());
end
// Receive and compare
for (int p = 0; p < NUM_WORDS; p++) begin
axis_bfm.get(rx_pkt);
`ASSERT_ERROR(
tx_pkts[p].data[0] === rx_pkt.data[0],
$sformatf("Word %0d data mismatch! TX=%0h RX=%0h",
p, tx_pkts[p].data[0], rx_pkt.data[0])
)
end
// Verify FIFO is empty
repeat (20) @(posedge clk);
`ASSERT_ERROR(occupied == 0,
$sformatf("FIFO not empty after test, occupied = %0d", occupied))
`ASSERT_ERROR(space >= EXPECTED_SPACE_MIN && space <= EXPECTED_SPACE_MAX,
$sformatf("FIFO space out of range after drain, expected [%0d:%0d] got %0d",
EXPECTED_SPACE_MIN, EXPECTED_SPACE_MAX, space))
test.end_test();
endtask
//---------------------------------------------------------------------------
// Main Test Process
//---------------------------------------------------------------------------
initial begin : main
// stop all clock events for simulation performance
clk_gen.kill();
begin
string s;
$sformat(s, "axi_fifo_uram_tb - DEPTH=%0d WIDTH=%0d", DEPTH, WIDTH);
test.start_tb(s);
end
clk_gen.revive();
// Setup BFM
axis_bfm = new(i_axis.master, o_axis.slave);
axis_bfm.run();
// Reset
clk_gen.start();
clk_gen.reset(20);
@(negedge rst); // Wait for reset to deassert
repeat (10) @(posedge clk);
`ASSERT_ERROR(space >= EXPECTED_SPACE_MIN && space <= EXPECTED_SPACE_MAX,
$sformatf("FIFO space out of range after reset, expected [%0d:%0d] got %0d",
EXPECTED_SPACE_MIN, EXPECTED_SPACE_MAX, space))
//---------------------------------------
// Test 1: Full rate (no stalls)
//---------------------------------------
run_traffic_test("Full rate", 0, 0);
//---------------------------------------
// Test 2: Slow slave (backpressure)
//---------------------------------------
run_traffic_test("Slow slave / fast master", 0, 70);
//---------------------------------------
// Test 3: Slow master (starving)
//---------------------------------------
run_traffic_test("Slow master / fast slave", 70, 0);
//---------------------------------------
// Test 4: Both sides stalling
//---------------------------------------
run_traffic_test("Slow master / slow slave", 50, 50);
//---------------------------------------
// Done
//---------------------------------------
test.end_tb(0);
clk_gen.kill();
end : main
endmodule : axi_fifo_uram_tb
`default_nettype wire

View File

@ -22,3 +22,4 @@ top/x400/ip/eth_100g_bd/lbus_tb
top/x400/rf/sim
top/x300/dboards/obx/cpld/sim/obx_register_endpoints
top/x300/dboards/obx/cpld/sim/obx_spi_passthrough
lib/sim/fifo/axi_fifo