Если вы когда-нибудь пытались снять шум с аналогового сигнала на STM32, вы наверняка сталкивались с дилеммой: простой сглаживающий фильтр убирает помехи, но вносит задержку, а быстрый фильтр реагирует мгновенно, но пропускает шум. Особенно это больно, когда речь идет о работе с быстро меняющимися параметрами или когда нужно точно определить пороговые значения. Здесь на помощь приходят так называемые рафт-ранговые (или ранговые) фильтры. В этой статье я расскажу, как они устроены, почему они так эффективны для импульсных помех и, самое главное, как их реализовать в микроконтроллере STM32 без лишних вычислительных затрат.
Я не буду грузить вас математическими выкладками из учебников цифровой обработки сигналов. Мы разберем практическую сторону: как настроить такой фильтр, какие ошибки совершают новички и почему иногда лучше использовать стандартные библиотеки STM32, а иногда — писать свой код вручную.
Что такое рафт-ранговый фильтр и зачем он вам?
Давайте сразу проясним терминологию. В классической теории обработки сигналов «рафт» (raft) — это часто метафора для плавного спуска, но в контексте цифровой обработки сигналов (ЦОС) мы говорим о ранговых фильтрах (rank-order filters). Часто их путают с медианными, но это целый класс фильтров, включающий минимальные, максимальные и медианные значения. В инженерной среде их иногда называют «фильтрами типа рафт» из-за их способности «сплавлять» шумовые выбросы, сохраняя форму полезного сигнала.
Обычный скользящий усредняющий фильтр (Moving Average) берет N последних значений, складывает их и делит на N. Это работает отлично против белого шума (случайных мелких помех), но если в данных есть резкий выброс (например, искра, наводка от реле или скачок напряжения), усредняющий фильтр размажет этот выброс по всем точкам, искажая реальный сигнал.
Ранговый фильтр работает иначе. Он сортирует N отсчетов и выбирает значение, стоящее на определенном месте в этом ряду (ранге). Самый известный пример — медианный фильтр, где выбирается среднее значение (ранг N/2). Это значит, что если один из N отсчетов — это дикий выброс, он просто уйдет в конец списка при сортировке и не повлияет на результат.
В контексте STM32 мы говорим о реализации таких фильтров для ADC (аналогово-цифровых преобразователей). Задача стоит так: у нас есть поток данных с частотой дискретизации, например, 1 кГц или 10 кГц, и нам нужно отсечь широкочастотные помехи и одиночные провалы/пики, сохранив при этом скорость реакции системы.
Почему именно в STM32 это важно?
Микроконтроллеры STM32 — это не просто «мозги» устройства, это мощные процессоры, но они ограничены в ресурсах. У вас нет бесконечной вычислительной мощности, как на ПК. Реализация сортировок и буферов требует тактовых циклов.
Главная боль разработчика — баланс между точностью фильтрации и задержкой (latency). Если вы поставите буфер размером 100 отсчетов, фильтр будет идеальным, но ваша система управления (например, стабилизация дрона или регулировка температуры) будет реагировать с опозданием, что может привести к неустойчивости. Если буфер маленький (3-5 отсчетов), реакция будет быстрой, но фильтрация слабой.
Кроме того, ранговые фильтры требуют операций сравнения и перемещения данных. В STM32 серии F1 (Cortex-M3) у вас нет аппаратного ускорителя для математики (FPU), и сортировка 100 чисел может занять миллисекунды, что критично для прерываний. В сериях F4, F7 и H7, где есть FPU и DSP-инструкции, ситуация кардинально меняется. Нам нужно понимать эти отличия, чтобы писать эффективный код.
Алгоритм работы: как это работает «под капотом»
Принцип работы рафт-рангового фильтра достаточно прост, но его реализация требует аккуратности.
Представьте, что вы собираете данные в буфер (окно) размером N. На каждом шаге ADC (или по таймеру) в этот буфер записывается новое значение, а самое старое удаляется. После этого мы берем содержимое буфера и сортируем его по возрастанию.
Затем мы выбираем значение, находящееся на заданном индексе (ранге). Индекс может быть от 0 до N-1.
- Если выбрать индекс 0 — это фильтр минимума (отсечка пиков, оставление впадин).
- Если выбрать индекс N-1 — это фильтр максимума (отсечка впадин, оставление пиков).
- Если выбрать индекс N/2 — это классический медианный фильтр (баланс).
Самая сложная часть — не сортировка самого по себе, а ее скорость. Полная сортировка массива (например, пузырьком или быстрая сортировка) на каждом такте — это слишком дорого. При частоте 10 кГц и буфере 31 элемент полная сортировка может занять слишком много времени.
Поэтому на практике в STM32 мы используем оптимизированные методы:
- Insertion Sort (Сортировка вставками): когда новое значение добавляется, оно вставляется в уже отсортированный массив на нужное место. Это быстрее полной пересортировки, если массив уже отсортирован.
- Круговой буфер со статическим хранением: чтобы избежать динамического выделения памяти (malloc), буфер жестко зафиксирован в RAM.
- Выборочная сортировка: если нам нужен только медиан, нам не обязательно полностью сортировать весь массив. Достаточно найти N/2-й элемент.
Для STM32 критически важно, чтобы этот процесс занимал меньше времени, чем период прерывания ADC. Иначе вы получите пропуски данных или переполнение буфера.
Реализация: от простого к сложному
Давайте разберем, как это реализуется на практике. Я покажу два подхода: простой (для понимания и малых буферов) и оптимизированный (для реальных проектов).
Базовая реализация (Insertion Sort)
Этот метод отлично подходит для STM32 F103, F303 и для небольших буферов (до 15-21 элемента). Мы храним отсортированный массив. При поступлении нового отсчета:
- Удаляем самое старое значение (оно было на первом или последнем месте, в зависимости от логики).
- Вставляем новое значение в правильное место, сдвигая остальные элементы.
- Берем элемент по индексу.
Код выглядит примерно так (упрощенная логика):
// Пример на C для STM32
#define BUFFER_SIZE 15
int16_t filter_buffer[BUFFER_SIZE];
int16_t output;
void process_new_sample(int16_t new_sample) {
int i, j;
// 1. Удаляем самый старый элемент.
// В упрощенной версии мы сдвигаем весь массив влево
// (это не самый быстрый метод, но самый понятный)
for (i = 0; i < BUFFER_SIZE - 1; i++) {
filter_buffer[i] = filter_buffer[i+1];
}
// 2. Вставляем новый элемент на место последнего
filter_buffer[BUFFER_SIZE - 1] = new_sample;
// 3. Сортируем массив (вставка)
// Если мы обновляем массив последовательно,
// лучше поддерживать его отсортированным постоянно.
// Здесь для наглядности полная сортировка пузырьком (медленно, но просто)
for (i = 0; i < BUFFER_SIZE - 1; i++) {
for (j = 0; j < BUFFER_SIZE - i - 1; j++) {
if (filter_buffer[j] > filter_buffer[j+1]) {
int16_t temp = filter_buffer[j];
filter_buffer[j] = filter_buffer[j+1];
filter_buffer[j+1] = temp;
}
}
}
// 4. Выбираем медиану (центр массива)
output = filter_buffer[BUFFER_SIZE / 2];
}
Важно понимать: этот код на «голом железе» без оптимизации компилятора может работать медленно. Если у вас F103, компилятор должен быть настроен на `-O2` или `-O3`. На F4 это будет работать быстрее благодаря инструкции CLZ и аппаратному умножению, но все равно сортировка — это «дорогая» операция.
Оптимизированный подход (Кольцевой буфер + Сортировка)
Для реальных задач, где важна скорость, мы не сдвигаем массив. Мы используем указатели (head, tail). Но это усложняет сортировку, так как данные становятся разбросанными. Поэтому на практике часто используют компромисс: буфер фиксированного размера и быстрая сортировка только при заполнении или периодическая пересортировка.
Однако, для STM32 с DSP-библиотекой (CMSIS-DSP) есть готовые функции. Если вы используете Cortex-M4/M7, вам не нужно писать сортировку вручную. Библиотека CMSIS-DSP содержит функцию `arm_sort_q15` или `arm_sort_q31`, которая использует оптимизированные алгоритмы (например, битоническую сортировку или быструю сортировку).
Использование CMSIS-DSP — это лучший путь для профессиональной разработки. Это гарантирует, что код будет написан на ассемблере для конкретного ядра и работать максимально быстро.
Совет: Если у вас STM32F4 или выше, обязательно подключайте библиотеку CMSIS-DSP. Она содержит готовые реализации фильтров, включая ранговые, если вы сможете найти соответствующие модули, или как минимум очень быстрые функции сортировки, которые в 5-10 раз быстрее самодельных.
Таблица сравнения: Ранговый фильтр vs Скользящее среднее
Давайте посмотрим, почему кто-то может выбрать ранговый фильтр вместо привычного среднего. Это поможет вам принять решение при проектировании.
| Характеристика | Скользящее среднее (Moving Average) | Ранговый (Медианный) фильтр |
|---|---|---|
| Тип шумов | Эффективен против белого шума (случайных колебаний) | Эффективен против импульсных помех (выбросов, скачков) |
| Влияние на форму сигнала | Сглаживает резкие перепады, делает сигнал «мягким» | Сохраняет резкие перепады, убирает только выбросы |
| Вычислительная сложность | Низкая (просто сложение и вычитание) | Высокая (требует сортировок или сравнений) |
| Реакция на скачок | Медленная (скачок «размазывается») | Мгновенная (скачок игнорируется, если он в пределах N) |
| Применение в STM32 | Температура, напряжение питания, джойстики | Ток двигателя, датчики Холла, гистерезис |
Сценарии выбора: что и когда использовать?
Нет универсального решения. Выбор зависит от того, что именно вы измеряете и какая у вас ситуация.
Сценарий 1: Измерение температуры или статического напряжения.
Здесь шум обычно «белый» и равномерный. Резких скачков быть не должно (температура не меняется за микросекунду).
Решение: Используйте простое скользящее среднее или экспоненциальное сглаживание (IIR). Ранговый фильтр здесь избыточен и будет тратить процессорное время впустую. Если все же используете ранговый — берите малый буфер (3-5), чтобы не вносить лишнюю задержку.
Сценарий 2: Измерение тока на выходе инвертора или ШИМ.
Здесь часто возникают импульсные помехи от переключения транзисторов. Сигнал может иметь красивые синусоиды, но с острыми пиками-помехами.
Решение: Идеально подходит медианный фильтр. Он «отрубит» пиковые выбросы, не размывая основную синусоиду. Буфер можно брать побольше — 7, 9, 15, 31. Числа должны быть нечетными, чтобы медиана была единственным значением.
Сценарий 3: Определение пиков (Peak Detection).
Вам нужно найти максимальное значение напряжения в цикле (например, для RMS или пикового контроля).
Решение: Используйте ранговый фильтр с рангом N-1 (максимум). Это позволяет игнорировать шум внизу, но ловить самый высокий пик. Просто берите максимальное значение из окна.
Частые ошибки при настройке
На практике я видел много проектов, где разработчики убивали производительность микроконтроллера из-за неправильной настройки фильтров. Вот основные ошибки, которых стоит избегать.
Ошибка 1: Использование буферов четного размера
Если вы берете медиану из четного числа элементов (например, 10), вам придется усреднять два средних значения. Это усложняет код и замедляет работу.
Как надо: Всегда используйте нечетный размер буфера (3, 5, 7, 9, 11…). Это гарантирует, что медиана — это одно конкретное значение из списка, и вам не нужно ничего усреднять.
Ошибка 2: Слишком большой буфер
Размер буфера напрямую влияет на задержку. Если ваш фильтрующий массив из 51 элемента, а частота дискретизации 1 кГц, ваша задержка составит примерно половину длины окна (25 мс). Для управления двигателем это может быть критично.
Как надо: Начинайте с малого (3 или 5). Увеличивайте только если шум не убирается. Ограничивайте размер буфера так, чтобы он гарантированно укладывался в период прерывания.
Ошибка 3: Сортировка в прерывании (ISR)
Самая критичная ошибка. Если у вас частота опроса 10 кГц, у вас есть 100 мкс на обработку. Если вы запускаете сортировку массива из 31 элемента внутри прерывания ADC, вы рискуете пропустить следующий прерывание, переполнить буфер и получить хаос в данных.
Как надо: Либо используйте очень маленькие буферы, либо (что правильнее) записывайте данные в буфер в прерывании, а саму фильтрацию (сортировку) делайте в основном цикле (main loop) или в более низком приоритетном прерывании.
Ошибка 4: Игнорирование порядка прохождения данных
Если вы просто берете «последние N значений», вы должны быть уверены, что они записаны последовательно во времени. Если вы используете DMA для чтения ADC, убедитесь, что буфер не перезаписывается, пока вы его читаете.
Как надо: Используйте двойной буферинг (Double Buffering) с DMA, если работаете с высокими частотами.
Практические рекомендации и настройки
Чтобы настроить рафт-ранговый фильтр в вашей системе, следуйте этому алгоритму:
- Определите частоту выборки. Пусть это будет 1 кГц. Это даст вам 1 мс на цикл.
- Выберите размер окна N. Начните с 5. Это всегда нечетное число. Если помехи сильные, попробуйте 7 или 9. Не уходите выше 15, если у вас Cortex-M3/M4 без жестких требований.
- Выберите ранг. Для медианы это N/2 (целочисленное деление). Для фильтра минимума — 0, максимума — N-1.
- Реализуйте алгоритм. Используйте CMSIS-DSP, если есть возможность. Если нет — напишите функцию сортировки вставками (Insertion Sort), так как она работает быстрее всего на почти отсортированных массивах.
- Проверьте задержку. Поставьте маркер GPIO в начале и конце функции фильтрации. Измерьте осциллографом время работы. Оно должно быть значительно меньше периода выборки.
- Тестирование. Попробуйте «нагреть» сигнал искусственными выбросами. Если фильтр справляется, но сигнал становится слишком «запоздалым», уменьшите N.
Также важно помнить о типах данных. Если вы работаете с 12-битным ADC (значения 0-4095), используйте `uint16_t`. Если вы делаете сложные вычисления (например, вычитаете среднее), используйте `int32_t`, чтобы избежать переполнения при промежуточных вычислениях. В STM32 целочисленные операции делаются очень быстро, поэтому не бойтесь использовать 32-битные типы там, где это необходимо для безопасности расчетов.
Если вы используете CubeMX, там есть функция настройки DMA для ADC. Включите Circular Mode. Это позволит не переключать буферы вручную. Фильтр будет читать данные по указателю, который обновляется DMA. Это освободит процессор от рутины копирования.
Как выглядят результаты на практике?
Представьте, что у вас есть датчик тока. Сигнал — это плавная синусоида. Из-за коммутации реле вы получаете периодические выбросы на 20-30% выше нормы.
Если вы оставите сигнал как есть — ваша защита может сработать ложно.
Если вы поставите скользящее среднее с окном 10 — выбросы уйдут, но синусоида станет ниже по амплитуде и сдвинется по фазе. Это плохо для точных измерений.
Если вы поставите медианный фильтр (окно 5) — выбросы исчезнут, а синусоида сохранит свою форму и амплитуду. Именно в этом и заключается сила рангового фильтра. Он «интеллектуально» игнорирует то, что не похоже на основной тренд, не размывая сам тренд.
Итог
Работа с рафт-ранговыми (ранговыми) фильтрами в STM32 — это мощный инструмент, который часто недооценивают. Они не требуют сложных математических библиотек, но дают огромную выгоду в качестве сигнала при работе с импульсными помехами.
Ваш план действий:
- Если у вас малый буфер (до 9 элементов) — пишите код вручную, используя сортировку вставками. Это быстро и понятно.
- Если у вас большой буфер (15+ элементов) — ищите оптимизации или используйте CMSIS-DSP.
- Всегда используйте нечетные размеры буферов для медианы.
- Следите за временем выполнения функции фильтрации, чтобы не задушить процессор.
Правильно настроенный фильтр в STM32 позволяет получить чистые данные даже с дешевых датчиков, экономя деньги на «железе» за счет качества программного обеспечения. Попробуйте внедрить этот подход в свой следующий проект, и вы увидите разницу в устойчивости системы.
