Moving Average Filter in Verilog: Running Mean with Code
A moving-average (boxcar) filter outputs the mean of the last N input samples, making it the simplest and most popular low-pass smoothing filter in digital hardware. Instead of recomputing the whole sum every clock, efficient moving average filter Verilog code keeps a running accumulator and a small sample buffer, so each new output costs just one add and one subtract regardless of window size.
On this page you get two runnable, simulator-verified designs: a parameterized power-of-two window averager where division becomes a bit shift, and a running-sum variant with an enable. Both ship with self-checking testbenches that compare every output against a golden model computed in the bench.
This parameterized averager stores the last N = 2**LOG2N samples in a shift register and maintains a running accumulator updated as acc += x_new - x_oldest. Because N is a power of two, dividing the sum by N is a simple right shift of LOG2N bits, so there is no divider in the datapath.
module moving_avg #(
parameter DW = 8, // input data width
parameter LOG2N = 2 // window N = 2**LOG2N
) (
input wire clk,
input wire rst, // synchronous, active high
input wire [DW-1:0] x_in,
output wire [DW-1:0] y_out // average of last N samples
);
localparam N = (1 << LOG2N);
localparam ACW = DW + LOG2N; // accumulator width (no overflow)
reg [DW-1:0] buf_mem [0:N-1]; // sample shift register
reg [ACW-1:0] acc; // running sum of the N buffered samples
integer i;
always @(posedge clk) begin
if (rst) begin
acc <= {ACW{1'b0}};
for (i = 0; i < N; i = i + 1)
buf_mem[i] <= {DW{1'b0}};
end else begin
// running-sum update: add new sample, drop oldest
acc <= acc + x_in - buf_mem[N-1];
for (i = N-1; i > 0; i = i - 1)
buf_mem[i] <= buf_mem[i-1];
buf_mem[0] <= x_in;
end
end
assign y_out = acc[ACW-1:LOG2N]; // divide by N == right shift by LOG2N
// expose buffer sum for the testbench golden model (sim only)
// (synthesizable logic above is complete)
endmodule
module tb;
localparam DW = 8;
localparam LOG2N = 2;
localparam N = (1 << LOG2N);
reg clk, rst;
reg [DW-1:0] x;
wire [DW-1:0] y;
moving_avg #(.DW(DW), .LOG2N(LOG2N)) dut (
.clk(clk), .rst(rst), .x_in(x), .y_out(y));
// golden model: keep our own copy of the last N samples
reg [DW-1:0] gold [0:N-1];
integer k, step;
integer gsum;
reg [DW-1:0] gavg;
reg [DW-1:0] stim [0:9];
always #5 clk = ~clk;
initial begin
clk = 0; rst = 1; x = 0;
for (k = 0; k < N; k = k + 1) gold[k] = 0;
// known stimulus sequence (includes startup/fill region)
stim[0]=10; stim[1]=20; stim[2]=30; stim[3]=40;
stim[4]=50; stim[5]=4; stim[6]=100; stim[7]=200;
stim[8]=255; stim[9]=1;
@(negedge clk); rst = 0; // release reset
for (step = 0; step < 10; step = step + 1) begin
x = stim[step];
@(negedge clk); // sample clocked on the posedge just passed
// advance golden model exactly like the DUT
for (k = N-1; k > 0; k = k - 1) gold[k] = gold[k-1];
gold[0] = stim[step];
gsum = 0;
for (k = 0; k < N; k = k + 1) gsum = gsum + gold[k];
gavg = gsum >> LOG2N;
if (y !== gavg)
$display("FAIL step=%0d x=%0d got=%0d exp=%0d", step, stim[step], y, gavg);
else
$display("PASS step=%0d x=%0d avg=%0d", step, stim[step], y);
end
$display("ALL TESTS PASSED");
$finish;
end
endmodule
PASS step=0 x=10 avg=2 PASS step=1 x=20 avg=7 PASS step=2 x=30 avg=15 PASS step=3 x=40 avg=25 PASS step=4 x=50 avg=35 PASS step=5 x=4 avg=31 PASS step=6 x=100 avg=48 PASS step=7 x=200 avg=88 PASS step=8 x=255 avg=139 PASS step=9 x=1 avg=139 ALL TESTS PASSEDThe log drives a known 10-sample sequence through a 4-tap (N=4) filter and prints one PASS line per step. The testbench keeps its own copy of the last four samples, sums them, and shifts right by 2 to form the expected average, including the startup region where the buffer is still filling with zeros. Matching every step (including the overflow-safe case x=255) proves the running-sum update and the shift-based divide are correct.
Many pipelines want the running sum of the last N samples (no divide) plus an enable to gate sample advance. This version supports any window length N, sizes the output with
$clog2(N+1)so the sum never overflows, and only updates whenenis high.module moving_sum #( parameter DW = 8, // input data width parameter N = 8 // window length (any positive integer) ) ( input wire clk, input wire rst, // synchronous, active high input wire en, // advance one sample when high input wire [DW-1:0] x_in, output reg [DW+$clog2(N+1)-1:0] sum_out // running sum of last N samples ); // $clog2 is legal in Verilog-2005 for a constant expression reg [DW-1:0] shiftreg [0:N-1]; integer i; always @(posedge clk) begin if (rst) begin sum_out <= 0; for (i = 0; i < N; i = i + 1) shiftreg[i] <= 0; end else if (en) begin sum_out <= sum_out + x_in - shiftreg[N-1]; for (i = N-1; i > 0; i = i - 1) shiftreg[i] <= shiftreg[i-1]; shiftreg[0] <= x_in; end end endmodule module tb; localparam DW = 8; localparam N = 8; reg clk, rst, en; reg [DW-1:0] x; wire [DW+3:0] s; // DW + clog2(9)=4 -> DW+4 = 12 bits, [11:0] moving_sum #(.DW(DW), .N(N)) dut ( .clk(clk), .rst(rst), .en(en), .x_in(x), .sum_out(s)); reg [DW-1:0] gold [0:N-1]; integer k, step, gsum, seed_ok; always #5 clk = ~clk; initial begin clk = 0; rst = 1; en = 0; x = 0; for (k = 0; k < N; k = k + 1) gold[k] = 0; @(negedge clk); rst = 0; en = 1; seed_ok = 1; // sweep 40 deterministic samples; a simple reproducible generator for (step = 0; step < 40; step = step + 1) begin x = (step*37 + 13) & 8'hFF; // deterministic, no $random @(negedge clk); for (k = N-1; k > 0; k = k - 1) gold[k] = gold[k-1]; gold[0] = x; gsum = 0; for (k = 0; k < N; k = k + 1) gsum = gsum + gold[k]; if (s !== gsum[DW+3:0]) begin $display("FAIL step=%0d x=%0d got=%0d exp=%0d", step, x, s, gsum); seed_ok = 0; end end if (seed_ok) $display("PASS 40-sample running-sum sweep (N=%0d) matches golden", N); // check the 'en' hold: freeze accumulator for 3 cycles en = 0; x = 255; @(negedge clk); @(negedge clk); @(negedge clk); gsum = 0; for (k = 0; k < N; k = k + 1) gsum = gsum + gold[k]; if (s === gsum[DW+3:0]) $display("PASS en=0 holds sum at %0d", s); else $display("FAIL en=0 did not hold: got=%0d exp=%0d", s, gsum); if (seed_ok) $display("ALL TESTS PASSED"); $finish; end endmodulePASS 40-sample running-sum sweep (N=8) matches golden
PASS en=0 holds sum at 1140
ALL TESTS PASSEDThe log sweeps 40 deterministic samples (a fixed generator, never $random) through an 8-tap filter and checks the DUT sum against a golden sum recomputed from the bench's own sample history every cycle. It then drops
enfor three cycles and confirms the accumulator holds its value. PASS on both the sweep and the hold test proves the add/subtract accumulator, the no-overflow width, and the enable gating all work.Running sum vs recomputing every sample
A naive moving average adds all N buffered samples each clock, costing N-1 adders and scaling poorly as the window grows. The running-sum (recursive) form instead keeps one accumulator and updates it as
new_sum = sum + x_new - x_oldest. This is O(1) work per output no matter how large N is, which is why it dominates in hardware.The trade-off is that the running form accumulates the arithmetic exactly, so the oldest sample must be available to subtract. That is what the sample shift register (or a small circular buffer / RAM for large N) provides. As long as the accumulator is wide enough to hold the maximum possible sum, the recursive result is bit-exact with the recomputed sum.
Why a power-of-two window simplifies the divide
Averaging requires dividing the sum by N. A general divider is expensive in area and latency. When
N = 2^k, dividing by N is just discarding the lowkbits of the sum, i.e. a right shift byk, which in Verilog is a free wire slice:acc[ACW-1:LOG2N]. No logic is synthesized for it.If you truly need a non-power-of-two window, you either keep the running sum and divide downstream, multiply by a precomputed reciprocal constant, or accept the sum itself as the output (as in Example 2). Picking N a power of two is the standard way to keep a boxcar averager divider-free.
Startup (fill) behavior
After reset the buffer and accumulator are zero. During the first N-1 clocks the window is only partially filled with real data, so the divisor N is larger than the number of valid samples and the output reads low. In Example 1 the testbench deliberately models this: its golden average divides by the fixed N even while the buffer still holds reset zeros, and the DUT matches.
If a correct average is required from the very first sample, you must either track a valid-sample count and divide by that count until the window fills, or assert a
validoutput only after N samples have been clocked in. Most streaming designs simply flag the output valid after the fill latency.Relation to a boxcar FIR filter
A moving average is exactly an FIR filter whose impulse response is N equal taps of value 1/N:
h[n] = 1/Nfor0 ≤ n < N. Its frequency response is a sinc (Dirichlet) shape with nulls at multiples offs/N, which is why a boxcar is a cheap but gently-rolling low-pass with sidelobes rather than a sharp filter.The running-sum implementation is the recursive (CIC-like) realization of that FIR: a comb that subtracts the delayed sample feeding an integrator. A single-stage CIC decimator is literally this circuit. Understanding the averager therefore gives you the foundation for CIC filters used in high-rate decimation front-ends.
Fixed-point rounding and common mistakes
- Truncation vs rounding: a right-shift divide truncates toward zero (for unsigned, toward negative infinity for signed), introducing a small negative bias. To round-to-nearest, add
N/2to the sum before shifting:(acc + (1<<(LOG2N-1))) >> LOG2N.- Accumulator width: the sum of N samples of width DW needs
DW + ceil(log2(N))bits. Sizing the accumulator at only DW bits silently overflows and corrupts the average. Example 2 uses$clog2(N+1)to size it safely.- Signed data: declare the buffer, accumulator and ports
signedand use an arithmetic right shift, or the divide and the subtract both misbehave on negative samples.- Reset of the buffer: forgetting to clear the shift register on reset means the first N outputs reflect stale data, not a clean fill from zero.
FAQ
Is this synthesizable? Yes. The shift register maps to flip-flops (or inferred RAM for large N), the accumulator to an adder/subtractor, and the power-of-two divide to wiring. Does it clock at high speed? The critical path is one add plus one subtract into the accumulator, which is short and independent of N, so these filters run fast.
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