First-Order IIR Lowpass: The Universal Pre-Filter Every Control Loop Needs
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.
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
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.
Related Posts
- Butterworth Highpass Filter 2nd Order: Maximally Flat, But At What Phase Cost? — Companion piece covering 2nd-order Butterworth biquad design; the coefficient derivation parallels the bilinear method used here.
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?
