FIR Filter in Verilog: MAC-Based Digital Filter with Code

A finite-impulse-response (FIR) filter computes each output as a weighted sum of the current and past input samples, y[n] = ∑k h[k]·x[n−k]. Because the response to an impulse lasts only as many samples as there are coefficients (taps), the filter is always stable and, with symmetric coefficients, has exactly linear phase. This page gives working FIR filter Verilog code: a tapped delay line feeding a multiply-accumulate (MAC) datapath, built from reg/wire only so it synthesizes cleanly.

The first example is a parameterized 4-tap filter checked against a golden convolution; the second folds a symmetric 5-tap filter to halve the multiplier count. Both testbenches drive an impulse, a step and a ramp and self-check every cycle, so a real bug would print a FAIL line.

A clocked 4-tap FIR with signed coefficients h = [1, 2, 3, 4]. Each rising edge shifts a new sample into the delay line d0..d3, and a combinational MAC forms the output. The testbench keeps its own sample history and recomputes the convolution independently as a golden reference.

// Parameterized N-tap FIR filter (fixed signed coefficients)
// y[n] = h0*x[n] + h1*x[n-1] + h2*x[n-2] + h3*x[n-3]
module fir4 #(
    parameter W    = 8,   // input/coefficient word width
    parameter ACCW = 20   // accumulator / output width
)(
    input  wire                   clk,
    input  wire                   rst,     // synchronous, active high
    input  wire signed [W-1:0]    x,
    output wire signed [ACCW-1:0] y
);
    // Fixed signed coefficients (weighted moving sum)
    localparam signed [W-1:0] h0 = 1;
    localparam signed [W-1:0] h1 = 2;
    localparam signed [W-1:0] h2 = 3;
    localparam signed [W-1:0] h3 = 4;

    // Tapped delay line: d0 is newest sample
    reg signed [W-1:0] d0, d1, d2, d3;

    always @(posedge clk) begin
        if (rst) begin
            d0 <= 0; d1 <= 0; d2 <= 0; d3 <= 0;
        end else begin
            d0 <= x;
            d1 <= d0;
            d2 <= d1;
            d3 <= d2;
        end
    end

    // Combinational multiply-accumulate across the taps
    assign y = h0*d0 + h1*d1 + h2*d2 + h3*d3;
endmodule

module tb;
    localparam W    = 8;
    localparam ACCW = 20;

    reg                    clk, rst;
    reg  signed [W-1:0]    x;
    wire signed [ACCW-1:0] y;

    fir4 #(.W(W), .ACCW(ACCW)) dut (.clk(clk), .rst(rst), .x(x), .y(y));

    // Independent golden reference: full history + direct convolution
    reg signed [W-1:0] xh [0:255];
    integer n;            // sample index, 1-based
    integer errors;

    // coefficients mirrored in the TB reference (not shared with the DUT)
    function signed [ACCW-1:0] golden;
        input integer idx;
        reg signed [ACCW-1:0] acc;
        begin
            acc = 0;
            if (idx-0 >= 1) acc = acc + 1 * xh[idx-0];
            if (idx-1 >= 1) acc = acc + 2 * xh[idx-1];
            if (idx-2 >= 1) acc = acc + 3 * xh[idx-2];
            if (idx-3 >= 1) acc = acc + 4 * xh[idx-3];
            golden = acc;
        end
    endfunction

    // Apply one sample: set x before the rising edge, then check after it
    task step;
        input signed [W-1:0] xin;
        reg signed [ACCW-1:0] exp;
        begin
            n = n + 1;
            xh[n] = xin;
            x = xin;
            @(posedge clk);   // DUT samples x -> d0
            #1;               // let combinational y settle
            exp = golden(n);
            if (y !== exp) begin
                errors = errors + 1;
                $display("FAIL n=%0d x=%0d y=%0d expected=%0d", n, xin, y, exp);
            end else begin
                $display("PASS n=%0d x=%0d y=%0d", n, xin, y);
            end
        end
    endtask

    integer k;
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        errors = 0;
        n = 0;
        for (k = 0; k <= 255; k = k + 1) xh[k] = 0;
        x = 0; rst = 1;
        @(posedge clk); #1; rst = 0;   // clear the pipeline

        // Impulse
        step(10);
        step(0);
        step(0);
        step(0);
        step(0);
        // Unit step
        step(5);
        step(5);
        step(5);
        step(5);
        // Short ramp
        step(1);
        step(2);
        step(3);
        step(-4);
        step(0);

        if (errors == 0)
            $display("ALL TESTS PASSED");
        else
            $display("TESTS FAILED: %0d", errors);
        $finish;
    end
endmodule
PASS n=1 x=10 y=10
PASS n=2 x=0 y=20
PASS n=3 x=0 y=30
PASS n=4 x=0 y=40
PASS n=5 x=0 y=0
PASS n=6 x=5 y=5
PASS n=7 x=5 y=15
PASS n=8 x=5 y=30
PASS n=9 x=5 y=50
PASS n=10 x=1 y=46
PASS n=11 x=2 y=39
PASS n=12 x=3 y=30
PASS n=13 x=-4 y=12
PASS n=14 x=0 y=9
ALL TESTS PASSED

The log proves the hardware MAC matches an independent convolution reference on every cycle. The impulse response reads out as 10, 20, 30, 40 — exactly the coefficient set scaled by the input — confirming the tap ordering, and the step and ramp cases exercise the full delay line including a negative sample.

A symmetric (linear-phase) 5-tap FIR, h = [1, 2, 3, 2, 1], implemented with coefficient folding: the equal outer taps are added before multiplying, so only 3 multipliers are used instead of 5. The golden model is the plain unfolded convolution, so the test confirms folding is algebraically identical.

// Symmetric (linear-phase) 5-tap FIR using coefficient folding.
// Coefficients are symmetric: h = [1, 2, 3, 2, 1].
// Folding pairs the equal outer taps BEFORE multiplying, so only
// 3 multipliers are needed instead of 5.
module fir_sym5 #(
    parameter W    = 8,
    parameter ACCW = 20
)(
    input  wire                   clk,
    input  wire                   rst,
    input  wire signed [W-1:0]    x,
    output wire signed [ACCW-1:0] y
);
    // Half-set of the symmetric coefficients
    localparam signed [W-1:0] g0 = 1;  // taps 0 and 4
    localparam signed [W-1:0] g1 = 2;  // taps 1 and 3
    localparam signed [W-1:0] g2 = 3;  // center tap 2

    reg signed [W-1:0] d0, d1, d2, d3, d4;  // d0 newest

    always @(posedge clk) begin
        if (rst) begin
            d0 <= 0; d1 <= 0; d2 <= 0; d3 <= 0; d4 <= 0;
        end else begin
            d0 <= x;
            d1 <= d0;
            d2 <= d1;
            d3 <= d2;
            d4 <= d3;
        end
    end

    // Pre-adder folding: add the symmetric pairs, then multiply once each.
    wire signed [W:0] s0 = d0 + d4;   // extra bit guards the pre-add overflow
    wire signed [W:0] s1 = d1 + d3;

    assign y = g0*s0 + g1*s1 + g2*d2;
endmodule

module tb;
    localparam W    = 8;
    localparam ACCW = 20;

    reg                    clk, rst;
    reg  signed [W-1:0]    x;
    wire signed [ACCW-1:0] y;

    fir_sym5 #(.W(W), .ACCW(ACCW)) dut (.clk(clk), .rst(rst), .x(x), .y(y));

    // Golden: direct (unfolded) 5-tap convolution over full history.
    reg signed [W-1:0] xh [0:255];
    integer n, errors, k;

    function signed [ACCW-1:0] golden;
        input integer idx;
        reg signed [ACCW-1:0] acc;
        begin
            acc = 0;
            if (idx-0 >= 1) acc = acc + 1 * xh[idx-0];
            if (idx-1 >= 1) acc = acc + 2 * xh[idx-1];
            if (idx-2 >= 1) acc = acc + 3 * xh[idx-2];
            if (idx-3 >= 1) acc = acc + 2 * xh[idx-3];
            if (idx-4 >= 1) acc = acc + 1 * xh[idx-4];
            golden = acc;
        end
    endfunction

    task step;
        input signed [W-1:0] xin;
        reg signed [ACCW-1:0] exp;
        begin
            n = n + 1;
            xh[n] = xin;
            x = xin;
            @(posedge clk);
            #1;
            exp = golden(n);
            if (y !== exp) begin
                errors = errors + 1;
                $display("FAIL n=%0d x=%0d y=%0d expected=%0d", n, xin, y, exp);
            end else begin
                $display("PASS n=%0d x=%0d y=%0d", n, xin, y);
            end
        end
    endtask

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        errors = 0; n = 0;
        for (k = 0; k <= 255; k = k + 1) xh[k] = 0;
        x = 0; rst = 1;
        @(posedge clk); #1; rst = 0;

        // Impulse -> should reproduce the symmetric coefficient set
        step(7);
        step(0); step(0); step(0); step(0); step(0);
        // Step input
        step(4); step(4); step(4); step(4); step(4); step(4);
        // Deterministic pseudo-sweep (fixed vectors, not $random)
        step(-3); step(6); step(-1); step(2); step(-8); step(5);
        step(0); step(0); step(0); step(0);

        if (errors == 0)
            $display("ALL TESTS PASSED");
        else
            $display("TESTS FAILED: %0d", errors);
        $finish;
    end
endmodule
PASS n=1 x=7 y=7
PASS n=2 x=0 y=14
PASS n=3 x=0 y=21
PASS n=4 x=0 y=14
PASS n=5 x=0 y=7
PASS n=6 x=0 y=0
PASS n=7 x=4 y=4
PASS n=8 x=4 y=12
PASS n=9 x=4 y=24
PASS n=10 x=4 y=32
PASS n=11 x=4 y=36
PASS n=12 x=4 y=36
PASS n=13 x=-3 y=29
PASS n=14 x=6 y=24
PASS n=15 x=-1 y=14
PASS n=16 x=2 y=16
PASS n=17 x=-8 y=2
PASS n=18 x=5 y=-1
PASS n=19 x=0 y=-11
PASS n=20 x=0 y=1
PASS n=21 x=0 y=2
PASS n=22 x=0 y=5
ALL TESTS PASSED

The log shows the folded datapath produces the same result as a direct 5-tap convolution across an impulse, a step and a deterministic mixed-sign sweep. The symmetric impulse response 7, 14, 21, 14, 7 confirms linear phase, and every cycle matches the reference — folding saved two multipliers with no change in behaviour.

Tapped-delay-line structure

An FIR filter is a shift register of past input samples (the taps) feeding a bank of multipliers and a summing tree. On each clock the newest sample enters d0 and every stored sample moves one position down the line, so after the edge d0 holds x[n], d1 holds x[n−1], and so on. The multiply-accumulate then evaluates y = h0·d0 + h1·d1 + ….

In the code the delay line is just non-blocking assignments in a clocked always block, and the MAC is a single combinational assign. Keep the register updates non-blocking (<=) so all taps shift from their old values in parallel; using blocking = here would corrupt the line into a single stage.

Fixed-point scaling and coefficient quantization

Real filter coefficients are fractions, but hardware multipliers work on integers, so coefficients are quantized to a fixed-point format Qm.f — store round(h[k] × 2^f) as a signed integer and shift the accumulator right by f bits at the end. The examples here use small integer coefficients for clarity, but the datapath is identical: only the final right-shift (and rounding) is added.

  • Accumulator width. For an N-tap filter with W-bit inputs and C-bit coefficients, the full-precision sum needs about W + C + ceil(log2(N)) bits to never overflow. The examples use a generous ACCW = 20.
  • Quantization error. Rounding coefficients perturbs the frequency response (passband ripple, reduced stopband attenuation). More fractional bits reduce the error at the cost of wider multipliers.
  • Signedness. Declare inputs, coefficients and taps signed so * and + sign-extend correctly; a stray unsigned operand silently makes the whole expression unsigned.

Symmetric folding and linear phase

A linear-phase FIR has coefficients that are symmetric about the center, h[k] = h[N−1−k]. That symmetry lets you add the two samples that will be multiplied by the same coefficient before the multiply — the classic pre-adder folding shown in the second example. For N taps this cuts the multiplier count from N to roughly N/2, the most valuable saving in an FIR because multipliers dominate area and power.

Watch the pre-adder width: adding two W-bit samples needs W+1 bits, so the folded sum s = d0 + d4 is declared one bit wider to avoid overflow before the multiply. Linear phase matters because it delays every frequency by the same number of samples, preserving waveform shape — essential for audio, communications and any pulse-preserving path.

Pipelining the MAC and latency

The examples use a single combinational adder tree, which is fine at low clock rates but becomes the critical path as N grows. To hit a higher frequency, register the multiplier outputs and pipeline the adder tree into a balanced binary sum. Each pipeline stage adds one clock of latency but shortens the longest combinational path to a single add.

Two distinct latencies are worth separating:

  • Group delay is inherent to the filter: a length-N linear-phase FIR delays the signal by (N−1)/2 samples regardless of implementation.
  • Pipeline latency is an implementation choice: every register stage you insert in the MAC adds one more clock before the first valid output appears. Downstream logic must account for this fixed offset.

A transposed-form FIR is another option — it pushes the registers between the adders, giving short paths without an explicit pipeline, at the cost of a wide fan-out on the input.

Common mistakes

  • Blocking assignments in the delay line. Using = instead of <= makes the shifts execute sequentially, collapsing the line to one stage.
  • Accumulator too narrow. Summing many taps overflows a tight accumulator; size it to W + C + ceil(log2(N)) bits.
  • Forgetting signed. One unsigned operand makes the entire MAC unsigned, so negative samples wrap to large positives.
  • Dropping the fixed-point shift. With quantized fractional coefficients you must right-shift the accumulator by the number of fractional bits, or the gain is off by a factor of 2^f.
  • Ignoring reset of the taps. Without a reset the delay line powers up with unknown samples, producing X on the output until it fills.

FAQ

How many taps do I need?

The tap count sets the sharpness of the transition band: more taps give a steeper roll-off and more stopband attenuation, at the cost of more multipliers and more latency. A filter-design tool (e.g. Parks–McClellan) converts a frequency spec into a coefficient set and tap count.

FIR vs IIR in hardware?

FIR filters have no feedback, so they are unconditionally stable and can be exactly linear-phase, but they need more taps than an IIR filter for the same sharpness. IIR filters are cheaper for steep responses but can be unstable with finite-precision coefficients and have nonlinear phase.

Does folding change the result?

No. Folding is pure algebra — h·a + h·b = h·(a+b) — so a symmetric folded FIR is bit-identical to the direct form, which is exactly what the second testbench verifies against its unfolded golden model.

Why is the output wider than the input?

Each multiply roughly doubles the bit width and the sum of N products adds log2(N) more bits. The output is kept wide to preserve full precision; a real design then rounds and truncates back to the desired output width.

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