fpga: lib: Add AXI-Stream load split and merge modules

Original-commit: 32fb626ef1b1d3078fd331143b03cd38d1e155f0
This commit is contained in:
Wade Fife
2025-02-14 15:59:19 -06:00
parent 72432c9278
commit bb2bfbfe95
6 changed files with 1058 additions and 0 deletions
+2
View File
@@ -35,6 +35,8 @@ axis_width_conv.v \
axis_split.v \ axis_split.v \
axis_split_bus.v \ axis_split_bus.v \
axis_combine.v \ axis_combine.v \
axis_load_split.sv \
axis_load_merge.sv \
axis_packetize.v \ axis_packetize.v \
axis_pkt_throttle.sv \ axis_pkt_throttle.sv \
axis_data_if_packetize.v \ axis_data_if_packetize.v \
+302
View File
@@ -0,0 +1,302 @@
//
// Copyright 2025 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axis_load_merge
//
// Description:
//
// Takes in multiple AXI4-Stream interfaces and merges the contents into a
// single stream, packet by packet. The packets will be merged sequentially,
// first accepting a packet from port 0, then from port 1, and so on, in a
// circular manner. The module will wait for the packet to arrive on the
// expected port before moving onto the next port.
//
// This module matches the behavior of axis_load_split, such that the results
// from multiple processing instances can be merged back together in the same
// order they were received by the axis_load_split module.
//
// The handling of TUSER deserves some explanation. If the input-port width
// is the same as the output-port width, then the TUSER input is passed
// through along with the corresponding TDATA like normal. But, if the
// output-word size is larger than the input-word size, then there are
// multiple N TUSER inputs for each TUSER output. The USER_SEL parameter
// selects which one will be output. It can be in the range [0,N-1], where 0
// selects the first of the N TUSER inputs for the next TUSER output and N-1
// selects the last of the N TUSER inputs.
//
// Parameters:
//
// IN_DATA_W : Width of TDATA in bits for the input port.
// IN_FIFO_SIZE : Log base-2 of the input FIFO size, in units of
// IN_DATA_W-sized words. Typically, this would be large
// enough to buffer one entire packet. Set to -1 to remove.
// IN_NUM_PORTS : The number of input streams across which to distribute
// the packets.
// OUT_DATA_W : Width of TDATA in bits for the output port.
// OUT_FIFO_SIZE : Log base-2 of the output FIFO size, in units of
// OUT_DATA_W-sized words. Set to -1 to remove, 1 to add a
// register that cuts timing paths, or whichever size you
// desire.
// USER_W : Width of TUSER in bits for both input and output ports.
// USER_SEL : When the output word size is a multiple of the input word
// size, USER_SEL indicates which TUSER input value will be
// passed along with each TDATA output value. A value of 0
// indicates that the first TUSER value that was input for a
// corresponding output word will be used for that output
// word. It can be in the range from 0 up to the number of
// output words per input words.
//
`default_nettype none
module axis_load_merge #(
int IN_DATA_W = 32,
int IN_FIFO_SIZE = 10,
int IN_NUM_PORTS = 2,
int OUT_DATA_W = 64,
int OUT_FIFO_SIZE = 1,
int USER_W = 1,
int USER_SEL = 0
) (
input wire clk,
input wire rst,
// Input streams
input wire [IN_DATA_W-1:0] i_tdata [IN_NUM_PORTS],
input wire [ USER_W-1:0] i_tuser [IN_NUM_PORTS],
input wire i_tlast [IN_NUM_PORTS],
input wire i_tvalid [IN_NUM_PORTS],
output logic i_tready [IN_NUM_PORTS],
// Single output stream
output logic [OUT_DATA_W-1:0] o_tdata,
output logic [ USER_W-1:0] o_tuser,
output logic o_tlast,
output logic o_tvalid,
input wire o_tready
);
// Elaboration-time assertions
if (IN_DATA_W > OUT_DATA_W) begin : check_size
$error("IN_DATA_W must not exceed OUT_DATA_W");
end
if (OUT_DATA_W % IN_DATA_W != 0) begin : check_multiple
$error("OUT_DATA_W must be a multiple of IN_DATA_W");
end
//-------------------------------------------------------------------------
// Resize
//-------------------------------------------------------------------------
logic [OUT_DATA_W-1:0] resize_tdata [IN_NUM_PORTS];
logic [ USER_W-1:0] resize_tuser [IN_NUM_PORTS];
logic resize_tlast [IN_NUM_PORTS];
logic resize_tvalid [IN_NUM_PORTS];
logic resize_tready [IN_NUM_PORTS];
if (IN_DATA_W == OUT_DATA_W) begin : gen_no_resize
assign resize_tdata = i_tdata;
assign resize_tuser = i_tuser;
assign resize_tlast = i_tlast;
assign resize_tvalid = i_tvalid;
assign i_tready = resize_tready;
end else begin : gen_resize
for (genvar idx = 0; idx < IN_NUM_PORTS; idx++) begin : gen_axis_width_conv
localparam int OUT_WORDS = OUT_DATA_W/IN_DATA_W;
localparam int COUNT_W = $clog2(OUT_WORDS);
if (USER_SEL < 0 || USER_SEL >= OUT_WORDS) begin : check_parameters
$error("USER_SEL is outside of allowed range");
end
//-----------------------------------------
// Data Width Conversion
//-----------------------------------------
axis_width_conv #(
.WORD_W (IN_DATA_W),
.IN_WORDS (1 ),
.OUT_WORDS(OUT_WORDS),
.SYNC_CLKS(1 ),
.PIPELINE ("INOUT" )
) axis_width_conv_i (
.s_axis_aclk (clk ),
.s_axis_rst (rst ),
.s_axis_tdata (i_tdata[idx] ),
.s_axis_tkeep ('1 ),
.s_axis_tlast (i_tlast[idx] ),
.s_axis_tvalid(i_tvalid[idx] ),
.s_axis_tready(i_tready[idx] ),
.m_axis_aclk (clk ),
.m_axis_rst (rst ),
.m_axis_tdata (resize_tdata[idx] ),
.m_axis_tkeep ( ),
.m_axis_tlast (resize_tlast[idx] ),
.m_axis_tvalid(resize_tvalid[idx]),
.m_axis_tready(resize_tready[idx])
);
//-----------------------------------------
// TUSER Handling
//-----------------------------------------
// We write to the TUSER FIFO at a rate of 1/OUT_WORDS, but read from
// the TUSER FIFO in lock-step with the output of axis_width_conv
// module. The USER_SEL parameter selects which TUSER gets input.
logic user_fifo_i_tvalid;
logic user_fifo_i_tready;
logic user_fifo_o_tvalid;
logic [COUNT_W-1:0] word_count = '0;
// This counter tells when we're inputting the selected TUSER for the
// current output word, so we know when to write it to the FIFO. The
// counter is initialized such that it will assert after the first
// USER_SEL cycles, then repeats every OUT_WORDS cycles after that.
always_ff @(posedge clk) begin
if (i_tvalid[idx] && i_tready[idx]) begin
if (word_count == OUT_WORDS-1) begin
word_count <= '0;
end else begin
word_count <= word_count + 1;
end
end
if (rst) begin
word_count <= '0;
end
end
assign user_fifo_i_tvalid = (word_count == USER_SEL) && i_tvalid[idx] && i_tready[idx];
axi_fifo #(
.WIDTH(USER_W),
.SIZE (2 )
) axi_fifo_tuser (
.clk (clk ),
.reset (rst ),
.clear ('0 ),
.i_tdata (i_tuser[idx] ),
.i_tvalid(user_fifo_i_tvalid ),
.i_tready(user_fifo_i_tready ),
.o_tdata (resize_tuser[idx] ),
.o_tvalid(user_fifo_o_tvalid ),
.o_tready(resize_tvalid[idx] && resize_tready[idx]),
.occupied( ),
.space ( )
);
// Make sure we don't overflow/underflow the TUSER FIFO
//synthesis translate_off
always_ff @(posedge clk) begin
if (user_fifo_i_tvalid && !user_fifo_i_tready) begin
$error("TUSER FIFO overflow");
end
if (resize_tvalid[idx] && resize_tready[idx] && !user_fifo_o_tvalid) begin
$error("TUSER FIFO underflow");
end
end
//synthesis translate_on
end
end
//---------------------------------------------------------------------------
// Input FIFOs
//---------------------------------------------------------------------------
logic [OUT_DATA_W-1:0] fifo_tdata [IN_NUM_PORTS];
logic [ USER_W-1:0] fifo_tuser [IN_NUM_PORTS];
logic fifo_tlast [IN_NUM_PORTS];
logic fifo_tvalid [IN_NUM_PORTS];
logic fifo_tready [IN_NUM_PORTS];
for (genvar idx = 0; idx < IN_NUM_PORTS; idx++) begin : gen_axi_fifos
axi_fifo #(
.WIDTH(1 + USER_W + OUT_DATA_W ),
.SIZE (IN_FIFO_SIZE - $clog2(OUT_DATA_W/IN_DATA_W))
) axi_fifo_in (
.clk (clk ),
.reset (rst ),
.clear (1'b0 ),
.i_tdata ({resize_tlast[idx], resize_tuser[idx], resize_tdata[idx]}),
.i_tvalid(resize_tvalid[idx] ),
.i_tready(resize_tready[idx] ),
.o_tdata ({fifo_tlast[idx], fifo_tuser[idx], fifo_tdata[idx]} ),
.o_tvalid(fifo_tvalid[idx] ),
.o_tready(fifo_tready[idx] ),
.space ( ),
.occupied( )
);
end
//---------------------------------------------------------------------------
// Merge Logic
//---------------------------------------------------------------------------
logic [OUT_DATA_W-1:0] merge_tdata;
logic [ USER_W-1:0] merge_tuser;
logic merge_tlast;
logic merge_tvalid;
logic merge_tready;
// Currently selected port
logic [$clog2(IN_NUM_PORTS)-1:0] st_port;
// Merge state machine. Tracks and advances the currently selected port.
always_ff @(posedge clk) begin
if (rst) begin
st_port <= '0;
end else begin
if (merge_tvalid && merge_tready && merge_tlast) begin
if (st_port == IN_NUM_PORTS-1) begin
st_port <= '0;
end else begin
st_port <= st_port + 1;
end
end
end
end
// Connect the selected input to the output
assign merge_tdata = fifo_tdata[st_port];
assign merge_tuser = fifo_tuser[st_port];
assign merge_tlast = fifo_tlast[st_port];
assign merge_tvalid = fifo_tvalid[st_port];
for (genvar idx = 0; idx < IN_NUM_PORTS; idx++) begin : gen_merge
assign fifo_tready[idx] = merge_tready && (st_port == idx);
end
//---------------------------------------------------------------------------
// Output FIFOs
//---------------------------------------------------------------------------
axi_fifo #(
.WIDTH(1 + USER_W + OUT_DATA_W),
.SIZE (OUT_FIFO_SIZE )
) axi_fifo_out (
.clk (clk ),
.reset (rst ),
.clear (1'b0 ),
.i_tdata ({merge_tlast, merge_tuser, merge_tdata}),
.i_tvalid(merge_tvalid ),
.i_tready(merge_tready ),
.o_tdata ({o_tlast, o_tuser, o_tdata} ),
.o_tvalid(o_tvalid ),
.o_tready(o_tready ),
.space ( ),
.occupied( )
);
endmodule : axis_load_merge
`default_nettype wire
+280
View File
@@ -0,0 +1,280 @@
//
// Copyright 2025 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axis_load_split
//
// Description:
//
// Takes an AXI4-Stream interface and distributes the input packets evenly
// among the output ports. The packets will be distributed sequentially, with
// the first packet going to output port 0, the second to port 1, and so on,
// in a circular manner. The data output can be optionally resized to a new
// data width.
//
// The purpose of this module is to take a high-throughput input that needs
// to be processed and farm it out to multiple instances of a processing
// module, sending one packet to each instance.
//
// Parameters:
//
// IN_DATA_W : Width of TDATA in bits for the input port.
// IN_FIFO_SIZE : Log base-2 of the input FIFO size, in units of
// IN_DATA_W-sized words. Set to -1 to remove, 1 to add a
// register that cuts timing paths, or whichever size you
// desire.
// OUT_DATA_W : Width of TDATA in bits for the output port.
// OUT_FIFO_SIZE : Log base-2 of the output FIFO size used for each output,
// in units of OUT_DATA_W-sized words. Typically, this would
// be large enough to buffer one entire packet. Set to -1 to
// remove.
// OUT_NUM_PORTS : The number of output streams across which to distribute
// the packets.
// USER_W : Width of TUSER in bits for both input and output ports.
//
`default_nettype none
module axis_load_split #(
int IN_DATA_W = 64,
int IN_FIFO_SIZE = 1,
int OUT_DATA_W = 32,
int OUT_FIFO_SIZE = 10,
int OUT_NUM_PORTS = 2,
int USER_W = 1
) (
input wire clk,
input wire rst,
// Single input stream
input wire [IN_DATA_W-1:0] i_tdata,
input wire [ USER_W-1:0] i_tuser,
input wire i_tlast,
input wire i_tvalid,
output logic i_tready,
// Output streams
output logic [OUT_DATA_W-1:0] o_tdata [OUT_NUM_PORTS],
output wire [ USER_W-1:0] o_tuser [OUT_NUM_PORTS],
output logic o_tlast [OUT_NUM_PORTS],
output logic o_tvalid [OUT_NUM_PORTS],
input wire o_tready [OUT_NUM_PORTS]
);
// Elaboration-time assertions
if (OUT_DATA_W > IN_DATA_W) begin : check_size
$error("OUT_DATA_W must not exceed IN_DATA_W");
end
if (IN_DATA_W % OUT_DATA_W != 0) begin : check_multiple
$error("IN_DATA_W must be a multiple of OUT_DATA_W");
end
//---------------------------------------------------------------------------
// Input FIFO
//---------------------------------------------------------------------------
logic [IN_DATA_W-1:0] in_fifo_tdata;
logic [ USER_W-1:0] in_fifo_tuser;
logic in_fifo_tlast;
logic in_fifo_tvalid;
logic in_fifo_tready;
axi_fifo #(
.WIDTH(1 + USER_W + IN_DATA_W),
.SIZE (IN_FIFO_SIZE )
) axi_fifo_in (
.clk (clk ),
.reset (rst ),
.clear (1'b0 ),
.i_tdata ({i_tlast, i_tuser, i_tdata} ),
.i_tvalid(i_tvalid ),
.i_tready(i_tready ),
.o_tdata ({in_fifo_tlast, in_fifo_tuser, in_fifo_tdata}),
.o_tvalid(in_fifo_tvalid ),
.o_tready(in_fifo_tready ),
.space ( ),
.occupied( )
);
//---------------------------------------------------------------------------
// Splitter Logic
//---------------------------------------------------------------------------
logic [IN_DATA_W-1:0] split_tdata [OUT_NUM_PORTS];
logic [ USER_W-1:0] split_tuser [OUT_NUM_PORTS];
logic split_tlast [OUT_NUM_PORTS];
logic split_tvalid [OUT_NUM_PORTS];
logic split_tready [OUT_NUM_PORTS];
// Currently selected port
logic [$clog2(OUT_NUM_PORTS)-1:0] st_port;
// Splitter state machine. Tracks and advances the currently selected port.
always_ff @(posedge clk) begin
if (rst) begin
st_port <= '0;
end else begin
if (in_fifo_tvalid && in_fifo_tready && in_fifo_tlast) begin
if (st_port == OUT_NUM_PORTS-1) begin
st_port <= '0;
end else begin
st_port <= st_port + 1;
end
end
end
end
// Distribute the input to each output. Only the currently selected output
// port will receive the data.
assign in_fifo_tready = split_tready[st_port];
for (genvar idx = 0; idx < OUT_NUM_PORTS; idx++) begin : gen_splitter
assign split_tdata [idx] = in_fifo_tdata;
assign split_tuser [idx] = in_fifo_tuser;
assign split_tlast [idx] = in_fifo_tlast;
assign split_tvalid[idx] = in_fifo_tvalid && (st_port == idx);
end
//---------------------------------------------------------------------------
// Output FIFOs
//---------------------------------------------------------------------------
logic [IN_DATA_W-1:0] out_fifo_tdata [OUT_NUM_PORTS];
logic [ USER_W-1:0] out_fifo_tuser [OUT_NUM_PORTS];
logic out_fifo_tlast [OUT_NUM_PORTS];
logic out_fifo_tvalid [OUT_NUM_PORTS];
logic out_fifo_tready [OUT_NUM_PORTS];
for (genvar idx = 0; idx < OUT_NUM_PORTS; idx++) begin : gen_axi_fifos
axi_fifo #(
.WIDTH(1 + USER_W + IN_DATA_W ),
.SIZE (OUT_FIFO_SIZE - $clog2(IN_DATA_W/OUT_DATA_W))
) axi_fifo_out (
.clk (clk ),
.reset (rst ),
.clear (1'b0 ),
.i_tdata ({split_tlast[idx], split_tuser[idx], split_tdata[idx]} ),
.i_tvalid(split_tvalid[idx] ),
.i_tready(split_tready[idx] ),
.o_tdata ({out_fifo_tlast[idx], out_fifo_tuser[idx], out_fifo_tdata[idx]}),
.o_tvalid(out_fifo_tvalid[idx] ),
.o_tready(out_fifo_tready[idx] ),
.space ( ),
.occupied( )
);
end
//-------------------------------------------------------------------------
// Resize
//-------------------------------------------------------------------------
if (IN_DATA_W == OUT_DATA_W) begin : gen_no_resize
assign o_tdata = out_fifo_tdata;
assign o_tuser = out_fifo_tuser;
assign o_tlast = out_fifo_tlast;
assign o_tvalid = out_fifo_tvalid;
assign out_fifo_tready = o_tready;
end else begin : gen_resize
for (genvar idx = 0; idx < OUT_NUM_PORTS; idx++) begin : gen_axis_width_conv
localparam IN_WORDS = IN_DATA_W/OUT_DATA_W;
localparam COUNT_W = $clog2(IN_WORDS);
//-----------------------------------------
// Data Width Conversion
//-----------------------------------------
axis_width_conv #(
.WORD_W (OUT_DATA_W),
.IN_WORDS (IN_WORDS ),
.OUT_WORDS(1 ),
.SYNC_CLKS(1 ),
.PIPELINE ("INOUT" )
) axis_width_conv_i (
.s_axis_aclk (clk ),
.s_axis_rst (rst ),
.s_axis_tdata (out_fifo_tdata[idx] ),
.s_axis_tkeep ('1 ),
.s_axis_tlast (out_fifo_tlast[idx] ),
.s_axis_tvalid(out_fifo_tvalid[idx]),
.s_axis_tready(out_fifo_tready[idx]),
.m_axis_aclk (clk ),
.m_axis_rst (rst ),
.m_axis_tdata (o_tdata[idx] ),
.m_axis_tkeep ( ),
.m_axis_tlast (o_tlast[idx] ),
.m_axis_tvalid(o_tvalid[idx] ),
.m_axis_tready(o_tready[idx] )
);
//-----------------------------------------
// TUSER Handling
//-----------------------------------------
// We write to the TUSER FIFO in lock-step with the axis_width_conv
// module. But read it out at a rate of 1/IN_WORDS so that the TUSER
// output matches the same data it did on the way in.
logic user_fifo_i_tready;
logic user_fifo_o_tvalid;
logic user_fifo_o_tready;
logic [COUNT_W-1:0] word_count;
// This counter tells when we're outputting the last TUSER for the
// current input word, so we know when to pop the output from the FIFO.
always_ff @(posedge clk) begin
if (o_tvalid[idx] && o_tready[idx]) begin
if (word_count == IN_WORDS-1) begin
word_count <= '0;
end else begin
word_count <= word_count + 1;
end
end
if (rst) begin
word_count <= '0;
end
end
assign user_fifo_o_tready = (word_count == IN_WORDS-1) && o_tvalid[idx] && o_tready[idx];
axi_fifo #(
.WIDTH(USER_W),
.SIZE (2 )
) axi_fifo_tuser (
.clk (clk ),
.reset (rst ),
.clear ('0 ),
.i_tdata (out_fifo_tuser[idx] ),
.i_tvalid(out_fifo_tvalid[idx] && out_fifo_tready[idx]),
.i_tready(user_fifo_i_tready ),
.o_tdata (o_tuser[idx] ),
.o_tvalid(user_fifo_o_tvalid ),
.o_tready(user_fifo_o_tready ),
.occupied( ),
.space ( )
);
// Make sure we don't overflow/underflow the TUSER FIFO
//synthesis translate_off
always_ff @(posedge clk) begin
if (out_fifo_tvalid[idx] && out_fifo_tready[idx] && !user_fifo_i_tready) begin
$error("TUSER FIFO overflow");
end
if (user_fifo_o_tready && !user_fifo_o_tvalid) begin
$error("TUSER FIFO underflow");
end
end
//synthesis translate_on
end
end
endmodule : axis_load_split
`default_nettype wire
@@ -0,0 +1,34 @@
#
# Copyright 2024 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)
# Include viv_sim_preamble after defining BASE_DIR
include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak
#-------------------------------------------------
# Testbench Specific
#-------------------------------------------------
# Define only one top-level module
SIM_TOP = axis_load_split_merge_all_tb
# Add test bench, user design under test, and
# additional user created files
SIM_SRCS = $(abspath \
axis_load_split_merge_tb.sv \
axis_load_split_merge_all_tb.sv \
)
#-------------------------------------------------
# Bottom-of-Makefile
#-------------------------------------------------
# Include all simulator specific makefiles here
# Each should define a unique target to simulate
# e.g. xsim, vsim, etc and a common "clean" target
include $(BASE_DIR)/../tools/make/viv_simulator.mak
@@ -0,0 +1,88 @@
//
// Copyright 2025 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axis_load_split_merge_all_tb
//
// Description:
//
// Top-level testbench for axis_load_split and axis_load_merge modules. This
// instantiates multiple instances of axis_load_split_merge_tb to test
// different configurations.
//
module axis_load_split_merge_all_tb;
localparam int MAX_PKT_LEN = 16;
localparam int INT_FIFO_SIZE = $clog2(MAX_PKT_LEN);
localparam int DATA_W = 8;
localparam int USER_W = 4;
axis_load_split_merge_tb #(
.MAX_PKT_LEN (MAX_PKT_LEN),
.DATA_W (4*DATA_W),
.USER_W (USER_W),
.USER_SEL (3),
.INT_DATA_W (DATA_W),
.NUM_PORTS (4),
.INT_FIFO_SIZE(INT_FIFO_SIZE),
.EXT_FIFO_SIZE(1)
) tb_32b_to_4x8b();
axis_load_split_merge_tb #(
.MAX_PKT_LEN (MAX_PKT_LEN),
.DATA_W (2*DATA_W),
.USER_W (USER_W),
.USER_SEL (0),
.INT_DATA_W (DATA_W),
.NUM_PORTS (2),
.INT_FIFO_SIZE(INT_FIFO_SIZE),
.EXT_FIFO_SIZE(1)
) tb_16b_to_2x8b();
axis_load_split_merge_tb #(
.MAX_PKT_LEN (MAX_PKT_LEN),
.DATA_W (4*DATA_W),
.USER_W (USER_W),
.USER_SEL (1),
.INT_DATA_W (DATA_W),
.NUM_PORTS (3),
.INT_FIFO_SIZE(INT_FIFO_SIZE),
.EXT_FIFO_SIZE(1)
) tb_32b_to_3x8b();
axis_load_split_merge_tb #(
.MAX_PKT_LEN (MAX_PKT_LEN),
.DATA_W (4*DATA_W),
.USER_W (USER_W),
.USER_SEL (2),
.INT_DATA_W (DATA_W),
.NUM_PORTS (2),
.INT_FIFO_SIZE(INT_FIFO_SIZE),
.EXT_FIFO_SIZE(1)
) tb_32b_to_2x8b();
axis_load_split_merge_tb #(
.MAX_PKT_LEN (MAX_PKT_LEN),
.DATA_W (DATA_W),
.USER_W (USER_W),
.USER_SEL (0),
.INT_DATA_W (DATA_W),
.NUM_PORTS (4),
.INT_FIFO_SIZE(INT_FIFO_SIZE),
.EXT_FIFO_SIZE(4)
) tb_8b_to_4x8b();
axis_load_split_merge_tb #(
.MAX_PKT_LEN (MAX_PKT_LEN),
.DATA_W (2*DATA_W),
.USER_W (USER_W),
.USER_SEL (0),
.INT_DATA_W (DATA_W),
.NUM_PORTS (4),
.INT_FIFO_SIZE(INT_FIFO_SIZE),
.EXT_FIFO_SIZE(4)
) tb_16b_to_4x8b();
endmodule
@@ -0,0 +1,352 @@
//
// Copyright 2025 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axis_load_split_merge_tb
//
// Description:
//
// Testbench for axis_load_split and axis_load_merge modules. It tests both
// modules together but taking an input data stream and splitting it into
// multiple streams, then combining them back together to recover the
// original data stream.
//
// Parameters:
//
// MAX_PKT_LEN : Maximum packet length to test, in number of DATA_W words.
// DATA_W : Width of the input data port to axis_load_split and the
// output data port of axis_load_merge.
// USER_W : Width of TUSER to test.
// USER_SEL : USER_SEL value to test.
// INT_DATA_W : Internal data width, at the output of axis_load_split and
// the input to axis_load_merge.
// NUM_PORTS : The number of ports to split into and merge.
// INT_FIFO_SIZE : Log base 2 of the FIFO size (in units of DATA_W sized
// words) to configure for the output FIFOs in
// axis_load_split and the input FIFOs in axis_load_merge.
// EXT_FIFO_SIZE : Log base 2 of the FIFO size (in units of DATA_W sized
// words) to configure for the input FIFO in axis_load_split
// and the output FIFO in axis_load_merge.
//
module axis_load_split_merge_tb #(
int MAX_PKT_LEN = 16,
int DATA_W = 32,
int USER_W = 4,
int USER_SEL = 0,
int INT_DATA_W = 8,
int NUM_PORTS = DATA_W/INT_DATA_W,
int INT_FIFO_SIZE = $clog2(MAX_PKT_LEN),
int EXT_FIFO_SIZE = 1
);
`include "test_exec.svh"
`include "usrp_utils.svh"
import PkgTestExec::*;
import PkgAxiStreamBfm::*;
import PkgRandom::*;
// Use random or sequential data (easier for debug)
localparam bit USE_RANDOM = 1;
localparam real CLK_PER = 5;
// Number of packets to test
localparam NUM_PKTS = NUM_PORTS*100;
// Width of packet count that's put in each packet
localparam int PKT_COUNT_W = 8;
if (INT_DATA_W < PKT_COUNT_W) begin : check_data_width
$error("INT_DATA_W must be at least PKT_COUNT_W");
end
//---------------------------------------------------------------------------
// Clocks and Resets
//---------------------------------------------------------------------------
bit clk;
bit rst;
sim_clock_gen #(.PERIOD(CLK_PER), .AUTOSTART(0))
clk_gen (.clk(clk), .rst(rst));
//---------------------------------------------------------------------------
// Bus Functional Models
//---------------------------------------------------------------------------
// AXI-Stream Interfaces
AxiStreamIf #(DATA_W, USER_W) i_axis (clk, rst);
AxiStreamIf #(DATA_W, USER_W) o_axis (clk, rst);
// AXI-Stream BFMs
typedef AxiStreamBfm #(DATA_W, USER_W) bfm_t;
typedef bfm_t::AxisPacket_t pkt_t;
bfm_t bfm = new(i_axis, o_axis);
//---------------------------------------------------------------------------
// Device Under Test (DUT)
//---------------------------------------------------------------------------
logic [INT_DATA_W-1:0] split_tdata [NUM_PORTS];
logic [ USER_W-1:0] split_tuser [NUM_PORTS];
logic split_tlast [NUM_PORTS];
logic split_tvalid [NUM_PORTS];
logic split_tready [NUM_PORTS];
// Convert the internal FIFO size from the external word size (DATA_W) to the
// internal word size (INT_DATA_W).
localparam int INT_WORD_FIFO_SIZE = INT_FIFO_SIZE + $clog2(DATA_W/INT_DATA_W);
logic [USER_W-1:0] masked_tuser [NUM_PORTS];
axis_load_split #(
.IN_DATA_W (DATA_W ),
.IN_FIFO_SIZE (EXT_FIFO_SIZE ),
.OUT_DATA_W (INT_DATA_W ),
.OUT_FIFO_SIZE(INT_WORD_FIFO_SIZE),
.OUT_NUM_PORTS(NUM_PORTS ),
.USER_W (USER_W )
) axis_load_split_i (
.clk (clk ),
.rst (rst ),
.i_tdata (i_axis.tdata ),
.i_tuser (i_axis.tuser ),
.i_tlast (i_axis.tlast ),
.i_tvalid(i_axis.tvalid),
.i_tready(i_axis.tready),
.o_tdata (split_tdata ),
.o_tuser (split_tuser ),
.o_tlast (split_tlast ),
.o_tvalid(split_tvalid ),
.o_tready(split_tready )
);
axis_load_merge #(
.IN_DATA_W (INT_DATA_W ),
.IN_FIFO_SIZE (INT_WORD_FIFO_SIZE),
.IN_NUM_PORTS (NUM_PORTS ),
.OUT_DATA_W (DATA_W ),
.OUT_FIFO_SIZE(EXT_FIFO_SIZE ),
.USER_W (USER_W ),
.USER_SEL (USER_SEL )
) axis_load_merge_i (
.clk (clk ),
.rst (rst ),
.i_tdata (split_tdata ),
.i_tuser (masked_tuser ),
.i_tlast (split_tlast ),
.i_tvalid(split_tvalid ),
.i_tready(split_tready ),
.o_tdata (o_axis.tdata ),
.o_tuser (o_axis.tuser ),
.o_tlast (o_axis.tlast ),
.o_tvalid(o_axis.tvalid),
.o_tready(o_axis.tready)
);
//---------------------------------------------------------------------------
// User Mask
//---------------------------------------------------------------------------
//
// In order to verify that TUSER is used as expected, we mask the TUSER words
// that should be ignored by axis_load_merge by changing them to X.
//
//---------------------------------------------------------------------------
for (genvar port = 0; port < NUM_PORTS; port++) begin : gen_user_check
int count;
assign masked_tuser[port] = (count == USER_SEL) ? split_tuser[port] : 'X;
always_ff @(posedge clk) begin
if (split_tvalid[port] && split_tready[port]) begin
if (split_tlast[port] || count == DATA_W/INT_DATA_W-1) begin
count <= 0;
end else begin
count <= count + 1;
end
end
if (rst) begin
count <= 0;
end
end
end
//---------------------------------------------------------------------------
// Check Port Order
//---------------------------------------------------------------------------
//
// Here we double check that the ports are used in a round robin order as
// expected. This is done by checking the counter that's embedded in each
// packet and making sure it corresponds to the correct port.
//
//---------------------------------------------------------------------------
bit pkt_count_rst;
for (genvar port = 0; port < NUM_PORTS; port++) begin : gen_port_check
bit [PKT_COUNT_W-1:0] pkt_count = port;
bit start_of_packet = 1;
always_ff @(posedge clk) begin
if (split_tvalid[port] && split_tready[port]) begin
if (start_of_packet) begin
`ASSERT_ERROR(
split_tdata[port][0+:PKT_COUNT_W] == pkt_count,
$sformatf(
"Unexpected packet count on port %0d. Expected %0d, read %0d.",
port, pkt_count, split_tdata[port]
)
);
pkt_count <= pkt_count + NUM_PORTS;
end
start_of_packet <= split_tlast[port];
end
if (pkt_count_rst) begin
pkt_count <= port;
start_of_packet <= 1;
end
end
end
//---------------------------------------------------------------------------
// Test Procedures
//---------------------------------------------------------------------------
task automatic test_packets(int num_pkts = NUM_PKTS);
pkt_t::data_t data_count = 0;
mailbox pkt_mb = new();
// Reset the packet counters
clk_gen.clk_wait_f();
pkt_count_rst <= 1;
clk_gen.clk_wait_f();
pkt_count_rst <= 0;
fork
begin : send_thread
for (int pkt_count = 0; pkt_count < num_pkts; pkt_count++) begin
pkt_t pkt;
int pkt_length;
logic [DATA_W-1:0] data;
logic [USER_W-1:0] user;
pkt = new();
pkt_length = $urandom_range(1, MAX_PKT_LEN);
repeat (pkt_length) begin
data = USE_RANDOM ? Rand#(DATA_W)::rand_logic() : data_count;
user = USE_RANDOM ? Rand#(DATA_W)::rand_logic() : ~data_count;
pkt.data.push_back(data);
pkt.user.push_back(user);
data_count++;
end
// Put the packet count in the first byte of the packet so we can
// easily tell the packets apart.
pkt.data[0][0+:PKT_COUNT_W] = pkt_count;
bfm.put(pkt);
pkt_mb.put(pkt.copy());
end
end
begin : recv_thread
for (int pkt_count = 0; pkt_count < num_pkts; pkt_count++) begin
pkt_t act_pkt, exp_pkt;
bfm.get(act_pkt);
act_pkt.keep = {}; // Not using keep, so remove the X's
pkt_mb.get(exp_pkt);
// Verify that we got the expected data
`ASSERT_ERROR(
exp_pkt.equal(act_pkt),
$sformatf("On packet %0d, actual does not match expected", pkt_count)
);
end
end
join
endtask : test_packets
//---------------------------------------------------------------------------
// Main
//---------------------------------------------------------------------------
initial begin : main
string tb_name;
// Generate a string for the name of this instance of the testbench
tb_name = $sformatf({
"rfnoc_block_fft_tb\n",
"\tMAX_PKT_LEN = %0d\n",
"\tDATA_W = %0d\n",
"\tUSER_W = %0d\n",
"\tUSER_SEL = %0d\n",
"\tINT_DATA_W = %0d\n",
"\tNUM_PORTS = %0d\n",
"\tINT_FIFO_SIZE = %0d\n",
"\tEXT_FIFO_SIZE = %0d"},
MAX_PKT_LEN, DATA_W, USER_W, USER_SEL, INT_DATA_W, NUM_PORTS,
INT_FIFO_SIZE, EXT_FIFO_SIZE
);
// Initialize the test exec object for this testbench
test.start_tb(tb_name);
// Start the clocks
clk_gen.start();
// Start the BFMs
bfm.run();
//--------------------------------
// Reset
//--------------------------------
test.start_test("Reset");
clk_gen.reset();
@(negedge rst);
test.end_test();
//--------------------------------
// Tests
//--------------------------------
test.start_test("Packet tests (normal)");
bfm.set_slave_stall_prob(50);
bfm.set_master_stall_prob(50);
test_packets();
test.end_test();
test.start_test("Packet tests (back-pressure)");
bfm.set_slave_stall_prob(95);
bfm.set_master_stall_prob(5);
test_packets();
test.end_test();
test.start_test("Packet tests (underflow)");
bfm.set_slave_stall_prob(95);
bfm.set_master_stall_prob(5);
test_packets();
test.end_test();
//--------------------------------
// Finish Up
//--------------------------------
// Display final statistics and results
test.end_tb(0);
// Kill the clocks to end this instance of the testbench
clk_gen.kill();
end
endmodule