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 PASSEDThe 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 endmodulePASS 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 PASSEDThe 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, 7confirms 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
d0and every stored sample moves one position down the line, so after the edged0holdsx[n],d1holdsx[n−1], and so on. The multiply-accumulate then evaluatesy = h0·d0 + h1·d1 + ….In the code the delay line is just non-blocking assignments in a clocked
alwaysblock, and the MAC is a single combinationalassign. 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— storeround(h[k] × 2^f)as a signed integer and shift the accumulator right byfbits 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 withW-bit inputs andC-bit coefficients, the full-precision sum needs aboutW + C + ceil(log2(N))bits to never overflow. The examples use a generousACCW = 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
signedso*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. ForNtaps this cuts the multiplier count fromNto roughlyN/2, the most valuable saving in an FIR because multipliers dominate area and power.Watch the pre-adder width: adding two
W-bit samples needsW+1bits, so the folded sums = d0 + d4is 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
Ngrows. 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-
Nlinear-phase FIR delays the signal by(N−1)/2samples 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
Nproducts addslog2(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 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