The Power Line Interference Problem

Every biosignal application—whether ECG, EMG, or photoplethysmography—battles 50Hz (Europe/Asia) or 60Hz (Americas) mains hum. This electromagnetic interference couples capacitively into analog front-ends, swamping low-amplitude physiological signals. A single-stage notch filter rarely suffices: you need steep attenuation without destroying adjacent frequency content or introducing nonlinear phase distortion that smears QRS complexes or PPG pulse morphology.

In production PPG apps processing heart rate from smartphone cameras or wearable LEDs, 50Hz noise manifests as a sinusoidal artifact riding atop the 1-3Hz cardiac waveform. Naive high-pass filtering above 0.5Hz removes DC wander but leaves mains interference intact. A properly designed cascaded notch architecture achieves 60+ dB rejection at the interference frequency while preserving the 0.5-10Hz band where pulse morphology lives.

Second-Order IIR Notch Filter Fundamentals

The biquad notch filter places conjugate zeros on the unit circle at the notch frequency f₀ and poles slightly inside the circle to create a narrow rejection band. Transfer function in the z-domain:

H(z) = (1 - 2cos(ω₀)z⁻¹ + z⁻²) / (1 - 2rcos(ω₀)z⁻¹ + r²z⁻²)

where ω₀ = 2πf₀/fs (normalized frequency) and r controls the pole radius. The Q-factor (quality factor) determines bandwidth: Q = 1/(1-r). For 50Hz rejection at 100Hz sample rate, ω₀ = π. A Q of 30 yields a 3dB bandwidth of 1.67Hz—narrow enough to spare adjacent content but wide enough to handle mains frequency drift (±0.2Hz typical).

Direct-form II transposed structure minimizes quantization noise in fixed-point implementations. Coefficients for f₀=50Hz, fs=100Hz, Q=30:

  • b0 = 1.0
  • b1 = 0.0 (zeros on unit circle at ±π)
  • b2 = 1.0
  • a1 = 0.0
  • a2 = 0.9333 (r ≈ 0.9667)

Single-precision float suffices for mobile; 16-bit fixed-point requires scaling to prevent overflow in intermediate multiplications.

Cascading for Steeper Rejection

A single second-order section achieves roughly 40dB attenuation at f₀ with Q=30. Clinical-grade apps demand 60-80dB. Cascading three identical notch stages in series multiplies the transfer function: H_total(z) = H(z)³, tripling the dB rejection. Each stage operates on the output of the previous, requiring three sets of delay states.

Phase response becomes critical. IIR filters exhibit nonlinear phase—group delay varies with frequency. A single notch induces ~15ms delay at f₀ for Q=30, fs=100Hz. Three stages compound this to ~45ms, acceptable for non-real-time analysis but problematic for live biofeedback. Zero-phase filtering (forward-backward pass, a la filtfilt) doubles latency and isn't causal; for real-time, accept the delay or use linear-phase FIR alternatives at 10× the computational cost.

Cascading also amplifies coefficient sensitivity. Small errors in a2 shift pole locations, detuning the notch. On ARM Cortex-A cores, double-precision intermediate accumulation (vmlal in NEON) prevents catastrophic error growth. For ultra-low-power MCUs, second-order sections with scaled coefficients (a2 × 2^14) and 32-bit accumulators maintain stability.

Adaptive Notch Tuning

Mains frequency drifts with grid load. A fixed 50.0Hz notch misses 49.8Hz interference. Adaptive notch filters estimate f₀ in real-time via least-mean-squares (LMS) or recursive least-squares (RLS) and update coefficients on-the-fly. For mobile biosignal apps, a lightweight approach: run an FFT every 10 seconds on a 2048-sample window, locate the spectral peak near 50Hz, recompute biquad coefficients if the peak shifts >0.1Hz.

Coefficient update latency matters. Recomputing cos(ω₀) and r takes ~200 CPU cycles on Cortex-A53; batch updates every 5-10 seconds amortize this cost. Store a lookup table of precomputed coefficients for 49.5-50.5Hz in 0.05Hz steps (21 entries) to avoid transcendental function calls entirely. Index the table based on FFT peak location.

ARM NEON Implementation

NEON SIMD processes four biquad sections in parallel using float32x4_t vectors. Interleave state variables (four channels' z1 and z2 delays) in a single vector, apply the same coefficients to all. For a three-stage cascade on a single PPG channel, unroll the loop and pipeline loads/stores:

float32x4_t process_cascade(float32x4_t x, float32x4_t *state, const float *coef) {
  float32x4_t b0 = vld1q_dup_f32(&coef[0]);
  float32x4_t b2 = vld1q_dup_f32(&coef[2]);
  float32x4_t a2 = vld1q_dup_f32(&coef[4]);
  float32x4_t z1 = state[0], z2 = state[1];
  float32x4_t y = vmlaq_f32(vmlaq_f32(vmulq_f32(x, b0), z1, b0), z2, b2);
  state[0] = vmlsq_f32(x, y, a2);
  state[1] = z1;
  return y;
}

Three calls to process_cascade with distinct state arrays implement the full chain. On a Snapdragon 8 Gen 2, this processes 100Hz PPG at ~0.3% CPU per channel, leaving headroom for ML inference and UI rendering. Batch processing 256 samples per callback (2.56s latency) improves cache locality; real-time apps tolerate 10-50ms, so 128-sample batches suffice.

Validating Rejection Depth

Inject a synthetic 50Hz sine wave at -20dBFS into a clean PPG signal, process through the cascade, measure output power at 50Hz via Welch's method (8192-point FFT, Hann window, 50% overlap). Expect >60dB suppression with three Q=30 stages. Verify adjacent bins (49Hz, 51Hz) remain within 0.5dB of unfiltered to confirm narrow bandwidth.

Phase linearity check: impulse response should show symmetric ringing around the main lobe. Asymmetry indicates pole/zero mismatch. Group delay plot (computed via grpdelay in MATLAB or Python's scipy.signal) should peak at 50Hz and stay flat elsewhere. Deviations >5ms across 1-10Hz distort pulse transit time measurements in PPG-based blood pressure estimation.

Production Gotchas

Startup transients: IIR filters with non-zero initial state exhibit ringing for ~3/BW seconds. Pre-fill delay lines with the first input sample (assuming quasi-stationary signal) or run 2-3 seconds of dummy data through before reporting results. In HearingAid Pro's DSP pipeline, we buffer 1 second of audio, apply the notch offline, then switch to real-time mode to avoid user-facing artifacts.

Coefficient precision: 32-bit float coefficients store ~7 decimal digits. For Q>50, pole radius approaches 0.98, and 1-r loses precision. Use double internally, cast to float only at multiply-accumulate. On iOS Metal, half-precision (16-bit float) degrades Q>20 notches; stick to float for biosignal work.

When Not to Use Cascaded Notches

If mains interference is broadband (harmonics at 100Hz, 150Hz due to rectification), a comb filter (multiple notches at integer multiples of f₀) is more appropriate. If phase linearity is non-negotiable (e.g., ECG QRS detection for arrhythmia classification), invest in a 256-tap linear-phase FIR. If the signal bandwidth overlaps 50Hz (e.g., 40-60Hz tremor analysis in Parkinson's research), notch filtering destroys the feature—use spatial filtering or independent component analysis instead.

For mobile apps targeting global markets, parameterize f₀ by region: 50Hz for EU/Asia, 60Hz for Americas. Auto-detect via GPS or let users toggle in settings. A single codebase with runtime coefficient selection covers both without doubling binary size.