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.
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 accumulator does not saturate. A partial sum beyond the limit wraps, and that is no problem if the following terms bring it back. The SAT flag in FMAC_SR marks a wrap.
- Gain 2^R. R from 0 to 7 — from 0 to 42 dB in 6 dB steps; the sum is multiplied by 2^R before output.
- The output is truncated. The extra low bits are simply dropped; with CLIPEN = 1 a value outside q1.15 saturates to 0x7FFF or 0x8000, with CLIPEN = 0 it wraps.
- P is the number of coefficients, not the order: in formula 18.3.6 a vector of length N + 1 is given as P = N + 1.
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
- X2 overlaps X1 or Y. The filter writes outputs and samples over the coefficients (RM0440, 18.3.8). Place the buffers back to back, but not overlapping.
- Threshold greater than the headroom. FULL_WM greater than log₂(d1) — the buffer is "full" before N samples have been collected, and the filter asks for no more.
- Y not loaded for IIR. The first output is computed from the first M words of Y, whether they were loaded or not (18.3.9). Without "Load Y" the filter starts from garbage.
- The sign of the feedback. The FMAC adds a·y. Coefficients from a design program with a minus are stored as −a1, −a2 — as in CMSIS (81а.4).
- A coefficient greater than 1. It saturates to 0x7FFF: 0.677 instead of 0.973 at 50 Hz. Divide everything by 2^R and set R.
- CLIPEN is off. An output beyond ±1 changes sign; in IIR the wrap goes into the feedback and corrupts hundreds of samples.
- The flag is read immediately. 3 cycles of latency on X1FULL and YEMPTY — you can write an extra sample (OVFL, the write is lost) or read an empty Y (UNFL).
- Configuring while START = 1. X1BUFCFG, YBUFCFG, the X2 size, FUNC, P, Q and R can be changed only with the function stopped. Of the buffer settings, only X2_BASE can be changed on the fly — that is how coefficient sets are switched.
- Is the accumulator limit ±8 or ±4? 18.3.7 states q4.22 (±8), while 18.3.12 gives ±4 in q3.23 for the same 0x1FFFFFF. Calculate the headroom against the smaller limit.
- Direct form 2 and cascades. The block supports only direct form 1. A cascade of sections means separate runs with different X2_BASE or one section of higher order (18.3.12).
46б.8 Summary
- The FMAC is on G431, G474, H723, U585 and H563; F407 and H743 do not have it. A multiplier of 16 × 16, a 26-bit accumulator, 256 words of memory.
- FIR needs 2N + 1 words — up to 127 coefficients; IIR needs 2N + 2M, up to N = 64 with M = 63. A 31-coefficient FIR with headroom 2 and 2 takes 66 words.
- The RM0440-based model: the 81а FIR in q1.15 gives 0.49821 at the cutoff against 0.49820 in float, an error of up to 2.58 LSB, an offset of −0.50 from truncation.
- IIR: divide the coefficients by 2, R = 1: 0.70698 at 100 Hz against 0.70711; without dividing, 0.67724 instead of 0.97291 at 50 Hz.
- CLIPEN = 1 is mandatory: without it, in IIR 840 samples out of 1000 are corrupted by wrapping.
- One sample is a write to WDATA, a wait on YEMPTY and a read of RDATA: ten instructions with -Os. RM0440 does not give the cycles per coefficient.
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".