Основы · 11 мин чтения

Фильтрация данных датчиков на 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.

Ультразвуковой датчик расстояния HC-SR04
Датчики вроде HC-SR04 иногда дают ошибочные показания; медианный фильтр убирает такие выбросы.

Почему данные датчиков зашумлены?

Считать погрешность измерения чем-то единым — ошибка. Разным видам шума нужны разные фильтры, поэтому сначала нужно понять, с какой проблемой ты имеешь дело. Для этого достаточно некоторое время выводить исходные значения в монитор порта и смотреть на числа. Если значения постоянно колеблются вокруг одного числа, это случайный шум; если большинство показаний верны, но иногда приходит совершенно нелепое значение, это выбросы; если значение медленно уплывает, проблема не в шуме, а в дрейфе.

Вид шумаКак выглядитТипичная причинаПодходящее решение
Случайный шумЗначение колеблется на ±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 UnoESP32Raspberry Pi Pico
Движок потенциометраA0GPIO 34GP26 (ADC0)
Средняя точка делителя LDR + 10 kΩA1GPIO 35GP27 (ADC1)
HC-SR04 TRIGD9GPIO 5GP2
HC-SR04 ECHOD10GPIO 18 (через делитель)GP3 (через делитель)
Светодиод или реле (пример с гистерезисом)D8--
ПитаниеПотенциометр и LDR — 5V, HC-SR04 — 5VПотенциометр и LDR — 3V3, HC-SR04 — 5VПотенциометр и LDR — 3V3, HC-SR04 — VBUS
На ESP32 и Pico подключай потенциометр и делитель с LDR к 3V3; на аналоговые пины никогда не должно поступать больше 3,3 V. HC-SR04 питается от 5 V, и на его выходе ECHO тоже 5 V, поэтому для ESP32 и Pico снизь напряжение ECHO до 3,3 V делителем из резисторов 1 kΩ и 2 kΩ. Проверить номиналы можно в утилите делителя напряжения.

1. Усреднение нескольких измерений

Самый простой фильтр — выполнить несколько измерений подряд и взять их среднее. Случайный шум отклоняет каждое показание в свою сторону, поэтому при суммировании отклонения компенсируются. В статистике есть правило: при усреднении N независимых измерений шум уменьшается примерно в √N раз. 4 измерения уменьшают шум вдвое, 16 — вчетверо; 64 — в восемь раз, но требуют в четыре раза больше времени. На Arduino Uno один вызов analogRead() занимает около 112 µs, поэтому усреднение 16 измерений завершается менее чем за 2 мс. У метода всего два недостатка: при каждом получении результата он ненадолго занимает процессор и плохо справляется с выбросами. Если одно из 16 показаний нелепо, оно портит и среднее.

C++ (Arduino IDE)
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 мс; для потенциометра или температуры это несущественно, а в быстром роботе уже заметно.

C++ (Arduino IDE)
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.

C++ (Arduino IDE)
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}
Медиану и усреднение можно сочетать: сначала убрать выбросы медианой по 5 точкам, затем передать результат скользящему среднему или EMA, чтобы сгладить оставшиеся колебания. Эта пара — самое распространённое решение для показаний, в которых есть и выбросы, и дрожание, например у датчиков расстояния и расхода. Не делай окно медианы большим: для большинства задач хватает 3 или 5 точек, а 9 и больше лишь добавляют лишнюю задержку.

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 и сдвиг (>>) вместо деления.

C++ (Arduino IDE)
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,5310–20 мсОчень быстрый, слабое сглаживание
0,2950 мсБыстрый, лёгкое сглаживание
0,119100 мсСбалансированный, хорошая отправная точка для большинства датчиков
0,0539200 мсСтабильный, медленный
0,011991 сОчень стабильный, только для медленно меняющихся величин

«Эквивалентное окно» в таблице — примерный размер окна скользящего среднего с таким же сглаживанием; оно рассчитывается как N ≈ 2/α − 1. Главное преимущество EMA — расход памяти: скользящему среднему из 100 элементов нужно 200 байт ОЗУ, а EMA — одна переменная. Если нужно фильтровать несколько датчиков в 2 КБ ОЗУ Arduino Uno, разница существенна. Недостаток в том, что EMA так же плохо справляется с выбросами, как и усреднение: большой выброс смещает результат на несколько измерений.

Сравнение фильтров в плоттере

По числам разницу между фильтрами увидеть трудно, а на графике она очевидна с первого взгляда. Код ниже считывает потенциометр с частотой 100 Hz, обрабатывает одно показание четырьмя разными способами и выводит все результаты в формате label:value. Чтобы разница была заметнее, код добавляет искусственный выброс примерно к 3 процентам показаний; в реальном проекте убери эту строку. На графике видно, что медиана полностью убирает выбросы, а скользящее среднее и EMA превращают их в небольшие бугорки.

  1. Загрузи код и закрой монитор порта, если он открыт: монитор порта и плоттер нельзя открыть одновременно.
  2. В Arduino IDE открой «Инструменты › Плоттер по последовательному соединению» (Tools > Serial Plotter) или нажми значок графика справа вверху.
  3. Установи скорость справа внизу на 115200 бод.
  4. Поворачивай потенциометр сначала медленно, затем быстро и наблюдай за откликом каждой линии.
  5. Включай и выключай линии флажками рядом с метками, чтобы напрямую сравнить два фильтра.
C++ (Arduino IDE)
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 секунды, поэтому проходящая тень или краткая вспышка фар тоже не меняют решение.

C++ (Arduino IDE)
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 / DHT221 с / 2 сПри более частом чтении возвращается то же значение или ошибка
DS18B20 (12 бит)750 мсСтолько занимает преобразование
HC-SR04Не менее 60 мсДождись затухания предыдущего эха
Потенциометр, джойстик10–20 мс50–100 Hz более чем достаточно для движений человека
LDR (для подавления мерцания ламп)Каждую 1 мс в течение 20 мс, затем усреднитьМерцание от сети 50 Hz компенсируется
MPU60505–10 мсОбычный диапазон для измерения движения с частотой 100–200 Hz

Краткое знакомство с фильтром Калмана

Фильтр Калмана учитывает, насколько зашумлено измерение и как быстро может меняться реальная величина, и подстраивает свой коэффициент усиления. В простейшем одномерном варианте у него две настройки: R — дисперсия шума измерения (насколько дрожат показания датчика); Q — шум процесса (насколько быстро, по твоему предположению, меняется реальная величина). На каждом шаге фильтр сначала увеличивает неопределённость на Q, затем рассчитывает усиление K = P / (P + R) и сдвигает оценку к измерению на долю K. Если измерение шумное (большое R), K мало и фильтр меньше доверяет показанию; если ожидаются быстрые изменения (большое Q), K растёт и фильтр реагирует быстро. Интересный результат: при постоянных Q и R коэффициент K со временем стабилизируется, и одномерный фильтр Калмана фактически становится EMA. Настоящая сила фильтра Калмана проявляется при совместной оценке нескольких величин, например при объединении данных акселерометра и гироскопа для определения угла наклона. Его более простой родственник, комплементарный фильтр, описан в статье про MPU6050.

C++ (Arduino IDE)
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.

C++ (Arduino IDE)
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).

MicroPython
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 и выполни калибровку
Перед выбором фильтра всегда смотри на исходные данные. Несколько минут наблюдения за исходным значением в плоттере сразу покажут, что вызывает проблему: дрожание, выбросы, периодические пульсации или дрейф. Затем начни с самого простого фильтра и переходи к следующему только при необходимости. В большинстве проектов медиана по 5 точкам вместе с EMA при α = 0,1 работает лучше сложного фильтра Калмана, а понять их гораздо проще.

Фильтрация данных датчиков — самый дешёвый способ сделать измерения надёжными: дополнительные детали не нужны, а несколько строк кода обеспечивают более стабильную работу проектов. Теперь, когда ты знаешь, как сочетать усреднение, скользящее среднее, медиану, 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
#11Новичок

Измерение освещённости с LDR

Хочешь увидеть яркость своей комнаты в виде числа? LDR — маленькая деталь, реагирующая на свет, — измеряет освещённость вокруг, а Arduino каждые полсекунды выводит значение на экран компьютера. Закрой

4.6(81)
141
Измерение расстояния HC-SR04 (вывод в порт)
#27Новичок

Измерение расстояния HC-SR04 (вывод в порт)

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

4.6(47)
74
Ночник на LDR
#12Новичок

Ночник на LDR

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

4.8(103)
210
Парктроник на HC-SR04
#28Средний
Premium

Парктроник на HC-SR04

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

4.8(64)
85
Измерение роста
#33Средний
Premium

Измерение роста

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

4.5(25)
34
Управление сервоприводом потенциометром
#36Новичок
Premium

Управление сервоприводом потенциометром

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

4.6(37)
53
Измерение освещённости модулем LDR на ESP32
#113Новичок

Измерение освещённости модулем LDR на ESP32

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

4.6(36)
55
Ультразвуковой датчик расстояния HC-SR04 на ESP32
#116Средний

Ультразвуковой датчик расстояния HC-SR04 на ESP32

Ультразвуковой датчик HC-SR04 вычисляет расстояние по времени, за которое испущенная им звуковая волна отражается от предмета и возвращается (Time of Flight). В этом проекте ESP32 подаёт импульс 10 микросекунд на пин TRIG датчика, а длительность эха читает с пина ECHO через pulseIn(). Вычисленное расстояние в сантиметрах выводится на I2C LCD 16x2. HC-SR04 работает от 5V, поэтому на пине ECHO нужен делитель напряжения.

4.9(28)
46