Skip to content

NOTES

FIR — Finite Impulse Response Filter

Always stable, no feedback, linear phase possible. Designed via the window method, implemented as convolution.


Algorithm

Design Flow (Window Method)

\[ \text{Ideal freq response} \xrightarrow{\text{IFFT}} \text{Infinite impulse response} \xrightarrow{\text{window truncation}} \text{FIR coeffs} \;(h[0], \dots, h[M-1]) \]

Step 1 — Specify cutoff \(f_c\), transition width \(\Delta f\), stopband attenuation

Step 2 — Generate ideal impulse response (low-pass example):

\[ hd[n] = \frac{\sin(\omegac (n - M/2))}{\pi (n - M/2)}, \quad \omegac = 2\pi fc / f_s \]

HP/BP/BS via frequency shift from the LP prototype.

Step 3 — Window truncation: \(h[n] = h_d[n] \cdot w[n]\)

  • Rectangular


    Narrowest main lobe, -13 dB sidelobe
    Sharpest transition

  • Hanning


    Medium main lobe, -31 dB
    General purpose

  • Hamming


    Medium main lobe, -41 dB
    Speech processing

  • Blackman


    Widest main lobe, -57 dB
    High attenuation

Filtering

Batch (direct form):

\[ y[n] = \sum_{k=0}^{M-1} h[k] \cdot x[n - k], \quad n = 0, \dots, L-1 \]

This is convolution — accelerated by ESP-DSP on ESP32.

Real-time streaming

Allocate a delay line on init, call `tinyfirprocess_sample` per sample. Ideal for edge devices.

Always stable

FIR has zeros only (no poles), all poles at origin → unconditional stability.

Linear phase

Symmetric coefficients (\(h[k] = h[M-1-k]\)) → linear phase response. Essential when phase preservation matters.


API Reference

Design

int tiny_fir_design_lowpass(float *coeffs, int max_taps,
                            float cutoff_norm, tiny_fir_window_t window);
// HP / BP / BS — similar

Normalized frequency: \(f{\text{norm}} = f / (f_s / 2)\), range [0, 1]}

Batch Filtering

tiny_error_t tiny_fir_filter_f32(const float *input, int len,
                                 const float *coeffs, int num_taps,
                                 float *output);

Real-time Filtering

typedef struct {
    float *coefficients;
    int num_taps;
    float *delay_line;     // internal state
    // ...
} tiny_fir_state_t;

tiny_error_t tiny_fir_init(tiny_fir_state_t *state,
                           const float *coeffs, int num_taps);
tiny_error_t tiny_fir_deinit(tiny_fir_state_t *state);
float        tiny_fir_process_sample(tiny_fir_state_t *state, float input);
void         tiny_fir_reset(tiny_fir_state_t *state);

Notes

More taps = more compute

Each output sample needs \(M\) MACs. On ESP32-S3 @ 240 MHz, \(M=128\) costs ~0.5 ms.

Transition width = window main lobe width

Sharp transition → Rectangular (high sidelobes). High attenuation → Blackman (wide transition).

Normalized frequency

Design functions use [0, 1] where 1 = Nyquist (\(f_s/2\)). Keeps sampling rate and cutoff independent.