From bb2bfbfe95e5d01e95104f85c04d60d71a610b32 Mon Sep 17 00:00:00 2001 From: Wade Fife Date: Wed, 2 Oct 2024 17:23:44 -0500 Subject: [PATCH] fpga: lib: Add AXI-Stream load split and merge modules Original-commit: 32fb626ef1b1d3078fd331143b03cd38d1e155f0 --- lib/axi/Makefile.srcs | 2 + lib/axi/axis_load_merge.sv | 302 +++++++++++++++ lib/axi/axis_load_split.sv | 280 ++++++++++++++ lib/sim/axi/axis_load_split_merge/Makefile | 34 ++ .../axis_load_split_merge_all_tb.sv | 88 +++++ .../axis_load_split_merge_tb.sv | 352 ++++++++++++++++++ 6 files changed, 1058 insertions(+) create mode 100644 lib/axi/axis_load_merge.sv create mode 100644 lib/axi/axis_load_split.sv create mode 100644 lib/sim/axi/axis_load_split_merge/Makefile create mode 100644 lib/sim/axi/axis_load_split_merge/axis_load_split_merge_all_tb.sv create mode 100644 lib/sim/axi/axis_load_split_merge/axis_load_split_merge_tb.sv diff --git a/lib/axi/Makefile.srcs b/lib/axi/Makefile.srcs index a9d45a6..08c8362 100644 --- a/lib/axi/Makefile.srcs +++ b/lib/axi/Makefile.srcs @@ -35,6 +35,8 @@ axis_width_conv.v \ axis_split.v \ axis_split_bus.v \ axis_combine.v \ +axis_load_split.sv \ +axis_load_merge.sv \ axis_packetize.v \ axis_pkt_throttle.sv \ axis_data_if_packetize.v \ diff --git a/lib/axi/axis_load_merge.sv b/lib/axi/axis_load_merge.sv new file mode 100644 index 0000000..5035751 --- /dev/null +++ b/lib/axi/axis_load_merge.sv @@ -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 diff --git a/lib/axi/axis_load_split.sv b/lib/axi/axis_load_split.sv new file mode 100644 index 0000000..3601e21 --- /dev/null +++ b/lib/axi/axis_load_split.sv @@ -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 diff --git a/lib/sim/axi/axis_load_split_merge/Makefile b/lib/sim/axi/axis_load_split_merge/Makefile new file mode 100644 index 0000000..6da659b --- /dev/null +++ b/lib/sim/axi/axis_load_split_merge/Makefile @@ -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 diff --git a/lib/sim/axi/axis_load_split_merge/axis_load_split_merge_all_tb.sv b/lib/sim/axi/axis_load_split_merge/axis_load_split_merge_all_tb.sv new file mode 100644 index 0000000..9deccc7 --- /dev/null +++ b/lib/sim/axi/axis_load_split_merge/axis_load_split_merge_all_tb.sv @@ -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 diff --git a/lib/sim/axi/axis_load_split_merge/axis_load_split_merge_tb.sv b/lib/sim/axi/axis_load_split_merge/axis_load_split_merge_tb.sv new file mode 100644 index 0000000..eda69f8 --- /dev/null +++ b/lib/sim/axi/axis_load_split_merge/axis_load_split_merge_tb.sv @@ -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