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

81б. Алгоритм Гёрцеля: найти тон без БПФ

В разделе 81а сигнал фильтровали: пропустить одни частоты, погасить другие. В разделе 117 брали весь спектр сразу — быстрым преобразованием Фурье (БПФ). Часто нужно меньше: есть ли в сигнале тон 697 Гц, звенит ли зуммер на 2 кГц, пришла ли цифра тонового набора. Для нескольких частот БПФ считает сотни лишних бинов. Алгоритм Гёрцеля считает один бин — рекурсией второго порядка с одним умножением на отсчёт. Здесь — как он устроен, как выбрать длину блока и номер бина, сколько он стоит против БПФ, как не переполниться в q31 и как собрать из него детектор тонового набора (DTMF), который не срабатывает на музыку.

Опоры: своя модель на ПК на чистом Python (_verify/goertzel81c/goertzel81c.py) — алгоритм сверен с прямым расчётом ДПФ, детектор прогнан на тысячах блоков с шумом и расстройкой. Код на C собран armclang для STM32F407 и STM32G474 без предупреждений; тот же файл собран gcc на ПК и выдал те же мощности в q31, что и модель, бит в бит. Листинг внутреннего цикла — дизассемблер fromelf. На плате не запускалось, такты не мерились.

Как это называется в документации

Оригинал Перевод
Goertzel algorithm алгоритм Гёрцеля: один бин ДПФ рекурсией второго порядка
DFT bin бин ДПФ: частота k·fs/N
frequency resolution разрешение по частоте: ширина бина fs/N
spectral leakage утечка: тон растекается по соседним бинам
Hamming window окно Хэмминга
DTMF (dual-tone multi-frequency) тоновый набор: цифра — два тона сразу
twist перекос: разница уровней двух тонов цифры
second harmonic вторая гармоника: удвоенная частота
guard bits, headroom запас разрядов под рост числа

81б.1 Одна частота вместо спектра

ДПФ блока из N отсчётов в бине k — сумма X[k] = Σ x[n]·e^(−j2πkn/N). Гёрцель получает её модуль, не храня блок и не считая синусов на каждом отсчёте. На каждый отсчёт — одно звено второго порядка:

s[n] = x[n] + c·s[n−1] − s[n−2],     c = 2cos(2πk/N)

После последнего отсчёта блока из двух чисел состояния s1 = s[N−1] и s2 = s[N−2] получается мощность бина:

P = s1² + s2² − c·s1·s2 = |X[k]|²

Амплитуда тона, попавшего в бин, — 2√P / N. На отсчёт — одно умножение и два сложения; коэффициент c постоянный, считается один раз.

Модель на ПК сверила это с прямой суммой ДПФ на случайном блоке из 205 отсчётов: во всех бинах от 0 до 102 мощности совпали до 11 знаков. Номер бина не обязан быть целым: с k = 17,861 (точная частота 697 Гц) Гёрцель дал ту же мощность, что сумма ДПФ на этой частоте.

Звено Гёрцеля второго порядка: отсчёт x(n) складывается с c·s(n−1) и −s(n−2), где c = 2cos(2πk/N), после N отсчётов мощность P = s1² + s2² − c·s1·s2 равна квадрату модуля бина ДПФ; ниже клавиатура тонового набора при 8000 Гц и N = 205 — строки 697, 770, 852 и 941 Гц в бинах 18, 20, 22 и 24, столбцы 1209, 1336, 1477 и 1633 Гц в бинах 31, 34, 38 и 42, бин 39,02 Гц, тоны не дальше 0,27 бина от центров, вторые гармоники в бинах 36…84


81б.2 N и k: разрешение и утечка

Ширина бина — fs / N, длина блока — N / fs. Пример — тоновый набор: цифра — один тон из нижней группы (697, 770, 852, 941 Гц) и один из верхней (1209, 1336, 1477, 1633 Гц). При fs = 8000 Гц и N = 205 блок длится 25,6 мс, бин — 39,02 Гц. Ближе всего друг к другу 697 и 770 Гц: 73 Гц, 1,87 бина — различимы.

Номер бина k = f·N / fs округляют до целого, и тон оказывается не в центре бина. Модель — наихудшая потеря амплитуды по 16 фазам тона, без окна:

Тон, Гц f·N/fs k Центр бина, Гц Сдвиг, бина Потеря, дБ
697 17,86 18 702,4 −0,14 −0,31
770 19,73 20 780,5 −0,27 −1,12
852 21,83 22 858,5 −0,17 −0,44
941 24,11 24 936,6 +0,11 −0,21
1209 30,98 31 1209,8 −0,02 −0,01
1336 34,23 34 1326,8 +0,23 −0,84
1477 37,85 38 1482,9 −0,15 −0,35
1633 41,85 42 1639,0 −0,15 −0,36

Почему именно 205: модель перебрала все N от 100 до 250 и для каждого нашла наихудший сдвиг восьми тонов от центров бинов. Лучшее — N = 205, 0,269 бина; следом 113 (0,292) и 185 (0,297). Привычные для БПФ 256 отсчётов дают 0,360. Гёрцелю степень двойки не нужна — N выбирают под частоты.

Утечка — вторая сторона того же. Тон попадает не только в свой бин, но и в соседние. Без окна тон 770 Гц перевешивает соседа по группе всего на 16,2 дБ. Хуже с расстройкой: если частота ушла на 1,5 % (у 1633 Гц это 24,5 Гц, 0,63 бина), наихудшая потеря амплитуды — −11,7 дБ.


81б.3 Окно и цена против БПФ

Окно. Блок умножают на окно Хэмминга до звена — так же, как перед БПФ в разделе 117 окно Ханна. Бин становится шире и терпимее к расстройке: при ±1,5 % наихудшая потеря −4,4 дБ вместо −11,7, а перевес тона над соседом по группе — 27,3 дБ вместо 16,2. Цена — одно умножение на отсчёт, общее для всех частот, и таблица окна: 205 чисел q15 — 410 байт. Окно уменьшает и сам тон: усиление окна для тона — 0,538, и амплитуду до сравнения с порогом делят на него.

Умножения. Гёрцель на одну частоту — N умножений в цикле и 3 в конце (последний член удобно считать как s2·(s2 − c·s1)). Простое БПФ по основанию 2 — N/2·log2N комплексных умножений, каждое из них — четыре вещественных:

Умножений на блок Память
Гёрцель, 8 тонов DTMF, N = 205 1664 8 × 12 байт
Гёрцель, 16 бинов (тоны и гармоники) 3328 192 байта
окно Хэмминга, на все бины 205 410 байт таблицы
БПФ, N = 256 1024 комплексных = 4096 вход и выход 256 float — 2048 байт

На частоту Гёрцель хранит три числа: s1, s2 и коэффициент — 12 байт. Блок отсчётов хранить не нужно: каждый отсчёт уходит в звенья сразу, хоть из прерывания АЦП. БПФ требует весь блок в памяти и таблицы поворотов — у CMSIS-DSP на 256 отсчётов это 2920 байт во флеш-памяти (раздел 117).

При одинаковом N = 256 Гёрцель на одну частоту — 259 умножений, и БПФ выгоднее начиная примерно с 16 частот (2·log2N). Для восьми тонов DTMF Гёрцель дешевле — и работает с N = 205, которое БПФ по основанию 2 не берёт.


81б.4 Фиксированная точка: запас на рост

Звено — резонатор: на своей частоте состояние растёт от отсчёта к отсчёту. Сколько — зависит от k. Граница роста за блок — сумма модулей импульсной характеристики звена; модель посчитала её для всех 16 бинов: наибольшая при k = 18 — 249,0 (почти 2⁸), наименьшая — 131,2. Синус полной шкалы точно на частоте бина разгоняет состояние до 195,6 — граница с запасом.

В коде вход q15 сдвигается в состояние int32 на 7 бит. Худший случай — произведение c·s1, где c почти 2: 32768·2^7·249·2 = 2 088 763 392 — меньше 2³¹ − 1 = 2 147 483 647. Со сдвигом 8 выходит 4 177 526 784 — не помещается. Это условие стоит в коде _Static_assert: сборка не пройдёт, если сдвиг увеличат.

Модель проверила полную шкалу на бине 18 с точной копией кода q31:

Сдвиг входа c·s1 вышло за int32 Состояние завернулось Мощность
7 0 раз 0 раз как в double
8 13 раз 0 раз ошибка 812 %
9 68 раз 44 раза ошибка 46 %

Точность. Тон с разной громкостью (697 и 941 Гц, по 8 фаз) сравнили с тем же расчётом в double на тех же отсчётах q15. Второй вариант — состояние в int16: чтобы рост в 249 раз поместился в 16 бит, вход приходится сдвигать вправо на 9, и от 16 бит отсчёта остаётся 7.

Уровень тона Ошибка мощности, q31 Ошибка мощности, состояние q15
0 дБ меньше 0,001 дБ 0,02 дБ
−20 дБ меньше 0,001 дБ 0,32 дБ
−40 дБ меньше 0,001 дБ 0,92 дБ
−60 дБ меньше 0,001 дБ 20,90 дБ
−80 дБ 0,0012 дБ 40,85 дБ

q31 с 64-битным произведением точен во всём диапазоне отсчёта q15; состояние q15 годится только для громких тонов. Float на Cortex-M4 — 24 бита мантиссы, и на слабом бине рядом с сильным тоном он уступает q31: на бине 20 при полной шкале на бине 18 (на 117,8 дБ ниже) float на ПК ошибся в 2,6 раза, q31 — на 0,4 %. Для порогов DTMF это неважно, для измерения слабой гармоники рядом с сильной — важно.


81б.5 Код на C и что делает компилятор

Блок в q31. Коэффициент хранится как cos·2³¹ (2cos в q31 не помещается), а произведение сдвигается на 30 вместо 31 — это и есть умножение на 2cos:

int64_t goertzel_q31(const int16_t *x, uint32_t n, int32_t cq)
{
    int32_t s1 = 0, s2 = 0;
    GZ_ONE_BY_ONE
    for (uint32_t i = 0; i < n; i++) {
        int32_t s0 = (int32_t)(((int64_t)cq * s1) >> 30) - s2 + x[i] * (1 << GZ_SHIFT);
        s2 = s1;
        s1 = s0;
    }
    int32_t t = (int32_t)(((int64_t)cq * s1) >> 30);       /* 2cos·s1 */
    return (int64_t)s1 * s1 + (int64_t)s2 * s2 - (int64_t)t * s2;
}

Мощность — в единицах 2⁴⁴ (два раза по 15 бит отсчёта и 7 бит сдвига): P = результат / 2⁴⁴. Рядом в файле — вариант во float, таблица из 16 номеров бинов и коэффициентов (тоны и вторые гармоники) и два _Static_assert на запас разрядов.

F407 и G474 — одно и то же ядро Cortex-M4 с FPU (в заголовках паков у обоих core_cm4.h и __FPU_PRESENT 1), и armclang с -O2 дал для них одинаковый код. Сам он разворачивает цикл по четыре отсчёта; GZ_ONE_BY_ONE — прагма, которая это запрещает, чтобы листинг показал один отсчёт. Внутренний цикл goertzel_q31 по fromelf:

цикл:   MOV    lr,r12
        SMULL  r4,r12,r12,r2
        LSRS   r4,r4,#30
        ORR    r12,r4,r12,LSL #2
        LDRSH  r4,[r0,#2]!
        SUB    r3,r12,r3
        ADD    r12,r3,r4,LSL #7
        SUBS   r1,#1
        MOV    r3,lr
        BNE    цикл

Десять команд на отсчёт. Полезных шесть: SMULL — 64-битное произведение cos·s1, LSRS и ORR — сдвиг на 30 из двух половин, LDRSH — отсчёт, SUB и ADD — вычесть s2 и прибавить отсчёт, сдвинутый на 7. Две MOV передвигают состояние, SUBS и BNE — счётчик. Во float — девять команд: VLDR, VMUL, VADD, VSUB и пять служебных. Конец блока компилятор сам переписал как s1² + s2·(s2 − c·s1). На 16 бинов и блок 205 отсчётов — 32 800 команд цикла за 25,6 мс.

Проверка на ПК: тот же файл, собранный gcc с -DGZ_HOST_TEST, прогнал 19 блоков — 16 цифр, слабый тон, полную шкалу и пилу — по 16 бинам. Все 304 мощности q31 совпали с моделью бит в бит.


81б.6 Детектор DTMF и ложные срабатывания

Восемь звеньев дают восемь амплитуд. Правила модели:

Прогоны — по 256 цифр: точные частоты; частоты с отклонением до ±1,5 %; белый шум с отношением сигнал/шум 10 дБ и 0 дБ. Тоны по 0,3 полной шкалы, перекос до ±4 дБ, фазы случайные. Ложные срабатывания — пила от 60 до 500 Гц с шагом 1 Гц (441 сигнал) с гармониками до 3900 Гц: сигнал, богатый гармониками, как голос или музыка.

Правила Ложных на пиле Точные ±1,5 % Шум 10 дБ Шум 0 дБ
без окна 12 256 229 219 152
окно Хэмминга 43 256 256 256 180
окно + гармоника обоих тонов 11 240 241 231 70
окно + гармоника верхнего тона 17 256 256 255 114
окно + доля мощности 0 256 256 253 53

Что видно:

Детектор на q31 с окном в q15 принял на 768 цифрах те же решения, что в double.


81б.7 Грабли

Тон есть, а детектор молчит. Частота ушла от центра бина. Без окна уход на 1,5 % стоит до 11,7 дБ, с окном Хэмминга — 4,4 дБ.

N = 256 «как у БПФ». Тоны DTMF ложатся до 0,36 бина от центров вместо 0,27 при N = 205. Гёрцелю степень двойки не нужна.

Порог по амплитуде «съехал» после окна. Окно уменьшает тон: усиление 0,538. Амплитуду делят на него или порог считают с окном.

Цифры набираются от музыки. Одни правила уровня и перевеса пропускают пилу: 43 ложных из 441 с окном. Нужна вторая гармоника или доля мощности.

Проверка гармоник режет верные цифры. Гармоника нижнего тона 1540 Гц — в 1,15 бина от верхнего 1477 Гц: её бин видит утечку верхнего тона. Гармонику проверяют у верхнего тона или берут долю мощности.

Мощность q31 — мусор на громком сигнале. Сдвиг входа больше 7: состояние растёт до 249 раз, и c·s1 выходит за int32 — ошибка 812 % при сдвиге 8. Запас ставят _Static_assert.

Слабые тоны пропадают в q15. Состояние int16 требует сдвига входа на 9: на −60 дБ ошибка мощности 20,9 дБ. Состояние — int32, произведение — 64 бита.

Мощность растёт от блока к блоку. s1 и s2 не обнулили перед новым блоком: звено продолжает прошлый блок.


81б.8 Итоги

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

goertzel81c.c — алгоритм Гёрцеля: блок в q31 и во float.

/* Раздел 81б. Алгоритм Гёрцеля: блок в q31 и во float.
 *
 * Для STM32F407 и STM32G474 (Cortex-M4F) собирается armclang без предупреждений;
 * с -DGZ_HOST_TEST — gcc на ПК: читает блоки отсчётов из stdin и печатает мощности,
 * проверка сверяет их с моделью goertzel81c.py. */
#include <stdint.h>

#define GZ_N      205u      /* отсчётов в блоке: 25,6 мс при 8 кГц */
#define GZ_BINS   16u       /* 8 тонов DTMF и 8 вторых гармоник */
#define GZ_SHIFT  7         /* вход q15 сдвигается в состояние на 7 бит */
#define GZ_GROWTH 250       /* рост состояния за блок, с запасом: модель даёт 249,0 при k = 18 */

/* цикл по отсчёту без разворота: так листинг показывает ровно один отсчёт;
 * -DGZ_UNROLL оставляет armclang его обычный разворот */
#if defined(__clang__) && !defined(GZ_UNROLL)
#define GZ_ONE_BY_ONE _Pragma("clang loop unroll(disable)")
#else
#define GZ_ONE_BY_ONE
#endif

/* 2cos·s1 должно поместиться в int32 — иначе переполнение знакового, неопределённое поведение */
_Static_assert((int64_t)32768 * (1 << GZ_SHIFT) * GZ_GROWTH * 2 <= INT32_MAX, "запас по переполнению");
_Static_assert((int64_t)32768 * (1 << (GZ_SHIFT + 1)) * GZ_GROWTH * 2 > INT32_MAX, "сдвиг 8 уже не помещается");

/* номера бинов k и cos(2πk/N)·2^31: тоны 697…1633 Гц, затем их вторые гармоники */
const uint8_t gz_k[GZ_BINS] = {18, 20, 22, 24, 31, 34, 38, 42, 36, 39, 44, 48, 62, 68, 76, 84};
const int32_t gz_cos_q31[GZ_BINS] = {
    1828878034, 1756489927, 1677503679, 1592215995, 1248909980, 1083228028, 848335877, 600708818,
    967599315, 787480650, 473275393, 213560429, -694828920, -1054685608, -1477235229, -1811414887,
};

int64_t goertzel_q31(const int16_t *x, uint32_t n, int32_t cq)
{
    int32_t s1 = 0, s2 = 0;
    GZ_ONE_BY_ONE
    for (uint32_t i = 0; i < n; i++) {
        int32_t s0 = (int32_t)(((int64_t)cq * s1) >> 30) - s2 + x[i] * (1 << GZ_SHIFT);
        s2 = s1;
        s1 = s0;
    }
    int32_t t = (int32_t)(((int64_t)cq * s1) >> 30);       /* 2cos·s1 */
    return (int64_t)s1 * s1 + (int64_t)s2 * s2 - (int64_t)t * s2;
}

float goertzel_f32(const float *x, uint32_t n, float c)
{
    float s1 = 0.0f, s2 = 0.0f;
    GZ_ONE_BY_ONE
    for (uint32_t i = 0; i < n; i++) {
        float s0 = x[i] + c * s1 - s2;
        s2 = s1;
        s1 = s0;
    }
    return s1 * s1 + s2 * s2 - c * s1 * s2;
}

#ifdef GZ_HOST_TEST
#include <math.h>
#include <stdio.h>

int main(void)
{
    int16_t q[GZ_N];
    float f[GZ_N];
    char name[32];
    int blocks = 0;
    while (scanf("%31s", name) == 1) {
        for (uint32_t i = 0; i < GZ_N; i++) {
            int v;
            if (scanf("%d", &v) != 1)
                return 1;
            q[i] = (int16_t)v;
            f[i] = (float)v / 32768.0f;
        }
        for (uint32_t b = 0; b < GZ_BINS; b++) {
            float c = (float)(2.0 * cos(2.0 * 3.14159265358979323846 * gz_k[b] / GZ_N));
            printf("%s %u q31 %lld f32 %.9e\n", name, (unsigned)gz_k[b], (long long)goertzel_q31(q, GZ_N, gz_cos_q31[b]),
                   (double)goertzel_f32(f, GZ_N, c));
        }
        blocks++;
    }
    printf("блоков %d\n", blocks);
    return 0;
}
#endif

Дальше: раздел 82 «Индикация на прототипе: ШИМ, RGB, сдвиговые регистры».

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