CIC Filter in Verilog: Cascaded Integrator-Comb with Code
A cascaded integrator-comb (CIC) filter is a multiplier-free decimation (or interpolation) filter built from nothing but adders, subtractors and registers. Because it needs no multipliers and no coefficient storage, it is the workhorse front end for sigma-delta ADCs and software-defined-radio (SDR) receivers, where a very high sample rate has to be dropped to a manageable one at minimal hardware cost. This page gives working, simulator-verified CIC filter Verilog code for a decimator, from a one-stage version up to a general N-stage design with the correct internal word growth.
A decimating CIC runs N integrator stages at the fast input rate, down-samples by the rate-change factor R, and then runs N comb stages at the slow output rate. The two examples below build exactly that: a single-stage decimator you can trace by hand, and a parameterised N-stage version that implements the CIC bit-growth rule and is checked against an independent convolution model.
This is the clearest form: one integrator that accumulates every clock, a decimate-by-R counter, and one comb that differences the integrator at the decimation instants. For N=1, M=1 the output is simply the moving sum of the last R input samples, which the testbench checks against a directly computed golden value.
// Single-stage (N=1) CIC decimation filter: one integrator at the high rate,
// decimate-by-R, one comb at the decimated rate (M=1).
module cic_n1 #(parameter W = 16, parameter R = 4) (
input wire clk,
input wire rst, // synchronous, active high
input wire signed [W-1:0] x,
output reg signed [W-1:0] y,
output reg y_valid
);
reg signed [W-1:0] integ; // integrator accumulator (runs every cycle)
reg signed [W-1:0] comb_prev; // comb delay register (updates at low rate)
integer cnt; // decimation counter 0..R-1
always @(posedge clk) begin
if (rst) begin
integ <= 0;
comb_prev <= 0;
cnt <= 0;
y <= 0;
y_valid <= 0;
end else begin
integ <= integ + x; // integrator: accumulate every cycle
if (cnt == R-1) begin
cnt <= 0;
// sample the (post-update) integrator value and difference it
y <= (integ + x) - comb_prev;
comb_prev <= (integ + x);
y_valid <= 1'b1;
end else begin
cnt <= cnt + 1;
y_valid <= 1'b0;
end
end
end
endmodule
module tb;
localparam W = 16;
localparam R = 4;
localparam NS = 24; // number of input samples
localparam NO = NS/R; // number of decimated outputs
reg clk = 1'b0;
reg rst = 1'b1;
reg signed [W-1:0] x;
wire signed [W-1:0] y;
wire y_valid;
cic_n1 #(.W(W), .R(R)) dut (.clk(clk), .rst(rst), .x(x), .y(y), .y_valid(y_valid));
always #5 clk = ~clk;
integer i, k, run, errors;
reg signed [W-1:0] xs [0:NS-1];
integer golden [0:NO-1];
reg signed [W-1:0] expv;
initial begin
errors = 0;
for (i = 0; i < NS; i = i + 1) xs[i] = (i*3) - 10; // deterministic stimulus
// golden = moving sum of R input samples at each decimation block
for (k = 0; k < NO; k = k + 1) begin
run = 0;
for (i = 0; i < R; i = i + 1) run = run + xs[k*R + i];
golden[k] = run;
end
@(negedge clk); rst = 1'b1; x = 0;
@(negedge clk); rst = 1'b0;
k = 0;
for (i = 0; i < NS; i = i + 1) begin
x = xs[i];
@(negedge clk);
if (y_valid) begin
expv = golden[k];
if (y !== expv) begin
$display("FAIL decimated[%0d]: got %0d expected %0d", k, y, expv);
errors = errors + 1;
end else begin
$display("PASS decimated[%0d] = %0d (moving-sum of %0d samples)", k, y, R);
end
k = k + 1;
end
end
if (k !== NO) begin
$display("FAIL: produced %0d outputs, expected %0d", k, NO);
errors = errors + 1;
end
if (errors == 0) $display("ALL TESTS PASSED");
else $display("TESTS FAILED: %0d error(s)", errors);
$finish;
end
endmodule
PASS decimated[0] = -22 (moving-sum of 4 samples) PASS decimated[1] = 26 (moving-sum of 4 samples) PASS decimated[2] = 74 (moving-sum of 4 samples) PASS decimated[3] = 122 (moving-sum of 4 samples) PASS decimated[4] = 170 (moving-sum of 4 samples) PASS decimated[5] = 218 (moving-sum of 4 samples) ALL TESTS PASSEDThe log proves the single-stage decimator produces one output every R input samples, and that each decimated sample equals the arithmetic sum of its block of R inputs (the N=1 CIC response). The golden values are computed independently in the testbench as plain moving sums, so any arithmetic or timing error would print a FAIL line.
This variant generalises to N cascaded integrators and N cascaded combs (shown with N=3, R=4) and sizes every register with the CIC bit-growth rule, so the wrap-around integrators never lose data. It is self-checked against a completely independent reference: the CIC impulse response is boxcar_R convolved N times, and the decimated outputs must equal that convolution sampled at n = k*R.
// N-stage CIC decimator: N cascaded integrators at the high rate, decimate-by-R, // then N cascaded combs at the decimated rate. Register width follows the CIC // bit-growth rule W = W_IN + N*ceil(log2(R*M)). module cic_multi #( parameter W_IN = 8, parameter R = 4, parameter N = 3, parameter M = 1, parameter W = W_IN + N*$clog2(R*M) ) ( input wire clk, input wire rst, // synchronous, active high input wire signed [W_IN-1:0] x, output reg signed [W-1:0] y, output reg y_valid ); reg signed [W-1:0] integ [0:N-1]; // integrator accumulators (run every cycle) reg signed [W-1:0] comb [0:N-1]; // comb delay registers (update at low rate) integer cnt; integer g; reg signed [W-1:0] chain; always @(posedge clk) begin if (rst) begin for (g = 0; g < N; g = g + 1) begin integ[g] <= 0; comb[g] <= 0; end cnt <= 0; y <= 0; y_valid <= 1'b0; end else begin // integrator cascade: full word width prevents overflow despite wrap-around integ[0] <= integ[0] + x; for (g = 1; g < N; g = g + 1) integ[g] <= integ[g] + integ[g-1]; if (cnt == R-1) begin cnt <= 0; // comb cascade at the decimated rate (differentiators, delay M=1) chain = integ[N-1]; for (g = 0; g < N; g = g + 1) begin comb[g] <= chain; // store current value chain = chain - comb[g]; // difference against previous low-rate value end y <= chain; y_valid <= 1'b1; end else begin cnt <= cnt + 1; y_valid <= 1'b0; end end end endmodule module tb; localparam W_IN = 8; localparam R = 4; localparam N = 3; localparam M = 1; localparam W = W_IN + N*2; // clog2(R*M)=clog2(4)=2 localparam NS = 40; localparam HLEN = N*(R-1) + 1; // CIC impulse-response length reg clk = 0, rst = 1; reg signed [W_IN-1:0] x; wire signed [W-1:0] y; wire y_valid; cic_multi #(.W_IN(W_IN), .R(R), .N(N), .M(M)) dut ( .clk(clk), .rst(rst), .x(x), .y(y), .y_valid(y_valid)); always #5 clk = ~clk; integer i, j, k, errors; reg signed [W_IN-1:0] xs [0:NS-1]; integer h [0:HLEN-1]; integer tmp [0:HLEN-1]; integer yfull [0:NS-1]; integer boxlen, acc, gain, ncap; reg signed [W-1:0] expv; initial begin errors = 0; for (i = 0; i < NS; i = i + 1) xs[i] = ((i*5 + 3) % 29) - 14; // deterministic signed stream // Independent golden model: impulse response h = boxcar_R convolved N times. // (CIC transfer function is [ (1-z^-R)/(1-z^-1) ]^N = boxcar_R ** N.) for (i = 0; i < HLEN; i = i + 1) h[i] = 0; h[0] = 1; boxlen = 1; for (j = 0; j < N; j = j + 1) begin for (i = 0; i < HLEN; i = i + 1) tmp[i] = 0; for (i = 0; i < boxlen; i = i + 1) for (k = 0; k < R; k = k + 1) tmp[i+k] = tmp[i+k] + h[i]; boxlen = boxlen + (R-1); for (i = 0; i < HLEN; i = i + 1) h[i] = tmp[i]; end // Invariant: DC gain of a CIC must equal (R*M)^N. gain = 0; for (i = 0; i < HLEN; i = i + 1) gain = gain + h[i]; if (gain !== R*R*R) begin $display("FAIL: golden DC gain %0d != (R*M)^N %0d", gain, R*R*R); errors = errors + 1; end else $display("PASS: CIC DC gain = %0d = (R*M)^N", gain); // full-rate convolution reference for (i = 0; i < NS; i = i + 1) begin acc = 0; for (j = 0; j < HLEN; j = j + 1) if (i-j >= 0) acc = acc + h[j]*xs[i-j]; yfull[i] = acc; end @(negedge clk); rst = 1; x = 0; @(negedge clk); rst = 0; ncap = 0; k = 0; for (i = 0; i < NS; i = i + 1) begin x = xs[i]; @(negedge clk); if (y_valid) begin expv = yfull[k*R]; // k-th decimated output == full-rate conv at n = k*R if (y !== expv) begin $display("FAIL out[%0d]: got %0d expected %0d", k, y, expv); errors = errors + 1; end else begin $display("PASS out[%0d] = %0d (matches boxcar^%0d conv, width=%0d bits)", k, y, N, W); end k = k + 1; ncap = ncap + 1; end end if (ncap < 8) begin $display("FAIL: only %0d outputs captured", ncap); errors = errors + 1; end if (errors == 0) $display("ALL TESTS PASSED"); else $display("TESTS FAILED: %0d error(s)", errors); $finish; end endmodulePASS: CIC DC gain = 64 = (R*M)^N PASS out[0] = -11 (matches boxcar^3 conv, width=14 bits) PASS out[1] = -177 (matches boxcar^3 conv, width=14 bits) PASS out[2] = 142 (matches boxcar^3 conv, width=14 bits) PASS out[3] = 14 (matches boxcar^3 conv, width=14 bits) PASS out[4] = -156 (matches boxcar^3 conv, width=14 bits) PASS out[5] = -36 (matches boxcar^3 conv, width=14 bits) PASS out[6] = -32 (matches boxcar^3 conv, width=14 bits) PASS out[7] = -28 (matches boxcar^3 conv, width=14 bits) PASS out[8] = 92 (matches boxcar^3 conv, width=14 bits) PASS out[9] = 96 (matches boxcar^3 conv, width=14 bits) ALL TESTS PASSEDThe log first confirms the DC gain invariant (R*M)^N = 64, then shows every decimated output matching a full-rate convolution with the boxcar^3 impulse response. That convolution is derived from the CIC transfer function independently of the integrator/comb recurrence in the RTL, so agreement across all samples is a genuine cross-check rather than a restatement of the design. The reported 14-bit width is exactly W_IN + N*ceil(log2(R*M)).
Integrator and comb equations
A CIC decimator has two halves joined by a down-sampler. The integrator section runs at the high (input) rate f_s. Each stage is a simple accumulator with transfer function H_I(z) = 1 / (1 - z^-1), i.e. y[n] = y[n-1] + x[n]. Cascading N of them gives (1 / (1 - z^-1))^N.
After decimating by R, the comb section runs at the low rate f_s/R. Each comb is a differentiator H_C(z) = 1 - z^-(RM), which at the low rate is just y[k] = x[k] - x[k-M]. M is the differential delay, almost always 1 or 2; this page uses M = 1.
The magic identity is that the whole chain collapses to:
H(z) = [ (1 - z^-(RM)) / (1 - z^-1) ]^Nand (1 - z^-R)/(1 - z^-1) = 1 + z^-1 + ... + z^-(R-1) is an R-tap boxcar (moving-sum) filter. So an N-stage CIC is mathematically an R-point running sum raised to the N-th power - which is exactly the convolution golden model used in Example 2.
Decimation and the clock enable
The down-sampler is the hinge of the design. In hardware you do not literally use two clocks; you run everything from the fast clock and gate the comb section with a clock enable that asserts once every R cycles. In both examples a counter walks 0..R-1 and fires when it reaches R-1.
Two things matter for correctness. First, the integrators must keep accumulating on every cycle - they never skip. Second, the comb stages must update only on the enable pulse, so that z^-1 at the comb rate equals z^-R at the input rate. Mixing these up is the single most common CIC bug: if the comb runs every cycle you get a different (and wrong) filter.
In the testbenches the decimated output is flagged by
y_valid, and the checker only samplesywhen that flag is high, mirroring how a real downstream block would latch the slow-rate stream.The critical bit-growth / word-width rule
The integrators are unstable on their own - 1/(1 - z^-1) has a pole on the unit circle, so the accumulator value can grow without bound. CIC filters survive this because the comb section later subtracts that growth away, but only if the integrator registers are wide enough to let the value wrap around modulo 2^W without the final result being corrupted. This is the famous CIC bit-growth rule:
W = B_in + ceil( N * log2(R * M) )For Example 2 with B_in = 8, N = 3, R = 4, M = 1 that is 8 + 3*log2(4) = 8 + 6 = 14 bits, which the code computes with
$clog2. Every integrator and comb register must be this full width. Because two's-complement addition wraps cleanly, the integrators are allowed to overflow internally - the comb differences still produce the correct result as long as no single output sample exceeds the range, which the rule guarantees.Why you must not truncate early
Trimming integrator width to 'save area' before the combs have done their subtraction throws away the very bits that cancel the accumulator growth, and the output becomes garbage. Any pruning must follow the register-pruning analysis (Hogenauer), never a naive truncation of the accumulators.
Why a CIC needs no multipliers
Look at both listings: there is not a single
*in the datapath. Integrators are additions, combs are subtractions, decimation is a counter. That is the entire appeal of the CIC. Compared with a general FIR running at the high input rate, a CIC moves almost all of its work to additions and performs the expensive filtering implicitly through the boxcar^N response.This is why CICs sit at the very front of sigma-delta modulators and SDR receivers: the data rate there is highest, multipliers would be most expensive, and a multiplier-free structure that also provides the first big rate reduction is exactly what you want. Finer, multiplier-based FIR shaping is then done afterwards at the much lower output rate.
Passband droop and compensation
The price of the boxcar^N response is passband droop. A moving-sum filter has a sinc-shaped magnitude response, so an N-stage CIC rolls off like sinc^N across its passband - signals near the top of the band are attenuated well before the band edge. The deeper the decimation and the more stages, the worse the droop.
The standard fix is a short CIC compensation filter (sometimes called an inverse-sinc or 'CFIR') running at the low output rate. It has a mild high-frequency boost that pre-emphasises the band to cancel the sinc^N roll-off. Because it runs after decimation, it is cheap even though it does use multipliers. Choosing N is a trade-off: more stages give better alias attenuation at the decimation images but worse droop and more hardware.
FAQ
Is this code synthesizable? Yes. Both modules are plain synchronous RTL - registered
always @(posedge clk)blocks, synchronous reset, no simulation-only constructs in the design modules (only thetbmodule uses delays and$display).Can I use it for interpolation? The dual CIC interpolator swaps the order - comb stages first at the low rate, then zero-stuffing up-sampling, then integrators at the high rate - and the same bit-growth rule applies. The decimator shown here is the more common ADC/SDR front-end case.
What sets the number of outputs? One output appears per R input samples; with R = 4 and 24 inputs you get 6 decimated samples, as the Example 1 log shows.
Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres