Skip to main content

Command Palette

Search for a command to run...

Notch Filter 2nd Order: Surgically Remove a Single Frequency Without Touching the Rest

Updated
6 min read

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₀.

https://youtu.be/sTu5AvJzDfw

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

Notch Filter 2nd Order Bode Plot — f₀=1000 Hz, fs=44100 Hz, Q=1/√2

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?

More from this blog

S

SW related to Power electronics

18 posts

SW related to Power electronics