top of page
Adiuvo Engineering & Training logo
MicroZed Chronicles icon

MicroZed Chronicles: IIR Filters

  • 3 minutes ago
  • 8 min read

FPGA Conference - FPGA Horizons US East - April 29th, 30th 2026 - THE FPGA Conference, find out more and get Tickets here.


Several times in this blog series we have examined signal processing and filtering signals. When we have done this, we have generally used Finite Impulse Response (FIR) filters.


In this blog we are going to look at using Infinite Impulse Response (IIR) filters. IIR filters can offer us a sharper cut-off filter more efficiently in terms of resource usage. However, due to how they are implemented, they can be unstable and they do not have a perfectly linear phase.


Unlike FIR filters, which only look at the current sample and past samples, an IIR filter uses both current inputs, past inputs, and past outputs. This feedback of past outputs is what can create instability. This also means a sample can influence the filter output theoretically forever.


The equation for an IIR filter in its simplest form as a single order filter is:


y[n] = b0 · x[n] − a1 · y[n−1]


When it comes to implementing IIR filters, we often use a second-order biquad filter:


y[n] = b0·x[n] + b1·x[n−1] + b2·x[n−2] − a1·y[n−1] − a2·y[n−2]


This needs four storage elements, two for the x values and two for the y values. The equation above is often called a Direct Form I implementation when implemented in an FPGA. However, intermediate values can become large when we are using fixed-point math.


We can combine the two delay lines into one shared state variable, often called a Direct Form II implementation:


w[n] = x[n] − a1·w[n−1] − a2·w[n−2]y[n] = b0·w[n] + b1·w[n−1] + b2·w[n−2]


The downside with this is that the internal state can have a larger dynamic range and may overflow when working with FPGA implementations.


Perhaps the most popular and best performing implementation is the transposed Direct Form II:


y[n] = b0·x[n] + s1[n−1]s1[n] = b1·x[n] − a1·y[n] + s2[n−1]s2[n] = b2·x[n] − a2·y[n]



When it comes to higher-order IIR filters, e.g. 4th, 6th, or 8th order, we should cascade second-order filters.


Before we jump into designing and implementing a simple IIR filter for our FPGA, we need to take a quick look at stability.


Stability is based upon the unit circle, which is a circle of radius 1 on the Z-plane. For a filter to be stable, all poles must lie inside the unit circle (i.e. have a magnitude less than 1). If a pole lies at a magnitude of 0.95, the feedback results in a decay of approximately 5% for each sample, and the output remains bounded, so the filter is stable. If a pole lies at 1.05, the feedback results in growth for each sample, and the filter is unstable.

When we look at the equation for the filter, we see the a and b coefficients—how do these relate to the stability and location of poles?


In an IIR filter, the b coefficients define the zeros and determine frequencies which are attenuated, and can be fully cancelled if the zero lies on the unit circle. The a coefficients define the poles, which shape the system response and determine the stability of the filter.


An easy way to think about this is that poles pull the frequency response up, while zeros push it down. This means the poles have a strong influence over the shape of the filter and whether it remains stable, while the zeros define frequencies which are rejected or reduced. For stability, all poles must lie inside the unit circle, whereas zeros can lie on or off the unit circle without making the filter unstable.


Let’s develop a simple example in the transposed form. First, we need to understand how to generate the b and a coefficients.


For a filter sampled at 200 MHz with a cut-off at 30 MHz, we can use the following approach.


First, we need to express the cut-off as a fraction of the sample rate:


ωc = 2π · Fc / Fs


This gives the digital frequency in radians per sample. As with bilinear transforms, there is frequency warping across the spectrum, so we compensate for this using pre-warping:


ωa = tan(ωc / 2)


We can now calculate the coefficients using a second-order Butterworth low-pass filter:


K = ωa² / (1 + √2·ωa + ωa²)


This enables us to determine b and a using the equations:


b₀ = Kb₁ = 2·Kb₂ = Ka₁ = 2·(ωa² − 1) / (1 + √2·ωa + ωa²)a₂ = (1 − √2·ωa + ωa²) / (1 + √2·ωa + ωa²)


For this example, the coefficients are:


b₀ 0.1311064399 b₁ 0.2622128798 b₂ 0.1311064399 a₁ −0.7477891783 a₂ 0.2722149379



We can therefore create an RTL module which implements the filter as shown below:


LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.numeric_std.ALL;
USE ieee.fixed_float_types.ALL;
USE ieee.fixed_pkg.ALL;

ENTITY iir_filter IS
  GENERIC (
    g_data_width : natural := 16;  -- Input/Output data width
    g_coef_width : natural := 18;  -- Coefficient width
    g_coef_frac  : natural := 16   -- Fractional bits in coefficients
  );
  PORT (
    i_clk     : IN  std_logic;                                  -- System clock (200 MHz)
    i_rst_n   : IN  std_logic;                                  -- Active low reset
    i_valid   : IN  std_logic;                                  -- Input data valid
    i_data    : IN  std_logic_vector(g_data_width - 1 DOWNTO 0); -- Input sample
    o_valid   : OUT std_logic;                                  -- Output data valid
    o_data    : OUT std_logic_vector(g_data_width - 1 DOWNTO 0)  -- Output sample
  );
END ENTITY iir_filter;

ARCHITECTURE rtl OF iir_filter IS

  ---------------------------------------------------------------------------
  -- UltraFast DSP48 Attributes
  -- Use DSP48 internal registers for timing closure
  -- AREG/BREG: Input registers (1 or 2 stages)
  -- MREG: Post-multiplier register
  -- PREG: Post-adder register
  ---------------------------------------------------------------------------
  ATTRIBUTE use_dsp : string;
  ATTRIBUTE use_dsp OF rtl : ARCHITECTURE IS "yes";

  ---------------------------------------------------------------------------
  -- Fixed-Point Format Definitions
  -- Input data:   sfixed(15 downto 0)   - 16-bit signed integer
  -- Coefficients: sfixed(1 downto -16)  - Q2.16 format (range ~ -2 to +2)
  -- Accumulator:  sfixed(17 downto -16) - Full precision after multiply
  ---------------------------------------------------------------------------
  CONSTANT c_data_hi   : integer := g_data_width - 1;   -- 15
  CONSTANT c_data_lo   : integer := 0;                   -- 0
  CONSTANT c_coef_hi   : integer := g_coef_width - g_coef_frac - 1;  -- 1
  CONSTANT c_coef_lo   : integer := -g_coef_frac;        -- -16
  CONSTANT c_acc_hi    : integer := c_data_hi + c_coef_hi + 1;  -- 17
  CONSTANT c_acc_lo    : integer := c_coef_lo;           -- -16

  ---------------------------------------------------------------------------
  -- Filter Coefficients (Q2.16 format)
  -- Using to_sfixed for proper fixed-point conversion
  ---------------------------------------------------------------------------
  CONSTANT c_b0 : sfixed(c_coef_hi DOWNTO c_coef_lo) := to_sfixed(0.1311064399, c_coef_hi, c_coef_lo);
  CONSTANT c_b1 : sfixed(c_coef_hi DOWNTO c_coef_lo) := to_sfixed(0.2622128798, c_coef_hi, c_coef_lo);
  CONSTANT c_b2 : sfixed(c_coef_hi DOWNTO c_coef_lo) := to_sfixed(0.1311064399, c_coef_hi, c_coef_lo);
  CONSTANT c_a1 : sfixed(c_coef_hi DOWNTO c_coef_lo) := to_sfixed(-0.7477891783, c_coef_hi, c_coef_lo);
  CONSTANT c_a2 : sfixed(c_coef_hi DOWNTO c_coef_lo) := to_sfixed(0.2722149379, c_coef_hi, c_coef_lo);

  ---------------------------------------------------------------------------
  -- Internal signals - Fixed Point
  ---------------------------------------------------------------------------
  SIGNAL s_input_sample  : sfixed(c_data_hi DOWNTO c_data_lo);
  SIGNAL s_output_sample : sfixed(c_data_hi DOWNTO c_data_lo);

  -- Multiplication results (full precision)
  -- x * coef: sfixed(15,0) * sfixed(1,-16) = sfixed(17,-16)
  SIGNAL s_mult_b0 : sfixed(c_acc_hi DOWNTO c_acc_lo);
  SIGNAL s_mult_b1 : sfixed(c_acc_hi DOWNTO c_acc_lo);
  SIGNAL s_mult_b2 : sfixed(c_acc_hi DOWNTO c_acc_lo);
  SIGNAL s_mult_a1 : sfixed(c_acc_hi DOWNTO c_acc_lo);
  SIGNAL s_mult_a2 : sfixed(c_acc_hi DOWNTO c_acc_lo);

  -- State registers (delay elements w1 and w2)
  -- Extra guard bits for accumulation
  CONSTANT c_guard_bits : integer := 2;
  CONSTANT c_state_hi   : integer := c_acc_hi + c_guard_bits;
  CONSTANT c_state_lo   : integer := c_acc_lo;

  SIGNAL s_w1 : sfixed(c_state_hi DOWNTO c_state_lo);
  SIGNAL s_w2 : sfixed(c_state_hi DOWNTO c_state_lo);

  -- Accumulator for output
  SIGNAL s_y_acc : sfixed(c_state_hi DOWNTO c_state_lo);

  -- Pipeline valid signals
  SIGNAL s_valid_d1 : std_logic;
  SIGNAL s_valid_d2 : std_logic;
  SIGNAL s_valid_d3 : std_logic;

  ---------------------------------------------------------------------------
  -- UltraFast Design Rules: Two register stages before DSP48
  -- DSP48 internal AREG=2, BREG=2 for optimal timing
  ---------------------------------------------------------------------------
  SIGNAL s_input_reg1 : sfixed(c_data_hi DOWNTO c_data_lo);
  SIGNAL s_input_reg2 : sfixed(c_data_hi DOWNTO c_data_lo);

BEGIN

  ---------------------------------------------------------------------------
  -- Input conversion: std_logic_vector to sfixed
  ---------------------------------------------------------------------------
  s_input_sample <= to_sfixed(signed(i_data), c_data_hi, c_data_lo);

  ---------------------------------------------------------------------------
  -- Pipeline Stage 1: Double register input for DSP48 (AREG=2, BREG=2)
  -- UltraFast: Two register stages before DSP multiplication
  -- Using SYNCHRONOUS reset per UltraFast guidelines
  ---------------------------------------------------------------------------
  proc_input_reg : PROCESS(i_clk)
  BEGIN
    IF rising_edge(i_clk) THEN
      IF i_rst_n = '0' THEN
        s_input_reg1 <= (OTHERS => '0');
        s_input_reg2 <= (OTHERS => '0');
        s_valid_d1   <= '0';
      ELSE
        s_valid_d1   <= i_valid;
        s_input_reg1 <= s_input_sample;
        s_input_reg2 <= s_input_reg1;
      END IF;
    END IF;
  END PROCESS proc_input_reg;

  ---------------------------------------------------------------------------
  -- Pipeline Stage 2: Feedforward multiplications (uses DSP48 MREG)
  -- Using SYNCHRONOUS reset per UltraFast guidelines
  ---------------------------------------------------------------------------
  proc_mult : PROCESS(i_clk)
  BEGIN
    IF rising_edge(i_clk) THEN
      IF i_rst_n = '0' THEN
        s_mult_b0  <= (OTHERS => '0');
        s_mult_b1  <= (OTHERS => '0');
        s_mult_b2  <= (OTHERS => '0');
        s_valid_d2 <= '0';
      ELSE
        s_valid_d2 <= s_valid_d1;
        IF s_valid_d1 = '1' THEN
          -- Feedforward path multiplications using double-registered input
          s_mult_b0 <= resize(s_input_reg2 * c_b0, c_acc_hi, c_acc_lo);
          s_mult_b1 <= resize(s_input_reg2 * c_b1, c_acc_hi, c_acc_lo);
          s_mult_b2 <= resize(s_input_reg2 * c_b2, c_acc_hi, c_acc_lo);
        END IF;
      END IF;
    END IF;
  END PROCESS proc_mult;

  ---------------------------------------------------------------------------
  -- Pipeline Stage 3: Compute y[n], feedback, and state update
  -- Note: IIR feedback requires using current output for state equations
  -- Using SYNCHRONOUS reset per UltraFast guidelines
  ---------------------------------------------------------------------------
  proc_output : PROCESS(i_clk)
    VARIABLE v_y_acc   : sfixed(c_state_hi DOWNTO c_state_lo);
    VARIABLE v_y_out   : sfixed(c_data_hi DOWNTO c_data_lo);
    VARIABLE v_mult_a1 : sfixed(c_acc_hi DOWNTO c_acc_lo);
    VARIABLE v_mult_a2 : sfixed(c_acc_hi DOWNTO c_acc_lo);
    VARIABLE v_w1_temp : sfixed(c_state_hi + 2 DOWNTO c_state_lo);
    VARIABLE v_w2_temp : sfixed(c_state_hi + 1 DOWNTO c_state_lo);
  BEGIN
    IF rising_edge(i_clk) THEN
      IF i_rst_n = '0' THEN
        s_y_acc         <= (OTHERS => '0');
        s_output_sample <= (OTHERS => '0');
        s_mult_a1       <= (OTHERS => '0');
        s_mult_a2       <= (OTHERS => '0');
        s_w1            <= (OTHERS => '0');
        s_w2            <= (OTHERS => '0');
        s_valid_d3      <= '0';
      ELSE
        s_valid_d3 <= s_valid_d2;
        IF s_valid_d2 = '1' THEN
          -- y[n] = b0*x[n] + w1[n-1]
          v_y_acc := resize(
            arg            => resize(s_mult_b0, c_state_hi, c_state_lo) + s_w1,
            left_index     => c_state_hi,
            right_index    => c_state_lo,
            overflow_style => fixed_saturate,
            round_style    => fixed_truncate
          );
          s_y_acc <= v_y_acc;

          -- Scale output
          v_y_out := resize(
            arg            => v_y_acc,
            left_index     => c_data_hi,
            right_index    => c_data_lo,
            overflow_style => fixed_saturate,
            round_style    => fixed_truncate
          );
          s_output_sample <= v_y_out;

          -- Feedback multiplications using current output
          v_mult_a1 := resize(v_y_out * c_a1, c_acc_hi, c_acc_lo);
          v_mult_a2 := resize(v_y_out * c_a2, c_acc_hi, c_acc_lo);
          s_mult_a1 <= v_mult_a1;
          s_mult_a2 <= v_mult_a2;

          -- w1[n] = b1*x[n] - a1*y[n] + w2[n-1]
          v_w1_temp := resize(s_mult_b1, c_state_hi, c_state_lo)
                     - resize(v_mult_a1, c_state_hi, c_state_lo)
                     + s_w2;
          s_w1 <= resize(v_w1_temp, c_state_hi, c_state_lo, fixed_saturate, fixed_truncate);

          -- w2[n] = b2*x[n] - a2*y[n]
          v_w2_temp := resize(s_mult_b2, c_state_hi, c_state_lo)
                     - resize(v_mult_a2, c_state_hi, c_state_lo);
          s_w2 <= resize(v_w2_temp, c_state_hi, c_state_lo, fixed_saturate, fixed_truncate);
        END IF;
      END IF;
    END IF;
  END PROCESS proc_output;

  ---------------------------------------------------------------------------
  -- Output assignment: sfixed to std_logic_vector
  ---------------------------------------------------------------------------
  o_valid <= s_valid_d3;
  o_data  <= to_slv(s_output_sample);

END ARCHITECTURE rtl;

Once this is completed, we can run a simulation which injects signals in and out of the passband to check its attenuation.


When we run the simulation, we see that a 10 MHz signal is not attenuated, while one at 80 MHz is attenuated by 17.9 dB, which is consistent with the expected roll-off of a second-order Butterworth filter.



When it comes to designing IIR filters, we will typically use an application like Python or MATLAB.


IIR filters are key for many FPGA DSP applications. Hopefully, you now understand a little more about how they can be used and how to go about designing them.


FPGA Conference

FPGA Horizons US East - April 28th, 29th 2026 - THE FPGA Conference, find out more and get Tickets here.


FPGA Journal

Read about cutting edge FPGA developments, in the FPGA Horizons Journal or contribute an article.


Workshops and Webinars:

If you enjoyed the blog why not take a look at the free webinars, workshops and training courses we have created over the years. Highlights include:



Boards

Get an Adiuvo development board:

  • Adiuvo Embedded System Development board - Embedded System Development Board

  • Adiuvo Embedded System Tile - Low Risk way to add a FPGA to your design.

  • SpaceWire CODEC - SpaceWire CODEC, digital download, AXIS Interfaces

  • SpaceWire RMAP Initiator - SpaceWire RMAP Initiator,  digital download, AXIS & AXI4 Interfaces

  • SpaceWire RMAP Target - SpaceWire Target, digital download, AXI4 and AXIS Interfaces

  • Other Adiuvo Boards & Projects.


Embedded System Book   

Do you want to know more about designing embedded systems from scratch? Check out our book on creating embedded systems. This book will walk you through all the stages of requirements, architecture, component selection, schematics, layout, and FPGA / software design. We designed and manufactured the board at the heart of the book! The schematics and layout are available in Altium here.  Learn more about the board (see previous blogs on Bring up, DDR validation, USB, Sensors) and view the schematics here.

bottom of page