Digital Filters & DSP

Digital filters process sampled signals in software or hardware. Learn FIR vs IIR, moving-average filters, windowed-sinc FIR design, linear phase and group delay, IIR biquads, filter order estimation, fixed-point implementation, DSP processors and MAC operations — with a Python FIR design.

Noise-cancelling headphones, ECG monitors, 5G modems and car radars all run digital filters millions of times per second. Unlike analog filters, digital filters are perfectly repeatable, easily changed and can have exactly linear phase.

An analogy

An FIR filter is like a weighted average of the last N samples — a recipe telling how much of each recent sample to mix into the output.

FIR vs IIR

FIR: no feedback, always stable, can have exactly linear phase, needs more taps. IIR: feedback, much lower order for sharp responses, non-linear phase, must be checked for stability and quantisation effects.

Design methods

FIR: windowed sinc, Parks–McClellan (equiripple), frequency sampling. IIR: bilinear transform of Butterworth/Chebyshev/elliptic prototypes, implemented as cascaded biquads for numerical robustness.

Implementation

Each tap is a multiply-accumulate (MAC). DSP processors, MCU DSP extensions (Cortex-M4/M7, CMSIS-DSP), FPGAs and GPUs execute MACs efficiently. Fixed-point (Q15/Q31) needs scaling to avoid overflow.

Multirate and adaptive

Decimation and interpolation with polyphase filters change sample rates efficiently. Adaptive filters (LMS, RLS) update coefficients in real time for echo and noise cancellation and channel equalisation.

Formulas

Worked example: Filter order

Given: 60 dB attenuation, transition 500 Hz, f_s 8 kHz

Find: Taps.

Answer: ≈ 45 taps

Worked example: Delay and load

Given: 51-tap FIR at 8 kHz

Find: Group delay and MACs/s.

Answer: 3.1 ms; 0.41 MMAC/s — trivial for a Cortex-M4

Windowed-sinc FIR low-pass design

import numpy as np

# Windowed-sinc low-pass FIR: fs = 8 kHz, cutoff 1 kHz, 51 taps (Hamming)
fs, fc, taps = 8000, 1000, 51
n = np.arange(taps) - (taps - 1) / 2
h = 2 * fc / fs * np.sinc(2 * fc / fs * n) * np.hamming(taps)
h /= h.sum()                                 # unity gain at DC

def gain_db(f):
    w = 2 * np.pi * f / fs
    H = np.sum(h * np.exp(-1j * w * np.arange(taps)))
    return 20 * np.log10(abs(H))

for f in (0, 500, 1000, 1500, 2000, 3000):
    print(f"{f:5d} Hz: {gain_db(f):7.1f} dB")
#     0 Hz:    -0.0 dB
#   500 Hz:     0.0 dB
#  1000 Hz:    -6.0 dB
#  1500 Hz:   -58.2 dB
#  2000 Hz:   -61.0 dB
#  3000 Hz:   -66.2 dB

# Filter a signal: 300 Hz (keep) + 3 kHz (reject)
t = np.arange(800) / fs
x = np.sin(2*np.pi*300*t) + np.sin(2*np.pi*3000*t)
y = np.convolve(x, h, mode="same")

An ideal low-pass impulse response (sinc) is truncated to 51 taps and tapered with a Hamming window to reduce ripple. Windowed-sinc filters are −6 dB at the design cut-off and give ≈ 50–60 dB stop-band attenuation with Hamming.

Common mistakes

Applications

Key takeaways

Going further