Notch Filter 2nd Order: Surgically Remove a Single Frequency Without Touching the Rest
A motor drive switching at 10 kHz induces 50 Hz ripple in your torque estimate. A resolver excitation tone bleeds into your current feedback. A 60 Hz mains hum corrupts an ADC reading. In each case you need a filter that nulls exactly one frequency while leaving everything else intact. That is precisely what the 2nd-order IIR notch biquad does — a single discrete-time structure with two poles, two zeros, unity gain at DC and Nyquist, and an infinitely deep null at f₀.
Transfer Function
Using the Audio EQ Cookbook (Bristow-Johnson) band-stop formulation with Q = 1/√2 and f₀ = 1000 Hz, fs = 44100 Hz:
Design equations:
ω₀ = 2π·f₀/fs = 2π·1000/44100 = 0.142476 rad/sample
α = sin(ω₀)/(2Q) = 0.142093 / 1.41421 = 0.100476
a₀_raw = 1 + α = 1.100476
Unnormalized → Normalized (÷ a₀_raw):
| Coefficient | Unnormalized | Normalized |
|---|---|---|
| b₀_raw | +1.000000000 | b₀ = +0.908714850 |
| b₁_raw | −1.979702070 | b₁ = −1.798732540 |
| b₂_raw | +1.000000000 | b₂ = +0.908714850 |
| a₀_raw | +1.100476000 | (1) |
| a₁_raw | −1.979702070 | a₁ = −1.798732540 |
| a₂_raw | +0.899524000 | a₂ = +0.817429700 |
H(z) in standard biquad form:
b₀ + b₁·z⁻¹ + b₂·z⁻² 0.90871 − 1.79873·z⁻¹ + 0.90871·z⁻²
H(z) = ─────────────────────────── = ─────────────────────────────────────────
1 + a₁·z⁻¹ + a₂·z⁻² 1 − 1.79873·z⁻¹ + 0.81743·z⁻²
Key notch property: b₁ = a₁ exactly. The numerator and denominator share the same z⁻¹ coefficient, guaranteeing unity gain at DC (z = 1) and Nyquist (z = −1). The zeros sit on the unit circle at ±ω₀, creating an infinitely deep null.
Difference equation:
y[n] = b₀·x[n] + b₁·x[n−1] + b₂·x[n−2] − a₁·y[n−1] − a₂·y[n−2]
Frequency Response
Figure: Magnitude (top) and phase (bottom) response. The null at 1000 Hz is theoretically infinite; in double-precision floating-point it exceeds −300 dB. Passband outside ±BW/2 = ±1414 Hz is within 0.01 dB of 0 dB. Phase swings 180° through the notch — a characteristic to watch in closed-loop systems.
Quantitative observations:
Notch depth: > −300 dB at f₀ (limited only by floating-point precision; hardware SNR sets practical depth to ~60–80 dB)
−3 dB bandwidth: f₀/Q = 1000 Hz / 0.707 = 1414 Hz (from ~293 Hz to ~3414 Hz in this design)
DC gain: 0.00 dB — DC bias is preserved exactly
Phase at f₀: −180° (full phase reversal through the null)
Group delay peak: occurs at the notch band edges; worst-case group delay ≈ 2/BW samples
Python Implementation
import numpy as np
from scipy import signal
FS, F0, Q = 44100.0, 1000.0, 1.0/np.sqrt(2.0)
w0 = 2*np.pi*F0/FS
alpha = np.sin(w0)/(2*Q)
a0 = 1 + alpha
B = np.array([1, -2*np.cos(w0), 1]) / a0 # b0, b1, b2
A = np.array([1, -2*np.cos(w0), 1 - alpha]) / a0 # 1, a1, a2
A[0] = 1.0 # ensure leading 1
def filter_signal(x, fs=FS):
return signal.lfilter(B, A, x)
# Verify notch depth
_, H = signal.freqz(B, A, worN=[F0], fs=FS)
print(f"|H(f0)| = {20*np.log10(abs(H[0])+1e-12):.1f} dB") # ≪ -40 dB
The b1 == a1 property means the numerator factor (1 − 2cos(ω₀)z⁻¹ + z⁻²) cancels exactly from numerator and denominator when evaluated on the unit circle at ω₀, leaving H(e^{jω₀}) = 0.
C Implementation
/* Direct Form II Transposed — f0=1000Hz, fs=44100Hz, Q=1/√2 */
#define B0 ( 0.908714850)
#define B1 (-1.798732540) /* == A1 */
#define B2 ( 0.908714850) /* == B0 */
#define A1 (-1.798732540)
#define A2 ( 0.817429700)
typedef struct { double w1, w2; } NotchState;
double notch_process(NotchState *s, double x)
{
double y = B0*x + s->w1;
s->w1 = B1*x - A1*y + s->w2;
s->w2 = B2*x - A2*y;
return y;
}
Fixed-point note: With Q1.15 scaling (×32768): B0≈29784, B1=A1≈−58951, A2≈26784. Because |B1| > 1, a 32-bit accumulator is mandatory — 16-bit saturation will corrupt the null. In Q2.13 all coefficients fit in 16 bits if you accept the 0.5 dB of representational error.
MATLAB Implementation
fs = 44100; f0 = 1000; Q = 1/sqrt(2);
w0 = 2*pi*f0/fs; alpha = sin(w0)/(2*Q);
B = [1, -2*cos(w0), 1] / (1 + alpha);
A = [1, -2*cos(w0), 1 - alpha] / (1 + alpha); A(1) = 1;
freqz(B, A, 8192, fs); % Bode plot
zplane(B, A); % zeros on unit circle at ±ω₀
zplane will show two zeros exactly on the unit circle at angle ±ω₀ — the direct cause of the infinite null. The two poles are inside the unit circle at the same angle but at radius √a2 = √0.81743 ≈ 0.9041, providing the steep notch roll-off.
Design Trade-offs
Q controls bandwidth, not depth. The null depth is theoretically infinite for any Q (zeros are always on the unit circle). Q only sets how wide the null is: higher Q → narrower notch → less phase distortion away from f₀ but more sensitive to coefficient quantization.
Coefficient quantization sensitivity. Because b₁ = a₁, any rounding of this shared coefficient slightly displaces both the zeros and poles simultaneously. If they no longer cancel at ω₀ the null fills in. With 32-bit float, residual notch depth is ~−150 dB; with 16-bit integer, ~−60 dB — still sufficient for most industrial noise rejection.
Phase distortion in control loops. The 180° phase swing through the notch adds phase lag at nearby frequencies. For a notch placed at a mechanical resonance, verify that the resulting phase margin at the crossover frequency is acceptable. Pair with a phase-lead compensator if needed.
Alternative: parametric notch (iirnotch). scipy.signal.iirnotch(f0, Q, fs) generates the same biquad but lets you specify Q directly. The underlying math is identical to the Cookbook formulation.
Key Takeaways
One biquad, infinite null: Two zeros on the unit circle at ±ω₀ guarantee H(e^{jω₀}) = 0 regardless of coefficient precision — as long as b₁ = a₁ exactly.
Q sets bandwidth, not depth: Higher Q narrows the notch; useful when the interference is spectrally tight (e.g., 50/60 Hz mains) and surrounding signal components must be preserved.
Fixed-point: use 32-bit accumulator. The a₁ coefficient exceeds unity in magnitude at typical control loop sample rates; 16-bit saturation destroys the null.
Phase warning for closed-loop: The phase swing at the notch frequency interacts with loop gain; always verify phase margin after inserting a notch into a control loop.
Notch ≠ bandpass complement: Unlike a Butterworth bandpass, the notch does not have a matched bandpass sibling (the sum H_notch + H_bandpass ≠ 1 in general); use the state-variable filter if you need simultaneous outputs.
Engineering question: In your application, does the notch need to track a drifting interference frequency (e.g., a variable-speed motor harmonic), or is a fixed-coefficient design sufficient? If tracking is needed, how would you implement an adaptive notch using an LMS or RLS update rule?
