
В этом тексте я бы хотел написать про своеобразный простенький фильтр нижних частот. Про гистерезисный фильтр на триггерах Шмитта (ТШ).
Постановка задачи
В микроконтроллерной системе аналого-цифровой преобразователь измеряет напряжение на канале. Это напряжение надо конвертировать в дискретный уровень, в зависимости от значения. Сигнал зашумлен. Надо реализовать программный гистерезис. Напишите функцию, которая выполнит эту работу.
float hysteresis(float input_percent);
На графике это выглядит так

Теория
Триггеры Шмитта можно рассматривать как однобитные ячейки памяти. Получается, что фильтр из четырех триггеров формально может пребывать в (2^4=16) шестнадцати состояниях. В предельных случаях каждый новый семпл переключает каждый триггер.
Когда поступает очередной семпл мы подаем его на вход каждому триггеру: 1-му, 2-му, 3-му, 4-му. Где-то произойдет переключение состояние, а где-то нет. Теоретически регистр фильтра может быть в 16ти состояниях.

Для простоты рассуждений положим, что гистерезис равен нулю. То есть триггеры Шмитта вырождаются в компараторы. Надо сразу отметить, что некоторые состояния фильтра просто невозможны. Например не может быть входной семпл одновременно меньше 12 и больше 87. Вот перед Вами все запретные состояния фильтра.

Фильтр будет работать только в пяти состояниях термокода. Термокод - это способ двоичного кодирования чисел при который просто увеличивается количество непрерывных в ряд единиц.
dec |
термокод |
hex |
0 |
000000 |
0x0 |
1 |
100000 |
0x1 |
2 |
110000 |
0x3 |
3 |
111000 |
0x7 |
4 |
111100 |
0xF |
5 |
111110 |
0x1F |
Вот рабочий диапазон значений триггеров

В общем таблица состояний получается вот такая.

Реализация
Перед вами псевдокод программной реализации фильтра на триггерах Шмитта. Код пере-использует готовый программный компонент отдельного триггера Шмитта в четырех экземплярах.
static uint8_t SchTrigStateToU8(const SchmittTriggerState_t state) { uint8_t val = 0; switch (state) { case SCHMITT_TRIGGER_STATE_UP: val = 1; break; case SCHMITT_TRIGGER_STATE_DOWN: val = 0; break; default: break; } return val; } static const int32_t StateValLUT[16] = { 0, 1, -1, 2, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1, 4, }; static int32_t hist_filter_state_to_out( HistFilterHandle_t* const Node) { int32_t out_sample = 0; Node->state.tgrigger0 = SchTrigStateToU8(Node->SchmittTrigger[0].state); Node->state.tgrigger1 = SchTrigStateToU8(Node->SchmittTrigger[1].state); Node->state.tgrigger2 = SchTrigStateToU8(Node->SchmittTrigger[2].state); Node->state.tgrigger3 = SchTrigStateToU8(Node->SchmittTrigger[3].state); Node->state.res = 0; out_sample = StateValLUT[Node->state.byte]; return out_sample; } int32_t hist_filter_proc_sample(uint8_t num, const float in_sample) { int32_t out_sample = 0; HistFilterHandle_t *Node = HistFilterGetNode(num); if (Node) { uint32_t i = 0; for (i = 0; i < 4; i++) { schmitt_trigger_proc_val_ll(&Node->SchmittTrigger[i], in_sample); } out_sample = hist_filter_state_to_out(Node); LOG_DEBUG(HIST_FILTER, "HIST_FILTER_%u,in:%f,Out:%d",num ,in_sample,out_sample); } return out_sample; }
LUT реализует вот эту таблицу

Решение №2
На самом деле задачу можно решить проще на основе конечного автомата из пяти состояний. Шаг 1. Перечисляем все состояния. За состояние примем выходной сигнал.

Шаг второй: перечисляем все входные воздействия. Интерес представляют промежутки однозначности (gap) и зоны гистерезиса (hist).

Шаг третий: строим таблицу переходов. Вот в таблицу переходов мы и заложим эффект триггера Шмитта.

Выход автомата это его состояние. По сути получился автомат Мура. Реализация на основе одного LUT + классификация входа x . Вот так просто и не затейливо.
Проверка работы фильтра
Как можно заметить фильтр убирает высокочастотную составляющую входного сигнала.

Достоинства фильтра на ТШ в его простоте реализации и малом количестве операций на один семпл ( в сравнении с FIR фильтрами). Фильтр на триггерах Шмитта стабильный и не склонен к генерации как IIR фильтры.
Итог
Вот такой простой фильтр. Можно и дальше наращивать квантование добавляя количество отдельных триггеров Шмитта. Надеюсь этот алгоритм пригодится кому-нибудь в разработке трактов обработки сигналов.
Сокращение |
Расшифровка |
LUT |
LookUpTable |
ТШ |
триггер Шмитта |
FIR |
finite impulse response |
IIR |
Infinite impulse response |
Источники
Название |
URL |
Медианный фильтр на двух бинарных кучах |
|
Синтез Цифрового БИХ Фильтра Низких Частот |
|
Триггер Шмитта |
|
https://docs.google.com/spreadsheets/d/1dAsB0f5b-lm0g4HAjEvebUA9jxHLjEZ7dJht3QiOLoY/edit?gid=0#gid=0 |
Комментарии (25)

alcotel
01.03.2026 12:17Как-то всё сложно у вас получилось. Триггер, конечно, обладает памятью, но состояний схемы ведь всего два: в любой момент времени точка на фазовой диаграмме движется либо по красной ветке гистерезиса, либо по синей. Не важно, сколько там порогов.

aabzel Автор
01.03.2026 12:17Надо чтобы проходил вот такой модульный тест
bool test_hist_filter_proc2(void){ bool res = true; ASSERT_EQ(0, hist_filter_proc_sample(TEST_HIST_FILTER, 1.0f)); ASSERT_EQ(1, hist_filter_proc_sample(TEST_HIST_FILTER, 20.0f)); ASSERT_EQ(1, hist_filter_proc_sample(TEST_HIST_FILTER, 12.0f)); ASSERT_EQ(3, hist_filter_proc_sample(TEST_HIST_FILTER, 87.0f)); return res; }А для этого два триггера должны быть в разных состояниях (TG0=1 TG3=0).

alcotel
01.03.2026 12:17Вариантов выходных состояний, да, побольше, целых 5. Я про количество внутренних состояний, которые нужно помнить. Их явно не 16.
И у вас сделана наивная реализация, имитирующая реальное железо. В железе, да, так удобнее, наверное, потому что оно работает параллельно. Но софт в мк так не умеет, и эффективнее сделать один конечный автомат, а не 4 независимых. И сравнение с порогами сделать оптимальнее. Тем более, что взялись за LUT, и аналоговые пороги не какие попало.
Иначе вот это:
Можно и дальше наращивать квантование добавляя количество отдельных триггеров Шмитта.
приведёт к ужасно медленному коду.

sim2q
01.03.2026 12:17если ему завести ОС и двигать порог у получившегося компаратора, то кажется получится дельта-сигма.... надо собрать как-нибудь на рассыпухе, а то никогда не понимал как оно работает...

aabzel Автор
01.03.2026 12:17Вариантов выходных состояний 5. количество внутренних состояний, которые нужно помнить. Их явно не 16.
И у вас сделана наивная реализация, имитирующая реальное железо. В железе, да, так удобнее, наверное, потому что оно работает параллельно. Но софт в мк так не умеет, и эффективнее сделать один конечный автомат, а не 4 независимых. И сравнение с порогами сделать оптимальнее. Тем более, что взялись за LUT, и аналоговые пороги не какие попало.
Да. В самом деле. Можно переписать в виде автомата Мура.
Если бы не ваше замечание, то я бы до такого не додумался.

alcotel
01.03.2026 12:17Я тоже попробовал ради интереса, и точно такой же LUT получил. Большой, да, но зато регулярный. Значит при большом числе порогов его проще описать формулой, например
state = input / 2 + (input & (state > (input / 2)))Если и пороги тоже регулярны, то остаётся найти формулу для перевода аналогового сигнала в
input.

asoneofus
01.03.2026 12:17У нас про это в лекциях было.
Год 90й? ))) только там без микроконтроллера :D
Хех, так скоро до применения мажоритарных элементов дойдём ... и цифровых фильтров)))

VT100
01.03.2026 12:17Ой, не фильтр это... не фильтр. При шуме выше гистерезиса - на выход будут проходить куски шума. В отличие от фильтров, которые сделают шуму положенные "минус N dB" вне зависимости от текущего значения сигнала.

alcotel
01.03.2026 12:17В фотошопе похожая обработка тоже называется "фильтром", не вижу ничего странного. Нелинейный, не ФНЧ, конечно, но что-то связанное с квантованием.

shkal
01.03.2026 12:17Причём тут Фотошоп? В обработке сигналов слово "фильтр" имеет совершенно однозначную интерпретацию, и это НЕ фильтр, поскольку у него нет частотной избирательности.

alcotel
01.03.2026 12:17Обработку двумерных и 3-мерных сигналов ведь не Святой Дух делает. И математика-то не сильно отличается от 1D.
Медианный и фазовый фильтры с вами точно не согласны.

shkal
01.03.2026 12:17Постройте АЧХ медианного фильтра этой схемы и поймёте.

alcotel
01.03.2026 12:17Нет технической возможности (с)
Он во-первых нелинейный. Импульсный отклик, если вообще можно так выразиться, у медианного ровно нулевой. Хотя против крутого прямоугольного фронта фильтр бессилен - всё, как было, пропускает.
Давайте я лучше АЧХ фазового фильтра построю. Который all-pass.

ALT0105
01.03.2026 12:17Если сигнал оцифрован и на нём шум - проще всего сделать усредняющий фильтр. Например, скользящее окно шириной в 3 или 5 точек с записью в среднюю точку среднего значения

aabzel Автор
01.03.2026 12:17То что вы предлагаете это Медианный фильтр https://habr.com/ru/articles/935750/

ALT0105
01.03.2026 12:17С поправкой: медианный фильтр отсекает одиночные выбросы, а непрерывные колебания, как на рисунке, не отфильтровывает. Поэтому нужно вычисление среднеарифметического значения

aabzel Автор
01.03.2026 12:17Поэтому нужно вычисление среднеарифметического значения
Тогда получается FIR(КИХ) фильтр скользящего среднего. Это у которого все коэффициенты одинаковые и в сумме дают 1.

alcotel
01.03.2026 12:17Шум бывает и низкочастотным, а вовсе не в виде наивной бахромы на графике. Если б автор не поленился привести пример задачи, где гистерезисная обработка применяется, было бы понятней.
А это обычная обработка аналоговых датчиков положения, абсолютных энкодеров. Ну и в цифровой индикации тоже полезно.
Я вот MIDI- контроллером пользуюсь - там то же самое, напряжение с обычных потенциометров квантуется в 128 уровней. Фильтруют, чтобы без движения цифру не колбасило между двумя соседними значениями, и интерфейс не засорялся.

aabzel Автор
01.03.2026 12:17Если б автор не поленился привести пример задачи, где гистерезисная обработка применяется, было бы понятней.
Нигде. Просто в 2019 был такой вопрос на собеседовании при устройстве на работу программистом МК.

gena_k
01.03.2026 12:17Очень интересная концепция. Встречали такой фильтр, реализованный в железе/аналоговой схеме? Если его совместить с flash adc, то должен получиться быстрый и помехоустойчивый захват, не требующий устройства выборки/хранения.

kovserg
Какой же это фильтр "Нижних Частот" - это квантование. Вы АЧХ постройте и сразу будет всё ясно.