Skip to main content

Command Palette

Search for a command to run...

First-Order IIR Lowpass: The Universal Pre-Filter Every Control Loop Needs

Updated
6 min read

The first-order IIR lowpass is the workhorse of embedded DSP. One multiply, one add, one state variable — yet it is the default pre-filter on PID derivative terms, the ADC post-smoother in motor drives, and the exponential moving average (EMA) in sensor fusion. Here is how to design it rigorously using the bilinear transform.

https://youtu.be/CP-zZ7mIDHc


Transfer Function

Design parameters: fc = 1000 Hz, fs = 44 100 Hz

Bilinear-Transform Derivation

The analog prototype is H(s) = ωc / (s + ωc). Applying the bilinear transform with frequency pre-warping:

ωd = tan(π · fc / fs) = tan(π · 1000 / 44100) = 0.07135868

Unnormalized coefficients:

Coefficient Expression Value
a0 (norm.) 1 + ωd 1.07135868
b0_raw ωd 0.07135868
b1_raw ωd 0.07135868
a1_raw −(1 − ωd) −0.92864132

Normalize every coefficient by a0:

Coefficient Value
b0 0.06660578
b1 0.06660578
a1 −0.86678844
b2 0
a2 0

Standard H(z) biquad form:

H(z) = (b0 + b1·z⁻¹) / (1 + a1·z⁻¹)
     = (0.06660578 + 0.06660578·z⁻¹) / (1 − 0.86678844·z⁻¹)

Difference equation:

y[n] = b0·x[n] + b1·x[n−1] − a1·y[n−1]
     = 0.06660578·x[n] + 0.06660578·x[n−1] + 0.86678844·y[n−1]

DC gain (z = 1): (b0+b1)/(1+a1) = 0.13321156/0.13321156 = 1.000
Nyquist gain (z = −1): (b0−b1)/(1−a1) = 0 ✓


Frequency Response

Bode Plot — First-Order IIR Lowpass

Figure: Magnitude (top) and phase (bottom) for fc = 1000 Hz, fs = 44 100 Hz. −3 dB crossover is exact at 1000 Hz due to bilinear pre-warping.

Quantitative observations:

  • −3 dB at exactly 1000 Hz — pre-warping guarantees this.

  • −20 dB/decade rolloff (6 dB/octave) — identical to an analog RC.

  • Phase at fc: −45° — the classic first-order lag, critical for loop margin.

  • Group delay at DC: τ = −(da/dω)|ω→0 ≈ 1/(2π·fc) ≈ 0.159 ms — flat passband group delay.

  • At 10 kHz (one decade above fc): magnitude ≈ −20 dB, phase ≈ −84°.


Python Implementation

import numpy as np
import scipy.signal as signal

def iir1_lowpass_coeffs(fc: float, fs: float):
    wd = np.tan(np.pi * fc / fs)
    b0 = wd / (1.0 + wd)
    b1 = wd / (1.0 + wd)
    a1 = -(1.0 - wd) / (1.0 + wd)
    return np.array([b0, b1]), np.array([1.0, a1])

# Sample-by-sample (stateful)
class IIR1Lowpass:
    def __init__(self, fc, fs):
        self.b, self.a = iir1_lowpass_coeffs(fc, fs)
        self._z = 0.0

    def filter_sample(self, x: float) -> float:
        b0, b1 = self.b
        a1     = self.a[1]
        y      = b0 * x + self._z
        self._z = b1 * x - a1 * y
        return y

C Implementation

Direct Form II Transposed — single delay state, round-trip stable on 32-bit float:

typedef struct {
    float b0, b1, a1;
    float z;            /* single delay state */
} FilterState;

void filter_init(FilterState *f, float fc, float fs) {
    float wd = tanf(M_PI * fc / fs);
    f->b0 =  wd / (1.0f + wd);
    f->b1 =  wd / (1.0f + wd);
    f->a1 = -(1.0f - wd) / (1.0f + wd);
    f->z  = 0.0f;
}

float filter_sample(FilterState *f, float x) {
    float y = f->b0 * x + f->z;
    f->z    = f->b1 * x - f->a1 * y;
    return y;
}

Fixed-point (Q1.15): Scale coefficients by 2¹⁵ = 32768 → b0_q = b1_q = 2183, a1_q = 28408 (absolute). Use a 32-bit accumulator to absorb the product without overflow. This runs on any Cortex-M0 without an FPU.


MATLAB Implementation

fc = 1000; fs = 44100;
wd = tan(pi * fc / fs);
b = [wd  wd ] / (1 + wd);
a = [1  -(1 - wd) / (1 + wd)];

[H, W] = freqz(b, a, 8192, fs);
semilogx(W, 20*log10(abs(H))); grid on;
xlabel('Hz'); ylabel('dB');
title('First-Order IIR Lowpass');
zplane(b, a);

Design Trade-offs

Aspect Bilinear-Transform Form EMA Form (y[n] = α·x[n] + (1−α)·y[n−1])
fc accuracy Exact at any fc/fs ratio Accurate only when fc ≪ fs/2π
Coefficients b0=b1 (two adds), a1 One multiply α, one add
Nyquist gain Exactly 0 Near-zero but nonzero
Group delay Flat below fc Nearly identical

When fc < fs/20 (e.g., 200 Hz at 44 100 Hz), the EMA form α = e^{−2π·fc/fs} is indistinguishable from the bilinear version and is preferred for microcontrollers with no floating-point unit.

Phase lag is the dominant cost: −45° at fc means your PID loop loses nearly half a cycle of margin at the crossover frequency. If fc must sit at or above your loop bandwidth, consider a Bessel or Butterworth 2nd-order design with lower phase lag per decade.


Key Takeaways

  • The first-order IIR lowpass costs exactly one multiply + one add per sample — the lowest overhead of any recursive filter.

  • The bilinear transform guarantees the −3 dB point lands exactly at fc, regardless of the fc/fs ratio.

  • DC gain = 1, Nyquist gain = 0 — confirmed analytically and numerically.

  • Phase lag of −45° at fc is the fundamental design constraint; account for it in your loop stability budget.

  • For fixed-point embedded targets, Q1.15 encoding of b0, b1, a1 fits in a single 16-bit register pair, and the 32-bit accumulator prevents overflow.



Engineering question: In your control loop, where do you place the first-order IIR lowpass relative to the plant — before the ADC read, before the derivative term, or at the output of the controller? How does that placement change your effective loop bandwidth?

More from this blog

S

SW related to Power electronics

18 posts

SW related to Power electronics