Biquad on Hardware

Real-time biquad filters on STM32F4 and ESP32

The biquad is the workhorse of embedded audio and sensor DSP. Every parametric equalizer, crossover network, and feedback control loop on a microcontroller is built from cascaded second-order sections. The structure is minimal, five coefficients and two state variables, making it ideal for platforms with tight memory and cycle budgets.

This page covers practical biquad implementations for STM32F4 and ESP32 targets. For the theory, filter structures, and coefficient design, see the main biquad page. For why we cascade biquads instead of implementing high-order filters directly, see the SOS discussion in the filter design chapter.


STM32F4: Direct Form II biquad

The STM32F4 series (Cortex-M4F, up to 180 MHz on the NUCLEO-F446RE, single-precision FPU) is the natural home for biquad filters. A single biquad section takes about a dozen cycles with the hardware FPU (see Counting the cycles for where that number comes from), leaving room for dozens of cascaded sections at audio sample rates.

Bare-metal implementation

The Direct Form II implementation uses two state variables (w[1] and w[2]) and computes the output in two steps:

// Direct Form II biquad, single section, single sample
// b0, b1, b2: numerator coefficients
// a1, a2: denominator coefficients (negated, see note below)
// w[]: state variables, persisted between calls

void biquad_df2(float x, float *y,
                float b0, float b1, float b2,
                float a1, float a2, float w[3])
{
    w[0] = x + a1*w[1] + a2*w[2];
    *y   = b0*w[0] + b1*w[1] + b2*w[2];
    w[2] = w[1];
    w[1] = w[0];
}
Negated denominator coefficients

In this implementation, a1 and a2 are stored with their signs flipped relative to the transfer function \(H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}\). The code computes w[0] = x + a1*w[1] rather than w[0] = x - a1*w[1], so the stored a1 is \(-a_1\) from the transfer function.

This convention matches CMSIS-DSP and many DSP textbooks. It replaces a subtraction with an addition in the inner loop, saving one cycle on architectures without a fused negate-multiply-accumulate instruction. When porting coefficients from SciPy (which uses the un-negated convention), negate a1 and a2 before loading them into the filter.

Cascading multiple sections

A higher-order filter is a cascade of biquad sections. Each section’s output feeds the next section’s input:

#define MAX_SECTIONS 6

typedef struct {
    float b0, b1, b2;
    float a1, a2;       // negated convention
    float w[3];         // state variables
} biquad_section_t;

typedef struct {
    biquad_section_t sections[MAX_SECTIONS];
    int n_sections;
} biquad_cascade_t;

float biquad_cascade_process(biquad_cascade_t *cascade, float x)
{
    float signal = x;
    for (int k = 0; k < cascade->n_sections; k++) {
        biquad_section_t *s = &cascade->sections[k];
        s->w[0] = signal + s->a1 * s->w[1] + s->a2 * s->w[2];
        signal   = s->b0 * s->w[0] + s->b1 * s->w[1] + s->b2 * s->w[2];
        s->w[2]  = s->w[1];
        s->w[1]  = s->w[0];
    }
    return signal;
}

Each section adds 5 multiplies and 4 adds. A 6th-order Butterworth (3 sections) takes roughly 27 operations (3 x 9), well under 1 µs at 180 MHz with hardware FPU.

Counting the cycles

The arithmetic in one biquad section is 5 multiplies and 4 adds, so you might expect 9 cycles per sample. In practice the naive C inner loop above costs closer to 27 cycles per sample, and getting near the arithmetic floor takes real work. Yiu’s cycle-by-cycle analysis of a Direct Form II biquad on the Cortex-M4F (Yiu 2014, ch. 21) is worth following, because the same reasoning applies to any tight DSP loop on this core:

Version cycles / sample
Naive C inner loop 27
Split each MAC into a separate multiply and add, reordered so no result feeds the next instruction 18
Rotate the state in place, unroll by 3, group the loads and stores 12.67
Arithmetic floor (9 stall-free arithmetic cycles + 1 load + 1 store) 11

The gap between 9 and 27 is the FPU pipeline. A floating-point result on the M4F is not ready for the very next instruction; if you use it immediately the core stalls a few cycles, and the textbook ordering (w0 = x + a1*w1 + a2*w2; then y = b0*w0 + ...) chains every operation onto the previous one. Splitting each multiply-accumulate into an independent multiply and a later add, and interleaving them so the result of one operation is never needed on the next cycle, hides that latency and is the single biggest win.

The remaining gains are mechanical: the w[2]=w[1]; w[1]=w[0] copies disappear if you instead relabel which variable is “newest” each iteration (a rotation with period 3), unrolling by three samples amortises the loop overhead, and grouping memory accesses lets all but the first load or store cost a single cycle. The floor of 11 assumes the five coefficients are pre-loaded into FPU registers before the loop and never re-fetched, and that the state rotation keeps the state in registers too, so the only memory traffic left per sample is reading the new input and writing the output: one load and one store. Unrolling has diminishing returns, though, because the core runs out of registers to hold intermediate values:

Unroll factor cycles / sample
3 12.67
6 11.83
9 11.55
12 11.41

So budget about a dozen cycles per section for well-optimised C, and 11 at the floor; the “roughly ten cycles” rule of thumb is a touch optimistic. The practical lesson is that you rarely write this by hand: CMSIS-DSP already applies every one of these transformations, which is the main reason to reach for it.

CMSIS-DSP alternative

ARM’s CMSIS-DSP library provides optimised biquad implementations, optimised for the Cortex-M4 FPU; the fixed-point Q15/Q31 variants additionally use SIMD integer instructions:

#include "arm_math.h"

#define NUM_SECTIONS  3
#define BLOCK_SIZE    1

// Coefficient array: {b0, b1, b2, a1, a2} per section (a1, a2 negated)
static float32_t coeffs[5 * NUM_SECTIONS];
// State array: 4 state variables per section
static float32_t state[4 * NUM_SECTIONS];
static arm_biquad_casd_df1_inst_f32 filter;   // CMSIS type is "casd" (cascade direct form), not "casc"

void init_filter(void) {
    arm_biquad_cascade_df1_init_f32(&filter, NUM_SECTIONS,
                                     coeffs, state);
}

void process_sample(float32_t *in, float32_t *out) {
    arm_biquad_cascade_df1_f32(&filter, in, out, BLOCK_SIZE);
}

The CMSIS-DSP implementation uses Direct Form I (not DF-II) with four state variables per section. This is deliberate: DF-I is more robust for fixed-point variants (Q15, Q31) because the separate input and output delay lines prevent feedback overflow from corrupting input history. The floating-point version uses the same structure for API consistency.

Tip

For block processing (e.g., processing 64 samples at a time from a DMA buffer), set BLOCK_SIZE to the block length. CMSIS-DSP will process all samples in a single call with loop-unrolled inner code, significantly faster than calling once per sample.

Real-time I/O: clocking samples in and out

Every snippet so far assumes a sample x is handed to the filter and an output y is taken away. On real hardware those two arrows are the hard part. A biquad running on a NUCLEO-F446RE as a live audio resonator sits in the middle of a fixed-rate chain:

analog in -> anti-alias LPF -> ADC -> biquad -> DAC -> reconstruction LPF -> analog out
                                ^
                                |
                  timer (TIM6 TRGO) sets the sample rate f_s

The single most important property is that the sample clock is set by hardware, not by software. A while loop calling the ADC “as fast as it can” produces jitter that smears the frequency response. The clean pattern lets a timer trigger the ADC at exactly f_s; each completed conversion then runs one biquad step and writes one output sample to the DAC. Only the ADC is hardware-triggered, but because exactly one DAC write happens per timer tick, the output inherits the same steady rate (a small constant processing delay, not jitter).

The ADC must be triggered, not just read

HAL_ADC_GetValue only reads the ADC data register; it does not start a conversion. Polling it in a loop returns the same stale sample until something triggers a new conversion. Here the timer’s TRGO output is wired to the ADC trigger input, so a conversion starts every 1 / f_s seconds with no software involvement; the end-of-conversion interrupt then reads that fresh, timer-triggered sample with HAL_ADC_GetValue. See the model-based filtering embedded page for a variant that triggers and polls inside the interrupt instead.

The sampling clock

A basic timer (TIM6 is ideal, its only job is to emit a TRGO pulse) sets f_s. On the F446RE at 180 MHz, the APB1 prescaler is 4, so APB1 runs at 45 MHz, but the timer clock doubles to 90 MHz whenever that prescaler is greater than 1 (a classic STM32 trap; the STM32F4 clock tree doubles the APBx timer clock when the APB prescaler is not 1, which you can confirm in the reference-manual clock diagram or the CubeMX Clock Configuration tab for your exact part). For a 48 kHz audio rate:

\[ f_s = \frac{f_\text{timer}}{\text{ARR} + 1} = \frac{90\,\text{MHz}}{1874 + 1} = 48\,\text{kHz} \]

With a 48 kHz sample rate the per-sample cycle budget is 180 MHz / 48 kHz = 3750 cycles. A float biquad costs about a dozen cycles (see Counting the cycles), so even a long cascade leaves the core almost entirely idle.

Wiring the chain in HAL

#include "stm32f4xx_hal.h"

// Fixed-rate real-time chain on a NUCLEO-F446RE.
// TIM6 TRGO triggers ADC1 at f_s; the end-of-conversion interrupt runs one
// biquad step and writes one sample to DAC1 (PA4). One sample in, one sample
// out, per timer tick.
//
// Handles generated by CubeMX in main.c:
//   extern ADC_HandleTypeDef hadc1;
//   extern DAC_HandleTypeDef hdac;
//   extern TIM_HandleTypeDef htim6;
//
// CubeMX settings that matter:
//   ADC1: continuous mode OFF, external trigger = TIM6 TRGO (rising edge),
//         end-of-conversion interrupt enabled. Input on PA0 (ADC1_IN0).
//   TIM6: TRGO source = update event, ARR = 1874 (48 kHz from a 90 MHz clock).
//   DAC1: channel 1 on PA4, output buffer enabled, no trigger.

static biquad_section_t resonator;   // coefficients + state, see above

// 12-bit ADC code (0..4095) centred on mid-rail 2048, mapped to float in [-1, 1).
static inline float adc_to_float(uint16_t code) {
    return ((float)code - 2048.0f) / 2048.0f;
}

// Float sample mapped back to a 12-bit DAC code centred on mid-rail 2048.
// The gain is 2047 (not 2048) so full-scale +1.0 lands on 4095, the largest
// code a 12-bit DAC can emit, instead of overflowing to 4096 (which the 12-bit
// register would wrap to 0). The clip is mandatory: a resonator's output is
// not bounded by its input.
static inline uint16_t float_to_dac(float y) {
    if (y >  1.0f) y =  1.0f;
    if (y < -1.0f) y = -1.0f;
    return (uint16_t)(y * 2047.0f + 2048.0f);
}

// Fires once per TIM6 tick, after the timer-triggered conversion completes.
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    float x  = adc_to_float((uint16_t)HAL_ADC_GetValue(hadc));
    float w0 = x + resonator.a1 * resonator.w[1] + resonator.a2 * resonator.w[2];
    float y  = resonator.b0 * w0 + resonator.b1 * resonator.w[1]
                                 + resonator.b2 * resonator.w[2];
    resonator.w[2] = resonator.w[1];
    resonator.w[1] = w0;
    HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, float_to_dac(y));
}

void start_realtime_chain(void) {
    HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
    HAL_ADC_Start_IT(&hadc1);     // arm the ADC; conversions wait for TIM6 TRGO
    HAL_TIM_Base_Start(&htim6);   // start the sample clock
}

Each timer tick produces exactly one DAC write, so the output is paced at f_s with only a small constant processing delay, never a jittery or bursty one. At 48 kHz that is 48000 interrupts per second, each costing a few dozen cycles out of the 3750-cycle budget, so the core stays almost idle.

For higher rates or many channels the per-sample interrupt rate becomes the bottleneck, and the next step is block processing with DMA: the timer fills an ADC DMA buffer and drains a matching DAC DMA buffer, both paced by the timer, and you process a half-buffer at a time in the transfer-complete interrupts (the “ping-pong” pattern). Keep the DAC on its own timer-paced DMA so the output stays evenly spaced. Writing a whole block with HAL_DAC_SetValue inside one callback would emit the samples as a burst, not at f_s, which smears the very response you are trying to measure.

The analog edges

The two ends of that diagram are analog, and a teaching demo lives or dies on them:

  • Anti-alias filter. An analog low-pass before the ADC, with its corner below f_s / 2, stops out-of-band energy folding into the passband. Without it the resonator faithfully amplifies an alias.
  • Level shift. Line-level audio swings around 0 V, but the ADC only accepts 0 to 3.3 V. A bias network centres the signal on mid-rail (about 1.65 V). That mid-rail is exactly the 2048 subtracted in adc_to_float, so the digital path sees a zero-centred signal again.
  • Reconstruction filter. An analog low-pass after the DAC smooths the 12-bit staircase back into a continuous waveform.

Generate a tone on a laptop, feed it in, capture the output, and compare spectra against the same biquad run in Python. Inside the analog passband (above the bias network’s high-pass corner, below the anti-alias and reconstruction roll-off) the difference should match the resonator’s designed response; near the band edges the analog filters add their own shaping, and 12-bit quantization sets a noise floor. To compare against the ideal Python biquad with no analog coloring, tap the digital samples instead, dumping the ADC codes and DAC codes over UART.

Tuning the resonance at runtime

To make the resonant frequency adjustable with a potentiometer (the original practicum assignment), read a second ADC channel, map it to a centre frequency, and recompute the coefficients. Do the recompute in the main loop, never in the audio callback, since the coefficient formulas call trigonometric functions that are far too slow for the per-sample budget. Hand the finished coefficients to the resonator between blocks, and apply the smoothing from Coefficient updates and click avoidance below so the change does not click.


ESP32: C++ biquad for real-time audio

The ESP32-S3 (dual-core Xtensa LX7, 240 MHz, single-precision FPU) is well-suited for audio biquad processing. It lacks CMSIS-DSP, but the biquad is simple enough to implement directly. The built-in I2S peripheral connects directly to MEMS microphones and DAC codecs without external ADC hardware.

Biquad class

A C++ biquad class suitable for real-time audio on ESP32:

class BiquadSection {
public:
    float b0, b1, b2, a1, a2;  // a1, a2 negated
    float w1 = 0.0f, w2 = 0.0f;

    void set_coefficients(float _b0, float _b1, float _b2,
                          float _a1, float _a2) {
        b0 = _b0; b1 = _b1; b2 = _b2;
        a1 = _a1; a2 = _a2;
    }

    float process(float x) {
        float w0 = x + a1 * w1 + a2 * w2;
        float y  = b0 * w0 + b1 * w1 + b2 * w2;
        w2 = w1;
        w1 = w0;
        return y;
    }

    void reset() { w1 = 0.0f; w2 = 0.0f; }
};

class BiquadCascade {
public:
    static constexpr int MAX_SECTIONS = 8;
    BiquadSection sections[MAX_SECTIONS];
    int n_sections = 0;

    void add_section(float b0, float b1, float b2,
                     float a1, float a2) {
        if (n_sections < MAX_SECTIONS) {
            sections[n_sections].set_coefficients(b0, b1, b2, a1, a2);
            n_sections++;
        }
    }

    float process(float x) {
        float signal = x;
        for (int k = 0; k < n_sections; k++) {
            signal = sections[k].process(signal);
        }
        return signal;
    }

    void reset() {
        for (int k = 0; k < n_sections; k++)
            sections[k].reset();
    }
};

I2S microphone setup

Legacy API

The code below uses the ESP-IDF v4.x I2S API (driver/i2s.h), which was removed in ESP-IDF v5.2. For the v5.x API (driver/i2s_std.h), see the pitch detection or beamforming embedded pages.

The ESP32 I2S peripheral connects directly to digital MEMS microphones like the INMP441:

#include "driver/i2s.h"

i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = 16000,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .dma_buf_count = 4,
    .dma_buf_len = 64,
    .use_apll = true,
};

i2s_pin_config_t pin_config = {
    .bck_io_num = 26,
    .ws_io_num = 25,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = 22,
};

void audio_init() {
    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pin_config);
}

The audio processing loop reads I2S samples, runs the biquad cascade, and writes the filtered output:

BiquadCascade eq;

void audio_task(void *param) {
    int32_t raw_samples[64];
    size_t bytes_read;

    while (true) {
        i2s_read(I2S_NUM_0, raw_samples, sizeof(raw_samples),
                 &bytes_read, portMAX_DELAY);

        int n_samples = bytes_read / sizeof(int32_t);
        if (n_samples > 64) n_samples = 64;  // clamp to buffer size
        for (int i = 0; i < n_samples; i++) {
            // Convert 32-bit I2S to float [-1, 1]
            float x = (float)raw_samples[i] / 2147483648.0f;
            float y = eq.process(x);
            // Output y to DAC, I2S TX, or buffer for further processing
        }
    }
}

Pin the audio task to core 1 (xTaskCreatePinnedToCore(audio_task, "audio", 4096, NULL, 5, NULL, 1)) and keep Wi-Fi on core 0 to avoid jitter from wireless stack interrupts.


Fixed-point considerations

On platforms without a hardware FPU (or where power or throughput demands it), the biquad’s coefficients and state are stored as fixed-point integers, typically Q15 or Q31. The general mechanics, Q-format representation, overflow, and the wrap-versus-saturate choice, are covered in the finite word-length effects topic. Here we cover what is specific to the biquad.

Why accumulator width matters

A single biquad multiply-accumulate step multiplies a Q15 coefficient by a Q15 sample, producing a Q30 result in 32 bits. Summing five such products can overflow a 32-bit accumulator. The solution is a 64-bit accumulator for Q31 coefficients, or a 32-bit accumulator for Q15:

Coefficient format Multiply result Accumulator width needed
Q15 × Q15 Q30 (32 bits) 32 bits (tight, CMSIS-DSP uses 64-bit accumulator for safety)
Q31 × Q31 Q62 (64 bits) 64 bits

ARM Cortex-M4 provides the SMLAL instruction (signed multiply-accumulate long) that multiplies two 32-bit values and accumulates into a 64-bit register pair, exactly what is needed for Q31 biquad processing.

Limit cycles

In fixed-point biquad implementations, rounding errors in the feedback path can cause limit cycles: small persistent oscillations at the output even when the input is zero. Using a wider accumulator and rounding (rather than truncating) the output reduces this problem. CMSIS-DSP’s Q15 biquad uses a 64-bit accumulator internally for this reason. See limit cycles for the general treatment, including which structures are provably free of them.

Coefficients greater than 1

Biquad coefficients can exceed \(\pm 1\) (e.g., a peaking EQ with high gain). Since Q-format represents only \([-1, 1)\), these coefficients cannot be stored directly. Solutions:

  1. Pre-scale coefficients so all values fit in \([-1, 1)\), and apply a compensating gain at the output
  2. Use Q14 or Q30 format with a wider range \([-2, 2)\) at the cost of one bit of precision
  3. Store the integer and fractional parts separately (rarely done for biquads)

The coefficient quantization demo on the theory page shows how Q15 quantization shifts the response of a narrow bandpass filter.


Audio EQ on embedded

A parametric equalizer on a microcontroller is a cascade of biquad sections with user-adjustable parameters. A typical 3-band EQ (low shelf, parametric mid, high shelf) requires just three biquad sections, under 100 bytes of memory and less than 1 µs of processing per sample.

Coefficient updates and click avoidance

When the user adjusts an EQ knob, the biquad coefficients change. Swapping coefficients instantaneously between samples produces discontinuities in the output, audible as clicks or pops. Two approaches to smooth updates:

Crossfade. Maintain two copies of the filter state. When coefficients change, start feeding input to the new filter while fading out the old one over a short window (typically 5–10 ms, i.e., 80–160 samples at 16 kHz). This doubles the memory and computation during the crossfade but guarantees a smooth transition.

// Simplified crossfade between old and new biquad
for (int i = 0; i < fade_len; i++) {
    float alpha = (float)i / fade_len;
    float y_old = biquad_cascade_process(&old_filter, x[i]);
    float y_new = biquad_cascade_process(&new_filter, x[i]);
    output[i] = (1.0f - alpha) * y_old + alpha * y_new;
}

Parameter smoothing. Instead of updating coefficients in one step, interpolate the design parameters (frequency, Q, gain) smoothly and recompute coefficients each block. This is more expensive (trigonometric functions in the coefficient formulas) but produces more natural-sounding transitions. Many audio plugin frameworks use this approach, recomputing coefficients once per block (e.g., every 64 samples) with smoothed parameters.

Note

Linear interpolation of biquad coefficients does not correspond to linear interpolation of the frequency response. Intermediate states during a coefficient crossfade may have unexpected resonance peaks; see Going further on the theory page.


Platform comparison

Feature STM32F4 ESP32
Clock 180 MHz (NUCLEO-F446RE) 240 MHz
FPU Yes (Cortex-M4F) Yes (Xtensa LX7)
CMSIS-DSP biquad Yes (arm_biquad_cascade_df1_f32, Q15, Q31) No
I2S Via external codec Built-in
Memory (SRAM) 192 KB 512 KB
Audio latency (typical) < 1 ms (bare-metal, sample-by-sample) 4–8 ms (FreeRTOS, DMA blocks)
Wi-Fi/BT No (needs external module) Built-in
Real-time determinism Excellent (bare-metal or RTOS) Good (with core pinning)
Unit cost ~EUR 10 ~EUR 5
Best for Deterministic DSP, production audio Prototyping, IoT audio

Recommendation

  • STM32F4 for production audio DSP: deterministic timing, CMSIS-DSP library with optimised fixed-point variants, and established use in professional audio equipment. Choose this when latency, reproducibility, or fixed-point performance matter.
  • ESP32 for prototyping and connected devices: cheaper, built-in I2S and wireless, easier to get audio flowing with minimal external hardware. Choose this for EQ demos, IoT sensor filtering, or wireless audio experiments.

FIR comparison

For contrast, here is a general-purpose FIR filter using a circular buffer, the standard embedded technique that avoids shifting the entire delay line on every sample:

// FIR filter with circular buffer
// N: number of taps, buf[]: circular sample buffer
// coeffs[]: filter coefficients, ind: current buffer index

void fir_filter(float x, float *y, float *buf, const float *coeffs,
                int N, int *ind)
{
    buf[*ind] = x;
    if (*ind < N - 1) (*ind)++; else *ind = 0;

    float acc = 0.0f;
    int k = *ind;
    for (int i = 0; i < N; i++) {
        acc += buf[k] * coeffs[N - i - 1];
        if (k < N - 1) k++; else k = 0;
    }
    *y = acc;
}

The circular buffer wraps the index instead of shifting data: each sample requires \(N\) multiplies but zero memory moves. The memmove-based approach (shift the buffer on every sample) is simpler but costs \(O(N)\) memory operations on top of the \(N\) multiplies.

When to use FIR vs biquad

Criterion FIR Biquad (IIR)
Phase Linear (symmetric coefficients) Nonlinear
Memory \(N\) coefficients + \(N\) samples 5 coefficients + 2 state variables per section
Computation \(N\) MACs per sample ~5 MACs per section per sample
Stability Always stable Can be unstable if poles outside unit circle
Sharp cutoff Requires many taps (100+) A few sections suffice
Typical use Anti-aliasing, matched filtering, linear-phase EQ Audio EQ, control loops, sensor filtering

Rule of thumb for microcontrollers: if you need linear phase or very specific impulse response shapes (e.g., matched filtering), use FIR. If you need a sharp frequency-selective filter with minimal memory and computation (e.g., audio crossover, DC removal, bandpass for sensor data), use cascaded biquads. On a Cortex-M4F at 180 MHz (NUCLEO-F446RE) with 8 kHz sample rate, you can run a 256-tap FIR or a 12th-order IIR (6 biquad sections), but the biquad cascade uses ~48 bytes of state (6 sections × 2 floats) versus ~1 KB for the FIR’s 256-sample delay line (2 KB including its coefficients).

References

Yiu, Joseph. 2014. The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors. 3rd ed. Oxford: Newnes.