В этом примере мы разберём базовый принцип цифровой фильтрации на микроконтроллере STM32F411.
Задача простая: у нас есть полезный сигнал, к нему добавлены две высокочастотные помехи, после чего мы пропускаем полученный сигнал через цифровой фильтр нижних частот и получаем очищенный сигнал.Сам сигнал мы сформируем внутри микроконтроллера: STM32 сам создаёт рабочий сигнал, добавляет к нему помехи, фильтрует результат и отправляет данные по UART для отображения на графике.
В нашем примере используются такие параметры:
Полезный сигнал: 200 Гц
Первая помеха: 600 Гц
Вторая помеха: 900 Гц
Частота дискретизации: 4000 Гц
Фильтр: ФНЧ 4-го порядка
Частота среза фильтра: 300 Гц
То есть полезный сигнал находится на частоте 200 Гц, а помехи находятся выше — на 600 Гц и 900 Гц.
Задача фильтра — оставить сигнал 200 Гц и подавить помехи 600/900 Гц.
Сначала формируется нормальный полезный сигнал:
clean = sinf(2.0f * PI_F * 200.0f * t);
Это обычная синусоида частотой 200 Гц.
Её можно представить так:
clean(t) = sin(2π · 200 · t)

Дальше к полезному сигналу добавляются две дополнительные синусоиды:
noise = 0.35f * sinf(2.0f * PI_F * 600.0f * t)
+ 0.25f * sinf(2.0f * PI_F * 900.0f * t);
Здесь:
600 Гц — первая помеха
900 Гц — вторая помеха
Амплитуды помех специально сделаны меньше амплитуды полезного сигнала:
0.35 — амплитуда помехи 600 Гц
0.25 — амплитуда помехи 900 Гц
После этого формируется общий входной сигнал:
input = clean + noise;
То есть на вход фильтра подаётся уже не чистая синусоида, а искажённый сигнал:
input(t) = полезный сигнал 200 Гц + помеха 600 Гц + помеха 900 Гц
Такой сигнал уже нельзя нормально использовать как рабочий. На графике он выглядит как синусоида, на которую наложена быстрая высокочастотная рябь.

Цифровой фильтр работает не с непрерывным сигналом, а с отдельными точками — отсчётами.
В нашем примере частота дискретизации:
Fs = 4000 Гц
Это значит, что микроконтроллер обрабатывает новый отсчёт каждые:
1 / 4000 = 0.00025 с = 250 мкс
Для этого используется таймер TIM2, который вызывает прерывание с частотой 4000 Гц. В каждом прерывании формируется новый отсчёт сигнала и сразу пропускается через фильтр.
Нам нужно оставить полезный сигнал 200 Гц и убрать помехи 600 Гц и 900 Гц.
Поэтому частоту среза фильтра выбираем между полезным сигналом и помехой:
Полезный сигнал: 200 Гц
Частота среза: 300 Гц
Помеха: 600 Гц и 900 Гц
Если поставить частоту среза слишком низко, например 200 Гц, фильтр начнёт сильно ослаблять полезный сигнал.
Если поставить частоту среза слишком высоко, например 500 Гц, помеха 600 Гц будет подавляться плохо.
Поэтому частота среза 300 Гц хорошо подходит для нашего примера.
В программе используется цифровой фильтр нижних частот Butterworth 4-го порядка.
Фильтр нижних частот пропускает низкие частоты и подавляет высокие.
В нашем случае:
200 Гц — должен пройти
600 Гц — должен подавиться
900 Гц — должен подавиться ещё сильнее
Фильтр Butterworth удобен тем, что у него ровная амплитудная характеристика в полосе пропускания. Это значит, что полезный сигнал в области до частоты среза не получает сильной ряби по амплитуде.
Чем выше порядок фильтра, тем резче он подавляет частоты выше частоты среза.
Фильтр 1-го порядка был бы слишком слабым.
Фильтр 2-го порядка уже работает лучше, но помеха 600 Гц подавлялась бы не так сильно.
Фильтр 4-го порядка хорошо подавляет обе помехи, при этом его можно реализовать из двух секций второго порядка.
Фильтр 4-го порядка реализован как две секции второго порядка. Такие секции часто называют biquad.
Одна секция работает по формуле:
y[n] = b0·x[n] + b1·x[n-1] + b2·x[n-2] - a1·y[n-1] - a2·y[n-2]
Где:
x[n] — текущий входной отсчёт
x[n-1] — предыдущий входной отсчёт
x[n-2] — входной отсчёт два шага назад
y[n] — текущий выходной отсчёт
y[n-1] — предыдущий выходной отсчёт
y[n-2] — выходной отсчёт два шага назад
b0, b1, b2 — коэффициенты прямой части фильтра
a1, a2 — коэффициенты обратной связи
Главная идея фильтра в том, что он использует не только текущий входной сигнал, но и предыдущие значения входа и выхода.
То есть фильтр имеет память.
Для каждого нового отсчёта выполняются такие действия:
1. Получаем новый входной отсчёт x[n].
2. Считаем новый выход y[n] по формуле фильтра.
3. Старый x[n-1] переносим в x[n-2].
4. Текущий x[n] сохраняем как x[n-1].
5. Старый y[n-1] переносим в y[n-2].
6. Текущий y[n] сохраняем как y[n-1].
7. Возвращаем y[n] как результат фильтрации.
В программе это реализовано функцией:
static float Biquad_Process(BiquadFilter *f, float x)
{
float y;
y = f->b0 * x
+ f->b1 * f->x1
+ f->b2 * f->x2
- f->a1 * f->y1
- f->a2 * f->y2;
f->x2 = f->x1;
f->x1 = x;
f->y2 = f->y1;
f->y1 = y;
return y;
}
Одна biquad-секция — это фильтр второго порядка.
Чтобы получить фильтр 4-го порядка, используются две секции подряд:
input → biquad section 1 → biquad section 2 → filtered
В программе это выглядит так:
static float LowPassFilter_Process(float x)
{
float y;
y = Biquad_Process(&lp_section1, x);
y = Biquad_Process(&lp_section2, y);
return y;
}
Сначала сигнал проходит первую секцию, затем результат первой секции подаётся на вход второй секции.
В программе используются уже рассчитанные коэффициенты для условий:
Fs = 4000 Гц
Fc = 300 Гц
Тип фильтра = Butterworth Low-pass
Порядок = 4
Первая секция фильтра:
.b0 = 0.001782610f
.b1 = 0.003565220f
.b2 = 0.001782610f
.a1 = -1.255440470f
.a2 = 0.409013780f
Вторая секция фильтра:
.b0 = 1.000000000f
.b1 = 2.000000000f
.b2 = 1.000000000f
.a1 = -1.518241840f
.a2 = 0.703962660f
Эти коэффициенты подходят именно для выбранной частоты дискретизации и частоты среза. Если изменить частоту дискретизации, частоту среза или порядок фильтра, коэффициенты нужно пересчитывать.
Для расчёта коэффициентов можно использовать Python и библиотеку SciPy. Ниже приведён пример программы, которая рассчитывает коэффициенты фильтра Butterworth и выводит их в формате, удобном для вставки в C-код:
import numpy as np
from scipy import signal
# Частота дискретизации
Fs = 4000.0
# Частота среза фильтра
Fc = 300.0
# Порядок фильтра
order = 4
# Расчёт Butterworth low-pass фильтра
sos = signal.butter(
order,
Fc,
btype='low',
fs=Fs,
output='sos'
)
print("SOS coefficients:")
print(sos)
print("\nCoefficients for C code:")
for i, section in enumerate(sos):
b0, b1, b2, a0, a1, a2 = section
# Обычно a0 = 1, но лучше всё равно выполнить нормирование
b0 = b0 / a0
b1 = b1 / a0
b2 = b2 / a0
a1 = a1 / a0
a2 = a2 / a0
print(f"\nSection {i + 1}:")
print(f".b0 = {b0:.9f}f,")
print(f".b1 = {b1:.9f}f,")
print(f".b2 = {b2:.9f}f,")
print(f".a1 = {a1:.9f}f,")
print(f".a2 = {a2:.9f}f,")
После запуска программа выведет коэффициенты для двух biquad-секций. Именно эти значения затем переносятся в структуру фильтра в программе для STM32F411.
Общая схема работы такая:
TIM2 вызывает прерывание 4000 раз в секунду
↓
создаётся чистый сигнал 200 Гц
↓
добавляются помехи 600 Гц и 900 Гц
↓
получается шумный сигнал
↓
сигнал проходит через ФНЧ 4-го порядка
↓
чистый, шумный и отфильтрованный сигналы выводятся по UART
UART выводит три значения:
clean_x1000,input_x1000,filtered_x1000
Пример строки:
309,782,1
Это значит:
clean = 0.309
input = 0.782
filtered = 0.001
Значения умножены на 1000 специально, чтобы не использовать печать float через printf. Это упрощает работу в Keil и снижает вероятность проблем с библиотеками.
Для частоты дискретизации 4000 Гц используется TIM2.
При текущей настройке STM32F411:
SYSCLK = 84 МГц
APB1 = 42 МГц
TIM2 clock = 84 МГц
Настройка таймера:
Prescaler = 83
Period = 249
Расчёт:
84 000 000 / (83 + 1) = 1 000 000 Гц
1 000 000 / (249 + 1) = 4000 Гц
То есть таймер вызывает прерывание каждые 250 мкс.
В программе используется UART2 со скоростью:
921600 baud
В терминале нужно поставить:
Baudrate: 921600
Data: 8 bit
Parity: None
Stop: 1
В программе стоит:
#define UART_DECIMATION 4
Это значит, что фильтр работает на частоте 4000 Гц, но в UART отправляется только каждый четвёртый отсчёт:
4000 / 4 = 1000 строк в секунду
Это сделано для стабильной работы UART и терминала.
Если всё работает стабильно, значение UART_DECIMATION можно поставить равным 1. В этом случае в UART будет отправляться каждый отсчёт.
На выходе получится такой график:

Выходной сигнал немного смещён относительно входного. Это нормальное поведение IIR-фильтра: при фильтрации возникает фазовый сдвиг.
Программный код :
/* USER CODE BEGIN Includes */
#include <math.h>
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
/* USER CODE BEGIN PTD */
typedef struct{
float b0;
float b1;
float b2;
float a1;
float a2;
float x1;
float x2;
float y1;
float y2;
} BiquadFilter;
/* USER CODE END PTD */
/* USER CODE BEGIN PD */
#define FS_HZ 4000.0f
#define PI_F 3.14159265358979323846f
/*
Safe mode:
filter sample rate = 4000 Hz
UART output rate = 1000 lines/sec
*/
#define UART_DECIMATION 1
/* USER CODE END PD */
/* USER CODE BEGIN PV */
static BiquadFilter lp_section1 =
{
.b0 = 0.001782610f,
.b1 = 0.003565220f,
.b2 = 0.001782610f,
.a1 = -1.255440470f,
.a2 = 0.409013780f,
.x1 = 0.0f,
.x2 = 0.0f,
.y1 = 0.0f,
.y2 = 0.0f
};
static BiquadFilter lp_section2 =
{
.b0 = 1.000000000f,
.b1 = 2.000000000f,
.b2 = 1.000000000f,
.a1 = -1.518241840f,
.a2 = 0.703962660f,
.x1 = 0.0f,
.x2 = 0.0f,
.y1 = 0.0f,
.y2 = 0.0f
};
volatile int32_t uart_clean_x1000 = 0;
volatile int32_t uart_input_x1000 = 0;
volatile int32_t uart_filtered_x1000 = 0;
volatile uint8_t uart_data_ready = 0;
static uint32_t signal_phase_index = 0;
static uint32_t sample_counter = 0;
/* USER CODE END PV */
/* USER CODE BEGIN PFP */
static void UART_SendString(const char *s);
static int32_t Float_To_X1000(float value);
static void LowPassFilter_Reset(void);
static float Biquad_Process(BiquadFilter *f, float x);
static float LowPassFilter_Process(float x);
static void DSP_ProcessOneSample(void);
/* USER CODE END PFP */
/* USER CODE BEGIN 0 */
static void UART_SendString(const char *s){
HAL_UART_Transmit(&huart2, (uint8_t *)s, strlen(s), HAL_MAX_DELAY);
}
static int32_t Float_To_X1000(float value){
if (value >= 0.0f) {
return (int32_t)(value * 1000.0f + 0.5f);
}
else {
return (int32_t)(value * 1000.0f — 0.5f);
}
}
static void LowPassFilter_Reset(void){
lp_section1.x1 = 0.0f;
lp_section1.x2 = 0.0f;
lp_section1.y1 = 0.0f;
lp_section1.y2 = 0.0f;
lp_section2.x1 = 0.0f;
lp_section2.x2 = 0.0f;
lp_section2.y1 = 0.0f;
lp_section2.y2 = 0.0f;
signal_phase_index = 0u;
sample_counter = 0u;
uart_data_ready = 0u;
}
static float Biquad_Process(BiquadFilter *f, float x){
float y;
y = f->b0 * x
+ f->b1 * f->x1
+ f->b2 * f->x2
— f->a1 * f->y1
— f->a2 * f->y2;
f->x2 = f->x1;
f->x1 = x;
f->y2 = f->y1;
f->y1 = y;
return y;
}
static float LowPassFilter_Process(float x){
float y;
y = Biquad_Process(&lp_section1, x);
y = Biquad_Process(&lp_section2, y);
return y;
}
static void DSP_ProcessOneSample(void){
float t;
float clean;
float noise;
float input;
float filtered;
t = (float)signal_phase_index / FS_HZ;
clean = sinf(2.0f * PI_F * 200.0f * t);
noise = 0.35f * sinf(2.0f * PI_F * 600.0f * t)
+ 0.25f * sinf(2.0f * PI_F * 900.0f * t);
input = clean + noise;
filtered = LowPassFilter_Process(input);
signal_phase_index++;
if (signal_phase_index >= 4000u)
{
signal_phase_index = 0u;
}
sample_counter++;
if ((sample_counter % UART_DECIMATION) == 0u) {
uart_clean_x1000 = Float_To_X1000(clean);
uart_input_x1000 = Float_To_X1000(input);
uart_filtered_x1000 = Float_To_X1000(filtered);
uart_data_ready = 1u;
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if (htim->Instance == TIM2) {
DSP_ProcessOneSample();
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
HAL_Delay(500);
LowPassFilter_Reset();
UART_SendString(«\r\n»);
UART_SendString(«STM32F411 DSP filter\r\n»);
UART_SendString(«Fs=4000Hz, clean=200Hz, noise=600Hz+900Hz, LPF Fc=300Hz\r\n»);
UART_SendString(«clean_x1000,input_x1000,filtered_x1000\r\n»);
if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK){
Error_Handler();
}
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
char line[80];
int32_t clean_i = 0;
int32_t input_i = 0;
int32_t filtered_i = 0;
uint8_t ready = 0;
__disable_irq();
ready = uart_data_ready;
if (ready != 0u)
{
clean_i = uart_clean_x1000;
input_i = uart_input_x1000;
filtered_i = uart_filtered_x1000;
uart_data_ready = 0u;
}
__enable_irq();
if (ready != 0u)
{
snprintf(line, sizeof(line), «%ld,%ld,%ld\r\n»,
(long)clean_i,
(long)input_i,
(long)filtered_i);
UART_SendString(line);
}
/* USER CODE END WHILE */