Embedded HandbookПриложения
Все разделы › III. Периферия
STM32G474, STM32G431 · уровень средний · 13 мин чтения
Открыть в интерактивном справочнике

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.

Путь отсчёта через FMAC: запись в WDATA, кольцевой буфер X1, умножение на коэффициенты из X2 с накоплением в 26-битном аккумуляторе, усиление 2 в степени R и усечение с насыщением по CLIPEN, буфер Y и чтение из RDATA, у БИХ прошлые выходы из Y возвращаются в сумму; ниже раскладка 256 слов памяти: КИХ на 31 коэффициент с запасом 2 и 2 занимает 66 слов, БИХ-биквад 14 слов, наибольший КИХ на 127 коэффициентов — 255 слов


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.

Формула БИХ в 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 Грабли


46б.8 Итоги

Полные исходники

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 «Зачем нужна операционная система».

Открыть в интерактивном справочнике