Embedded HandbookApps
All sections › III. Peripherals
STM32G474, STM32G431 · level intermediate · 13 min read
Open in the interactive handbook

46б. FMAC: the G4 filter accelerator — FIR and IIR without the processor

In section 81а the digital filters were computed by the processor, with CMSIS-DSP functions. In section 46а, next to the CORDIC, the second G4 coprocessor was mentioned — the FMAC. It does the same work by itself: the coefficients sit in its memory, and the processor or DMA only writes an input sample and takes the output one. Here: how the buffers are organised, how much memory a filter needs, what happens to the numbers inside, and how not to get a garbage generator instead of a filter.

Sources: the reference manual RM0440 (G4, chapter 18 "Filter math accelerator") and the headers from the Keil STM32G4xx_DFP pack, with the HAL FMAC driver from the same place. The coefficients are the same filters as in section 81а. The arithmetic is checked with our own PC model written from the RM0440 description; the code is built with armclang for G474. QEMU does not model this block; not tested on a board.

What it is called in the documentation

Original Meaning
FMAC, filter math accelerator the filter accelerator
MAC, multiply and accumulate multiplication with accumulation
X1, X2, Y buffer the buffers of input samples, coefficients and output samples
circular buffer a ring buffer
FIR, convolution a finite impulse response filter, convolution
IIR, direct form 1 an infinite impulse response filter in direct form 1
P, Q, R the number of feed-forward and feedback coefficients, the gain exponent
watermark, FULL_WM, EMPTY_WM the buffer fill threshold
headroom d spare space in a buffer beyond what is required
clipping, CLIPEN saturation of the output
wrap overflow with a change of sign

46б.1 What it is and who has it

The FMAC (RM0440, 18.2) is a 16 × 16-bit multiplier, a 26-bit accumulator ("24 + 2"), its own memory of 256 words of 16 bits and a state machine that walks the buffers by itself. Inputs and outputs are 16 bits. The functions are loading the X1, X2 and Y buffers, convolution (FIR) and IIR in direct form 1. Correlation is the same convolution with the coefficients in reverse order.

The block is present in the pack headers of the STM32G431 and G474, STM32H723, STM32U585 and STM32H563; the F407 and H743 do not have it.

The work per sample goes like this: the processor writes x[n] to the WDATA register, the sample goes into the ring buffer X1; the state machine multiplies the last P samples by P coefficients from X2, accumulates the sum, multiplies it by 2^R, truncates it to 16 bits and puts y[n] into buffer Y; the processor reads it from RDATA. For IIR the sum also includes Q past outputs from the same Y.

The path of a sample through the FMAC: a write to WDATA, the ring buffer X1, multiplication by the coefficients from X2 with accumulation in a 26-bit accumulator, a gain of 2 to the power R and truncation with saturation by CLIPEN, buffer Y and a read from RDATA; for IIR past outputs from Y return into the sum; below is the layout of the 256 words of memory: a 31-coefficient FIR with headroom 2 and 2 takes 66 words, an IIR biquad 14 words, the largest FIR of 127 coefficients 255 words


46б.2 Memory: three buffers in 256 words

Each buffer is defined by a base and a size in the X1BUFCFG, X2BUFCFG and YBUFCFG registers (RM0440, 18.3.2); base plus size is less than 256. X2 holds the coefficients and does not move. X1 and Y are circular: X1 receives samples, Y gives out results.

How much memory is needed (RM0440, 18.3.8 and 18.3.9):

Filter Minimum words Example Limit
FIR with N coefficients 2N + 1 N = 31: 63 N = 127: 255 words
IIR, N feed-forward and M feedback 2N + 2M biquad N = 3, M = 2: 10 M = N − 1, N = 64: 254 words

Hence the limits of the P field: for FIR from 2 to 127, for IIR P up to 64 and Q from 1 to 63 — the same numbers appear in the checks of the HAL driver (FMAC_PARAM_P_MAX_FIR, FMAC_PARAM_P_MAX_IIR, FMAC_PARAM_Q_MAX).

On top of the minimum, headroom d1 is added to the input and d2 to the output so that the filter does not stall waiting. The layout for a 31-coefficient FIR with headroom 2 and 2, following the RM example:

Buffer Addresses Register
X2, coefficients 0…30 X2BUFCFG = 0x0000 1F00
X1, samples: 31 + d1 31…63 X1BUFCFG = 0x0100 211F, threshold FULL_WM = 1 (two words)
Y, outputs: d2 64…65 YBUFCFG = 0x0000 0240

66 words out of 256 are used. The FULL_WM threshold must be no greater than log₂(d1), otherwise the buffer will declare itself full before it holds N samples.


46б.3 Numbers inside: q1.15, the accumulator and 2^R

Inputs, coefficients and outputs are q1.15: a sign and 15 fractional bits, from −1 (0x8000) to 1 − 2⁻¹⁵ (0x7FFF). The accumulator, per RM0440 (18.3.7), is q4.22: 26 bits, 22 of them fractional, range from −8 to +7.99999976.

The IIR formula in the RM: y[n] = 2^R · (Σ b[k]·x[n−k] + Σ a[k]·y[n−k]) — the feedback is with a "+" sign, as in CMSIS-DSP (section 81а.4).


46б.4 The FIR model: a check against floating point

The PC model repeats the RM0440 description: coefficients and samples in q1.15, the products accumulate in a 26-bit q4.22 accumulator with wrapping, then 2^R, truncation and saturation. The filter is the FIR from section 81а: 31 coefficients, cutoff 100 Hz at 1000 Hz. In q1.15 the middle one, 0.19966, is code 6542 (0x198E), the sum of all codes is 32766, and the symmetry is preserved.

A sine of 0.9, 2000 samples, gain from the steady-state output:

Frequency float FMAC model
50 Hz 0.98902 0.98905
100 Hz 0.49820 0.49821
200 Hz 0.00029 0.00037
300 Hz 0.00069 0.00062

In the passband the difference is in the fifth digit; in the stopband the 16-bit coefficients shift the zeros of the frequency response, but the attenuation stays deep: −68.5 dB at 200 Hz and −64.2 dB at 300 Hz.

On 4000 samples of a sine mixed with noise the largest error against float is 2.58 LSB, the mean is −0.50: truncation always rounds down and shifts the output by half an LSB. Where exactly the products are truncated, the RM does not say; if each one is cut to 2⁻²², the output changes by no more than 1 unit. The partial sum in this run is no higher than 0.743 — far from wrapping.

Gain and saturation. The same FIR with R = 1 (gain 2) and a sine of 0.6 at 50 Hz: the output is 1.19 — beyond q1.15. With CLIPEN = 1 the peaks are cut off: 90 samples out of 300 sit at 0x7FFF or 0x8000. With CLIPEN = 0 90 samples changed sign — on the positive half-wave the output drops to −0.871.


46б.5 IIR: divide the coefficients by 2

The 100 Hz Butterworth biquad from section 81а: −a1 = 1.142981 is greater than one and does not fit into q1.15. RM0440 (18.3.12, tip 4) suggests the same as postShift in CMSIS: all coefficients are divided by 2^R, and R restores the scale at the output. With R = 1 the contents of X2 are b0, b1, b2, a1, a2:

1105 2210 1105 18727 -6763      R = 1

These are the same numbers as for arm_biquad_cascade_df1_q15 in 81а.5, only without the zero word after b0. The model against float:

Frequency float FMAC model
50 Hz 0.97291 0.97285
100 Hz 0.70711 0.70698
200 Hz 0.19612 0.19607
300 Hz 0.05564 0.05563

Without dividing by 2 (R = 0) the coefficient 1.14298 saturates to 32767, and the gain at 50 Hz is 0.67724 instead of 0.97291: that is a different filter.

Saturation matters more for IIR than for FIR: the output goes back into the sum. On steps of ±0.99 the Butterworth overshoot hits the limit in 20 samples out of 1000 — with CLIPEN = 1 the filter hardly notices it. With CLIPEN = 0 the wrap goes into the feedback: 840 samples out of 1000 differ by more than 0.1, the largest difference is 1.990, almost the full range.


46б.6 Code: write, wait, read

Initialisation per RM0440 (18.3.8): reset the block, the three BUFCFG registers, load the coefficients with the "Load X2" function, preload X1 with zeros — this is the zero history of the filter — then CR and start the "FIR" function with the START bit. The load functions finish by themselves after the required number of writes — START is cleared in hardware; the HAL driver waits for this in the same way. The fields are checked at build time:

_Static_assert(FMAC_BASE == 0x40021400u, "FMAC на AHB1");
_Static_assert(BUFCFG(X1_BASE, NTAPS + D1, 1) == 0x0100211Fu, "X1: база 31, 33 слова, порог 2");
_Static_assert(PARAM(F_FIR, NTAPS, 0, 0) == 0x8800001Fu, "КИХ, P = 31, R = 0, START");

X1 is preloaded with 30 samples of the 31 needed, so the very first written sample produces the first output (RM: the calculation starts when X1 has collected N samples). After that it is one input, one output:

int16_t fmac_sample(int16_t x)
{
    FMAC->WDATA = (uint16_t)x;
    while (FMAC->SR & FMAC_SR_YEMPTY)                       /* ждём выход: один на каждый вход */
        ;
    return (int16_t)FMAC->RDATA;
}

armclang with -Os compiles this into ten instructions:

fmac_sample:  MOV   r1,#0x1414
              MOVT  r1,#0x4002
              UXTH  r0,r0
              STR   r0,[r1,#4]
              LDR   r0,[r1,#0]
              LSLS  r0,r0,#31
              BNE   {pc}-0x4
              LDR   r0,[r1,#8]
              SXTH  r0,r0
              BX    lr

SR is 0x4002 1414, WDATA is 0x4002 1418, RDATA is 0x4002 141C. The loop of three instructions is the wait on the YEMPTY flag. With -O2 the compiler unrolls it four times: 44 bytes instead of 24. The same function works for the biquad — fmac_biquad_init loads X2, X1 and Y and starts function 9 with R = 1.

Clock cycles. How many cycles the FMAC spends per coefficient, RM0440 does not say — chapter 18 has no timing table. The only number is the flag latency: X1FULL rises 3 cycles after a write to the last free location, YEMPTY 3 cycles after reading the last output. Hence the RM advice: a threshold of 2 or a pause before reading FMAC_SR. The filter time on a board is measured with the DWT counter (section 73).


46б.7 Pitfalls


46б.8 Summary

Full sources

fmac46b.c — FMAC on the STM32G474 at register level: FIR and IIR biquad.

/* 46б: FMAC STM32G474 по регистрам — КИХ на 31 коэффициент и БИХ-биквад, опрос флагов.
   Собирается armclang: -DSTM32G474xx. */
#include <stdint.h>

enum { F_LOAD_X1 = 1, F_LOAD_X2 = 2, F_LOAD_Y = 3, F_FIR = 8, F_IIR = 9 };   /* FUNC — RM0440, 18.4.4 */

#define NTAPS   31                       /* P: число коэффициентов */
#define D1      2                        /* запас во входном буфере */
#define D2      2                        /* запас в выходном буфере */
#define X2_BASE 0                        /* коэффициенты */
#define X1_BASE NTAPS                    /* входные отсчёты */
#define Y_BASE  (2 * NTAPS + D1)         /* выходные отсчёты */

#define BUFCFG(base, size, wm) (((uint32_t)(wm) << FMAC_X1BUFCFG_FULL_WM_Pos) | \
                                ((uint32_t)(size) << FMAC_X1BUFCFG_X1_BUF_SIZE_Pos) | (uint32_t)(base))
#define PARAM(func, p, q, r)   (FMAC_PARAM_START | ((uint32_t)(func) << FMAC_PARAM_FUNC_Pos) | \
                                ((uint32_t)(r) << FMAC_PARAM_R_Pos) | ((uint32_t)(q) << FMAC_PARAM_Q_Pos) | (uint32_t)(p))

_Static_assert(FMAC_BASE == 0x40021400u, "FMAC на AHB1");
_Static_assert(FMAC_X2BUFCFG_X2_BUF_SIZE_Pos == FMAC_X1BUFCFG_X1_BUF_SIZE_Pos &&
               FMAC_YBUFCFG_Y_BUF_SIZE_Pos == FMAC_X1BUFCFG_X1_BUF_SIZE_Pos &&
               FMAC_YBUFCFG_EMPTY_WM_Pos == FMAC_X1BUFCFG_FULL_WM_Pos, "одна раскладка трёх BUFCFG");
_Static_assert(BUFCFG(X1_BASE, NTAPS + D1, 1) == 0x0100211Fu, "X1: база 31, 33 слова, порог 2");
_Static_assert(BUFCFG(Y_BASE, D2, 0) == 0x00000240u, "Y: база 64, 2 слова");
_Static_assert(PARAM(F_FIR, NTAPS, 0, 0) == 0x8800001Fu, "КИХ, P = 31, R = 0, START");
_Static_assert(PARAM(F_IIR, 3, 2, 1) == 0x89010203u, "БИХ, P = 3, Q = 2, R = 1, START");
_Static_assert(Y_BASE + D2 <= 256, "всё в 256 словах");

static void fmac_load(uint32_t func, uint32_t p, uint32_t q, const int16_t *v, uint32_t n)
{
    FMAC->PARAM = PARAM(func, p, q, 0);
    for (uint32_t i = 0; i < n; i++)
        FMAC->WDATA = (uint16_t)(v ? v[i] : 0);
    while (FMAC->PARAM & FMAC_PARAM_START)                  /* после n записей START сбросится сам */
        ;
}

static void fmac_reset(void)
{
    FMAC->CR = FMAC_CR_RESET;                               /* указатели, SR и PARAM — в исходное */
    while (FMAC->CR & FMAC_CR_RESET)
        ;
}

/* КИХ: коэффициенты — в X2, в X1 — 30 нулей (история); дальше каждый отсчёт даёт один выход */
void fmac_fir_init(const int16_t coef[NTAPS])
{
    RCC->AHB1ENR |= RCC_AHB1ENR_FMACEN;
    (void)RCC->AHB1ENR;
    fmac_reset();
    FMAC->X2BUFCFG = BUFCFG(X2_BASE, NTAPS, 0);
    FMAC->X1BUFCFG = BUFCFG(X1_BASE, NTAPS + D1, 1);         /* FULL_WM = 1 <= log2(D1) */
    FMAC->YBUFCFG  = BUFCFG(Y_BASE, D2, 0);
    fmac_load(F_LOAD_X2, NTAPS, 0, coef, NTAPS);
    fmac_load(F_LOAD_X1, NTAPS - 1, 0, 0, NTAPS - 1);
    FMAC->CR = FMAC_CR_CLIPEN;                              /* опрос, выход с насыщением */
    FMAC->PARAM = PARAM(F_FIR, NTAPS, 0, 0);                /* фильтр работает, пока START = 1 */
}

/* один отсчёт в фильтр, один из фильтра — и для КИХ, и для биквада */
int16_t fmac_sample(int16_t x)
{
    FMAC->WDATA = (uint16_t)x;
    while (FMAC->SR & FMAC_SR_YEMPTY)                       /* ждём выход: один на каждый вход */
        ;
    return (int16_t)FMAC->RDATA;
}

/* БИХ-биквад: X2 = b0 b1 b2 a1 a2, все поделены на 2 (a1 > 1), R = 1 возвращает масштаб */
void fmac_biquad_init(const int16_t coef[5])
{
    RCC->AHB1ENR |= RCC_AHB1ENR_FMACEN;
    (void)RCC->AHB1ENR;
    fmac_reset();
    FMAC->X2BUFCFG = BUFCFG(0, 5, 0);
    FMAC->X1BUFCFG = BUFCFG(5, 3 + D1, 1);
    FMAC->YBUFCFG  = BUFCFG(10, 2 + D2, 0);
    fmac_load(F_LOAD_X2, 3, 2, coef, 5);
    fmac_load(F_LOAD_X1, 2, 0, 0, 2);
    fmac_load(F_LOAD_Y, 2, 0, 0, 2);                        /* обратная связь — с нулей, не с мусора */
    FMAC->CR = FMAC_CR_CLIPEN;
    FMAC->PARAM = PARAM(F_IIR, 3, 2, 1);
}

void fmac_stop(void)
{
    FMAC->PARAM &= ~FMAC_PARAM_START;                       /* START = 0 останавливает фильтр */
}

Next: section 23 "Why you need an operating system".

Open in the interactive handbook