В этом тексте я бы хотел написать про своеобразный простенький фильтр нижних частот. Про гистерезисный фильтр на триггерах Шмитта (ТШ).

Постановка задачи

В микроконтроллерной системе аналого-цифровой преобразователь измеряет напряжение на канале. Это напряжение надо конвертировать в дискретный уровень, в зависимости от значения. Сигнал зашумлен. Надо реализовать программный гистерезис. Напишите функцию, которая выполнит эту работу.

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://habr.com/ru/articles/935750/

Синтез Цифрового БИХ Фильтра Низких Частот

https://habr.com/ru/articles/836830/

Триггер Шмитта

https://ru.wikipedia.org/wiki/Триггер_Шмитта

Аналитика по фильтру

https://docs.google.com/spreadsheets/d/1dAsB0f5b-lm0g4HAjEvebUA9jxHLjEZ7dJht3QiOLoY/edit?gid=0#gid=0

Комментарии (25)


  1. kovserg
    01.03.2026 12:17

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


  1. alcotel
    01.03.2026 12:17

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


    1. 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).


      1. alcotel
        01.03.2026 12:17

        Вариантов выходных состояний, да, побольше, целых 5. Я про количество внутренних состояний, которые нужно помнить. Их явно не 16.

        И у вас сделана наивная реализация, имитирующая реальное железо. В железе, да, так удобнее, наверное, потому что оно работает параллельно. Но софт в мк так не умеет, и эффективнее сделать один конечный автомат, а не 4 независимых. И сравнение с порогами сделать оптимальнее. Тем более, что взялись за LUT, и аналоговые пороги не какие попало.

        Иначе вот это:

        Можно и дальше наращивать квантование добавляя количество отдельных триггеров Шмитта.

        приведёт к ужасно медленному коду.


        1. sim2q
          01.03.2026 12:17

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


          1. alcotel
            01.03.2026 12:17

            Почти с языка сняли) Буквально недавно искал, к чему лишний ОУ в схеме приложить. Попытался индикацию заряда на мигающем светодиоде сделать.

            Типа того


            1. aabzel Автор
              01.03.2026 12:17

              Что это за программа в которой Вы схему смоделировали?


              1. alcotel
                01.03.2026 12:17

                Qucs-Spice при поддержке нашего товарища @vv_kuznetsov


        1. aabzel Автор
          01.03.2026 12:17

          Вариантов выходных состояний 5. количество внутренних состояний, которые нужно помнить. Их явно не 16.

          И у вас сделана наивная реализация, имитирующая реальное железо. В железе, да, так удобнее, наверное, потому что оно работает параллельно. Но софт в мк так не умеет, и эффективнее сделать один конечный автомат, а не 4 независимых. И сравнение с порогами сделать оптимальнее. Тем более, что взялись за LUT, и аналоговые пороги не какие попало.


          Да. В самом деле. Можно переписать в виде автомата Мура.

          https://docs.google.com/spreadsheets/d/1dAsB0f5b-lm0g4HAjEvebUA9jxHLjEZ7dJht3QiOLoY/edit?gid=636705967#gid=636705967

          Если бы не ваше замечание, то я бы до такого не додумался.


          1. alcotel
            01.03.2026 12:17

            Я тоже попробовал ради интереса, и точно такой же LUT получил. Большой, да, но зато регулярный. Значит при большом числе порогов его проще описать формулой, например

            state = input / 2 + (input & (state > (input / 2)))

            Если и пороги тоже регулярны, то остаётся найти формулу для перевода аналогового сигнала в input.


  1. asoneofus
    01.03.2026 12:17

    У нас про это в лекциях было.

    Год 90й? ))) только там без микроконтроллера :D

    Хех, так скоро до применения мажоритарных элементов дойдём ... и цифровых фильтров)))


  1. VT100
    01.03.2026 12:17

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


    1. alcotel
      01.03.2026 12:17

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


      1. shkal
        01.03.2026 12:17

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


        1. alcotel
          01.03.2026 12:17

          Обработку двумерных и 3-мерных сигналов ведь не Святой Дух делает. И математика-то не сильно отличается от 1D.

          Медианный и фазовый фильтры с вами точно не согласны.


          1. shkal
            01.03.2026 12:17

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


            1. alcotel
              01.03.2026 12:17

              Нет технической возможности (с)

              Он во-первых нелинейный. Импульсный отклик, если вообще можно так выразиться, у медианного ровно нулевой. Хотя против крутого прямоугольного фронта фильтр бессилен - всё, как было, пропускает.

              Давайте я лучше АЧХ фазового фильтра построю. Который all-pass.


  1. ALT0105
    01.03.2026 12:17

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


    1. aabzel Автор
      01.03.2026 12:17

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


      1. ALT0105
        01.03.2026 12:17

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


        1. aabzel Автор
          01.03.2026 12:17

          Поэтому нужно вычисление среднеарифметического значения

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


    1. alcotel
      01.03.2026 12:17

      Шум бывает и низкочастотным, а вовсе не в виде наивной бахромы на графике. Если б автор не поленился привести пример задачи, где гистерезисная обработка применяется, было бы понятней.

      А это обычная обработка аналоговых датчиков положения, абсолютных энкодеров. Ну и в цифровой индикации тоже полезно.

      Я вот MIDI- контроллером пользуюсь - там то же самое, напряжение с обычных потенциометров квантуется в 128 уровней. Фильтруют, чтобы без движения цифру не колбасило между двумя соседними значениями, и интерфейс не засорялся.


      1. aabzel Автор
        01.03.2026 12:17

         Если б автор не поленился привести пример задачи, где гистерезисная обработка применяется, было бы понятней.

        Нигде. Просто в 2019 был такой вопрос на собеседовании при устройстве на работу программистом МК.


  1. gena_k
    01.03.2026 12:17

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


    1. alcotel
      01.03.2026 12:17

      Ещё как встречается.