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 PASSED

The 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
endmodule
PASS: 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 PASSED

The 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) ]^N

and (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 samples y when 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 the tb module 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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD