46б. FMAC: фильтр-ускоритель G4 — КИХ и БИХ без процессора
В разделе 81а цифровые фильтры считал процессор — функциями CMSIS-DSP. В разделе 46а рядом с CORDIC упоминался второй сопроцессор G4 — FMAC. Он делает ту же работу сам: коэффициенты лежат в его памяти, процессор или DMA только пишет входной отсчёт и забирает выходной. Здесь — как устроены буферы, сколько памяти нужно фильтру, что происходит с числами внутри и как не получить вместо фильтра генератор мусора.
Опоры: справочное руководство RM0440 (G4, глава 18 «Filter math accelerator») и заголовки из пака Keil STM32G4xx_DFP, драйвер HAL FMAC оттуда же. Коэффициенты — те же фильтры, что в разделе 81а. Арифметика проверена своей моделью на ПК, написанной по описанию RM0440; код собран armclang для G474. QEMU этот блок не моделирует, на плате не проверено.
Как это называется в документации
Оригинал Перевод FMAC, filter math accelerator фильтр-ускоритель MAC, multiply and accumulate умножение с накоплением X1, X2, Y buffer буферы входных отсчётов, коэффициентов и выходных отсчётов circular buffer кольцевой буфер FIR, convolution КИХ-фильтр, свёртка IIR, direct form 1 БИХ-фильтр в прямой форме 1 P, Q, R число коэффициентов прямой и обратной связи, показатель усиления watermark, FULL_WM, EMPTY_WM порог заполнения буфера headroom d запас в буфере сверх необходимого clipping, CLIPEN насыщение выхода wrap заворот: переполнение со сменой знака
46б.1 Что это и у кого есть
FMAC (RM0440, 18.2) — умножитель 16 × 16 бит, аккумулятор на 26 бит («24 + 2»), своя память 256 слов по 16 бит и автомат, который сам ходит по буферам. Входы и выходы — 16 бит. Функции — загрузка буферов X1, X2 и Y, свёртка (КИХ) и БИХ в прямой форме 1. Корреляция — та же свёртка с коэффициентами в обратном порядке.
Блок есть в заголовках пака у STM32G431 и G474, STM32H723, STM32U585 и STM32H563; у F407 и у H743 его нет.
Работа на один отсчёт такая: процессор пишет x[n] в регистр WDATA, отсчёт ложится в кольцевой буфер X1; автомат умножает последние P отсчётов на P коэффициентов из X2, копит сумму, умножает её на 2^R, обрезает до 16 бит и кладёт y[n] в буфер Y; процессор читает его из RDATA. У БИХ в сумму добавляются и Q прошлых выходов из того же Y.
46б.2 Память: три буфера в 256 словах
Каждый буфер задаётся базой и размером в регистрах X1BUFCFG, X2BUFCFG и YBUFCFG (RM0440, 18.3.2); база плюс размер — меньше 256. X2 хранит коэффициенты и не движется. X1 и Y — кольцевые: X1 принимает отсчёты, Y отдаёт результаты.
Сколько памяти нужно (RM0440, 18.3.8 и 18.3.9):
| Фильтр | Минимум слов | Пример | Предел |
|---|---|---|---|
| КИХ на N коэффициентов | 2N + 1 | N = 31: 63 | N = 127: 255 слов |
| БИХ, N прямых и M обратных | 2N + 2M | биквад N = 3, M = 2: 10 | M = N − 1, N = 64: 254 слова |
Отсюда и пределы поля P: для КИХ от 2 до 127, для БИХ P до 64 и Q от 1
до 63 — те же числа стоят в проверках драйвера HAL
(FMAC_PARAM_P_MAX_FIR, FMAC_PARAM_P_MAX_IIR, FMAC_PARAM_Q_MAX).
Сверх минимума добавляют запас d1 во вход и d2 в выход, чтобы фильтр не стоял в ожидании. Раскладка для КИХ на 31 коэффициент с запасом 2 и 2 по образцу RM:
| Буфер | Адреса | Регистр |
|---|---|---|
| X2, коэффициенты | 0…30 | X2BUFCFG = 0x0000 1F00 |
| X1, отсчёты: 31 + d1 | 31…63 | X1BUFCFG = 0x0100 211F, порог FULL_WM = 1 (два слова) |
| Y, выходы: d2 | 64…65 | YBUFCFG = 0x0000 0240 |
Занято 66 слов из 256. Порог FULL_WM должен быть не больше log₂(d1), иначе буфер объявит себя полным раньше, чем в нём окажутся N отсчётов.
46б.3 Числа внутри: q1.15, аккумулятор и 2^R
Входы, коэффициенты и выходы — q1.15: знак и 15 дробных бит, от −1 (0x8000) до 1 − 2⁻¹⁵ (0x7FFF). Аккумулятор, по RM0440 (18.3.7), — q4.22: 26 бит, из них 22 дробных, диапазон от −8 до +7,99999976.
- Аккумулятор не насыщается. Частичная сумма за пределом заворачивается, и это не беда, если дальнейшие слагаемые вернут её обратно. Флаг SAT в FMAC_SR отмечает заворот.
- Усиление 2^R. R от 0 до 7 — от 0 до 42 дБ шагом 6 дБ; сумма умножается на 2^R перед выводом.
- Выход — усечение. Лишние младшие биты просто отбрасываются; при CLIPEN = 1 значение за пределами q1.15 насыщается до 0x7FFF или 0x8000, при CLIPEN = 0 — заворачивается.
- P — число коэффициентов, а не порядок: в формуле 18.3.6 вектор длиной N + 1 задаётся как P = N + 1.
Формула БИХ в RM: y[n] = 2^R · (Σ b[k]·x[n−k] + Σ a[k]·y[n−k]) — обратная связь со знаком «+», как у CMSIS-DSP (раздел 81а.4).
46б.4 Модель КИХ: сверка с плавающей точкой
Модель на ПК повторяет описание RM0440: коэффициенты и отсчёты в q1.15, произведения копятся в 26-битном аккумуляторе q4.22 с заворотом, затем 2^R, усечение и насыщение. Фильтр — КИХ раздела 81а: 31 коэффициент, срез 100 Гц при 1000 Гц. В q1.15 середина 0,19966 — код 6542 (0x198E), сумма всех кодов — 32766, симметрия сохраняется.
Синус 0,9, 2000 отсчётов, усиление по установившемуся выходу:
| Частота | float | модель FMAC |
|---|---|---|
| 50 Гц | 0,98902 | 0,98905 |
| 100 Гц | 0,49820 | 0,49821 |
| 200 Гц | 0,00029 | 0,00037 |
| 300 Гц | 0,00069 | 0,00062 |
В полосе пропускания разница — в пятом знаке; в полосе подавления 16-битные коэффициенты сдвигают нули АЧХ, но подавление остаётся глубоким: −68,5 дБ на 200 Гц и −64,2 дБ на 300 Гц.
На 4000 отсчётах смеси синуса и шума наибольшая ошибка против float — 2,58 единицы младшего разряда, средняя — −0,50: усечение всегда округляет вниз и сдвигает выход на полразряда. Где именно усекаются произведения, RM не пишет; если резать каждое до 2⁻²², выход меняется не больше чем на 1 единицу. Частичная сумма в этом прогоне не выше 0,743 — до заворота далеко.
Усиление и насыщение. Тот же КИХ с R = 1 (усиление 2) и синусом 0,6 на 50 Гц: на выходе 1,19 — за пределом q1.15. С CLIPEN = 1 вершины срезаны: 90 отсчётов из 300 стоят на 0x7FFF или 0x8000. С CLIPEN = 0 90 отсчётов сменили знак — на положительной полуволне выход падает до −0,871.
46б.5 БИХ: делить коэффициенты на 2
Биквад Баттерворта 100 Гц из раздела 81а: −a1 = 1,142981 больше единицы и в q1.15 не помещается. RM0440 (18.3.12, совет 4) предлагает то же, что postShift в CMSIS: все коэффициенты делятся на 2^R, а R возвращает масштаб на выходе. С R = 1 содержимое X2 — b0, b1, b2, a1, a2:
1105 2210 1105 18727 -6763 R = 1
Это те же числа, что у arm_biquad_cascade_df1_q15 в 81а.5, только без
нулевого слова после b0. Модель против float:
| Частота | float | модель FMAC |
|---|---|---|
| 50 Гц | 0,97291 | 0,97285 |
| 100 Гц | 0,70711 | 0,70698 |
| 200 Гц | 0,19612 | 0,19607 |
| 300 Гц | 0,05564 | 0,05563 |
Без деления на 2 (R = 0) коэффициент 1,14298 насыщается до 32767, и усиление на 50 Гц — 0,67724 вместо 0,97291: это уже другой фильтр.
Насыщение у БИХ важнее, чем у КИХ: выход идёт обратно в сумму. На ступеньках ±0,99 выброс Баттерворта упирается в предел в 20 отсчётах из 1000 — с CLIPEN = 1 фильтр этого почти не замечает. С CLIPEN = 0 заворот уходит в обратную связь: 840 отсчётов из 1000 отличаются больше чем на 0,1, наибольшая разница — 1,990, почти весь диапазон.
46б.6 Код: запись, ожидание, чтение
Инициализация по RM0440 (18.3.8): сброс блока, три BUFCFG, загрузка коэффициентов функцией «Load X2», предзагрузка X1 нулями — это нулевая история фильтра, — потом CR и запуск функции «КИХ» битом START. Функции загрузки заканчиваются сами после нужного числа записей — START сбрасывается аппаратно; драйвер HAL ждёт этого так же. Поля проверены при сборке:
_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 предзагружено 30 отсчётов из 31 нужного, поэтому первый же записанный отсчёт даёт первый выход (RM: расчёт начинается, когда в X1 набралось N отсчётов). Дальше — один вход, один выход:
int16_t fmac_sample(int16_t x)
{
FMAC->WDATA = (uint16_t)x;
while (FMAC->SR & FMAC_SR_YEMPTY) /* ждём выход: один на каждый вход */
;
return (int16_t)FMAC->RDATA;
}
armclang с -Os собирает это в десять команд:
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 — 0x4002 1414, WDATA — 0x4002 1418, RDATA — 0x4002 141C. Цикл
из трёх команд — ожидание флага YEMPTY. С -O2 компилятор разворачивает
его вчетверо: 44 байта вместо 24. Та же функция годится и для биквада —
fmac_biquad_init загружает X2, X1 и Y и запускает функцию 9 с R = 1.
Такты. Сколько тактов FMAC тратит на коэффициент, RM0440 не пишет — в главе 18 нет таблицы времени. Единственное число — задержка флагов: X1FULL поднимается через 3 такта после записи в последнюю свободную ячейку, YEMPTY — через 3 такта после чтения последнего выхода. Отсюда совет RM: порог 2 или пауза перед чтением FMAC_SR. Время фильтра на плате меряют счётчиком DWT (раздел 73).
46б.7 Грабли
- X2 пересекается с X1 или Y. Фильтр пишет выходы и отсчёты поверх коэффициентов (RM0440, 18.3.8). Буферы кладут встык, но не внахлёст.
- Порог больше запаса. FULL_WM больше log₂(d1) — буфер «полон» раньше, чем набралось N отсчётов, и новых фильтр больше не просит.
- Y не загружен у БИХ. Первый выход считается по первым M словам Y, загружены они или нет (18.3.9). Без «Load Y» фильтр стартует с мусора.
- Знак обратной связи. FMAC прибавляет a·y. Коэффициенты из программы расчёта с минусом кладут как −a1, −a2 — как в CMSIS (81а.4).
- Коэффициент больше 1. Насыщается до 0x7FFF: 0,677 вместо 0,973 на 50 Гц. Делить всё на 2^R и ставить R.
- CLIPEN выключен. Выход за ±1 меняет знак; у БИХ заворот уходит в обратную связь и портит сотни отсчётов.
- Флаг прочитан сразу. 3 такта задержки X1FULL и YEMPTY — можно записать лишний отсчёт (OVFL, запись пропадёт) или прочитать пустой Y (UNFL).
- Настройка при START = 1. X1BUFCFG, YBUFCFG, размер X2, FUNC, P, Q и R меняются только при остановленной функции. Из настроек буферов на ходу можно менять лишь X2_BASE — так переключают наборы коэффициентов.
- Предел аккумулятора — ±8 или ±4? 18.3.7 называет q4.22 (±8), а 18.3.12 для того же 0x1FFFFFF пишет ±4 в q3.23. Запас считают по меньшему пределу.
- Прямая форма 2 и каскады. Блок умеет только прямую форму 1. Каскад звеньев — отдельные запуски с разными X2_BASE или одно звено большего порядка (18.3.12).
46б.8 Итоги
- FMAC — у G431, G474, H723, U585 и H563; нет у F407 и H743. Умножитель 16 × 16, аккумулятор 26 бит, память 256 слов.
- КИХ требует 2N + 1 слов — до 127 коэффициентов; БИХ — 2N + 2M, до N = 64 при M = 63. КИХ на 31 коэффициент с запасом 2 и 2 — 66 слов.
- Модель по RM0440: КИХ 81а в q1.15 — 0,49821 на срезе против 0,49820 float, ошибка до 2,58 единицы младшего разряда, смещение −0,50 от усечения.
- БИХ: коэффициенты делить на 2, R = 1: 0,70698 на 100 Гц против 0,70711; без деления — 0,67724 вместо 0,97291 на 50 Гц.
- CLIPEN = 1 обязателен: без него у БИХ 840 отсчётов из 1000 испорчены заворотом.
- Один отсчёт — запись в WDATA, ожидание YEMPTY, чтение RDATA: десять команд с -Os. Тактов на коэффициент RM0440 не называет.
Полные исходники
fmac46b.c — FMAC STM32G474 по регистрам: КИХ и БИХ-биквад.
/* 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 останавливает фильтр */
}
Дальше: раздел 23 «Зачем нужна операционная система».