From 31be5b875a838ec58dbabbc20a51c06f6db37b54 Mon Sep 17 00:00:00 2001 From: Niels Garibaldi Date: Tue, 25 Aug 2026 15:58:15 +0200 Subject: [PATCH] fpga: dsp: add rounding to CIC filter barrel shifter Add an additional rounding step to the cic barrel shifter module to avoid any DC bias occuring, caused by bit truncation without rounding. --- fpga/usrp3/lib/dsp/cic_barrel_shift.sv | 85 +++++-- fpga/usrp3/lib/dsp/cic_filter_decim.sv | 5 +- fpga/usrp3/lib/dsp/cic_filter_interp.sv | 4 +- .../lib/dsp/sim/cic/barrel_shift/Makefile | 38 +++ .../barrel_shift/cic_barrel_shift_all_tb.sv | 52 +++++ .../cic/barrel_shift/cic_barrel_shift_tb.sv | 217 ++++++++++++++++++ .../cic_filter_decim/cic_filter_decim_tb.sv | 23 +- .../cic_filter_interp/cic_filter_interp_tb.sv | 23 +- .../lib/dsp/sim/cic/cic_filter_test_pkg.sv | 73 +++++- 9 files changed, 455 insertions(+), 65 deletions(-) create mode 100644 fpga/usrp3/lib/dsp/sim/cic/barrel_shift/Makefile create mode 100644 fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_all_tb.sv create mode 100644 fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_tb.sv diff --git a/fpga/usrp3/lib/dsp/cic_barrel_shift.sv b/fpga/usrp3/lib/dsp/cic_barrel_shift.sv index 1b5aadcf2..b05d398a2 100644 --- a/fpga/usrp3/lib/dsp/cic_barrel_shift.sv +++ b/fpga/usrp3/lib/dsp/cic_barrel_shift.sv @@ -10,8 +10,16 @@ // Rate-dependent output scaling stage shared by the CIC decimation and // interpolation filters. Accepts SPC I/Q samples at the full accumulator // width (IN_WIDTH bits per sample) and right-shifts each I and Q component -// to normalize the CIC gain, then extracts the OUT_WIDTH/2 LSBs of the -// shifted result to produce OUT_WIDTH-wide output samples. +// to normalize the CIC gain, then rounds the result to OUT_WIDTH/2 bits per +// component. +// +// Each component enters the shifter scaled up by one bit, so the most +// significant bit that the shift would otherwise discard survives as the LSB +// of the shifted value. The final quantization is then an ordinary +// axi_round_and_clip stage that removes that single bit with +// round-to-nearest. Rounding this way keeps the quantization error zero-mean +// instead of biasing every sample towards negative infinity as plain +// truncation does. // // The required shift for rate factor R is ceil(log2((R*M)^N)), where M=1 // (differential delay) and N=ORDER. A compile-time lookup table @@ -19,10 +27,8 @@ // // The shift itself is a SHIFT_W-stage binary barrel shifter: stage k // conditionally shifts right by 2^k positions when shift_amount[k] is set. -// Pipeline depth is SHIFT_W = ceil(log2(IN_WIDTH/2 - OUT_WIDTH/2 + 1)) cycles. -// -// Each sample lane passes through an axi_fifo (SIZE=1) at the output to -// decouple backpressure from the shift pipeline. +// Pipeline depth is SHIFT_W = ceil(log2(IN_WIDTH/2 - OUT_WIDTH/2 + 1)) cycles +// plus the two register stages of the round and clip. // // Parameters: // @@ -69,6 +75,12 @@ module cic_barrel_shift #( localparam int MAX_SHIFT = IN_WIDTH/2 - OUT_WIDTH/2; localparam int SHIFT_W = $clog2(MAX_SHIFT + 1); + localparam int IN_COMP_W = IN_WIDTH/2; + localparam int OUT_COMP_W = OUT_WIDTH/2; + // One extra LSB so the bit dropped by the shift is available for rounding. + localparam int SHIFT_COMP_W = IN_COMP_W + 1; + // The normalized result fits in the output range; retain the round bit. + localparam int ROUND_COMP_W = OUT_COMP_W + 1; //--------------------------------------------------------------------------- // Compile-time shift lookup table @@ -109,24 +121,23 @@ module cic_barrel_shift #( for (genvar samp_idx = 0; samp_idx < SPC; samp_idx++) begin : gen_shift_out // Arithmetic right-shift pipeline: stage k shifts right by 2^k if // shift_amount[1][k] is set. - logic signed [IN_WIDTH/2-1:0] q_shifted[SHIFT_W+1]; - logic signed [IN_WIDTH/2-1:0] i_shifted[SHIFT_W+1]; - logic shifter_tvalid[SHIFT_W+1]; - logic shifter_tlast[SHIFT_W+1]; + logic signed [SHIFT_COMP_W-1:0] q_shifted[SHIFT_W+1]; + logic signed [SHIFT_COMP_W-1:0] i_shifted[SHIFT_W+1]; + logic shifter_tvalid[SHIFT_W+1]; + logic shifter_tlast[SHIFT_W+1]; logic stage_tready [SHIFT_W+1]; - // Stage 0: connect directly to the input bus. - assign q_shifted[0] = data_in.tdata[IN_WIDTH*samp_idx +: IN_WIDTH/2]; - assign i_shifted[0] = data_in.tdata[IN_WIDTH*samp_idx + IN_WIDTH/2 +: IN_WIDTH/2]; + // Stage 0: input scaled up by one bit so the shift keeps the round bit. + assign q_shifted[0] = {data_in.tdata[IN_WIDTH*samp_idx +: IN_COMP_W], 1'b0}; + assign i_shifted[0] = {data_in.tdata[IN_WIDTH*samp_idx + IN_COMP_W +: IN_COMP_W], 1'b0}; assign shifter_tvalid[0] = data_in.tvalid; assign shifter_tlast[0] = data_in.tlast; - assign stage_tready[SHIFT_W] = data_out.tready; for (genvar shift_bit = 0; shift_bit < SHIFT_W; shift_bit++) begin : gen_shift_bits // Combinational next-stage values. - logic signed [IN_WIDTH/2-1:0] q_next; - logic signed [IN_WIDTH/2-1:0] i_next; + logic signed [SHIFT_COMP_W-1:0] q_next; + logic signed [SHIFT_COMP_W-1:0] i_next; always_comb begin if (shift_amount[1][shift_bit]) begin q_next = q_shifted[shift_bit] >>> (1 << shift_bit); @@ -137,12 +148,12 @@ module cic_barrel_shift #( end end - // FIFO stage of size 0 each second step and size 1 on the last stage - // -> mux 4:1 which fits LUT6 best for Xilinx architectures before each - // registered stage. + // Register every second stage and pass through the others. This places + // a 4:1 mux before each registered stage, which fits LUT6 best for + // Xilinx architectures. axi_fifo #( - .WIDTH (IN_WIDTH + 1), // {tlast, i, q} - .SIZE ((shift_bit == SHIFT_W-1) ? 1 : (shift_bit % 2 == 1) ? 0 : -1) + .WIDTH (2*SHIFT_COMP_W + 1), // {tlast, i, q} + .SIZE ((shift_bit % 2 == 1) ? 0 : -1) ) pipe_ff ( .clk (clk), .reset (rst), @@ -159,11 +170,35 @@ module cic_barrel_shift #( end : gen_shift_bits + // The upper shifted bits are redundant sign extension. Keep the output + // component and the extra LSB needed for round-to-nearest. + logic [OUT_WIDTH-1:0] rounded; + logic rounded_tvalid; + logic rounded_tlast; + + axi_round_and_clip_complex #( + .WIDTH_IN (ROUND_COMP_W), + .WIDTH_OUT(OUT_COMP_W), + .CLIP_BITS(0), + .FIFOSIZE (1) + ) round_clip_complex ( + .clk (clk), + // axi_round_and_clip has no clear input, so flush it through reset. + .reset (rst | clear), + .i_tdata ({i_shifted[SHIFT_W][ROUND_COMP_W-1:0], q_shifted[SHIFT_W][ROUND_COMP_W-1:0]}), + .i_tlast (shifter_tlast[SHIFT_W]), + .i_tvalid(shifter_tvalid[SHIFT_W]), + .i_tready(stage_tready[SHIFT_W]), + .o_tdata (rounded), + .o_tlast (rounded_tlast), + .o_tvalid(rounded_tvalid), + .o_tready(data_out.tready) + ); + assign lane_tready[samp_idx] = stage_tready[0]; - assign lane_tvalid[samp_idx] = shifter_tvalid[SHIFT_W]; - assign lane_tlast[samp_idx] = shifter_tlast[SHIFT_W]; - assign lane_tdata[samp_idx] = {i_shifted[SHIFT_W][OUT_WIDTH/2-1:0], - q_shifted[SHIFT_W][OUT_WIDTH/2-1:0]}; + assign lane_tvalid[samp_idx] = rounded_tvalid; + assign lane_tlast[samp_idx] = rounded_tlast; + assign lane_tdata[samp_idx] = rounded; end : gen_shift_out diff --git a/fpga/usrp3/lib/dsp/cic_filter_decim.sv b/fpga/usrp3/lib/dsp/cic_filter_decim.sv index 6bf8dc23c..2748a4337 100644 --- a/fpga/usrp3/lib/dsp/cic_filter_decim.sv +++ b/fpga/usrp3/lib/dsp/cic_filter_decim.sv @@ -51,8 +51,9 @@ // ORDER comb stages (y[n] = x[n] - x[n-D]) operate at the decimated rate. // // All stages use the same SAMP_W, which must be pre-sized to accommodate -// the full CIC bit growth of N*log2(R_max*D_max) (no internal clipping or -// rounding is performed). +// the full CIC bit growth of N*log2(R_max*D_max). Internal stages do not +// clip or round; the final rate-dependent output conversion rounds before +// reducing the result to SAMP_W. // // The decimation factor may be changed at runtime by asserting // config_changed for one clock cycle. Configuration must only be changed diff --git a/fpga/usrp3/lib/dsp/cic_filter_interp.sv b/fpga/usrp3/lib/dsp/cic_filter_interp.sv index 1fdc4faf4..07757094a 100644 --- a/fpga/usrp3/lib/dsp/cic_filter_interp.sv +++ b/fpga/usrp3/lib/dsp/cic_filter_interp.sv @@ -51,8 +51,8 @@ // // Internally, all stages operate at ACCUM_W bits, which is automatically // computed to accommodate the full CIC bit growth of N*log2(R_max*D_max). -// No internal clipping or rounding is performed. The output is normalized -// back to SAMP_W at the output. +// Internal stages do not clip or round. The output is normalized back to +// SAMP_W with signed round-to-nearest quantization. // // The interpolation factor may be changed at runtime by asserting // config_changed for one clock cycle. Configuration must only be changed diff --git a/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/Makefile b/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/Makefile new file mode 100644 index 000000000..7254c6b0a --- /dev/null +++ b/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/Makefile @@ -0,0 +1,38 @@ +# +# Copyright 2026 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# + +#------------------------------------------------- +# Top-of-Makefile +#------------------------------------------------- +# Define BASE_DIR to point to the "top" dir +BASE_DIR = $(abspath ../../../../../top) +# 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 + +#------------------------------------------------- +# Testbench Specific +#------------------------------------------------- +SIM_TOP = cic_barrel_shift_all_tb + +SIM_SRCS = \ +$(abspath ../cic_test_pkg.sv) \ +$(abspath ../comb_filter/cic_comb_filter_test_pkg.sv) \ +$(abspath ../cic_filter_test_pkg.sv) \ +$(abspath cic_barrel_shift_tb.sv) \ +$(abspath cic_barrel_shift_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/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_all_tb.sv b/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_all_tb.sv new file mode 100644 index 000000000..ac14a46e3 --- /dev/null +++ b/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_all_tb.sv @@ -0,0 +1,52 @@ +// +// Copyright 2026 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: cic_barrel_shift_all_tb +// +// Description: +// +// Run direct CIC barrel-shift rounding tests across single- and +// multisample configurations. +// + +`default_nettype none + +module cic_barrel_shift_all_tb; + + cic_barrel_shift_tb #( + .SPC (4), + .IN_WIDTH (32), + .OUT_WIDTH(16), + .MAX_RATE (4), + .ORDER (2) + ) tb_compact (); + + cic_barrel_shift_tb #( + .SPC (1), + .IN_WIDTH (96), + .OUT_WIDTH(32), + .MAX_RATE (255), + .ORDER (4) + ) tb_spc1 (); + + cic_barrel_shift_tb #( + .SPC (4), + .IN_WIDTH (96), + .OUT_WIDTH(32), + .MAX_RATE (255), + .ORDER (4) + ) tb_spc4 (); + + cic_barrel_shift_tb #( + .SPC (8), + .IN_WIDTH (96), + .OUT_WIDTH(32), + .MAX_RATE (255), + .ORDER (4) + ) tb_spc8 (); + +endmodule : cic_barrel_shift_all_tb + +`default_nettype wire diff --git a/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_tb.sv b/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_tb.sv new file mode 100644 index 000000000..be38451b7 --- /dev/null +++ b/fpga/usrp3/lib/dsp/sim/cic/barrel_shift/cic_barrel_shift_tb.sv @@ -0,0 +1,217 @@ +// +// Copyright 2026 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: cic_barrel_shift_tb +// +// Description: +// +// Direct testbench for cic_barrel_shift. Drives signed I/Q values with +// selected discarded-bit residues and checks round-to-nearest behavior on +// every SPC lane. +// + +`default_nettype none + +module cic_barrel_shift_tb #( + int SPC = 4, + int IN_WIDTH = 96, + int OUT_WIDTH = 32, + int MAX_RATE = 255, + int ORDER = 4 +); + + `include "test_exec.svh" + + import PkgTestExec::*; + import PkgAxiStreamBfm::*; + import cic_filter_test_pkg::*; + + localparam real CLK_PERIOD = 10.0; + localparam int IN_COMP_W = IN_WIDTH / 2; + localparam int OUT_COMP_W = OUT_WIDTH / 2; + localparam int RATE_W = $clog2(MAX_RATE + 1); + localparam int TEST_RATE = (MAX_RATE >= 2) ? 2 : 1; + localparam int TEST_SHIFT = $clog2(longint'(TEST_RATE) ** ORDER); + localparam longint signed OUTPUT_MAX = + (longint'(1) << (OUT_COMP_W - 1)) - 1; + localparam longint signed HALF_LSB = longint'(1) << (TEST_SHIFT - 1); + + typedef cic_filter_quantizer #( + .ACCUM_W(IN_WIDTH), + .SAMP_W (OUT_WIDTH) + ) quantizer_c; + typedef AxiStreamPacket #(SPC * IN_WIDTH) input_pkt_t; + typedef AxiStreamPacket #(SPC * OUT_WIDTH) output_pkt_t; + + logic clk; + logic rst; + logic clear = 1'b0; + logic [RATE_W-1:0] rate_factor; + + sim_clock_gen #( + .PERIOD (CLK_PERIOD), + .AUTOSTART(0) + ) clk_gen ( + .clk(clk), + .rst(rst) + ); + + AxiStreamIf #( + .DATA_WIDTH(SPC * IN_WIDTH), + .TKEEP (0), + .TUSER (0) + ) i_data ( + .clk(clk), + .rst(rst) + ); + + AxiStreamIf #( + .DATA_WIDTH(SPC * OUT_WIDTH), + .TKEEP (0), + .TUSER (0) + ) o_data ( + .clk(clk), + .rst(rst) + ); + + AxiStreamBfm #( + .DATA_WIDTH(SPC * IN_WIDTH), + .TKEEP (0), + .TUSER (0) + ) input_bfm = new( + .master(i_data), + .slave (null) + ); + + AxiStreamBfm #( + .DATA_WIDTH(SPC * OUT_WIDTH), + .TKEEP (0), + .TUSER (0) + ) output_bfm = new( + .master(null), + .slave (o_data) + ); + + cic_barrel_shift #( + .SPC (SPC), + .OUT_WIDTH(OUT_WIDTH), + .IN_WIDTH (IN_WIDTH), + .MAX_RATE (MAX_RATE), + .ORDER (ORDER) + ) dut ( + .clk (clk), + .rst (rst), + .clear (clear), + .rate_factor(rate_factor), + .data_in (i_data), + .data_out (o_data) + ); + + longint signed vector_i[SPC]; + longint signed vector_q[SPC]; + + task automatic check_vector(input int R, input string test_name); + input_pkt_t input_pkt; + output_pkt_t output_pkt; + logic [SPC * IN_WIDTH-1:0] input_word; + logic [SPC * OUT_WIDTH-1:0] expected_word; + int shift_amount; + + input_word = '0; + expected_word = '0; + rate_factor = R; + shift_amount = $clog2(longint'(R) ** ORDER); + + // Wait for the rate lookup pipeline to select the requested rate. + clk_gen.clk_wait_r(3); + + for (int lane = 0; lane < SPC; lane++) begin + input_word[IN_WIDTH*lane +: IN_COMP_W] = IN_COMP_W'(vector_q[lane]); + input_word[IN_WIDTH*lane + IN_COMP_W +: IN_COMP_W] = + IN_COMP_W'(vector_i[lane]); + expected_word[OUT_WIDTH*lane +: OUT_WIDTH] = + quantizer_c::round_shift_sample( + input_word[IN_WIDTH*lane +: IN_WIDTH], shift_amount); + end + + input_pkt = new(); + input_pkt.data.push_back(input_word); + input_bfm.put(input_pkt); + input_bfm.wait_complete(); + output_bfm.get(output_pkt); + + `ASSERT_ERROR(output_pkt.data.size() == 1, + $sformatf("%s: expected one output word, got %0d", + test_name, output_pkt.data.size())) + if (output_pkt.data.size() == 1) begin + `ASSERT_ERROR(output_pkt.data[0] === expected_word, + $sformatf("%s: R=%0d expected 0x%0h, got 0x%0h", + test_name, R, expected_word, output_pkt.data[0])) + end + + clk_gen.reset(); + @(negedge rst); + endtask : check_vector + + task automatic run_testcases(); + clk_gen.start(); + input_bfm.run(); + output_bfm.run(); + input_bfm.set_master_stall_prob(0); + output_bfm.set_slave_stall_prob(0); + clk_gen.reset(); + @(negedge rst); + + test.start_test("Signed CIC rounding residues", 10us); + for (int lane = 0; lane < SPC; lane++) begin + case (lane % 4) + 0: begin + vector_i[lane] = (2 * (longint'(1) << TEST_SHIFT)) + HALF_LSB - 1; + vector_q[lane] = -vector_i[lane]; + end + 1: begin + vector_i[lane] = (2 * (longint'(1) << TEST_SHIFT)) + HALF_LSB; + vector_q[lane] = -vector_i[lane]; + end + default: begin + vector_i[lane] = -((OUTPUT_MAX + 1) << TEST_SHIFT) + HALF_LSB + 1; + vector_q[lane] = -vector_i[lane]; + end + endcase + end + check_vector(TEST_RATE, "rounding residues"); + test.end_test(); + + test.start_test("R=1 output conversion", 10us); + for (int lane = 0; lane < SPC; lane++) begin + vector_i[lane] = 32 + lane; + vector_q[lane] = -32 - lane; + end + check_vector(1, "R=1"); + test.end_test(); + + if (MAX_RATE >= 3) begin + test.start_test("Non-power-of-two rate", 10us); + for (int lane = 0; lane < SPC; lane++) begin + vector_i[lane] = 16 + lane; + vector_q[lane] = -16 - lane; + end + check_vector(3, "R=3"); + test.end_test(); + end + endtask : run_testcases + + initial begin + test.start_tb($sformatf( + "CIC barrel shift rounding (SPC=%0d, IN_WIDTH=%0d, OUT_WIDTH=%0d, ORDER=%0d)", + SPC, IN_WIDTH, OUT_WIDTH, ORDER)); + run_testcases(); + test.end_tb(0); + clk_gen.kill(); + end + +endmodule : cic_barrel_shift_tb + +`default_nettype wire diff --git a/fpga/usrp3/lib/dsp/sim/cic/cic_filter_decim/cic_filter_decim_tb.sv b/fpga/usrp3/lib/dsp/sim/cic/cic_filter_decim/cic_filter_decim_tb.sv index 80258d379..9049dad34 100644 --- a/fpga/usrp3/lib/dsp/sim/cic/cic_filter_decim/cic_filter_decim_tb.sv +++ b/fpga/usrp3/lib/dsp/sim/cic/cic_filter_decim/cic_filter_decim_tb.sv @@ -87,6 +87,10 @@ module cic_filter_decim_tb #( .SPC (1), .SAMP_W(ACCUM_W) ) model_util_c; + typedef cic_filter_quantizer #( + .ACCUM_W(ACCUM_W), + .SAMP_W (SAMP_W) + ) quantizer_c; typedef util_c::sample_t sample_t; typedef util_c::comp_t comp_t; @@ -159,7 +163,6 @@ module cic_filter_decim_tb #( // Reference model (operates at ACCUM_W precision) cic_filter_decim_model #( .ACCUM_W(ACCUM_W), - .COMP_W (MODEL_COMP_W), .ORDER (ORDER) ) model = new(); @@ -188,23 +191,13 @@ module cic_filter_decim_tb #( //--------------------------------------------------------------------------- // shift_sample: normalize an ACCUM_W model output sample to SAMP_W // - // Matches the DUT's barrel shifter behavior. Arithmetic-right-shifts each - // MODEL_COMP_W-bit component by ceil(log2((R*MAX_DELAY)^ORDER)) bits, then - // extracts the lower COMP_W bits as the output. + // Matches the DUT's barrel shifter behavior, including signed rounding. + // Arithmetic-right-shifts each MODEL_COMP_W-bit component by + // ceil(log2((R*MAX_DELAY)^ORDER)) bits, then quantizes it. //--------------------------------------------------------------------------- function automatic sample_t shift_sample(model_sample_t samp_in, int R); - logic signed [MODEL_COMP_W-1:0] q_in = samp_in[MODEL_COMP_W-1:0]; - logic signed [MODEL_COMP_W-1:0] i_in = samp_in[ACCUM_W-1:MODEL_COMP_W]; int shift_amount = $clog2(longint'(R) ** ORDER); - logic signed [MODEL_COMP_W-1:0] q_shifted, i_shifted; - logic signed [COMP_W-1:0] q_out, i_out; - - q_shifted = q_in >>> shift_amount; - i_shifted = i_in >>> shift_amount; - q_out = q_shifted[COMP_W-1:0]; - i_out = i_shifted[COMP_W-1:0]; - - return {i_out, q_out}; + return quantizer_c::round_shift_sample(samp_in, shift_amount); endfunction : shift_sample //--------------------------------------------------------------------------- diff --git a/fpga/usrp3/lib/dsp/sim/cic/cic_filter_interp/cic_filter_interp_tb.sv b/fpga/usrp3/lib/dsp/sim/cic/cic_filter_interp/cic_filter_interp_tb.sv index 1ed04d628..87a7b0352 100644 --- a/fpga/usrp3/lib/dsp/sim/cic/cic_filter_interp/cic_filter_interp_tb.sv +++ b/fpga/usrp3/lib/dsp/sim/cic/cic_filter_interp/cic_filter_interp_tb.sv @@ -88,6 +88,10 @@ module cic_filter_interp_tb #( .SPC (1), .SAMP_W(ACCUM_W) ) model_util_c; + typedef cic_filter_quantizer #( + .ACCUM_W(ACCUM_W), + .SAMP_W (SAMP_W) + ) quantizer_c; typedef util_c::sample_t sample_t; typedef util_c::comp_t comp_t; @@ -159,7 +163,6 @@ module cic_filter_interp_tb #( //--------------------------------------------------------------------------- cic_filter_interp_model #( .ACCUM_W(ACCUM_W), - .COMP_W (MODEL_COMP_W), .ORDER (ORDER) ) model = new(); @@ -188,23 +191,13 @@ module cic_filter_interp_tb #( //--------------------------------------------------------------------------- // shift_sample: normalize an ACCUM_W model output sample to SAMP_W // - // Matches the DUT's barrel shifter behavior. Arithmetic-right-shifts each - // MODEL_COMP_W-bit component by ceil(log2(R^(ORDER-1))) bits, then extracts - // the lower COMP_W bits as the output. + // Matches the DUT's barrel shifter behavior, including signed rounding. + // Arithmetic-right-shifts each MODEL_COMP_W-bit component by + // ceil(log2(R^(ORDER-1))) bits, then quantizes it. //--------------------------------------------------------------------------- function automatic sample_t shift_sample(model_sample_t samp_in, int R); - logic signed [MODEL_COMP_W-1:0] q_in = samp_in[MODEL_COMP_W-1:0]; - logic signed [MODEL_COMP_W-1:0] i_in = samp_in[ACCUM_W-1:MODEL_COMP_W]; int shift_amount = $clog2(longint'(R) ** (ORDER-1)); - logic signed [MODEL_COMP_W-1:0] q_shifted, i_shifted; - logic signed [COMP_W-1:0] q_out, i_out; - - q_shifted = q_in >>> shift_amount; - i_shifted = i_in >>> shift_amount; - q_out = q_shifted[COMP_W-1:0]; - i_out = i_shifted[COMP_W-1:0]; - - return {i_out, q_out}; + return quantizer_c::round_shift_sample(samp_in, shift_amount); endfunction : shift_sample //--------------------------------------------------------------------------- diff --git a/fpga/usrp3/lib/dsp/sim/cic/cic_filter_test_pkg.sv b/fpga/usrp3/lib/dsp/sim/cic/cic_filter_test_pkg.sv index b6b913556..8b424c7ff 100644 --- a/fpga/usrp3/lib/dsp/sim/cic/cic_filter_test_pkg.sv +++ b/fpga/usrp3/lib/dsp/sim/cic/cic_filter_test_pkg.sv @@ -26,20 +26,82 @@ package cic_filter_test_pkg; import cic_test_pkg::*; import cic_comb_filter_test_pkg::*; + // Model the final signed CIC output quantization performed by + // cic_barrel_shift. The CIC stages remain at full accumulator precision. + class cic_filter_quantizer #( + int ACCUM_W = 96, + int SAMP_W = 32 + ); + + localparam int ACCUM_COMP_W = ACCUM_W / 2; + localparam int SAMP_COMP_W = SAMP_W / 2; + localparam logic [SAMP_COMP_W-1:0] MAX_OUTPUT = + {1'b0, {(SAMP_COMP_W-1){1'b1}}}; + localparam logic [SAMP_COMP_W-1:0] MIN_OUTPUT = + {1'b1, {(SAMP_COMP_W-1){1'b0}}}; + + typedef logic signed [ACCUM_COMP_W-1:0] accum_comp_t; + typedef logic signed [SAMP_COMP_W-1:0] samp_comp_t; + + // Round-half-up expressed as an addition of half an LSB before the shift, + // deliberately formulated differently from the RTL so that a conceptual + // error in cic_barrel_shift is not mirrored by this reference model. The + // RTL clips instead of wrapping, which only matters when the shifted + // result does not fit in SAMP_COMP_W bits. + static function automatic samp_comp_t round_shift_component( + input accum_comp_t comp_in, + input int shift_amount + ); + accum_comp_t rounded; + + if (shift_amount > 0) begin + rounded = (comp_in + (accum_comp_t'(1) << (shift_amount-1))) >>> + shift_amount; + end else begin + rounded = comp_in; + end + + if (rounded > accum_comp_t'(signed'(MAX_OUTPUT))) begin + return samp_comp_t'(MAX_OUTPUT); + end + if (rounded < accum_comp_t'(signed'(MIN_OUTPUT))) begin + return samp_comp_t'(MIN_OUTPUT); + end + return samp_comp_t'(rounded[SAMP_COMP_W-1:0]); + endfunction : round_shift_component + + static function automatic logic [SAMP_W-1:0] round_shift_sample( + input logic [ACCUM_W-1:0] sample_in, + input int shift_amount + ); + accum_comp_t q_in; + accum_comp_t i_in; + samp_comp_t q_out; + samp_comp_t i_out; + + q_in = sample_in[ACCUM_COMP_W-1:0]; + i_in = sample_in[ACCUM_W-1:ACCUM_COMP_W]; + q_out = round_shift_component(q_in, shift_amount); + i_out = round_shift_component(i_in, shift_amount); + return {i_out, q_out}; + endfunction : round_shift_sample + + endclass : cic_filter_quantizer + //--------------------------------------------------------------------------- // Bit-true simulation model of a full Nth-order CIC decimation filter. // // Chains ORDER integrators -> decimate by R -> ORDER combs (D=1), // processing one sample at a time (SPC-agnostic). All stages operate at - // ACCUM_W width with modular (wrapping) two's complement arithmetic, - // matching the RTL which performs no internal clipping or rounding. + // ACCUM_W width with modular (wrapping) two's complement arithmetic. The + // returned samples remain at ACCUM_W precision; use cic_filter_quantizer + // for the final rounded output conversion. // // State is created fresh on each call to process() (zero initial // conditions), so there is no need to explicitly clear between runs. //--------------------------------------------------------------------------- class cic_filter_decim_model #( int ACCUM_W = 96, - int COMP_W = 16, int ORDER = 4 ); @@ -111,15 +173,14 @@ package cic_filter_test_pkg; // Chains ORDER combs (D=1, input rate) -> upsample by R -> ORDER integrators // (full output rate), processing one sample at a time (SPC-agnostic). All // stages operate at ACCUM_W width with modular (wrapping) two's complement - // arithmetic, matching the RTL which performs no internal clipping or - // rounding. + // arithmetic. The returned samples remain at ACCUM_W precision; use + // cic_filter_quantizer for the final rounded output conversion. // // State is created fresh on each call to process() (zero initial // conditions), so there is no need to explicitly clear between runs. //--------------------------------------------------------------------------- class cic_filter_interp_model #( int ACCUM_W = 96, - int COMP_W = 16, int ORDER = 4 );