Выбор антидребезга — как выбор отвёртки: четыре биты, и каждая закрутит один и тот же винт. delay из форумов, окно на millis, библиотека Bounce2, RC-цепочка из резистора и конденсатора за три рубля. Работают все четыре — спорить о том, что «правильнее», скучно. Разница в цене: кто блокирует петлю, кто ест байты RAM, кому нужен паяльник, кто переживёт пятую кнопку в проекте. Здесь я раскладываю все четыре рядом с числами и условиями и говорю, что беру сам, — а механику самого дребезга разбирали в уроке про механику контакта. Сразу урегулирую частый спор из комментариев: способы не конкурируют, а складываются — на одном пине спокойно живут RC-цепочка на входе и окно в скетче, и двойная защита ничего не ломает.
Две оговорки на входе, чтобы не пересказывать чужие уроки. База с окном уже лежит в базовом уроке про антидребезг — здесь она лишь одна из четырёх бит. А антидребезг внутри прерывания — отдельная ветка со своими запретами: delay и Serial там под запретом, приём отсечки по millis разобран в уроке про антидребезг в ISR, и я упоминаю его только в таблице.
Программные: delay и millis-окно
delay — одна строка после детекции: поймали спад уровня, заснули на 20-30 мс, посмотрели на пин снова; уровень совпал — нажатие настоящее. Достоинство одно и оно честное: нечему сломаться, ни одной переменной. Цена — вся петля. Пока скетч спит, не опрашиваются другие кнопки, не идут датчики, не обновляется экран, а быстрые нажатия теряются целиком. Для скетча-игрушки с единственной кнопкой это мой выбор тоже; для прибора с тремя кнопками и дисплеем — нет: каждая кнопка с паузой превращает петлю в очередь ожиданий.
millis-окно — те же 20-30 мс, но без сна. Смена уровня запоминает время; нажатие подтверждается, когда уровень простоял окно; loop между делами крутится дальше. Цена — три переменных и пять строк на кнопку, которые в базовом уроке про антидребезг разобраны построчно. Скрытое достоинство окна, ради которого я терплю переменные: интервал живёт в одном месте скетча, и подгонка под новую кнопку — правка одного числа, без пайки и перепрошивки логики. Граница применимости наступает на третьей кнопке: пятёрки строк и пары переменных начинают путаться между собой, я рассказывал, как рассыпался пульт на шесть пар имён, в уроке про класс Button — там же и лекарство: свернуть пучок в объект.
Bounce2: чужая вылизанная рутина
Bounce2 упаковывает то же millis-окно в класс: Bounce knopka(2, 25); каждый круг loop зовём knopka.update(), а события читаем через fell() — кнопку нажали — и rose() — отпустили. Пара fell/rose закрывает половину вопросов, на которых ломаются самописные фильтры: например, счёт длины нажатия превращается в разность millis() между rose и fell без единого самодельного флага. Внутри ровно тот же алгоритм, что мы писали руками, только вылизанный годами чужих багов: окно отсчитывается от последней смены уровня, событие выдаётся один раз. Второй аргумент конструктора — интервал в миллисекундах. В примерах документации стоит 5 мс: для тугой фабричной кнопки хватает, для кнопки из дешёвого набора я ставлю 20-25 — и советую проверить на своей, потому что отскоки до 20 мс у ноунейма не редкость.
Библиотека окупается от трёх кнопок: тайминги меняются одной строкой knopka.interval(30) на всех, массив объектов крутится в цикле, код кнопки перестаёт размазываться по loop. Цена скромная: объект держит около десятка байт RAM, на Uno с двумя килобайтами это становится заметно у дюжины кнопок. Главная ловушка не в памяти, а в дисциплине: update() обязан зваться каждый круг. Забыли вызов в длинной ветке — события замирают на время её работы, и выглядит это как случайная глухота кнопки.
#include <Bounce2.h> const int PIN_A = 2;const int PIN_B = 3;const int PIN_C = 4;const int PIN_LED = 12; Bounce knopki[3];unsigned long counters[3]; void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); knopki[0].attach(PIN_A); knopki[0].interval(25); knopki[1].attach(PIN_B); knopki[1].interval(25); knopki[2].attach(PIN_C); knopki[2].interval(25); Serial.println("Bounce2: 3 knopki, interval 25 ms");} void loop() { for (int i = 0; i < 3; i++) { knopki[i].update(); // каждый круг, до всяких if if (knopki[i].fell()) { // нажали counters[i]++; Serial.print("knopka "); Serial.print(i); Serial.print(" = "); Serial.println(counters[i]); } }}Живая лента контакта: сырые фронты против засчитанных нажатий в трёх режимах защиты.
Подгружаем интерактив…
Аппаратный RC-фильтр
Программа — не единственное место, где можно резать дребезг: можно не пустить его на вход вообще. Классическая цепочка: кнопка через последовательный резистор 10 кОм тянет пин, конденсатор 100 нФ стоит между пином и землёй, подтяжка — штатная INPUT_PULLUP или внешний резистор. Контакт заряжает и разряжает конденсатор через резистор, постоянная цепи 10 000 Ом x 0,0000001 Ф = 1 мс: отскок в несколько миллисекунд не успевает заметно перекачать заряд, и на пине вместо сотни коротких скачков остаётся один плавный переход. Числа с условиями: 100 нФ хватает фабричным кнопкам с отскоком до 5 мс; для ноунейма из набора ставьте 1 мкФ — постоянная вырастет до 10 мс, реакция замедлится на те же миллисекунды, палец разницы не увидит.
Минус у чистого RC один, но коварный: фронт становится медленным, а вход микроконтроллера с порогом около половины питания в зашумлённой цепи может прочитать зону перехода несколько раз. На макетке с проводами по двадцать сантиметров это почти никогда не стреляет; как только линия уходит дальше полуметра или бежит вдоль обмотки реле — ставьте после цепочки триггер Шмитта 74HC14: его гистерезис честно разделяет пороги включения и выключения. Конденсатор берите керамический или плёночный: электролит у входа цифровой микросхемы — лишние утечки и деградация с годами.
Когда RC — мой выбор: концевики и герконы на длинных линиях, входы, которые должны жить независимо от скетча, и вообще всё, где паяльник доступнее редактирования кода. Бонус, который невозможно переоценить в поле: фильтр работает, даже когда loop() завис или плата спит. В таблице ниже все четыре способа рядом; на прерывание она смотрит отдельной строкой, а развилка «короткое против длинного нажатия» поверх любого из способов — в третьем уроке кластера.
| Способ | Блокирует loop | Детали и память | Когда беру |
|---|---|---|---|
| delay 20-50 мс после события | да, на каждый клик | ноль деталей, ноль переменных | скетч-игрушка с одной кнопкой, быстрая проверка на макетке |
| окно millis 20-30 мс | нет | ноль деталей, три переменных на кнопку | мой дефолт: одна-две кнопки в приборе с датчиками и экраном |
| Bounce2, interval 25 мс | нет | около 10 байт RAM на кнопку | от трёх кнопок: тайминги в одном месте, события fell/rose |
| RC: 10 кОм + 100 нФ (1 мкФ для ноунейма) | нет — работает даже при зависшем скетче | две детали; на длинных линиях плюс 74HC14 | концевики, герконы, линии дальше полуметра, входы RESET |
Таблица — не иерархия, и вот как я выбираю вживую. Начинаю с вопроса, где живёт нагрузка: если скетч обязан крутить дисплей, опрос датчика и Serial, delay отпадает без обсуждений, и выбор идёт между окном и Bounce2 — по числу кнопок, от трёх беру библиотеку. Если же вход физический, длинный или ответственный, добавляю RC-цепочку поверх любого программного способа и считаю эти две детали страховкой, а не конкуренцией. Один нюанс из практики: на пине с RC-цепочкой программное окно можно ужимать до 10 мс — фильтр уже снял крупную часть отскоков, и скорость реакции вырастает. И последнее: не стройте иерархию «правильных способов» — у меня в одном приборе живут RC на герконах двери и чистое окно на кнопке меню, и оба места работают год без правок.
- 1. Что стоит millis-окно антидребезга в скетче?
- 2. Второй аргумент Bounce knopka(2, 25) — что это?
- 3. Постоянная цепи из 10 кОм и 100 нФ — сколько это?
- 4. Когда RC-цепочке нужен триггер Шмитта после неё?
- 5. События кнопки на Bounce2 замирают в одной ветке loop. Что забыли?
- Все четыре способа режут один и тот же дребезг 10-50 мс — спорят не о точности, а о цене для петли, RAM и паяльника
- delay честен в игрушке с одной кнопкой и глушит петлю в приборе: быстрые нажатия теряются
- millis-окно — дефолт на одну-две кнопки; от трёх пучок переменных сворачивают в класс или Bounce2
- Bounce2: interval 25 мс вместо документных 5, update() каждый круг до всяких if
- RC 10 кОм + 100 нФ даёт тау 1 мс и живёт без скетча; дальше полуметра провода — добавляйте 74HC14
Частые вопросы по теме урока
- Что стоит millis-окно антидребезга в скетче?
- Окно — это метка millis() при смене уровня и сравнение с интервалом: пять строк, три переменных на кнопку, ни одной детали. Плата за него — только внимание к коду. Пять строк и три переменных, ноль деталей
- Второй аргумент Bounce knopka(2, 25) — что это?
- Это interval — окно, которое библиотека отмеряет от последней смены уровня. В примерах стоит 5 мс; для дешёвых кнопок я ставлю 20-25 и проверяю на своей. Окно антидребезга в миллисекундах
- Постоянная цепи из 10 кОм и 100 нФ — сколько это?
- Тау = R x C = 10 000 Ом x 0,1 мкФ = 1 мс. Хватает фабричным кнопкам с отскоком до 5 мс; для ноунейма конденсатор увеличивают до 1 мкФ, получая 10 мс. 1 мс
- Когда RC-цепочке нужен триггер Шмитта после неё?
- RC растягивает фронт, и зашумлённый сигнал дрожит у порога входа. Триггер Шмитта 74HC14 даёт гистерезис и разделяет пороги. На коротких макеточных линиях обычно обходятся без него. На длинных линиях и при помехах, когда медленный фронт читается по несколько раз
- События кнопки на Bounce2 замирают в одной ветке loop. Что забыли?
- update() обязан зваться каждый круг, до всяких if: он читает пин и отмеряет окно. Пропущенный вызов в длинной ветке глушит события на всё время её работы. Вызов knopka.update() в этой ветке