Фильтрация данных датчиков на Arduino: скользящее среднее, медиана, EMA и фильтр Калмана
Фильтрация данных датчиков на Arduino: скользящее среднее, медианный фильтр, EMA, гистерезис, частота дискретизации, сравнение в плоттере и введение в фильтр Калмана.
Фильтрация данных датчиков на Arduino превращает дрожащие, скачущие значения из analogRead() или датчика расстояния в стабильное, надёжное число для твоего проекта. Значения 511, 513, 510, 514 в мониторе порта, когда ты не трогаешь потенциометр, HC-SR04, который иногда без причины показывает 3 см или 400 см, ночник, мерцающий на границе света и темноты, — всё это проявления одной проблемы. Чаще всего датчик исправен: в измерение попадают электрические помехи, ошибки квантования АЦП (ADC) или редкое неверное эхо. Хорошая новость: фильтр из нескольких строк решает большинство таких проблем.
В этом руководстве ты сначала узнаешь о видах шума и первых мерах на уровне схемы. Затем мы напишем код для пяти методов, начиная с самого простого: усреднение нескольких измерений, скользящее среднее, медианный фильтр для удаления выбросов, экспоненциальный фильтр (EMA), которому нужно совсем мало памяти, и гистерезис для устойчивого срабатывания по порогу. Сравним их графики в «Плоттере по последовательному соединению» (Serial Plotter) Arduino IDE, обсудим выбор частоты дискретизации и кратко познакомимся с фильтром Калмана. Последние разделы посвящены шумному АЦП ESP32 и коду на MicroPython для Raspberry Pi Pico.

Почему данные датчиков зашумлены?
Считать погрешность измерения чем-то единым — ошибка. Разным видам шума нужны разные фильтры, поэтому сначала нужно понять, с какой проблемой ты имеешь дело. Для этого достаточно некоторое время выводить исходные значения в монитор порта и смотреть на числа. Если значения постоянно колеблются вокруг одного числа, это случайный шум; если большинство показаний верны, но иногда приходит совершенно нелепое значение, это выбросы; если значение медленно уплывает, проблема не в шуме, а в дрейфе.
| Вид шума | Как выглядит | Типичная причина | Подходящее решение |
|---|---|---|---|
| Случайный шум | Значение колеблется на ±2–10 отсчётов | Пульсации питания, длинные провода, собственный шум АЦП | Усреднение, скользящее среднее, EMA |
| Выброс | Изредка появляется значение, сильно отличающееся от остальных | Пропущенное эхо HC-SR04, плохой контакт, помехи от мотора | Медианный фильтр |
| Ступенчатая ошибка (квантование) | Значение переключается между двумя соседними числами | Разрешение АЦП (5 V / 1024 = 4,9 mV на Uno) | Усреднение, гистерезис |
| Периодический шум | Регулярные пульсации | Мерцание ламп с частотой 100 Hz, электросеть 50 Hz | Усреднение за интервалы, кратные 20 мс |
| Дрейф | Значение медленно меняется в течение нескольких минут | Температура, падение напряжения батареи | Калибровка и стабильное опорное напряжение, а не фильтр |
Сначала схема: уменьши шум в источнике
Программный фильтр не восстановит информацию, которую схема не смогла получить, поэтому первая задача — затруднить проникновение помех. Делай провода аналоговых датчиков короткими и прокладывай их подальше от проводов компонентов с большим потреблением тока, например моторов и реле. Керамический конденсатор 100 nF между аналоговым входом и GND, расположенный рядом с датчиком, поглощает значительную часть быстрых помех. Частота среза RC-фильтра нижних частот из резистора и конденсатора определяется формулой f = 1 / (2πRC): 10 kΩ и 1 µF дают около 16 Hz, поэтому более быстрые изменения подавляются. Номиналы можно рассчитать с помощью утилиты постоянной времени RC. В проектах с моторами также полезно вести GND датчиков к плате отдельно от GND драйвера мотора и питать плату от стабильного источника. Как работает АЦП и как на него влияет опорное напряжение, объясняется в статье «Что такое АЦП?».
Схема для экспериментов
Мы проверим код из статьи на трёх простых датчиках. Потенциометр отлично подходит для наблюдения случайного шума, фоторезистор LDR — периодического шума и решений по порогу, а HC-SR04 — выбросов. Подключи LDR между 5 V и аналоговым пином, а резистор 10 kΩ — между аналоговым пином и GND, чтобы показания росли с увеличением освещённости. Подробности — в статьях про датчик освещённости LDR и потенциометр.
| Подключение | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| Движок потенциометра | A0 | GPIO 34 | GP26 (ADC0) |
| Средняя точка делителя LDR + 10 kΩ | A1 | GPIO 35 | GP27 (ADC1) |
| HC-SR04 TRIG | D9 | GPIO 5 | GP2 |
| HC-SR04 ECHO | D10 | GPIO 18 (через делитель) | GP3 (через делитель) |
| Светодиод или реле (пример с гистерезисом) | D8 | - | - |
| Питание | Потенциометр и LDR — 5V, HC-SR04 — 5V | Потенциометр и LDR — 3V3, HC-SR04 — 5V | Потенциометр и LDR — 3V3, HC-SR04 — VBUS |
1. Усреднение нескольких измерений
Самый простой фильтр — выполнить несколько измерений подряд и взять их среднее. Случайный шум отклоняет каждое показание в свою сторону, поэтому при суммировании отклонения компенсируются. В статистике есть правило: при усреднении N независимых измерений шум уменьшается примерно в √N раз. 4 измерения уменьшают шум вдвое, 16 — вчетверо; 64 — в восемь раз, но требуют в четыре раза больше времени. На Arduino Uno один вызов analogRead() занимает около 112 µs, поэтому усреднение 16 измерений завершается менее чем за 2 мс. У метода всего два недостатка: при каждом получении результата он ненадолго занимает процессор и плохо справляется с выбросами. Если одно из 16 показаний нелепо, оно портит и среднее.
1const int POT_PIN = A0;
2
3// Art arda "adet" ölçüm al, ortalamasını döndür / take "adet" readings in a row, return the average
4int ortalamaOku(int pin, int adet) {
5 long toplam = 0; // taşmasın diye long / long to avoid overflow
6 for (int i = 0; i < adet; i++) {
7 toplam += analogRead(pin);
8 delayMicroseconds(100); // ölçümler arasında kısa ara / short gap between readings
9 }
10 return toplam / adet;
11}
12
13void setup() {
14 Serial.begin(115200);
15}
16
17void loop() {
18 int ham = analogRead(POT_PIN);
19 int ort4 = ortalamaOku(POT_PIN, 4);
20 int ort16 = ortalamaOku(POT_PIN, 16);
21 // Serial Plotter için etiket:değer biçimi / label:value format for the Serial Plotter
22 Serial.print("Ham:");
23 Serial.print(ham);
24 Serial.print(",Ort4:");
25 Serial.print(ort4);
26 Serial.print(",Ort16:");
27 Serial.println(ort16);
28 delay(20);
29}2. Скользящее среднее
Скользящее среднее усредняет последние N показаний: при каждом новом измерении удаляет самое старое и добавляет новое. Так ты делаешь одно измерение за проход цикла, но получаешь пользу от усреднения N измерений. Вместо повторного суммирования N чисел код хранит переменную sum: вычитает уходящее значение и прибавляет входящее. Поэтому скорость расчёта не зависит от размера окна. Цена скользящего среднего — задержка: выход отстаёт от реального значения примерно на (N−1)/2 измерений. Окно из 10 показаний с интервалом 10 мс даёт задержку около 45 мс; для потенциометра или температуры это несущественно, а в быстром роботе уже заметно.
1const int POT_PIN = A0;
2const int PENCERE = 10; // son 10 ölçüm / last 10 readings
3
4int tampon[PENCERE]; // dairesel tampon / circular buffer
5int sira = 0;
6long toplam = 0;
7bool dolu = false;
8
9int hareketliOrtalama(int yeni) {
10 toplam -= tampon[sira]; // en eski değeri çıkar / remove the oldest value
11 tampon[sira] = yeni;
12 toplam += yeni; // yeni değeri ekle / add the new value
13 sira = (sira + 1) % PENCERE;
14 if (sira == 0) dolu = true;
15 return dolu ? toplam / PENCERE : toplam / sira; // ilk turda bölen küçük / smaller divisor at start
16}
17
18unsigned long sonOlcum = 0;
19
20void setup() {
21 Serial.begin(115200);
22}
23
24void loop() {
25 if (millis() - sonOlcum >= 10) { // 100 Hz örnekleme / 100 Hz sampling
26 sonOlcum = millis();
27 int ham = analogRead(POT_PIN);
28 int filtreli = hareketliOrtalama(ham);
29 Serial.print("Ham:");
30 Serial.print(ham);
31 Serial.print(",Hareketli:");
32 Serial.println(filtreli);
33 }
34}| Окно (N) | Уменьшение шума | Задержка (измерение каждые 10 мс) | Для чего подходит |
|---|---|---|---|
| 4 | Примерно в 2 раза | 15 мс | Управление, требующее быстрого отклика |
| 10 | Примерно в 3 раза | 45 мс | Потенциометр, джойстик, LDR |
| 32 | Примерно в 5,7 раза | 155 мс | Температура, влажность, напряжение батареи |
| 100 | В 10 раз | 495 мс | Очень медленно меняющиеся величины, стабильные показания на дисплее |
3. Медианный фильтр: удаление выбросов
Усреднение плохо справляется с выбросами. Если HC-SR04 четыре раза измерит 50 см, а затем один раз пропустит эхо и выдаст 0, среднее пяти измерений составит 40 см. Это значение и неверно, и не указывает на ошибку. Медианный фильтр вместо этого сортирует последние N показаний по возрастанию и берёт среднее по позиции. В том же примере отсортированный список — 0, 50, 50, 50, 50, а значение посередине — 50; ошибочное показание вообще не попадает в результат. Медианный фильтр из 5 элементов полностью устраняет до 2 ошибочных показаний подряд. Ещё одно полезное свойство медианы: она не размывает резкое реальное изменение, как усреднение. Когда препятствие действительно приближается, за несколько измерений значение переходит на новый уровень чёткой ступенькой, а не плавной кривой. Код ниже считывает HC-SR04 каждые 60 мс и выводит медиану последних 5 измерений. Подробности — в статье про датчик расстояния HC-SR04.
1const int TRIG_PIN = 9;
2const int ECHO_PIN = 10;
3const int N = 5; // medyan penceresi, tek sayı olmalı / median window, must be odd
4
5int olcumler[N];
6int sira = 0;
7
8int mesafeOlc() {
9 digitalWrite(TRIG_PIN, LOW);
10 delayMicroseconds(2);
11 digitalWrite(TRIG_PIN, HIGH);
12 delayMicroseconds(10);
13 digitalWrite(TRIG_PIN, LOW);
14 unsigned long sure = pulseIn(ECHO_PIN, HIGH, 25000); // ~4 m zaman aşımı / ~4 m timeout
15 return sure / 58; // yankı yoksa 0 / 0 if no echo
16}
17
18int medyan() {
19 int kopya[N];
20 for (int i = 0; i < N; i++) kopya[i] = olcumler[i];
21 for (int i = 1; i < N; i++) { // eklemeli sıralama / insertion sort
22 int deger = kopya[i];
23 int j = i - 1;
24 while (j >= 0 && kopya[j] > deger) {
25 kopya[j + 1] = kopya[j];
26 j--;
27 }
28 kopya[j + 1] = deger;
29 }
30 return kopya[N / 2]; // ortadaki eleman / middle element
31}
32
33void setup() {
34 Serial.begin(115200);
35 pinMode(TRIG_PIN, OUTPUT);
36 pinMode(ECHO_PIN, INPUT);
37 for (int i = 0; i < N; i++) { // tamponu ilk ölçümlerle doldur / fill the buffer first
38 olcumler[i] = mesafeOlc();
39 delay(60);
40 }
41}
42
43void loop() {
44 int ham = mesafeOlc();
45 olcumler[sira] = ham;
46 sira = (sira + 1) % N;
47 Serial.print("Ham:");
48 Serial.print(ham);
49 Serial.print(",Medyan:");
50 Serial.println(medyan());
51 delay(60); // HC-SR04 için en az 60 ms / at least 60 ms for HC-SR04
52}4. Экспоненциальный фильтр (EMA): сглаживание с малым расходом памяти
Экспоненциальное скользящее среднее (EMA) обходится без буфера: оно помнит только предыдущий результат. С каждым новым измерением результат сдвигается к показанию на небольшую долю разницы между ними: y = y + α × (x − y). Здесь x — новое показание, y — отфильтрованное значение, а α (альфа) — коэффициент от 0 до 1. Чем меньше α, тем стабильнее, но медленнее фильтр. Он называется экспоненциальным, потому что влияние каждого измерения со временем убывает по экспоненте. Это программный аналог RC-фильтра нижних частот в электронике. Постоянная времени EMA примерно равна τ ≈ T / α (T — интервал измерений): при измерении каждые 10 мс и α = 0,1 фильтр достигает 63 процентов резкого изменения примерно за 100 мс, а 95 процентов — примерно за 300 мс. В коде есть версия с плавающей точкой и целочисленная версия, которая быстрее работает на небольших платах вроде Uno; целочисленная версия использует α = 1/16 и сдвиг (>>) вместо деления.
1const int LDR_PIN = A1;
2const float ALFA = 0.1; // 0-1 arası; küçük = daha sakin / 0-1; smaller = smoother
3
4float ema = -1; // henüz başlamadı / not started yet
5long emaX16 = -1; // tamsayılı EMA x16 / integer EMA times 16
6
7unsigned long sonOlcum = 0;
8
9void setup() {
10 Serial.begin(115200);
11}
12
13void loop() {
14 if (millis() - sonOlcum >= 10) {
15 sonOlcum = millis();
16 int ham = analogRead(LDR_PIN);
17
18 if (ema < 0) ema = ham; // ilk değerle başlat / start from the first reading
19 ema += ALFA * (ham - ema); // y = y + a(x - y)
20
21 if (emaX16 < 0) emaX16 = (long)ham << 4;
22 emaX16 += ham - (emaX16 >> 4); // alfa = 1/16, float yok / alpha = 1/16, no float
23
24 Serial.print("Ham:");
25 Serial.print(ham);
26 Serial.print(",EMA:");
27 Serial.print(ema, 1);
28 Serial.print(",EMA_int:");
29 Serial.println(emaX16 >> 4);
30 }
31}| α (альфа) | Примерное эквивалентное окно | Время отклика до 63% (измерение каждые 10 мс) | Характер работы |
|---|---|---|---|
| 0,5 | 3 | 10–20 мс | Очень быстрый, слабое сглаживание |
| 0,2 | 9 | 50 мс | Быстрый, лёгкое сглаживание |
| 0,1 | 19 | 100 мс | Сбалансированный, хорошая отправная точка для большинства датчиков |
| 0,05 | 39 | 200 мс | Стабильный, медленный |
| 0,01 | 199 | 1 с | Очень стабильный, только для медленно меняющихся величин |
«Эквивалентное окно» в таблице — примерный размер окна скользящего среднего с таким же сглаживанием; оно рассчитывается как N ≈ 2/α − 1. Главное преимущество EMA — расход памяти: скользящему среднему из 100 элементов нужно 200 байт ОЗУ, а EMA — одна переменная. Если нужно фильтровать несколько датчиков в 2 КБ ОЗУ Arduino Uno, разница существенна. Недостаток в том, что EMA так же плохо справляется с выбросами, как и усреднение: большой выброс смещает результат на несколько измерений.
Сравнение фильтров в плоттере
По числам разницу между фильтрами увидеть трудно, а на графике она очевидна с первого взгляда. Код ниже считывает потенциометр с частотой 100 Hz, обрабатывает одно показание четырьмя разными способами и выводит все результаты в формате label:value. Чтобы разница была заметнее, код добавляет искусственный выброс примерно к 3 процентам показаний; в реальном проекте убери эту строку. На графике видно, что медиана полностью убирает выбросы, а скользящее среднее и EMA превращают их в небольшие бугорки.
- Загрузи код и закрой монитор порта, если он открыт: монитор порта и плоттер нельзя открыть одновременно.
- В Arduino IDE открой «Инструменты › Плоттер по последовательному соединению» (Tools > Serial Plotter) или нажми значок графика справа вверху.
- Установи скорость справа внизу на 115200 бод.
- Поворачивай потенциометр сначала медленно, затем быстро и наблюдай за откликом каждой линии.
- Включай и выключай линии флажками рядом с метками, чтобы напрямую сравнить два фильтра.
1const int POT_PIN = A0;
2
3// Hareketli ortalama / moving average
4const int PENCERE = 10;
5int maTampon[PENCERE];
6int maSira = 0;
7long maToplam = 0;
8
9int hareketliOrtalama(int yeni) {
10 maToplam += yeni - maTampon[maSira];
11 maTampon[maSira] = yeni;
12 maSira = (maSira + 1) % PENCERE;
13 return maToplam / PENCERE;
14}
15
16// 5'lik medyan / median of 5
17int mdTampon[5];
18int mdSira = 0;
19
20int medyan5(int yeni) {
21 mdTampon[mdSira] = yeni;
22 mdSira = (mdSira + 1) % 5;
23 int a[5];
24 for (int i = 0; i < 5; i++) a[i] = mdTampon[i];
25 for (int i = 1; i < 5; i++) { // eklemeli sıralama / insertion sort
26 int d = a[i], j = i - 1;
27 while (j >= 0 && a[j] > d) { a[j + 1] = a[j]; j--; }
28 a[j + 1] = d;
29 }
30 return a[2];
31}
32
33// Üstel filtre / exponential filter
34float ema = 0;
35const float ALFA = 0.1;
36
37unsigned long sonOlcum = 0;
38
39void setup() {
40 Serial.begin(115200);
41 int ilk = analogRead(POT_PIN);
42 for (int i = 0; i < PENCERE; i++) maTampon[i] = ilk; // tamponları doldur / prefill buffers
43 maToplam = (long)ilk * PENCERE;
44 for (int i = 0; i < 5; i++) mdTampon[i] = ilk;
45 ema = ilk;
46 randomSeed(analogRead(A5));
47}
48
49void loop() {
50 if (millis() - sonOlcum < 10) return; // 100 Hz
51 sonOlcum = millis();
52
53 int ham = analogRead(POT_PIN);
54 if (random(100) < 3) { // TEST: %3 yapay sıçrama, projede sil / TEST: 3% fake spikes, remove in a project
55 ham = constrain(ham + random(-400, 400), 0, 1023);
56 }
57 ema += ALFA * (ham - ema);
58
59 Serial.print("Ham:");
60 Serial.print(ham);
61 Serial.print(",Hareketli:");
62 Serial.print(hareketliOrtalama(ham));
63 Serial.print(",Medyan:");
64 Serial.print(medyan5(ham));
65 Serial.print(",EMA:");
66 Serial.println(ema, 1);
67}На графике цветные метки появляются справа вверху. Если между метками нет запятой, а между меткой и значением — двоеточия, плоттер не сможет правильно разделить линии. Старый плоттер Arduino IDE 1.8 поддерживает метки лишь частично; если возникают проблемы, отправляй только числа через запятую. При проблемах с последовательным портом и скоростью передачи загляни в статью про монитор порта Arduino.
5. Гистерезис: устраняем частые переключения на пороге
Фильтр стабилизирует значение, но сам по себе не решает проблему принятия решения. Ночник с правилом «включить лампу, когда освещённость упадёт ниже 350» в сумерках включается и выключается несколько раз в секунду, пока значение гуляет между 349 и 351. Это быстро изнашивает контакты реле. Решение — гистерезис: отдельные пороги включения и выключения. Например, лампа включается при падении значения ниже 300, но для выключения оно должно подняться выше 400. «Мёртвая зона» шириной 100 отсчётов между ними не даёт небольшим колебаниям менять решение. Код ниже сочетает гистерезис с EMA и коротким временем подтверждения: условие должно непрерывно выполняться в течение 1 секунды, поэтому проходящая тень или краткая вспышка фар тоже не меняют решение.
1const int LDR_PIN = A1;
2const int LAMBA_PIN = 8; // LED ya da röle modülü / LED or relay module
3const int YAK_ESIK = 300; // bunun altı karanlık / below this is dark
4const int SONDUR_ESIK = 400; // bunun üstü aydınlık / above this is bright
5const unsigned long ONAY_MS = 1000; // koşul 1 sn sürmeli / condition must last 1 s
6
7float ema = -1;
8bool lambaAcik = false;
9unsigned long kosulBasladi = 0;
10bool kosulVar = false;
11
12void setup() {
13 Serial.begin(115200);
14 pinMode(LAMBA_PIN, OUTPUT);
15}
16
17void loop() {
18 int ham = analogRead(LDR_PIN);
19 if (ema < 0) ema = ham;
20 ema += 0.05 * (ham - ema); // yavaş EMA / slow EMA
21
22 // Durum değiştirmek için hangi koşul gerekiyor? / which condition flips the state?
23 bool degistir = lambaAcik ? (ema > SONDUR_ESIK) : (ema < YAK_ESIK);
24
25 if (degistir) {
26 if (!kosulVar) {
27 kosulVar = true;
28 kosulBasladi = millis(); // koşul şimdi başladı / condition just started
29 } else if (millis() - kosulBasladi >= ONAY_MS) {
30 lambaAcik = !lambaAcik; // 1 sn sürdü, kararı değiştir / lasted 1 s, flip the decision
31 digitalWrite(LAMBA_PIN, lambaAcik ? HIGH : LOW);
32 kosulVar = false;
33 }
34 } else {
35 kosulVar = false; // koşul bozuldu, sayacı sıfırla / condition broke, reset the timer
36 }
37
38 Serial.print("EMA:");
39 Serial.print(ema, 0);
40 Serial.print(",Yak:");
41 Serial.print(YAK_ESIK);
42 Serial.print(",Sondur:");
43 Serial.print(SONDUR_ESIK);
44 Serial.print(",Lamba:");
45 Serial.println(lambaAcik ? 600 : 100); // grafikte görünsün / visible on the plot
46 delay(10);
47}Перед настройкой порогов понаблюдай в плоттере за показаниями в комнате в течение дня. Мёртвая зона должна быть как минимум в два-три раза шире колебаний отфильтрованного значения. Тот же метод используют в термостатах (например, выключить нагреватель при 22 °C и включить при 20 °C), насосах водяных баков и схемах защиты батарей. Антидребезг кнопки — тоже разновидность времени подтверждения; подробности есть в статье «Как подключить кнопку».
Как выбрать частоту дискретизации
Чтобы фильтр работал хорошо, датчик нужно опрашивать с подходящей частотой. Слишком редкий опрос пропускает быстрые изменения; при слишком частом датчик возвращает одно и то же значение снова и снова или вообще не успевает подготовиться. Основное правило — правило Найквиста: частота дискретизации должна быть как минимум вдвое выше частоты самых быстрых изменений, которые ты хочешь отслеживать; на практике надёжнее запас в 5–10 раз. У некоторых датчиков есть собственный минимальный интервал, и более частое чтение не даёт пользы. Если важно мерцание света, учти: в сети 50 Hz лампы мерцают 100 раз в секунду. Самый простой способ убрать это мерцание в показаниях LDR или другого аналогового датчика — равномерно распределить измерения по интервалу, кратному 20 мс, и усреднить их. Поскольку окно охватывает целое число периодов сети, мерцание компенсируется.
| Датчик | Рекомендуемый интервал измерений | Примечание |
|---|---|---|
| DHT11 / DHT22 | 1 с / 2 с | При более частом чтении возвращается то же значение или ошибка |
| DS18B20 (12 бит) | 750 мс | Столько занимает преобразование |
| HC-SR04 | Не менее 60 мс | Дождись затухания предыдущего эха |
| Потенциометр, джойстик | 10–20 мс | 50–100 Hz более чем достаточно для движений человека |
| LDR (для подавления мерцания ламп) | Каждую 1 мс в течение 20 мс, затем усреднить | Мерцание от сети 50 Hz компенсируется |
| MPU6050 | 5–10 мс | Обычный диапазон для измерения движения с частотой 100–200 Hz |
Краткое знакомство с фильтром Калмана
Фильтр Калмана учитывает, насколько зашумлено измерение и как быстро может меняться реальная величина, и подстраивает свой коэффициент усиления. В простейшем одномерном варианте у него две настройки: R — дисперсия шума измерения (насколько дрожат показания датчика); Q — шум процесса (насколько быстро, по твоему предположению, меняется реальная величина). На каждом шаге фильтр сначала увеличивает неопределённость на Q, затем рассчитывает усиление K = P / (P + R) и сдвигает оценку к измерению на долю K. Если измерение шумное (большое R), K мало и фильтр меньше доверяет показанию; если ожидаются быстрые изменения (большое Q), K растёт и фильтр реагирует быстро. Интересный результат: при постоянных Q и R коэффициент K со временем стабилизируется, и одномерный фильтр Калмана фактически становится EMA. Настоящая сила фильтра Калмана проявляется при совместной оценке нескольких величин, например при объединении данных акселерометра и гироскопа для определения угла наклона. Его более простой родственник, комплементарный фильтр, описан в статье про MPU6050.
1const int POT_PIN = A0;
2
3struct Kalman1D {
4 float q; // süreç gürültüsü / process noise
5 float r; // ölçüm gürültüsü (varyans) / measurement noise (variance)
6 float p; // tahminin belirsizliği / estimate uncertainty
7 float x; // tahmin / estimate
8 bool ilk;
9};
10
11Kalman1D kf = {0.05, 9.0, 1.0, 0.0, true}; // r = 9: yaklaşık ±3 basamak titreşim / about ±3 counts of jitter
12
13float kalmanGuncelle(Kalman1D &k, float olcum) {
14 if (k.ilk) { k.x = olcum; k.ilk = false; return k.x; }
15 k.p += k.q; // 1. tahmin: belirsizlik artar / predict: uncertainty grows
16 float kazanc = k.p / (k.p + k.r); // 2. Kalman kazancı / Kalman gain
17 k.x += kazanc * (olcum - k.x); // 3. ölçüme doğru düzelt / correct toward the measurement
18 k.p *= (1.0 - kazanc); // 4. belirsizlik azalır / uncertainty shrinks
19 return k.x;
20}
21
22void setup() {
23 Serial.begin(115200);
24}
25
26void loop() {
27 int ham = analogRead(POT_PIN);
28 float tahmin = kalmanGuncelle(kf, ham);
29 Serial.print("Ham:");
30 Serial.print(ham);
31 Serial.print(",Kalman:");
32 Serial.println(tahmin, 1);
33 delay(10);
34}Оценить R несложно: сделай 100 исходных измерений, не трогая потенциометр, и посмотри на разброс значений. Если они колеблются примерно на ±3 отсчёта относительно среднего, стандартное отклонение составляет около 3, а дисперсия — около 9. Q подбирай экспериментально: малое Q (0.001) даёт очень стабильный, но медленный выход, большое Q (1) — быстрый, но более шумный. Для начала выбери Q в пределах от 1 до 10 процентов R, затем уточни его в плоттере, поворачивая потенциометр.
Шум АЦП и фильтрация на ESP32
12-битный АЦП ESP32 различает более мелкие шаги, чем АЦП Arduino Uno, но сильнее шумит и нелинеен, особенно у краёв диапазона: 0–0,1 V и 3,1–3,3 V. При включённом Wi-Fi шум ещё сильнее. Упростить работу помогают два приёма: используй analogReadMilliVolts(), которая применяет заводскую калибровку микросхемы вместо возврата исходного значения, и усредняй 16–64 измерения перед применением EMA. Код ниже считывает потенциометр в эти два этапа; в проектах с Wi-Fi всегда подключай датчик к пинам ADC1: GPIO 32–39.
1const int POT_PIN = 34; // ADC1 pini, Wi-Fi ile çalışır / ADC1 pin, works with Wi-Fi
2const int ORNEK = 32; // her ölçümde 32 örnek / 32 samples per reading
3const float ALFA = 0.15;
4
5float ema = -1;
6
7int millivoltOku() {
8 uint32_t toplam = 0;
9 for (int i = 0; i < ORNEK; i++) {
10 toplam += analogReadMilliVolts(POT_PIN); // kalibre mV / calibrated mV
11 }
12 return toplam / ORNEK;
13}
14
15void setup() {
16 Serial.begin(115200);
17 analogReadResolution(12); // 0-4095
18}
19
20void loop() {
21 int ham = analogRead(POT_PIN); // karşılaştırma için tek ham ölçüm / single raw reading for comparison
22 int mv = millivoltOku();
23 if (ema < 0) ema = mv;
24 ema += ALFA * (mv - ema);
25 Serial.printf("Ham:%d,mV:%d,EMA_mV:%.1f\n", ham, mv, ema);
26 delay(20);
27}Фильтрация на Raspberry Pi Pico и MicroPython
На Raspberry Pi Pico ADC.read_u16() возвращает значение от 0 до 65535, но реальное разрешение АЦП — 12 бит, а младшие биты зашумлены. В MicroPython функция sorted() делает расчёт медианы очень простым, а для скользящего среднего достаточно списка и текущей суммы. Код ниже считывает потенциометр с частотой 100 Hz и выводит результаты трёх фильтров в формате, который Thonny может отобразить в окне «Вид › Плоттер» (View > Plotter).
1from machine import ADC
2import time
3
4pot = ADC(26) # GP26 = ADC0
5
6PENCERE = 10
7tampon = [pot.read_u16()] * PENCERE # hareketli ortalama tamponu / moving average buffer
8toplam = sum(tampon)
9sira = 0
10son5 = tampon[:5] # medyan için son 5 değer / last 5 values for the median
11ema = float(tampon[0])
12ALFA = 0.1
13
14while True:
15 ham = pot.read_u16()
16
17 toplam += ham - tampon[sira] # hareketli ortalama / moving average
18 tampon[sira] = ham
19 sira = (sira + 1) % PENCERE
20 hareketli = toplam // PENCERE
21
22 son5.pop(0) # medyan / median
23 son5.append(ham)
24 medyan = sorted(son5)[2]
25
26 ema += ALFA * (ham - ema) # üstel filtre / exponential filter
27
28 print("Ham:", ham, "Hareketli:", hareketli, "Medyan:", medyan, "EMA:", int(ema))
29 time.sleep_ms(10)Плоттер Thonny показывает числа из каждой строки отдельными линиями, а текст перед числами использует как метки. MicroPython работает медленнее Arduino, поэтому не задавай фильтрам слишком высокую частоту; около 100 Hz с небольшими окнами вполне комфортно для Pico. Если ты только знакомишься с MicroPython, начни со статьи «Основы MicroPython».
Какой фильтр выбрать?
| Ситуация | Рекомендуемый фильтр | Почему? |
|---|---|---|
| Показания потенциометра, джойстика или аналогового датчика дрожат | Скользящее среднее или EMA (α = 0,1–0,2) | Просто, быстро, достаточно сглаживания |
| Иногда приходит нелепое значение (HC-SR04, датчик расхода) | Медиана по 3 или 5 точкам | Полностью убирает отдельные ошибки |
| Есть и выбросы, и дрожание | Сначала медиана, затем EMA | Решает обе проблемы по очереди |
| Очень мало ОЗУ, много датчиков | Целочисленный EMA | Одна переменная на датчик |
| Решение о включении и выключении постоянно меняется у порога | Гистерезис и время подтверждения | Решает проблему принятия решения; одного фильтра мало |
| Пульсации от ламп или электросети | Усреднение равномерных измерений за 20 мс | Охватывает целый период 50 Hz |
| Объединение нескольких датчиков (наклон, положение) | Фильтр Калмана или комплементарный фильтр | Объединяет сильные стороны каждого датчика |
Частые ошибки и их решения
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Отфильтрованное значение реагирует слишком поздно | Слишком большое окно или слишком малое α | Уменьши окно или увеличь α; рассчитай задержку по таблицам |
| При запуске скользящее среднее медленно растёт от нуля | Буфер изначально заполнен нулями | Заранее заполни буфер первым показанием или дели на число накопленных измерений, пока буфер не заполнится |
| При 64 измерениях среднее становится неверным | Переполнение суммы типа int (64 × 1023 = 65 472 > 32 767) | Задай переменной суммы тип long |
| Медианный фильтр ничего не меняет | Окно равно 2 или другому чётному числу либо выбросы идут подряд | Используй нечётное окно (3, 5, 7), большее числа выбросов подряд |
| Линии плоттера сливаются или имеют один цвет | Неверный формат меток | Используй label:value с запятыми между парами и завершай строку через println |
| Значение EMA никогда не меняется | Целочисленное деление: вместо 0.1 написано 1/10 | Запиши коэффициент как float (0.1) или используй целочисленную формулу EMA |
| Значения продолжают колебаться, несмотря на фильтр | Проблема не в шуме, а в дрейфе или слабом питании | Проверь питание, добавь конденсатор 100 nF и выполни калибровку |
Фильтрация данных датчиков — самый дешёвый способ сделать измерения надёжными: дополнительные детали не нужны, а несколько строк кода обеспечивают более стабильную работу проектов. Теперь, когда ты знаешь, как сочетать усреднение, скользящее среднее, медиану, EMA и гистерезис, дрожащие показания дисплеев, ложные тревоги и дребезжащие реле останутся позади. В приложении BlueGrays можно изучить проекты измерения освещённости с LDR, ночника, измерения расстояния с HC-SR04, парктроника и ростомера с пошаговыми схемами подключения и готовым кодом, добавить к ним эти фильтры и увидеть результат на практике.
Частые вопросы
Почему показания датчиков на Arduino постоянно меняются?
При аналоговых измерениях пульсации питания, длинные провода, расположенные рядом моторы и собственный шум АЦП сдвигают значение на несколько отсчётов. Это нормально и не означает, что датчик сломан. Конденсатор 100 nF на аналоговом входе и программный фильтр усреднения или EMA стабилизируют показания.
Что такое скользящее среднее?
Скользящее среднее — это среднее последних N показаний: с каждым новым измерением самое старое значение удаляется, а новое добавляется. Оно уменьшает шум примерно в √N раз, но задерживает выход на (N−1)/2 измерений. На Arduino для него достаточно нескольких строк с кольцевым буфером и одной переменной суммы.
Чем медианный фильтр отличается от усреднения?
Среднее учитывает каждое значение, поэтому одно нелепое показание портит результат. Медианный фильтр сортирует значения и берёт среднее по позиции, полностью игнорируя отдельные ошибочные показания. Поэтому медиана лучше подходит для датчиков вроде HC-SR04, которые иногда выдают неверные значения, а усреднение — для дрожащих аналоговых показаний.
Как выбрать коэффициент экспоненциального фильтра (EMA)?
Коэффициент α лежит между 0 и 1: малое α даёт более стабильный, но медленный выход, большое — более быстрый, но шумный. При измерении каждые 10 мс значение α = 0,1 даёт постоянную времени около 100 мс и подходит как отправная точка для большинства датчиков. Время отклика можно оценить по формуле τ ≈ T / α.
Как построить графики нескольких значений в плоттере Arduino?
Выводи значения в каждой строке в формате label:value через запятую и завершай строку через println, например Ham:512,EMA:509. Плоттер показывает каждую метку отдельной цветной линией. Сначала закрой монитор порта, если он открыт, и установи скорость в бодах в соответствии с кодом.
Можно ли использовать фильтр Калмана на Arduino?
Да, простой одномерный фильтр Калмана занимает несколько строк и работает даже на Arduino Uno. Он настраивается шумом измерения R и шумом процесса Q; для одного датчика его результаты обычно очень близки к хорошо настроенному EMA. Настоящая польза проявляется при объединении нескольких датчиков, например акселерометра и гироскопа.
Что собрать после прочтения
Все проекты
Измерение освещённости с LDR
Хочешь увидеть яркость своей комнаты в виде числа? LDR — маленькая деталь, реагирующая на свет, — измеряет освещённость вокруг, а Arduino каждые полсекунды выводит значение на экран компьютера. Закрой

Измерение расстояния HC-SR04 (вывод в порт)
Датчик HC-SR04 работает совсем как летучая мышь, которая находит дорогу в темноте: он посылает звук, которого ты не слышишь, слушает эхо и измеряет, насколько далеко предмет перед ним. Arduino каждые

Ночник на LDR
Маленький ночник, который сам включается, когда темнеет, и гаснет, когда наступает утро. Датчик освещённости измеряет, насколько светло в комнате, и Arduino включает светодиод, когда достаточно темно,

Парктроник на HC-SR04
Мини-версия автомобильного парктроника: чем ближе предмет, тем чаще сигнал. Датчик расстояния измеряет предмет перед собой; если он дальше 20 сантиметров, горит зелёный светодиод, а если ближе — мигае

Измерение роста
Ростомер, который вешается на дверную раму и сообщает в сантиметрах рост человека, стоящего под ним. Сначала, пока под ним никого нет, ты нажимаешь первую кнопку, и датчик расстояния измеряет расстоян

Управление сервоприводом потенциометром
Когда ты поворачиваешь ручку потенциометра, рычаг сервопривода поворачивается вместе с ней: одно крайнее положение ручки — ноль градусов, другое — пол-оборота. Arduino всё время читает положение ручки

Измерение освещённости модулем LDR на ESP32
Наблюдение в мониторе порта в реальном времени за сырым значением ADC модуля LDR и относительным процентом освещённости, вычисленным по опорным значениям темноты и света. Этот процент — не измерение в люксах. Учит логике калибровки модуля. Идеально выполнить его перед проектом 10, потому что он нужен для определения порога.

Ультразвуковой датчик расстояния HC-SR04 на ESP32
Ультразвуковой датчик HC-SR04 вычисляет расстояние по времени, за которое испущенная им звуковая волна отражается от предмета и возвращается (Time of Flight). В этом проекте ESP32 подаёт импульс 10 микросекунд на пин TRIG датчика, а длительность эха читает с пина ECHO через pulseIn(). Вычисленное расстояние в сантиметрах выводится на I2C LCD 16x2. HC-SR04 работает от 5V, поэтому на пине ECHO нужен делитель напряжения.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

