From c48f8bd5f15887707e54bd26f146015bd21dbccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=B6hler?= Date: Mon, 10 Feb 2025 14:24:18 +0100 Subject: [PATCH] fpga: rfnoc: lib: add AXI FIR filter for >1 SPC Original-commit: 90ad1fecb62b804f543def6e4c680706d76442b5 --- lib/dsp/Makefile.srcs | 1 + lib/dsp/axi_fir_multisample_filter.sv | 284 ++++++++++ .../axi_fir_multisample_filter_tb/Makefile | 45 ++ .../axi_fir_multisample_filter_tb.sv | 528 ++++++++++++++++++ .../axi_fir_multisample_filter_tb_wrapper.sv | 49 ++ 5 files changed, 907 insertions(+) create mode 100644 lib/dsp/axi_fir_multisample_filter.sv create mode 100644 lib/dsp/sim/axi_fir_multisample_filter_tb/Makefile create mode 100644 lib/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb.sv create mode 100644 lib/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb_wrapper.sv diff --git a/lib/dsp/Makefile.srcs b/lib/dsp/Makefile.srcs index a322511..e39dcd1 100644 --- a/lib/dsp/Makefile.srcs +++ b/lib/dsp/Makefile.srcs @@ -44,4 +44,5 @@ small_hb_int.v \ srl.v \ tx_frontend.v \ variable_delay_line.v \ +axi_fir_multisample_filter.sv \ )) diff --git a/lib/dsp/axi_fir_multisample_filter.sv b/lib/dsp/axi_fir_multisample_filter.sv new file mode 100644 index 0000000..78e7ad9 --- /dev/null +++ b/lib/dsp/axi_fir_multisample_filter.sv @@ -0,0 +1,284 @@ +// +// Copyright 2025 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Parameterized multi-sample FIR filter with AXI-stream interface. +// +// Description: +// +// For each sample per cycle a separate FIR filter with the given number of coefficients is +// instantiated. The filter is implemented as a chain of multiply-accumulate slices. +// A shift register is used to store the input samples as long as they are needed. +// The FIR filters are fed with the appropriate samples from the shift register. +// The indices into the shift registers are calculated at compile time. +// +// For the most efficient DSP slice inference use these settings: +// IN_WIDTH < 25, COEFF_WIDTH < 18, ACCUM_WIDTH < 48 +// +// Parameters (widths are in bits): +// +// IN_WIDTH - Input width of a single sample +// NUM_SPC - Samples per cycle +// COEFF_WIDTH - Coefficient width +// OUT_WIDTH - Output width of a single sample +// NUM_COEFFS - Number of coefficients / taps +// CLIP_BITS - If IN_WIDTH != OUT_WIDTH, number of MSBs to drop +// ACCUM_WIDTH - Accumulator width +// COEFFS_VEC - Vector of NUM_COEFFS values, each of width COEFF_WIDTH to +// initialize coeffs. Defaults to an impulse. +// RELOADABLE_COEFFS - Enable (1) or disable (0) reloading coefficients at runtime (via reload bus) +// BLANK_OUTPUT - Disable (1) or enable (0) output when initially filling internal pipeline +// USE_EMBEDDED_REGS_COEFFS - Reduce register usage by only using embedded registers in DSP slices. +// Updating taps while streaming will cause temporary output corruption! +// Notes: +// - If using USE_EMBEDDED_REGS_COEFFS, coefficients must be written at least once as COEFFS_VEC is ignored! +// - If using RELOADABLE_COEFFS, coefficients must be written in reverse order! + + +module axi_fir_multisample_filter #( + int IN_WIDTH = 16, + int NUM_SPC = 4, + int COEFF_WIDTH = 16, + int OUT_WIDTH = 16, + int NUM_COEFFS = 41, + int CLIP_BITS = $clog2(NUM_COEFFS), + int ACCUM_WIDTH = IN_WIDTH+COEFF_WIDTH+$clog2(NUM_COEFFS)-1, + bit [NUM_COEFFS*COEFF_WIDTH-1:0] COEFFS_VEC = + {{1'b0,{(COEFF_WIDTH-1){1'b1}}},{(COEFF_WIDTH*(NUM_COEFFS-1)){1'b0}}}, + bit RELOADABLE_COEFFS = 1, + bit BLANK_OUTPUT = 1, + bit USE_EMBEDDED_REGS_COEFFS = 1 +)( + // clocks and control signals + input logic clk, + input logic reset, + input logic clear, + + // AXI stream data input interface + input logic [NUM_SPC*IN_WIDTH-1:0] s_axis_data_tdata, + input logic s_axis_data_tlast, + input logic s_axis_data_tvalid, + output logic s_axis_data_tready, + + // AXI stream data output interface + output logic [NUM_SPC*OUT_WIDTH-1:0] m_axis_data_tdata, + output logic m_axis_data_tlast, + output logic m_axis_data_tvalid, + input logic m_axis_data_tready, + + // AXI stream coefficient interface + input logic [COEFF_WIDTH-1:0] s_axis_reload_tdata, + input logic s_axis_reload_tvalid, + input logic s_axis_reload_tlast, + output logic s_axis_reload_tready +); + + localparam int PIPELINE_DELAY = NUM_COEFFS + 5; // +4 pipeline depth in fir_filter_slice.v, +1 of shift register + localparam int SHIFT_REG_WIDTH = (NUM_SPC + 1) * NUM_COEFFS; // length of shift register + + logic [ACCUM_WIDTH-1:0] m_axis_data_tdata_int [NUM_SPC-1:0]; + logic [NUM_SPC-1:0] m_axis_data_tvalid_int; + logic [NUM_SPC-1:0] m_axis_data_tready_int; + logic [NUM_SPC-1:0] m_axis_data_tlast_int; + logic [NUM_SPC-1:0] m_axis_data_tvalid_array; + logic [NUM_SPC-1:0] m_axis_data_tlast_array; + + /////////////////////////////////////////////////////// + // + // Coefficient loading / reloading + // + /////////////////////////////////////////////////////// + + reg [COEFF_WIDTH-1:0] coeffs[0:NUM_COEFFS-1]; + reg coeff_load_stb = 1'b1; + generate + if (RELOADABLE_COEFFS) begin + // Use DSP slice registers to hold coefficients. While loading + // coefficients, input sample data should be throttled if corrupted + // output samples are unacceptable + if (USE_EMBEDDED_REGS_COEFFS) begin + always @(*) begin + coeff_load_stb <= s_axis_reload_tvalid & s_axis_reload_tready; + end + end else begin + always @(posedge clk) begin + if (reset | clear) begin + for (int k = 0; k < NUM_COEFFS; k = k + 1) begin + coeffs[k] <= COEFFS_VEC[COEFF_WIDTH*k +: COEFF_WIDTH]; + end + // Initialize coefficients at reset + coeff_load_stb <= 1'b1; + end else begin + if (s_axis_reload_tvalid & s_axis_reload_tready) begin + // Inverted direction to reload coeff + for (int k = NUM_COEFFS-1; k > 0; k = k - 1) begin + coeffs[k] <= coeffs[k-1]; + end + coeffs[0] <= s_axis_reload_tdata; + end + coeff_load_stb <= s_axis_reload_tvalid & s_axis_reload_tready & s_axis_reload_tlast; + end + end + end + end else begin + // Coefficients are static + initial begin + for (int k = NUM_COEFFS-1; k >= 0; k = k - 1) begin + coeffs[k] <= COEFFS_VEC[COEFF_WIDTH*k +: COEFF_WIDTH]; + coeff_load_stb <= 1'b1; + end + end + end + endgenerate + + assign s_axis_reload_tready = 1'b1; + + /////////////////////////////////////////////////////// + // + // Multisample FIR Filter + // + /////////////////////////////////////////////////////// + + reg [IN_WIDTH-1:0] data_shift_reg [0 : SHIFT_REG_WIDTH-1]; + + initial begin + for (int k= 0; k < SHIFT_REG_WIDTH; k = k + 1) begin + data_shift_reg[k] <= 0; + end + end + + // s_axis_data_tdata given as x[n-1],x[n-2],...,x[2],x[1],x[0]. + // data_shift_reg is organized to contain samples in natural order. + // data_shift_reg index ... | 5 | 4 | 3 | 2 | 1 | 0 | + // cycle 0: ....| ? | ? | ? | x0 | x1 | x2 | + // cycle 1: ....| x0 | x1 | x2 | x3 | x4 | x5 | + // Data from axi data port is stored in reversed order starting from shift register index 0. + // For remaining indices samples are shifted by NUM_SPC each cycle. + // data_shift_reg works like: + // <---- shift by NUM_SPC + + always @(posedge clk) begin + if (s_axis_data_tvalid & s_axis_data_tready) begin + // Wire input to lower register position + for (int k = 0; k < NUM_SPC; k = k + 1) begin + automatic int k_flipped = NUM_SPC-k-1; + data_shift_reg[k] <= s_axis_data_tdata [k_flipped *IN_WIDTH +: IN_WIDTH ]; + end + // Shift contents by NUM_SPC to the upper position of shift register + for (int k = NUM_SPC; k < SHIFT_REG_WIDTH; k = k + 1) begin + data_shift_reg[k] <= data_shift_reg[k-NUM_SPC]; + end + end + end + + // Counter to track pipeline fullness + reg [$clog2(PIPELINE_DELAY):0] cnt; + always @(posedge clk) begin + if (reset | clear) begin + cnt <= 0; + end else if (s_axis_data_tvalid & s_axis_data_tready) begin + if (cnt < PIPELINE_DELAY) begin + cnt <= cnt + 1; + end + end + end + + // tlast shift register + reg [PIPELINE_DELAY-1:0] tlast_shift_reg = 0; + always @(posedge clk) begin + if (s_axis_data_tvalid & s_axis_data_tready) begin + for (int k = 1; k < PIPELINE_DELAY; k = k + 1) begin + tlast_shift_reg[k] <= tlast_shift_reg[k-1]; + end + tlast_shift_reg[0] <= s_axis_data_tlast; + end + end + + // Instantiate NUM_SPC-numbers of DSP-chain + generate + for (genvar k = 0; k < NUM_SPC; k = k + 1) begin : gen_DSP_chain + // K_FLIPPED: refer to the documentation above of data_shift_reg + localparam int K_FLIPPED = NUM_SPC-k-1; + + wire [ACCUM_WIDTH-1:0] sample_accum [0 : NUM_COEFFS]; // [0:NUM_COEFFS] to make the + wire [COEFF_WIDTH-1:0] coeff_forward [0 : NUM_COEFFS]; // generate loop easier to read + + assign sample_accum[0] = 0; + assign coeff_forward[0] = s_axis_reload_tdata; + + // Build up FIR filter with multiply-accumulate slices (fir_filter_slice). + // Now generate the slices for each chain + for (genvar j = 0; j < NUM_COEFFS ; j = j + 1) begin : gen_slice + fir_filter_slice #( + .IN_WIDTH(IN_WIDTH), + .COEFF_WIDTH(COEFF_WIDTH), + .ACCUM_WIDTH(ACCUM_WIDTH), + .OUT_WIDTH(ACCUM_WIDTH)) + fir_filter_slice ( + .clk(clk), + .reset(reset), + .clear(clear), + .sample_in_stb(s_axis_data_tvalid & s_axis_data_tready), + // NUM_SPC is added j times due to the pipeline delay. + // +1 for selecting the next index for the FIR result calculation + // K_FLIPPED is the offset into data_shift_reg + .sample_in_a(data_shift_reg[ j*(NUM_SPC+1) + K_FLIPPED ]), + // sample_in_b is used to implement symmetric coefficients, always 0 if SYMMETRIC_COEFFS = 0 + .sample_in_b('0), // symmetric disabled, thus empty + .sample_forward(), + // For proper coeffient loading, coeff_forward must be shifted in backwards. coeffs[] is already backwards + .coeff_in(((USE_EMBEDDED_REGS_COEFFS == 1) && (RELOADABLE_COEFFS == 1)) ? coeff_forward[j] : coeffs[j]), + .coeff_forward(coeff_forward[j+1]), + .coeff_load_stb(coeff_load_stb), + .sample_accum(sample_accum[j]), + .sample_out(sample_accum[j+1]) + ); + end : gen_slice + + always_comb begin + // zero data and valid bit for the ring-in of the pipeline + if (BLANK_OUTPUT == 1 && cnt < PIPELINE_DELAY) begin + m_axis_data_tdata_int[k] = '0; + m_axis_data_tvalid_int[k] = '0; + end else begin + m_axis_data_tdata_int[k] = sample_accum[NUM_COEFFS]; + m_axis_data_tvalid_int[k] = s_axis_data_tvalid; + end + // tlast is masked the same way during ring-in. + // Depending on the blanking mode tlast will be delayed or taken from the input. + if (BLANK_OUTPUT == 1) begin + if (cnt < PIPELINE_DELAY) begin + m_axis_data_tlast_int[k] = 1'b0; + end else begin + m_axis_data_tlast_int[k] = tlast_shift_reg[PIPELINE_DELAY-1]; + end + end else begin + m_axis_data_tlast_int[k] = s_axis_data_tlast; + end + end + + axi_round_and_clip #( + .WIDTH_IN(ACCUM_WIDTH), + .WIDTH_OUT(OUT_WIDTH), + .CLIP_BITS(CLIP_BITS)) + inst_axi_round_and_clip ( + .clk(clk), + .reset(reset | clear), + .i_tdata(m_axis_data_tdata_int[k]), + .i_tlast(m_axis_data_tlast_int[k]), + .i_tvalid(m_axis_data_tvalid_int[k]), + .i_tready(m_axis_data_tready_int[k]), // output + .o_tdata(m_axis_data_tdata[k*OUT_WIDTH +: OUT_WIDTH]), + .o_tlast(m_axis_data_tlast_array[k]), + .o_tvalid(m_axis_data_tvalid_array[k]), + .o_tready(m_axis_data_tready) //input + ); + end : gen_DSP_chain + endgenerate + + assign s_axis_data_tready = m_axis_data_tready_int[0]; + assign m_axis_data_tvalid = m_axis_data_tvalid_array[0]; + assign m_axis_data_tlast = m_axis_data_tlast_array[0]; + +endmodule diff --git a/lib/dsp/sim/axi_fir_multisample_filter_tb/Makefile b/lib/dsp/sim/axi_fir_multisample_filter_tb/Makefile new file mode 100644 index 0000000..67e284e --- /dev/null +++ b/lib/dsp/sim/axi_fir_multisample_filter_tb/Makefile @@ -0,0 +1,45 @@ +# +# Copyright 2025 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 + +#------------------------------------------------- +# Design Specific +#------------------------------------------------- +# Include makefiles and sources for the DUT and its dependencies + +include $(BASE_DIR)/../lib/rfnoc/Makefile.srcs + + +DESIGN_SRCS += $(abspath \ +) + +#$(RFNOC_CORE_SRCS) \ + +#------------------------------------------------- +# Testbench Specific +#------------------------------------------------- +SIM_TOP = axi_fir_multisample_filter_tb_wrapper + +SIM_SRCS = \ +$(abspath axi_fir_multisample_filter_tb.sv) \ +$(abspath axi_fir_multisample_filter_tb_wrapper.sv) \ + +# MODELSIM_USER_DO = $(abspath wave.do) + +#------------------------------------------------- +# 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/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb.sv b/lib/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb.sv new file mode 100644 index 0000000..b5f06b1 --- /dev/null +++ b/lib/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb.sv @@ -0,0 +1,528 @@ +// +// Copyright 2025 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: axi_fir_multisample_filter_tb +// +// Description: +// +// Testbench for axi_fir_multisample_filter. +// +// Parameters: +// NUM_SPC: How much sample per cycle is set +// NUM_COEFFS: Number of coefficients / taps +// RELOADABLE_COEFFS - Enable (1) or disable (0) reloading coefficients at runtime (via reload bus) +// BLANK_OUTPUT - Disable (1) or enable (0) output when initially filling internal pipeline +// USE_EMBEDDED_REGS_COEFFS - Reduce register usage by only using embedded registers in DSP slices. +// Updating taps while streaming will cause temporary output corruption! +// + +module axi_fir_multisample_filter_tb #( + parameter NUM_SPC = 8, + parameter NUM_COEFFS = 41, + parameter RELOADABLE_COEFFS = 1, + parameter BLANK_OUTPUT = 1, + parameter USE_EMBEDDED_REGS_COEFFS = 1 +) (); + + `include "test_exec.svh" + + import PkgTestExec::*; + import PkgAxiStreamBfm::*; + import PkgRandom::*; + + //--------------------------------------------------------------------------- + // Testbench Configuration + //--------------------------------------------------------------------------- + // Local Parameters + //--------------------------------------------------------------------------- + + // Simulation parameters + localparam real AXI_CLK_PER = 10.0; // 100 MHz + + localparam int STALL_PROB = 38; // BFM stall probability, default 38 + + // DUT parameters to test + localparam int IN_WIDTH = 16; + localparam int AXI_WIDTH = IN_WIDTH * NUM_SPC; + localparam int COEFF_WIDTH = 16; + localparam int OUT_WIDTH = 16; + localparam int CLIP_BITS = $clog2(NUM_COEFFS); + localparam int ACCUM_WIDTH = IN_WIDTH + COEFF_WIDTH + CLIP_BITS - 1; + localparam int PIPELINE_DELAY = NUM_COEFFS + 5; + + // How many groups of multisampled-input needed + localparam int IN_GROUP_NUM = $ceil(NUM_COEFFS*1.0/NUM_SPC); + // +10 is pipeline compensation + localparam int FLUSH_CYCLE = ((NUM_SPC < NUM_COEFFS) ? NUM_COEFFS : NUM_SPC ) +10; + + localparam logic [COEFF_WIDTH*NUM_COEFFS-1:0] COEFFS_VEC_0 = { + 16'sd158, 16'sd0, 16'sd33, -16'sd0, -16'sd256, + 16'sd553, 16'sd573, -16'sd542, -16'sd1012, 16'sd349, + 16'sd1536, 16'sd123, -16'sd2097, -16'sd1012, 16'sd1633, + 16'sd1608, -16'sd3077, -16'sd5946, 16'sd3370, 16'sd10513, + -16'sd19295, // 16'sd19295, change to negative to avoid clipping + 16'sd10513, 16'sd3370, -16'sd5946, -16'sd3077, 16'sd1608, + 16'sd1633, -16'sd1012, -16'sd2097, 16'sd123, 16'sd1536, + 16'sd349, -16'sd1012, -16'sd542, 16'sd573, 16'sd553, + -16'sd256, -16'sd0, 16'sd33, 16'sd0, 16'sd158 + }; + + //--------------------------------------------------------------------------- + // Clocks and Resets + //--------------------------------------------------------------------------- + + bit axi_clk, axi_rst, axi_clear=0; + sim_clock_gen #(.PERIOD(AXI_CLK_PER)) axi_clk_gen (.clk(axi_clk), .rst(axi_rst)); + + //--------------------------------------------------------------------------- + // Bus Functional Models + //--------------------------------------------------------------------------- + + // Deafult interface data width is 64bits + typedef AxiStreamBfm #(AXI_WIDTH)::AxisPacket_t AxisPacket_in; + typedef AxiStreamBfm #(AXI_WIDTH)::AxisPacket_t AxisPacket_out; + typedef AxiStreamBfm #(COEFF_WIDTH)::AxisPacket_t AxisPacket_reload; + + AxiStreamIf #(AXI_WIDTH) AxisIf_m (axi_clk, axi_rst); + AxiStreamIf #(AXI_WIDTH) AxisIf_s (axi_clk, axi_rst); + AxiStreamIf #(COEFF_WIDTH) AxisIf_reload (axi_clk, axi_rst); + + // Connect BFM to interface + AxiStreamBfm #(AXI_WIDTH) AxisIf_sample_bfm = new(AxisIf_m,AxisIf_s); + AxiStreamBfm #(COEFF_WIDTH) AxisIf_coeff_bfm = new(AxisIf_reload, null); + + AxisPacket_in packet_in; + AxisPacket_out packet_out; + + //--------------------------------------------------------------------------- + // DUT + //--------------------------------------------------------------------------- + + // DUT Slave (Input) Port Signals + logic [NUM_SPC*IN_WIDTH-1:0] s_axis_data_tdata; + logic s_axis_data_tlast; + logic s_axis_data_tvalid; + logic s_axis_data_tready; + + // DUT Master (Output) Port Signals + logic [NUM_SPC*OUT_WIDTH-1:0] m_axis_data_tdata; + logic m_axis_data_tlast; + logic m_axis_data_tvalid; + logic m_axis_data_tready; + + // DUT Coefficient Reload (Input) Port Signals + logic [COEFF_WIDTH-1:0] s_axis_reload_tdata; + logic s_axis_reload_tlast; + logic s_axis_reload_tvalid; + logic s_axis_reload_tready; + + // Random Coeff + logic [COEFF_WIDTH*NUM_COEFFS-1:0] COEFFS_VEC_RANDOM; + + // Link to interface + assign s_axis_data_tdata = AxisIf_m.tdata; + assign s_axis_data_tlast = AxisIf_m.tlast; + assign s_axis_data_tvalid = AxisIf_m.tvalid; + assign AxisIf_m.tready = s_axis_data_tready; + + assign AxisIf_s.tdata = m_axis_data_tdata; + assign AxisIf_s.tlast = m_axis_data_tlast; + assign AxisIf_s.tvalid = m_axis_data_tvalid; + assign m_axis_data_tready = AxisIf_s.tready; + + assign s_axis_reload_tdata = AxisIf_reload.tdata; + assign s_axis_reload_tlast = AxisIf_reload.tlast; + assign s_axis_reload_tvalid = AxisIf_reload.tvalid; + assign AxisIf_reload.tready = s_axis_reload_tready; + + // Map the array of AXI to a flat vector for the DUT + axi_fir_multisample_filter #( + .IN_WIDTH(IN_WIDTH), + .NUM_SPC(NUM_SPC), + .COEFF_WIDTH(COEFF_WIDTH), + .OUT_WIDTH(OUT_WIDTH), + .NUM_COEFFS(NUM_COEFFS), + .CLIP_BITS(CLIP_BITS), + .ACCUM_WIDTH(ACCUM_WIDTH), + .COEFFS_VEC(COEFFS_VEC_0), + .RELOADABLE_COEFFS(RELOADABLE_COEFFS), + .BLANK_OUTPUT(BLANK_OUTPUT), + .USE_EMBEDDED_REGS_COEFFS(USE_EMBEDDED_REGS_COEFFS) + ) axi_fir__multisample_filter_i( + .clk(axi_clk), + .reset(axi_rst), + .clear(axi_clear), + .s_axis_data_tdata(s_axis_data_tdata), + .s_axis_data_tlast(s_axis_data_tlast), + .s_axis_data_tvalid(s_axis_data_tvalid), + .s_axis_data_tready(s_axis_data_tready), + .m_axis_data_tdata(m_axis_data_tdata), + .m_axis_data_tlast(m_axis_data_tlast), + .m_axis_data_tvalid(m_axis_data_tvalid), + .m_axis_data_tready(m_axis_data_tready), + .s_axis_reload_tdata(s_axis_reload_tdata), + .s_axis_reload_tvalid(s_axis_reload_tvalid), + .s_axis_reload_tlast(s_axis_reload_tlast), + .s_axis_reload_tready(s_axis_reload_tready) + ); + + + //--------------------------------------------------------------------------- + // Local Functions and Tasks + //--------------------------------------------------------------------------- + + // Local reset function + task reset_axi (input int rst_cyc = 100); + axi_clk_gen.clk_wait_f(); + axi_clk_gen.reset(); + wait(axi_rst == 0); + repeat (rst_cyc) axi_clk_gen.clk_wait_f(); + endtask : reset_axi + + // Local random coeff generation function + function automatic void random_coeff_generation (output [COEFF_WIDTH*NUM_COEFFS-1:0] COEFFS_VEC_RANDOM); + logic signed [COEFF_WIDTH-1:0] random_coeff; + for (int i = 0; i < NUM_COEFFS; i++) begin + // Truncate 32 bits to 16 bits and make it signed + random_coeff = Rand #(COEFF_WIDTH)::rand_sbit_range(-2000, 2000); + COEFFS_VEC_RANDOM[i*COEFF_WIDTH +: COEFF_WIDTH] = random_coeff; + end + endfunction:random_coeff_generation + + // Local function to collect single samples + // until all the samples collected, add into packet. + // Flush : 0 (send valid data)/ 1 (automatically flush) + task add_sample (input logic [IN_WIDTH-1:0] sample, input int flush = 0); + static logic [AXI_WIDTH-1:0] sample_collected = ('0); + static int count_SPC = 0; // NUM_SPC samples as one group + + // append sample + sample_collected [count_SPC * IN_WIDTH +: IN_WIDTH] = sample; + count_SPC = count_SPC + 1; + + // fill remaining data until vector is complete + if (flush == 1) begin + for (int i = count_SPC; i=0; i--) begin + packet_reload.data.push_back(COEFFS_VEC_0[COEFF_WIDTH*i +: COEFF_WIDTH]); + end + AxisIf_coeff_bfm.put(packet_reload); + AxisIf_coeff_bfm.wait_complete(); + test.end_test(); + end + end + + //----------------------------------------------------------------------- + // Test impulse response with default coefficients + //----------------------------------------------------------------------- + // + // Sending an impulse should cause the coefficients to be output. + // + //----------------------------------------------------------------------- + + begin + logic signed [COEFF_WIDTH-1:0] i_coeff, i_samp_int; + string s; + + test.start_test("Test impulse response (default coefficients)", 20us); + + packet_in.empty(); + + // Send single sample to DUT + // Function will automatically packet and send + add_sample(16'h7FFF); + for (int i = 1; i < NUM_COEFFS; i++) begin + add_sample(16'h0000); + end + // Compensate the unfilled data in last group + add_sample('0, 1); + AxisIf_sample_bfm.put(packet_in.copy()); + + // Enqueue flushing packet and Residue to push the data out + packet_in.empty(); + flush_axi(); + + // If BLANK_OUTPUT enabled, internal pipeline fullfilled. + // The correct output is supposed to apprear after. + // Keep grabbing until the first non_zero output + if (BLANK_OUTPUT == 0) begin + do begin + get_sample(i_samp_int, 0); + end while(i_samp_int== ('0)); + end + + // Correctness check + for (int i=0 ; i< NUM_COEFFS; i++) begin + if (BLANK_OUTPUT == 0 && i==0) begin + i_samp_int = i_samp_int; // verify the first one, which is already grabbed out + end else begin + get_sample(i_samp_int, i == 0); // ask one single sample from output + end + i_coeff = $signed(COEFFS_VEC_0[COEFF_WIDTH*i +: COEFF_WIDTH]); + + $sformat( + s, "Incorrect I value received on sample %0d! Expected: %0d, Received: %0d", + i, i_coeff, i_samp_int); + `ASSERT_ERROR( + (i_samp_int == i_coeff) || (i_samp_int-1 == i_coeff) || (i_samp_int+1 == i_coeff), + s); + end + + test.end_test(); + end + + //----------------------------------------------------------------------- + // Load random coefficients + //----------------------------------------------------------------------- + + // If RELOADABLE_COEFFS disabled, skip the rest + if (RELOADABLE_COEFFS==1) begin + begin + automatic AxisPacket_reload packet_reload = new(); + + test.start_test("Load random coefficients", 10us); + // Generate packet which contains new coefficients and enqueue it for transfer + packet_reload.empty(); + // reload must send data in reverse direction + for (int i= NUM_COEFFS-1 ; i>=0; i--) begin + packet_reload.data.push_back(COEFFS_VEC_RANDOM[COEFF_WIDTH*i +: COEFF_WIDTH]); + end + AxisIf_coeff_bfm.put(packet_reload); + AxisIf_coeff_bfm.wait_complete(); + + test.end_test(); + end + + //----------------------------------------------------------------------- + // Test impulse response with random coefficients + //----------------------------------------------------------------------- + // + // Sending an impulse should cause the coefficients to be output. + // + //----------------------------------------------------------------------- + + begin + logic signed [COEFF_WIDTH-1:0] i_coeff, i_samp_int; + string s; + + test.start_test("Test impulse response (random coefficients)", 20us); + + packet_in.empty(); + + // Send single sample to DUT + // Function will automatically packet and send + add_sample(16'h7FFF); + for (int i = 1; i < NUM_COEFFS; i++) begin + add_sample(16'h0000); + end + // Compensate the unfilled data in last group + add_sample('0, 1); + AxisIf_sample_bfm.put(packet_in.copy()); + + // Enqueue flushing packet and Residue to push the data out + packet_in.empty(); + flush_axi(); + + // When BLANK_OUTPUT enabled, no extra ignore needed + if (BLANK_OUTPUT == 0) begin + do begin + get_sample(i_samp_int, 0); + end while(i_samp_int== ('0)); + // Ignore the packet as the result of last flushing + end else begin + AxisIf_sample_bfm.get(packet_out); + end + + // Correctness check + for (int i=0 ; i< NUM_COEFFS; i++) begin + if (!(BLANK_OUTPUT == 0 && i==0)) begin + get_sample(i_samp_int, i == 0); // ask one single sample from output + end + i_coeff = $signed(COEFFS_VEC_RANDOM[COEFF_WIDTH*i +: COEFF_WIDTH]); + + $sformat( + s, "Incorrect I value received on sample %0d! Expected: %0d, Received: %0d", + i, i_coeff, i_samp_int); + `ASSERT_ERROR( + (i_samp_int == i_coeff) || (i_samp_int-1 == i_coeff) || (i_samp_int+1 == i_coeff), + s); + end + + test.end_test(); + end + + //----------------------------------------------------------------------- + // Test step response with random coefficients + //----------------------------------------------------------------------- + + begin + logic signed [COEFF_WIDTH-1:0] i_samp_int; + string s; + int coeff_sum; + + test.start_test("Test step response (random coefficients)", 20us); + + packet_in.empty(); + + // Send single sample to DUT + // Function will automatically packet and send + for (int i = 0; i < NUM_COEFFS; i++) begin + add_sample(16'h7FFF); + end + // Compensate the unfilled data in last group + add_sample('0, 1); + AxisIf_sample_bfm.put(packet_in.copy()); + + // Enqueue flushing packet and Residue to push the data out + packet_in.empty(); + flush_axi(); + + // When BLANK_OUTPUT enabled, no extra ignore needed + if (BLANK_OUTPUT == 0) begin + do begin + get_sample(i_samp_int, 0); + end while(i_samp_int== ('0)); + // Ignore the packet as the result of last flushing + end else begin + AxisIf_sample_bfm.get(packet_out); + end + + // Correctness check + for (int i=0 ; i< NUM_COEFFS; i++) begin + if (BLANK_OUTPUT == 0 && i==0) begin + i_samp_int = i_samp_int; // verify the first one, which is already grabbed out + end else begin + get_sample(i_samp_int, i == 0); // ask one single sample from output + end + coeff_sum += $signed(COEFFS_VEC_RANDOM[COEFF_WIDTH*i +: COEFF_WIDTH]); + + $sformat( + s, "Incorrect I value received on sample %0d! Expected: %0d, Received: %0d", + i, coeff_sum, i_samp_int); + `ASSERT_ERROR( + (i_samp_int == coeff_sum) || (i_samp_int-1 == coeff_sum) || (i_samp_int+1 == coeff_sum), + s); + end + + test.end_test(); + end + end + //------------------------------------------------------------------------- + // All done! + //------------------------------------------------------------------------- + // End the testbench and stop the clock module + test.end_tb(0); + axi_clk_gen.kill(); + + end : tb_main +endmodule diff --git a/lib/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb_wrapper.sv b/lib/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb_wrapper.sv new file mode 100644 index 0000000..e51cea4 --- /dev/null +++ b/lib/dsp/sim/axi_fir_multisample_filter_tb/axi_fir_multisample_filter_tb_wrapper.sv @@ -0,0 +1,49 @@ +// +// Copyright 2025 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: axi_fir_multisample_filter_tb_wrapper +// +// Description: +// +// testing various configurations of the filter +// + +module axi_fir_multisample_filter_tb_wrapper(); + +//--------------------------------------------------------------------------- +// Local Parameters +//--------------------------------------------------------------------------- +timeunit 1ns / 1ps; + +localparam integer NUM_SPC_TEST[2:0] = {2 , 4 , 8}; +localparam integer NUM_COEFFS_TEST[2:0] = {5 , 17 , 41}; +localparam integer PARA_TEST[1:0] = {0 , 1}; + +//--------------------------------------------------------------------------- +// Nested test cases +//--------------------------------------------------------------------------- + +// Full coverage nested test loop +generate + for (genvar i = 0; i < $size(NUM_SPC_TEST); i = i + 1) begin : test_NUM_SPC + for (genvar j = 0; j < $size(NUM_COEFFS_TEST); j = j + 1) begin : test_NUM_COEFFS + for (genvar k = 0; k < $size(PARA_TEST); k = k + 1) begin : test_RELOADABLE + for (genvar m = 0; m < $size(PARA_TEST); m = m + 1) begin : test_BLANK + for (genvar n = 0; n < $size(PARA_TEST); n = n + 1) begin : test_EMBEDDED + axi_fir_multisample_filter_tb #( + .NUM_SPC(NUM_SPC_TEST[i]), + .NUM_COEFFS(NUM_COEFFS_TEST[j]), + .RELOADABLE_COEFFS(PARA_TEST[k]), + .BLANK_OUTPUT(PARA_TEST[m]), + .USE_EMBEDDED_REGS_COEFFS(PARA_TEST[n]) + ) tb_i (); + end : test_EMBEDDED + end : test_BLANK + end : test_RELOADABLE + end : test_NUM_COEFFS + end : test_NUM_SPC +endgenerate + +endmodule