Если вы работаете с цифровой обработкой сигналов на STM32 и столкнулись с задачей реализации фильтра с конечной импульсной характеристикой, скорее всего вы уже поняли: стандартные функции CMSIS DSP — это хорошо, но они не всегда дают нужную производительность или гибкость. Особенно когда речь идёт о фильтрах высокого порядка, работающих в реальном времени с несколькими каналами. В таких случаях на помощь приходит аппаратная поддержка — в частности, рафт-ранговые (ranked) FIR-фильтры, которые можно реализовать через DMA и таймеры, либо используя специализированные периферийные модули в новых сериях STM32.
В этой статье разберём, как устроены FIR-фильтры в контексте STM32, когда имеет смысл переходить от софтварной реализации к аппаратно-ориентированным схемам, и как правильно настроить систему так, чтобы фильтр работал стабильно без перегрузки процессора.
Что такое FIR-фильтр и почему он важен для STM32
FIR (Finite Impulse Response) — это фильтр с конечной импульсной характеристикой. Его главное свойство: выходной сигнал зависит только от конечного числа предыдущих входных отсчётов. Это делает его по определению устойчивым и позволяет получить строго линейную фазу — то есть все частоты сигнала задерживаются на одинаковое время. Для задач обработки аудио, биомедицинских сигналов (ЭКГ, ЭЭГ) и измерительных систем это критически важно.
Математически работа FIR-фильтра описывается формулой:
y[n] = Σ b[k] · x[n−k], где k = 0..N−1
Здесь b[k] — коэффициенты фильтра (taps), x[n−k] — предыдущие отсчёты входного сигнала, N — порядок фильтра (количество отводов). Чем больше отводов, тем круче частотная характеристика, но тем больше вычислений нужно выполнить для каждого выходного отсчёта.
Когда стандартная реализация перестаёт справляться
В библиотеке CMSIS DSP от ST есть функция arm_fir_f32, которая реализует FIR-фильтр на процессоре Cortex-M4/M7 с поддержкой FPU. Для многих задач этого достаточно. Но проблемы начинаются, когда:
- порядок фильтра превышает 100–200 отводов;
- нужно обрабатывать несколько каналов одновременно;
- частота дискретизации высокая (например, 192 кГц и выше);
- процесор загружен другими задачами реального времени.
В этих случаях каждый новый отсчёт требует N умножений и N−1 сложений. При N=256 и частоте дискретизации 96 кГц это 24,5 миллиона операций умножения-сложения в секунду только на один канал. Процессор на 168 МГц потратит на это заметную часть своего времени.
Рафт-ранговый подход: суть идеи
Термин «рафт-ранговый» в контексте FIR-фильтров на STM32 означает организацию вычислений, при которой обработка распределена между аппаратными модулями и процессором по принципу ранжирования — приоритетные операции выполняются аппаратно, а процессор вмешивается минимально.
Идея в следующем: вместо того чтобы процессор сам читал данные из ADC, умножал их на коэффициенты и складывал, мы настраиваем систему так, чтобы:
- ADC по таймеру автоматически запускал преобразование и писал результат в память через DMA.
- DMA формировал кольцевой буфер с отсчётами.
- Процессор получал прерывание только когда накоплено достаточно данных для обработки блока.
- Вычисления выполнялись пакетно, с использованием оптимизированных функций CMSIS DSP или даже через аппаратный сопроцессор (если доступен).
Такой подход позволяет снизить нагрузку на процессор в 3–5 раз по сравнению с поотсчётной обработкой.
Архитектура системы с рафт-ранговым FIR-фильтром
Рассмотрим типичную архитектуру для STM32F4 или STM32F7. Система состоит из нескольких уровней:
| Уровень | Компонент | Функция |
|---|---|---|
| 1 (высший приоритет) | Таймер (TIM) | Генерация тактовой частоты дискретизации |
| 2 | ADC | Аналого-цифровое преобразование |
| 3 | DMA | Перенос данных из ADC в буфер без участия CPU |
| 4 | Кольцевой буфер | Хранение текущих N отсчётов |
| 5 (низший приоритет) | CPU | Вычисление FIR-фильтра по блоку данных |
Ключевой момент: уровни 1–3 работают полностью автономно. Процессор участвует только на уровне 5, когда нужно выполнить свёртку. Это и есть суть рафт-рангового подхода — ранжирование задач по приоритету и вынос рутины в аппаратуру.
Пошаговая настройка
Шаг 1. Настройка таймера для генерации частоты дискретизации
Таймер настраивается так, чтобы генерировать событие обновления (update event) с частотой, равной нужной частоте дискретизации. Например, для 48 кГц при тактовой частоте таймера 84 МГц:
Prescaler = 0, Period = 84000000 / 48000 − 1 = 1749
Триггерный выход таймера (TRGO) настраивается на событие обновления и подключается к входу запуска ADC.
Шаг 2. Настройка ADC в режиме непрерывного сканирования
ADC настраивается на запуск от внешнего триггера (TRGO таймера). Режим непрерывного преобразования отключён — каждый запуск строго по таймеру. Это гарантирует стабильную частоту дискретизации без джиттера.
Важно: время преобразования ADC должно быть меньше периода дискретизации. Для 48 кГц период составляет 20,8 мкс. При 12-битном разрешении и частоте ADC 30 МГц время преобразования — около 0,5 мкс, что с запасом укладывается.
Шаг 3. Настройка DMA для кольцевого буфера
DMA настраивается в режиме circular (кольцевой), данные передаются из регистра данных ADC в массив в SRAM. Ширина данных — полуслово (16 бит) для 12-битного ADC.
Размер буфера DMA выбирается кратным размеру блока обработки. Например, если обрабатываем блоками по 64 отсчёта, буфер DMA может быть 128 или 256 отсчётов — чтобы процессор мог работать с одной половиной буфера, пока DMA заполняет другую (double buffering).
Шаг 4. Организация кольцевого буфер для коэффициентов
Для FIR-фильтра нужно хранить не только входные отсчёты, но и коэффициенты. Коэффициенты заранее рассчитываются (например, в MATLAB или Python с помощью scipy.signal.firwin) и сохраняются в массиве с фикситочкой или в float32.
При использовании CMSIS DSP структура arm_fir_instance_f32 содержит указатель на коэффициенты и на состояние (state buffer). State buffer должен быть размером numTaps + blockSize − 1 — это обеспечивает непрерывность фильтрации между блоками.
Шаг 5. Обработка в прерывании DMA
Когда DMA заполняет половину буфера или весь буфер (в зависимости от схемы), генерируется прерывание. В обработчике:
- Определяем, какая половина буфера готова.
- Копируем данные в рабочий массив (или обрабатываем на месте).
- Вызываем
arm_fir_f32для блока. - Результат записываем в выходной буфер (для DAC или дальнейшей обработки).
Сравнение подходов к реализации
| Параметр | Поотсчётная (ISR) | Блочная (DMA + CMSIS) | Рафт-ранговая (DMA + блочная + приоритеты) |
|---|---|---|---|
| Нагрузка на CPU | Высокая | Средняя | Низкая |
| Задержка | Минимальная | Размер блока | Размер блока |
| Сложность настройки | Низкая | Средняя | Высокая |
| Устойчивость к пропуску данных | Низкая | Средняя | Высокая |
| Поддержка многоканальности | Сложно | Средне | Хорошо |
| Минимальный порядок фильтра | Любой | Любой | Любой |
| Оптимальный порядок фильтра | < 32 | 32–256 | > 128 |
Что выбрать в зависимости от задачи
Если у вас простой фильтр (до 32 отводов) и один канал — используйте стандартную CMSIS DSP функцию с обработкой в прерывании ADC. Не усложняйте. Настройка DMA и таймеров оправдана только когда есть реальная проблема с производительностью.
Если фильтр среднего порядка (32–128 отводов) и частота дискретизации до 96 кГц — блочная обработка через DMA с double buffering даст хороший результат. Процессор будет загружен на 30–50%, остальное время можно потратить на другие задачи.
Если фильтр высокого порядка (более 128 отводов), несколько каналов или частота выше 96 кГц — рафт-ранговый подход с чётким разделением приоритетов между периферией и процессором становится необходимостью. Рассмотрите также возможность использования STM32H7 с его более мощным DMA и кэшированием.
Частые ошибки
Ошибка 1: Неправильный размер state buffer. Если state buffer меньше чем numTaps + blockSize − 1, фильтр будет работать некорректно — на границах блоков появятся щелчки и искажения. Это самая частая проблема при переходе от поотсчётной обработки к блочной.
Ошибка 2: Отсутствие критических секций при double buffering. Если DMA и процессор одновременно работают с одной и той же областью памяти, данные могут быть повреждены. Нужно либо использовать полноценный double buffering (две независимые половины), либо отключать прерывание DMA на время обработки.
Ошибка 3: Переполнение буфера из-за задержки обработки. Если процессор не успевает обработать блок до того, как DMA перезапишет данные, произойдёт потеря отсчётов. Решение: увеличить размер буфера, оптимизировать код обработки или снизить частоту дискретизации.
Ошибка 4: Игнорирование выравнивания данных для DMA. На Cortex-M7 с кэшем данные, записанные DMA, могут быть невидны процессору без инвалидации кэша. Убедитесь, что буферы DMA находятся в не кэшируемой области памяти (например, SRAM2 на STM32F7) или используйте SCB_CleanInvalidateDCache().
Ошибка 5: Неправильный расёт коэффициентов. Коэффициенты для фиксированной точки нужно масштабировать с учётом разрядности. Если просто взять float-коэффициенты и привести их к Q15 без нормализации, фильтр может работать с неправильным усилением или даже нестабильно (для IIR, но для FIR это проявляется в неверной амплитуде).
Практические рекомендации
- Начинайте с float32. Даже если в финальной версии планируете фиксированную точку, сначала отладьте алгоритм на float32 через CMSIS DSP. Это избавит от множества проблем с масштабированием и переполнением.
- Используйте инструмент Filter Designer. MATLAB, scipy.signal (Python) или бесплатный онлайн-калькулятор — сгенерируйте коэффициенты и проверьте частотную характеристику до того, как загружать их в микроконтроллер.
- Профилируйте нагрузку. Замерьте, сколько тактов уходит на обработку одного блока. Если это больше чем время накопления блока (blockSize / sampleRate), система не будет работать в реальном времени.
- Рассмотрите децимацию. Если полоса сигнала узкая относительно частоты дискретизации, можно снизить частоту после первого каскада фильтрации и уменьшить нагрузку на последующие каскады.
- Тестируйте на реальных данных. Синусоидальный тест покажет частотную характеристику, но только реальные данные с шумом и артефактами выявят проблемы с переполнением или потерей отсчётов.
Итог
Рафт-ранговый подход к FIR-фильтрации на STM32 — это не конкретная функция или библиотека, а архитектурный принцип: вынести рутинные операции (запуск ADC, перенос данных, хранение отсчётов) в аппаратуру, а процессор задействовать только для собственно вычислений свёртки. Это даёт стабильную работу в реальном времени даже для фильтров высокого порядка и многоканальных систем.
Начните с простой блочной обработки через DMA и CMSIS DSP. Если производительности не хватает — переходите к полноценной рафт-ранговой архитектуре с чётким разделением приоритетов. И всегда проверяйте систему на реальных данных, а не только на синтетических тестах.
