81. Датчики на прототипе и борьба с шумом
Датчик превращает физическую величину в электрический сигнал, а микроконтроллер — сигнал в число. Между ними всегда есть шум: показания «дрожат», даже когда ручка потенциометра неподвижна. В этом разделе — какие бывают датчики, как правильно перевести код АЦП в нужные единицы и какой фильтр выбрать, чтобы убрать шум и не потерять скорость.
Все фильтры раздела собраны armclang для Cortex-M3 и M0 и проверены
тестом на компьютере: ступенькой, шумом и одиночным выбросом. Каждое
число теста пересчитано на Python заново написанными фильтрами.
Как это называется в документации
Оригинал Перевод sensor, transducer датчик, преобразователь analog / digital output аналоговый / цифровой выход resolution, LSB разрядность, шаг квантования full scale полная шкала ratiometric measurement логометрическое измерение noise, outlier (spike) шум, одиночный выброс moving average скользящее среднее exponential moving average (EMA) экспоненциальное сглаживание median filter медианный фильтр step response переходная характеристика — отклик на ступеньку fixed-point число с фиксированной точкой
81.1 Какие бывают датчики
По выходу — два больших класса:
- аналоговые выдают напряжение: потенциометр, терморезистор, фоторезистор, датчик тока. Их читает АЦП (раздел 14), и весь шум линии попадает в показание;
- цифровые сами оцифровывают величину и отдают число по I²C, SPI или своему однопроводному протоколу (разделы 17 и 18). Шум линии им почти не страшен, но появляется протокол, который может сбоить.
По принципу действия — резистивные (сопротивление зависит от величины), ёмкостные (сенсорные кнопки, влажность — раздел 45), индуктивные (приближение металла), пьезоэлектрические (удар, вибрация), фотоэлектрические (свет).
Для прототипа удобнее всего начать с потенциометра: это делитель напряжения (раздел 50.2), и его показание легко проверить глазами.
81.2 Код АЦП — в единицы, которые нужны
У Uno АЦП 10-разрядный — коды 0…1023. У F103 и F407 — 12-разрядный, 0…4095, и напряжение считают как код × Vref / 4095 (раздел 14). В целых числах, с округлением и без потери диапазона:
#define VREF_MV 3300
#define ADC_MAX 4095
uint16_t adc_to_mv(uint16_t code)
{
return (uint16_t)(((uint32_t)code * VREF_MV + ADC_MAX / 2) / ADC_MAX);
}
Код 2048 даёт 1650 мВ, 4095 — ровно 3300. Шаг квантования — 0,806 мВ.
Приведение к uint32_t обязательно: 4095 * 3300 не помещается в
16 бит, а на AVR int именно такой (раздел 80.1).
⚠️
map()делит шкалу неровно. В Arduino масштабируют функциейmap(x, 0, 1023, 0, 10). Она считает целочисленно с отбрасыванием, и значение 10 получается ровно из одного кода из 1024 — из 1023. Остальным десяти значениям достаётся по 102–103 кода. Ручка потенциометра почти никогда не даст «10». Чтобы разбить шкалу на 11 равных ступеней, считаютx * 11 / 1024— по 93–94 кода на ступень. Для непрерывной величины нужен перевод в единицы, какadc_to_mvвыше, а не ступени.
Логометрическое измерение. Потенциометр, запитанный от той же
опоры, что и АЦП, даёт код, не зависящий от напряжения питания: на
четверти хода это 1024 и при 3,0 В, и при 3,3, и при 3,6 В. Питание
делится на себя. У F103 в корпусах LQFP до 64 выводов опора АЦП — это
питание аналоговой части VDDA, так что достаточно питать потенциометр от
него. В корпусах на 100 выводов и больше опору подают на отдельный вывод VREF+. А вот если датчик питается
от отдельного источника 3,3 В, а опора просела до 3,0 В, код станет
1126 — изменение питания попадёт прямо в показание.
81.3 Откуда шум
- Питание. Импульсный преобразователь, светодиоды, радиомодуль — всё, что потребляет ток рывками, качает опору АЦП.
- Провода. Длинный провод к датчику — антенна; плюс падение на нём (раздел 49.8).
- Земля. Общий провод, по которому течёт ток мотора, сдвигает «ноль» измерения.
- Сопротивление источника. Конденсатор выборки АЦП заряжается через датчик, и при высоком сопротивлении и коротком времени выборки не успевает: код начинает зависеть от предыдущего канала. Помогают более долгая выборка, потенциометр меньшего сопротивления или конденсатор на входе (раздел 14).
- Сам датчик и АЦП — собственный шум, который не убрать ничем, кроме усреднения.
Сначала шум уменьшают в железе: конденсатор на входе АЦП (раздел 50.6), RC-фильтр (раздел 62.7), раздельная земля, питание датчика от опоры. Программный фильтр — последняя ступень, а не замена первым.
81.4 Три фильтра и один неправильный
Скользящее среднее — среднее последних N отсчётов. Сумма не пересчитывается заново: к ней прибавляют новый отсчёт и вычитают тот, что уходит из окна.
#define MA_N 16
typedef struct {
uint16_t buf[MA_N];
uint32_t sum;
uint8_t pos;
uint8_t filled;
} ma_t;
uint16_t ma_update(ma_t *f, uint16_t x)
{
f->sum += x;
f->sum -= f->buf[f->pos]; /* уходит самый старый — сумма не пересчитывается */
f->buf[f->pos] = x;
f->pos = (uint8_t)((f->pos + 1) % MA_N);
if (f->filled < MA_N)
f->filled++;
return (uint16_t)(f->sum / f->filled);
}
Экспоненциальное сглаживание — каждый новый отсчёт сдвигает результат на долю пути к себе: y ← y + (x − y) / 8. Памяти — одно число.
#define EMA_K 3 /* коэффициент 1/8 */
/* Неверно: результат хранится без дробной части, деление отбрасывает
* остаток — показание застревает, не дойдя до входа. */
uint16_t ema_naive(uint16_t *y, uint16_t x)
{
*y = (uint16_t)(*y + ((int32_t)x - *y) / (1 << EMA_K));
return *y;
}
/* Верно: состояние хранится умноженным на 2^K — дробная часть не теряется. */
uint16_t ema_update(uint32_t *acc, uint16_t x)
{
*acc += x - (*acc >> EMA_K);
return (uint16_t)(*acc >> EMA_K);
}
Именно этот фильтр в примерах часто называют «бегущим средним» — но это не среднее окна, а сглаживание: у него нет буфера и нет окна, а вклад старых отсчётов убывает постепенно.
Медиана трёх — средний по величине из трёх последних отсчётов.
uint16_t med3(uint16_t a, uint16_t b, uint16_t c)
{
uint16_t t;
if (a > b) { t = a; a = b; b = t; }
if (b > c) { t = b; b = c; c = t; }
return a > b ? a : b;
}
Что показал тест
Ступенька — вход скачком из 0 в 1000:
| Фильтр | До 95 % | Итог |
|---|---|---|
| среднее на 16 | 16 отсчётов (и сразу 100 %) | 1000 |
| сглаживание 1/8 | 23 отсчёта | 1000 |
| сглаживание 1/8 наивное | 23 отсчёта | 993 — и дальше ни на шаг |
Наивный вариант выглядит так же, пока не посмотришь на последние знаки:
как только до цели остаётся меньше восьми кодов, (x − y) / 8 равно
нулю, и показание застревает. Недостача в 7 кодов — это 5,6 мВ, ошибка,
которая не уходит никогда. Цифра 23 сходится с формулой: (7/8)^k ≤ 0,05
при k = 23.
Шум — равномерный ±50 кодов вокруг 2000, пять тысяч отсчётов:
| Стандартное отклонение | Выигрыш | |
|---|---|---|
| без фильтра | 29,18 | — |
| среднее на 16 | 7,50 | в 3,9 раза (теория √16 = 4) |
| сглаживание 1/8 | 7,65 | в 3,8 раза (теория √15 ≈ 3,87) |
Одиночный выброс — один отсчёт 4095 среди 2000:
| Фильтр | Наибольшее отклонение |
|---|---|
| среднее на 16 | 130 — и держится 16 отсчётов |
| сглаживание 1/8 | 261 — и затухает |
| медиана трёх | 0 |
Как выбрать
| Среднее на 16 | Сглаживание 1/8 | Медиана трёх | |
|---|---|---|---|
| память | 40 байт | 4 байта | два прошлых отсчёта |
| против шума | хорошо | хорошо | слабо |
| против выбросов | размазывает | размазывает | убирает |
| задержка | ровно N отсчётов | плавная, 95 % за 23 | один отсчёт |
| деление | да: на M3 — udiv, на M0 — вызов функции |
нет, только сдвиги | нет |
Частое сочетание — медиана трёх, а за ней сглаживание: медиана убирает одиночные выбросы, сглаживание — шум. Коэффициент сглаживания берут степенью двойки: тогда деление превращается в сдвиг, и на Cortex-M0, где команды деления нет (раздел 75.4), фильтр остаётся дешёвым.
Фильтр — это всегда задержка. Сглаживание 1/8 при опросе раз в 10 мс доходит до 95 % за 230 мс — для термометра незаметно, для регулятора может оказаться слишком медленно (раздел 47).
81.5 Опрос датчика — без остановки программы
Пример «сложить 30 отсчётов в цикле и разделить» работает, но останавливает программу на всё время измерения. Фильтры выше устроены иначе: каждый вызов — один отсчёт, результат готов всегда. Отсчёты приходят из прерывания АЦП или по DMA (разделы 9 и 14), а программа берёт текущее отфильтрованное значение, когда оно нужно.
И ещё одно следствие того, что фильтр получает отсчёт, а не читает АЦП сам: его можно проверить на компьютере, как проверены функции этого раздела (раздел 79.5).
81.6 Грабли
Показания дрожат в последнем разряде. Нормально: шаг 12-разрядного АЦП — 0,8 мВ. Сначала железо — конденсатор, земля, опора, — потом фильтр.
Ручка потенциометра не даёт максимума. map() выделяет крайнему
значению один код из 1024.
Сглаженное значение не доходит до входа на несколько единиц. Экспоненциальное сглаживание хранит результат без дробной части.
После фильтра всплески остались, только стали шире. Среднее и сглаживание размазывают выброс; нужна медиана.
Регулятор стал раскачиваться после добавления фильтра. Фильтр добавил задержку.
Показание датчика «плывёт» вместе с напряжением питания. Датчик питается не от опоры АЦП.
Программа замирает на время измерения. Усреднение в цикле вместо фильтра по одному отсчёту.
Масштабирование даёт мусор на AVR и верно на STM32. Промежуточное
произведение не поместилось в 16-битный int.
81.7 Итоги
- Аналоговые датчики читает АЦП, и шум линии попадает в показание; цифровые отдают число, но добавляют протокол.
- Код 12-разрядного АЦП переводят в милливольты как код × 3300 / 4095 в
32-битной арифметике;
map()из Arduino делит шкалу неровно. - Датчик, запитанный от опоры АЦП, не чувствителен к колебаниям питания.
- Шум сначала давят в железе, потом фильтром.
- Скользящее среднее на 16 уменьшает шум в 3,9 раза и держит выброс 16 отсчётов; сглаживание 1/8 — в 3,8 раза при четырёх байтах памяти; медиана трёх убирает одиночные выбросы полностью.
- Экспоненциальное сглаживание в целых числах хранит состояние умноженным на 2^K — иначе застревает, не дойдя до входа.
- Фильтр работает по одному отсчёту и не останавливает программу; за подавление шума платят задержкой.
Дальше: раздел 81а «Цифровые фильтры: КИХ, БИХ и CMSIS-DSP» — фильтры с заданной полосой и готовые функции библиотеки.