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.
This commit is contained in:
Niels Garibaldi 2026-08-25 15:58:15 +02:00 committed by mkoop-ni
parent 02f11f970c
commit 31be5b875a
9 changed files with 455 additions and 65 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
//---------------------------------------------------------------------------

View File

@ -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
//---------------------------------------------------------------------------

View File

@ -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
);