IIR Filter in Verilog: Biquad / Direct-Form with Code

An infinite-impulse-response (IIR) filter computes each output from the current and past inputs and from its own past outputs: y[n] = b0·x[n] + b1·x[n-1] + … − a1·y[n-1] − a2·y[n-2] − …. That feedback path is what makes the impulse response infinitely long, and it lets a tiny number of taps realise sharp, resonant responses that a FIR would need hundreds of taps to match.

This page gives you runnable, simulator-verified IIR filter Verilog code: a parameterized first-order section and a second-order biquad, both in direct-form I with signed fixed-point coefficients and a recursive delay line of past outputs. Each testbench self-checks every sample against a bit-exact golden difference equation, so a real bug would print a FAIL line.

A parameterized first-order IIR section. Coefficients are signed Q14 fixed-point; the accumulator sums the two feed-forward products and subtracts the single feedback product, then an arithmetic right shift (>>>) rescales back to the sample format. The testbench drives an impulse followed by a step and checks each output against a golden model that reuses the identical integer arithmetic.

// First-order IIR direct-form I, signed fixed-point.
// y[n] = ( b0*x[n] + b1*x[n-1] - a1*y[n-1] ) >>> SHIFT
module iir1 #(
    parameter DW    = 16,   // sample width (signed)
    parameter CW    = 16,   // coefficient width (signed)
    parameter SHIFT = 14    // Q(CW-2) fixed-point scaling
)(
    input                     clk,
    input                     rst,     // sync active-high
    input  signed [CW-1:0]    b0,
    input  signed [CW-1:0]    b1,
    input  signed [CW-1:0]    a1,
    input  signed [DW-1:0]    x,
    output reg signed [DW-1:0] y
);
    reg signed [DW-1:0] x_d1;   // x[n-1]
    // accumulator must hold product + sum headroom
    reg signed [DW+CW:0] acc;

    always @(posedge clk) begin
        if (rst) begin
            x_d1 <= 0;
            y    <= 0;
        end else begin
            acc  = b0*x + b1*x_d1 - a1*y;   // blocking for this-cycle combine
            y    <= acc >>> SHIFT;           // arithmetic shift rescale
            x_d1 <= x;
        end
    end
endmodule

module tb;
    localparam DW = 16, CW = 16, SHIFT = 14;
    reg clk = 0, rst;
    reg signed [DW-1:0] x;
    reg signed [CW-1:0] b0, b1, a1;
    wire signed [DW-1:0] y;

    iir1 #(.DW(DW), .CW(CW), .SHIFT(SHIFT)) dut
        (.clk(clk), .rst(rst), .b0(b0), .b1(b1), .a1(a1), .x(x), .y(y));

    always #5 clk = ~clk;

    // bit-exact golden model mirroring the hardware arithmetic
    reg signed [DW-1:0] g_y, g_xd1;
    reg signed [DW+CW:0] g_acc;
    integer errors, k;
    reg signed [DW-1:0] stim [0:7];

    task golden_step(input signed [DW-1:0] xin);
        begin
            g_acc = b0*xin + b1*g_xd1 - a1*g_y;
            g_y   = g_acc >>> SHIFT;
            g_xd1 = xin;
        end
    endtask

    initial begin
        // Q14 coefficients: b0=0.5, b1=0.5, a1=-0.5  (a leaky integrator-ish LPF)
        b0 = 16'sd8192; b1 = 16'sd8192; a1 = -16'sd8192;
        errors = 0;
        g_y = 0; g_xd1 = 0;
        // impulse then step
        stim[0]=100; stim[1]=0;   stim[2]=0;   stim[3]=0;
        stim[4]=200; stim[5]=200; stim[6]=200; stim[7]=200;

        // reset
        rst = 1; x = 0; @(posedge clk); #1;
        rst = 0;

        for (k = 0; k < 8; k = k + 1) begin
            x = stim[k];
            golden_step(stim[k]);     // reference for the value latched this edge
            @(posedge clk); #1;       // after the edge y holds this sample's output
            if (y !== g_y) begin
                $display("FAIL n=%0d x=%0d got y=%0d exp=%0d", k, stim[k], y, g_y);
                errors = errors + 1;
            end else begin
                $display("PASS n=%0d x=%0d y=%0d", k, stim[k], y);
            end
        end

        if (errors == 0) $display("ALL TESTS PASSED");
        else             $display("TOTAL FAILURES=%0d", errors);
        $finish;
    end
endmodule
PASS n=0 x=100 y=50
PASS n=1 x=0 y=75
PASS n=2 x=0 y=37
PASS n=3 x=0 y=18
PASS n=4 x=200 y=109
PASS n=5 x=200 y=254
PASS n=6 x=200 y=327
PASS n=7 x=200 y=363
ALL TESTS PASSED

The log shows the impulse response decaying (50, 75, 37, 18) and then the step response settling toward a steady state, with each sample matching the golden difference equation exactly. Because the hardware and the reference use the same integer multiply and arithmetic shift, an off-by-one in the feedback term or a logical (vs arithmetic) shift would immediately produce a FAIL.

A second-order biquad in direct-form I — three feed-forward taps and two feedback taps — with wider Q15 coefficients. The two poles let it resonate, so the impulse response rings before decaying. The testbench sweeps an impulse and a short burst and checks all ten outputs against a bit-exact golden model.

// Biquad (second-order) IIR, direct-form I, signed fixed-point.
// y[n] = ( b0*x[n]+b1*x[n-1]+b2*x[n-2] - a1*y[n-1] - a2*y[n-2] ) >>> SHIFT
module biquad #(
    parameter DW    = 16,
    parameter CW    = 18,
    parameter SHIFT = 15
)(
    input                      clk,
    input                      rst,
    input  signed [CW-1:0]     b0, b1, b2, a1, a2,
    input  signed [DW-1:0]     x,
    output reg signed [DW-1:0] y
);
    reg signed [DW-1:0] x1, x2;   // x[n-1], x[n-2]
    reg signed [DW-1:0] y1;       // y[n-1]
    reg signed [DW+CW+1:0] acc;

    always @(posedge clk) begin
        if (rst) begin
            x1 <= 0; x2 <= 0; y1 <= 0; y <= 0;
        end else begin
            acc = b0*x + b1*x1 + b2*x2 - a1*y - a2*y1;
            y  <= acc >>> SHIFT;
            y1 <= y;      // old y[n-1] becomes y[n-2]
            x2 <= x1;
            x1 <= x;
        end
    end
endmodule

module tb;
    localparam DW = 16, CW = 18, SHIFT = 15;
    reg clk = 0, rst;
    reg signed [DW-1:0] x;
    reg signed [CW-1:0] b0,b1,b2,a1,a2;
    wire signed [DW-1:0] y;

    biquad #(.DW(DW), .CW(CW), .SHIFT(SHIFT)) dut
        (.clk(clk), .rst(rst), .b0(b0),.b1(b1),.b2(b2),.a1(a1),.a2(a2),
         .x(x), .y(y));

    always #5 clk = ~clk;

    // bit-exact golden model (same integer arithmetic/rounding as RTL)
    reg signed [DW-1:0] g_y, g_y1, g_x1, g_x2;
    reg signed [DW+CW+1:0] g_acc;
    integer errors, k;
    reg signed [DW-1:0] stim [0:9];

    task golden_step(input signed [DW-1:0] xin);
        reg signed [DW-1:0] new_y;
        begin
            g_acc = b0*xin + b1*g_x1 + b2*g_x2 - a1*g_y - a2*g_y1;
            new_y = g_acc >>> SHIFT;
            g_y1  = g_y;
            g_x2  = g_x1;
            g_x1  = xin;
            g_y   = new_y;
        end
    endtask

    initial begin
        // Q15 coefficients (a damped resonator-style set)
        b0 = 18'sd3277;    // 0.10
        b1 = 18'sd6554;    // 0.20
        b2 = 18'sd3277;    // 0.10
        a1 = -18'sd45875;  // -1.40
        a2 = 18'sd19661;   // 0.60
        errors = 0;
        g_y=0; g_y1=0; g_x1=0; g_x2=0;

        stim[0]=1000; stim[1]=0; stim[2]=0; stim[3]=0; stim[4]=0;
        stim[5]=500;  stim[6]=500; stim[7]=500; stim[8]=0; stim[9]=0;

        rst = 1; x = 0; @(posedge clk); #1;
        rst = 0;

        for (k = 0; k < 10; k = k + 1) begin
            x = stim[k];
            golden_step(stim[k]);
            @(posedge clk); #1;
            if (y !== g_y) begin
                $display("FAIL n=%0d x=%0d got y=%0d exp=%0d", k, stim[k], y, g_y);
                errors = errors + 1;
            end else begin
                $display("PASS n=%0d x=%0d y=%0d", k, stim[k], y);
            end
        end

        if (errors == 0) $display("ALL TESTS PASSED");
        else             $display("TOTAL FAILURES=%0d", errors);
        $finish;
    end
endmodule
PASS n=0 x=1000 y=100
PASS n=1 x=0 y=340
PASS n=2 x=0 y=516
PASS n=3 x=0 y=518
PASS n=4 x=0 y=415
PASS n=5 x=500 y=320
PASS n=6 x=500 y=349
PASS n=7 x=500 y=496
PASS n=8 x=0 y=635
PASS n=9 x=0 y=641
ALL TESTS PASSED

The impulse (x=1000 at n=0) produces a ringing, decaying response — the signature of a two-pole IIR — and the later burst excites it again. All ten samples match the golden model bit-for-bit, which confirms the feedback ordering is right: the RTL multiplies a1 by the registered y (that is y[n-1]) and a2 by y1 (y[n-2]), exactly as the golden step does.

How the direct-form I IIR works

A direct-form I section keeps two separate delay lines: one for past inputs (the feed-forward or zeros path, coefficients b) and one for past outputs (the feedback or poles path, coefficients a). Each clock edge the design forms one multiply-accumulate:

acc = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
y[n] = acc >>> SHIFT

The subtraction on the feedback terms follows the standard transfer function H(z) = (b0 + b1 z⁻¹ + b2 z⁻²) / (1 + a1 z⁻¹ + a2 z⁻²). Because a0 is normalised to 1, there is no divide in hardware — the recursion simply reuses the previously computed outputs.

Why fixed-point and the shift

Coefficients like 0.5 or -1.4 cannot be stored as integers, so they are scaled by 2SHIFT (Q-format). A coefficient times a sample therefore lands SHIFT bits too high, and the arithmetic right shift >>> brings the result back to the sample's scale while preserving sign. Using >>> on a signed accumulator is essential: a logical shift >> would corrupt negative outputs.

FIR vs IIR: feedback and stability

A FIR filter has no feedback — its output depends only on a finite window of inputs, so it is always stable and can be made exactly linear-phase. An IIR filter feeds its own outputs back, which is why it can hit a sharp magnitude response with very few taps, but it introduces two hazards that FIRs never have:

  • Stability. The poles (roots of the denominator) must lie strictly inside the unit circle. A coefficient typo or an over-aggressive design can push a pole outside, and the output grows without bound.
  • Limit cycles. Even a stable IIR can get stuck in small self-sustaining oscillations once quantisation is applied in the feedback loop, because a rounded output keeps re-exciting the recursion.

Neither of these can happen in a pure FIR. That trade — efficiency for the need to manage stability — is the heart of every IIR interview question.

Direct-form I vs direct-form II

Direct-form I (used above) stores delayed inputs and delayed outputs separately — for a biquad that is four delay registers. Direct-form II merges them into a single delay line on an intermediate state w[n], halving the number of delay elements to two for a biquad.

The catch is internal range. Direct-form II computes w[n] = x[n] - a1 w[n-1] - a2 w[n-2] first, and that intermediate node can swing much larger than the input or output, so it needs more guard bits to avoid overflow. In fixed-point hardware, direct-form I (or the transposed direct-form II) is usually preferred precisely because its internal signals stay closer to the input/output range and are easier to scale. For high-order filters you almost always cascade biquads rather than build one monolithic high-order section, because the poles of a long direct-form filter become numerically fragile.

Coefficient scaling, overflow, and the feedback quantiser

Choosing SHIFT is a balance. More fractional bits means finer coefficient resolution and a pole placed closer to where you intended — important because near the unit circle a tiny coefficient error moves the pole a lot and can change the filter from stable to unstable. Fewer fractional bits leaves more integer headroom against overflow.

  • Accumulator width. Size acc as sample-width + coefficient-width + log2(number of taps) so the sum of products never wraps before the shift. The examples use DW+CW (plus a bit) for exactly this reason.
  • Output saturation. Real IIR hardware usually saturates y instead of letting it wrap, because a wrap in the feedback path can be catastrophic — it feeds a wildly wrong value back into the recursion.
  • Feedback quantisation. The shift that rescales y[n] is a quantiser inside the loop. That rounding is what causes limit cycles and is why the golden model must round identically — see the next section.

Common mistakes and why the golden model must be bit-exact

  • Logical shift on signed data. Writing acc >> SHIFT instead of >>> breaks every negative output. Declare the accumulator signed and use >>>.
  • Wrong sign on the feedback taps. The denominator is 1 + a1 z⁻¹ + …, so the hardware subtracts a1*y[n-1]. Flipping that sign silently changes the pole locations.
  • Stale vs updated state. In direct-form I you must multiply by the old y[n-1]/y[n-2], not the value being computed this cycle. The non-blocking updates (y1 <= y;) handle this; the testbench golden step reproduces the same ordering.
  • Checking against floating point. If your reference model uses real-number math and rounds differently from the RTL shift, the two drift apart after a few samples even when the hardware is correct. The only trustworthy reference for a fixed-point IIR is a bit-exact integer model that performs the same multiply and the same arithmetic shift — which is exactly why both testbenches above recompute the difference equation with the identical operators.

Synthesis notes

Each example maps to one DSP/MAC block per multiply plus an adder tree, with the delay line as flip-flops. At one multiply-accumulate per clock the whole biquad can run in a single cycle, but the feedback multiply is in the critical path, so for high clock targets you pipeline the feed-forward sum and register the product terms — being careful that the extra latency does not break the single-sample feedback delay. Keep coefficients as parameters or input ports (as shown) so the same RTL serves many filter shapes, and add saturation logic on y before trusting it in a product. Verify the final quantised coefficients still place the poles inside the unit circle after rounding to CW bits.

FAQ

Can I make this a general Nth-order IIR? Yes, but cascade biquads instead of one long section — the poles of a high-order direct form are too sensitive to coefficient quantisation.

Why no divide? The transfer function is normalised so a0 = 1; the recursion only needs the previously computed outputs, never a division.

How do I stop limit cycles? Add a small amount of error feedback / proper rounding, use enough fractional bits, or saturate — and always simulate with the bit-exact model to catch them.

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