← All guides

An FIR filter in VHDL, from equation to FPGA

An FIR (finite impulse response) filter computes each output sample as a weighted sum of the M most recent input samples:

y[n] = ∑k=0M−1 bk⁢x[n−k]

M is the number of taps and bk are the coefficients. In hardware the delayed samples are held in registers, each coefficient is a multiplier, and the sum is one or more adders.

This page implements the simplest case, a two-tap moving average, in VHDL, shows the circuit it synthesizes to, and compares RapidRTL’s schematic with Vivado’s.

The two-tap moving average

With M = 2 and both coefficients equal to ½:

y[n] = x[n]+x[n−1]2

Each output is the average of the current sample and the previous one. This is a low-pass filter with magnitude response |H(ω)|=cos⁡(ω/2), where ω is in radians per sample. A constant input passes unchanged. An input alternating between +A and −A, a tone at half the sample rate, is removed.

From equation to hardware

TermHardwareCost for 8-bit samples
x[n-1]a register, clocked once per sample8 flip-flops
+an adder one bit wider than the samples, so the sum cannot overflowone 9-bit adder
/ 2a right shift by one bitnone

A right shift by one bit divides a binary number by two, in the same way that dropping the last digit of a decimal number divides it by ten. A shift by a fixed amount needs no logic: y is wired to bits 8 down to 1 of the sum, and bit 0 is left unconnected.

The VHDL

One 8-bit signed sample arrives on x every clock cycle.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

-- Two-tap moving average: y[n] = (x[n] + x[n-1]) / 2
entity fir_avg2 is
  port (
    clk : in  std_logic;
    x   : in  signed(7 downto 0);    -- x[n], one sample per clock
    y   : out signed(7 downto 0)     -- y[n]
  );
end entity;

architecture rtl of fir_avg2 is
  signal x_prev : signed(7 downto 0) := (others => '0');   -- x[n-1]
  signal sum    : signed(8 downto 0);                      -- one bit wider
begin
  -- The delay: a register that hands back last cycle's sample.
  process (clk) is
  begin
    if rising_edge(clk) then
      x_prev <= x;
    end if;
  end process;

  -- The adder: widen both samples to 9 bits first, so the sum cannot overflow.
  sum <= resize(x, 9) + resize(x_prev, 9);

  -- The divide by two: leave out the lowest bit. No logic, only wiring.
  y <= sum(8 downto 1);
end architecture;

The divide by two

A right shift by one bit moves every bit to the position below it, where it is worth half as much, so the value is halved. y <= sum(8 downto 1) is that shift: bit 8 of sum drives bit 7 of y, bit 7 drives bit 6, and so on. For 100 + 100:

Bit of sum876543210
Weight in sum-2561286432168421
sum = 200011001000
Drives bit of y76543210none
Weight in y-12864321684210

The three set bits are worth 128 + 64 + 8 = 200 in sum and 64 + 32 + 4 = 100 in y. The top bit has a negative weight because the value is signed. Bit 0 of the sum is unconnected, so the result rounds down: 5 gives 2 and -5 gives -3.

In the schematics below the divide is therefore not a component. It is the one output bit of the adder that is left unconnected.

The circuit

The schematic RapidRTL draws from that code, with each step boxed:

clk x y 0:7 8 0:7 8 7 7 1:8 12234
The moving average as synthesized. Only the register (1) and the adder (3) are logic. The rest is wiring.
  1. Delay. The register, drawn with a clock triangle. Its output is x_prev, the x[n-1] term.
  2. Sign extension. resize(x, 9). The first bar selects bit 7, the sign. The second joins it to bits 0:7 as bit 8. Wiring only.
  3. Adder. One 9-bit adder.
  4. Divide by two. The bar labelled 1:8 passes bits 1 to 8 of the sum to y. Bit 0 stops there. Wiring only.

The same design as a live schematic, next to its code:

Interactive schematic by RapidRTL

Response

A step input: x is 0, then 100 for three samples, then 0.

01234567clkx01000x_prev01000sum01002001000y050100500
A step in x becomes a ramp in y. The sum reaches 200, the value that needs the ninth bit.

In cycle 2 the input is 100 and the register still holds 0, so y is 50. In cycle 3 both are 100. The edge in x becomes a ramp in y, which is the low-pass behaviour seen in the time domain.

An input alternating between +50 and -50, a tone at half the sample rate:

012345clkx50-5050-5050-50x_prev050-5050-5050sum500y250
Each sample cancels the previous one, so y stays at 0 after the first cycle.

The first output is 25 because the register starts at 0.

y changes in the same cycle as x: the only register is on the delay path, so the output is combinational from the input. Registering y as well costs 8 more flip-flops and one cycle of latency.

Comparison with Vivado

The same file synthesized with Vivado 2025.1 for an Artix-7 (xc7a35t).

Elaborated design

The code as generic blocks, before it is mapped to the device.

Vivado's elaborated schematic of fir_avg2: input x feeds a register named x_prev_reg and one input of an adder named sum_i; the register feeds the adder's other input; the adder drives output y.
Vivado's elaborated design: one register (RTL_REG) and one adder (RTL_ADD).

This is the same register and adder. Vivado draws each bus as one line and does not draw the sign extension or the bit selection, so the divide by two shows only in the widths: the adder’s output is O[8:0] and the port it drives is y[7:0]. RapidRTL draws both, as the bars in boxes 2 and 4, which shows that the dropped bit is bit 0.

Synthesized design

An FPGA builds an adder from look-up tables and a carry chain. RapidRTL shows this mapping when an FPGA family is set as the target (free in the web editor, part of Pro in the VS Code extension). For Xilinx 7-series:

LUT2 I0 I1 O LUT2 I0 I1 O LUT2 I0 I1 O LUT2 I0 I1 O LUT2 I0 I1 O LUT2 I0 I1 O LUT2 I0 I1 O LUT2 I0 I1 O CARRY4 CI CYINIT DI S CO O CARRY4 CI CYINIT DI S CO O CARRY4 CI CYINIT DI S CO O FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q clk x y 0 000 1 0 1 2 3 0 1 2 3 0 1:3 0 1:3 0:2 3:6 7 1 0 2 7 4 5 3 6 0:3 4:7 3 3 1:3 0 111223334
RapidRTL, Xilinx 7-series target. The boxes mark each kind of primitive. Zoom the live version below to read the pins.
  1. Delay. Eight FDRE flip-flops, one per bit of x_prev.
  2. Adder, first stage. Eight LUT2 cells, each the XOR of one bit of x and the same bit of x_prev.
  3. Carry chain. Three CARRY4 cells, four bits each, chained from the top left to the bottom right. The first two produce bits 0 to 7 of the sum, the third bit 8.
  4. Divide by two. The bar labelled 1:3 on the output of the first CARRY4 passes bits 1 to 3 on, as bits 0 to 2 of y. Bit 0 is unconnected.

Interactive schematic by RapidRTL

Vivado’s result for the same file:

Vivado's synthesized schematic of fir_avg2: input buffers on the left, eight FDRE flip-flops, eight LUT2 cells and one LUT1 feeding three CARRY4 cells, and output buffers on the right.
Vivado's synthesized design. Click it for the full-size image.
PrimitiveRapidRTLVivadoFunction
FDRE88the register: one flip-flop per bit of x_prev
LUT288the adder’s first stage: the XOR of each pair of input bits
CARRY433the carry chain: four bits of the sum each
LUT101an inverter on the sign bit of x
IBUF, OBUFnot drawn9, 8a buffer on every pin
BUFGnot drawn1the clock buffer

Both schematics contain the same filter: 8 flip-flops for the delay, and 8 LUT2 cells feeding 3 CARRY4 cells for the 9-bit adder. The divide by two is in neither column because it is not a primitive. In Vivado’s schematic it is in the same place: the first CARRY4 (y_OBUF[2]_inst_i_1) has a four-bit output that drives only three output buffers, y[0] to y[2].

The results differ in two places:

Next steps