Embedded HandbookПриложения
Все разделы › X. Проектирование, сети и IoT
STM32F103, STM32F407 · уровень базовый · 9 мин чтения
Открыть в интерактивном справочнике

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 Какие бывают датчики

По выходу — два больших класса:

По принципу действия — резистивные (сопротивление зависит от величины), ёмкостные (сенсорные кнопки, влажность — раздел 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 выше, а не ступени.

map(x, 0, 1023, 0, 10): значению 10 достаётся один код из 1024 — ручка почти никогда его не даст

Логометрическое измерение. Потенциометр, запитанный от той же опоры, что и АЦП, даёт код, не зависящий от напряжения питания: на четверти хода это 1024 и при 3,0 В, и при 3,3, и при 3,6 В. Питание делится на себя. У F103 в корпусах LQFP до 64 выводов опора АЦП — это питание аналоговой части VDDA, так что достаточно питать потенциометр от него. В корпусах на 100 выводов и больше опору подают на отдельный вывод VREF+. А вот если датчик питается от отдельного источника 3,3 В, а опора просела до 3,0 В, код станет 1126 — изменение питания попадёт прямо в показание.


81.3 Откуда шум

Сначала шум уменьшают в железе: конденсатор на входе АЦП (раздел 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 — и дальше ни на шаг

Ступенька из 0 в 1000 через три фильтра: среднее доходит за 16 отсчётов, сглаживание — за 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 Итоги

Дальше: раздел 81а «Цифровые фильтры: КИХ, БИХ и CMSIS-DSP» — фильтры с заданной полосой и готовые функции библиотеки.

Открыть в интерактивном справочнике