Счётчик нажатий на дверном замке я повесил на пин D2 и за вечер получил загадку: пять нажатий пальцем, а Serial честно напечатал тринадцать. Скетч был трёх строк, пересматривал его дважды — в скетче всё сходилось. Виноват оказался не код: пин докладывает ровно то, что делает контакт, а контакт после соударения пластинок ещё несколько миллисекунд подпрыгивает, как монетка на столе. В базовом уроке про антидребезг уже лежит готовый рецепт с окном — я не пересказываю его, а спускаюсь этажом ниже: почему железо вообще так себя ведёт, сколько длится дребезг у разных кнопок и от чего это число зависит. И отдельно — почему delay(200) из форумных советов бьёт по скетчу сильнее самого дребезга.
Ставка урока — увидеть дребезг в цифрах, а не на словах. Соберём в одном скетче два счётчика: сырой, который глотает каждый переход уровня, и чистый с окном на millis. Сравним показания, посчитаем цену костыля и выведем правило подбора окна под свою кнопку. Всё, что нужно железно, — кнопка на D2 со встроенной подтяжкой и светодиод на D12, как в самой первой кнопке из базовых уроков.
Механика: пружинка, пластинки и оксид
Внутри тактовой кнопки нет никакой электроники — есть две упругие металлические пластинки и толкатель. При нажатии пластина прогибается и шлёпает на ответный контакт. Первый удар замыкает цепь, но пластина упругая: она отскакивает, бьёт снова, снова отскакивает — и каждое соударение для цифрового входа выглядит как настоящий переход уровня. Длится эта возня у фабричных тактовых кнопок обычно 1-10 мс; у дешёвых кнопок из наборов и у изношенных экземпляров отскоки дотягивают до 20 мс, а старые тумблеры и затёртые микрики дают паузы и до 50 мс. Число плавает от экземпляра к экземпляру, от температуры и от силы нажатия — поэтому окно защиты подбирают на своей кнопке, а не переписывают из чужого скетча.
Второй враг, о котором базовые уроки молчат, — оксидная плёнка. Контакты сигнальных цепей пропускают микроамперы и доли миллиампера, а при таком токе плёнка окислов на металле не пробивается: контакт то замкнут по-настоящему, то висит на полупроводящей корке. В промышленности это лечат током самоочистки — через концевик гоняют хотя бы 10-20 мА, чтобы поверхность подгорала и чистилась при каждом замыкании. Кнопка на пине Arduino живёт при сотнях микроампер через подтяжку, поэтому позолота на контактах хорошей кнопки — не эстетика, а выживание. Если кнопка после полугода в ящике начала срабатывать через раз, вы поймали именно плёнку.
Теперь масштаб со стороны платы. Uno на 16 МГц прокручивает пустой loop() десятки тысяч раз в секунду, один digitalRead стоит единицы микросекунд. За 15 мс дребезга скетч успевает спросить пин две-три тысячи раз — и каждый скачок уровня в этом хаосе честный по меркам входа. Ни одна умная обработка уровня не уберёт дребезг сама: вход сообщает правду, просто правда у контакта своя. Хорошая новость в том, что отскоки короткие и предсказуемо живущие сразу после события: они не разбросаны по времени нажатия, а кучкуются в первых миллисекундах. Именно на это кучкование и работает окно: сигнал обязан продержаться, а случайные иголки — нет.
Эксперимент: два счётчика в одном скетче
Проверка стоит копейки и десяти минут. Скетч ведёт два счётчика рядом. Сырой считает каждый спад уровня — любой фронт, каким бы коротким он ни был, включая все отскоки. Чистый принимает нажатие, только когда уровень простоял стабильным 30 мс: окно берёт отсчёт от последней смены уровня, а не от момента события — это важная разница, из-за которой наивная пауза после нажатия ловит не всё. Светодиод щёлкается принятыми нажатиями, Serial печатает бегущие цифры. Для чтения чисел из монитора пригодится урок про parseInt и числа в Serial — там, где захотите слать пороги с компьютера.
Каждый принятый клик печатает пару clean=… raw=… — читается она так: clean считает настоящие нажатия, raw держит всё, что проскочило через прерывание. Пока палец на кнопке, разница между ними не растёт: окно принимает первое нажатие и отсекает повторы до отпускания, а сырой счётчик за это время успевает доесть все отскоки замыкания. Основная дельта копится в момент нажатия — там и смотрите. Ещё полезно погонять кнопку с разной скоростью: у медленных нажатий отскоки виднее, потому что контакт мягко докатывается, а не хлопает с размаха.
const int PIN_BTN = 2; // кнопка на землю, INPUT_PULLUPconst int PIN_LED = 12; volatile unsigned long rawCount = 0; // сырой: КАЖДЫЙ спад уровняunsigned long cleanCount = 0; // чистый: подтверждённые окном int lastReading = HIGH;unsigned long tChange = 0; // millis() последней смены уровняbool counted = false; void onFront() { // прерывание: только счёт rawCount++;} void setup() { pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_LED, OUTPUT); Serial.begin(9600); attachInterrupt(digitalPinToInterrupt(PIN_BTN), onFront, FALLING); Serial.println("Dva schetchika: raw vs clean, okno 30 ms");} void loop() { int reading = digitalRead(PIN_BTN); unsigned long now = millis(); if (reading != lastReading) { // уровень сменился — окно заново lastReading = reading; tChange = now; counted = false; } if (reading == LOW && !counted && now - tChange >= 30) { counted = true; // нажатие простояло окно — принимаем cleanCount++; digitalWrite(PIN_LED, cleanCount % 2); Serial.print("clean="); Serial.print(cleanCount); Serial.print(" raw="); Serial.println(rawCount); }}Живая лента контакта: сырые фронты против засчитанных нажатий в трёх режимах защиты.
Подгружаем интерактив…
На моей макетке с кнопкой из набора за сто рублей пять нажатий дали clean=5 и raw=13 — отскоки по два-три на каждое замыкание. Соседний экземпляр с позолоченными контактами показал одинаково. Оговорка честности: повторите это на своей кнопке — у каких-то экземпляров сырой счётчик уезжает вдвое, у отслуживших своё впятеро. И заметьте направление вранья: сырой счётчик врёт только вверх. Отскоки не съедают нажатия, они их плодят, поэтому у дребезга нет режима «кнопка промолчала, когда нажали» — есть режим «нажали один раз, посчитали пять».
Отсюда же видно, почему лечить дребезг логикой на уровне бесполезно: любой if вокруг digitalRead видит те же тысячи честных переходов. Рабочие места атаки два — время (окно: сигнал обязан продержаться) и частотная фильтрация (RC-цепочка, ей посвящён второй урок этого кластера). Прерывание сюда тоже относится, но со своими запретами: там delay и Serial под полным запретом, а приём отсечки по millis внутри обработчика разобран в уроке про антидребезг в прерывании.
Почему delay(200) — костыль
Самый частый совет на форумах: поймали нажатие — засните на 200 мс. Дребезг действительно уходит: все отскоки короче паузы, и спящему скетчу нечего посчитать. Теперь числа. Реальному дребезгу хватает 10-20 мс; пауза перекрывает его с запасом в десять-двадцать раз, то есть девяносто и больше процентов паузы работают впустую. Каждый клик глушит loop() на пятую долю секунды: Serial, датчики, экран, мигалки — всё стоит. Быстрые нажатия чаще пяти в секунду теряются целиком: палец жмёт, а скетч ещё досматривает сон с прошлого раза. Заодно заметьте асимметрию: пауза защищает только то, что случилось до неё — отскоки отпускания, случившиеся после пробуждения, она пропускает без разговоров, и для честного счёта её пришлось бы лепить на оба фронта.
В скетче с одной кнопкой и одним светодиодом цена невидима, и я не буду ханжой: для мигающего примера delay(20) после детекции — честный выбор, одна строка без единой переменной. Ломается всё в приборе: три кнопки с delay-защитой превращают петлю в очередь ожиданий, три нажатия подряд — полсекунды мёртвого времени, опрос датчика дрожит в такт пальцу. Добавьте сюда дисплеи с обновлением кадра за десятки миллисекунд и зуммеры с тонами — и паузы начнут складываться в слышимые рывки интерфейса. Механику петли без единого delay я разбирал в уроке про кооперативный цикл, а переворот пауз на таймеры — в уроке про millis вместо delay.
Есть и экзотический тупик, на котором люди теряют вечер: delay внутри обработчика прерывания на AVR не просто плохо — он вешает плату насмерть. delay ждёт тики таймера, а тики стоят, потому что прерывания закрыты: со стороны это выглядит как зависший кристалл. Ту историю с застывшим временем я рассказывал в уроке про ISR по ссылке выше — там же приём, который работает там, где delay умирает. И последнее: окно на millis переносится между кнопками в класс — так собран класс Button из урока про ООП, где три кнопки живут на трёх объектах, а не на шести парах переменных.
| Подход | Что считает | Что глушит | Реакция на клик |
|---|---|---|---|
| сырой счётчик по спадам | каждый отскок: 2-5 событий на одно нажатие | ничего | мгновенно, но с мусором |
| delay(200) после события | одно нажатие — но теряет всё быстрее 5 раз в секунду | всю петлю на 200 мс, в 10-20 раз больше нужного | с паузой и замороженным Serial |
| окно millis 30 мс | одно нажатие, отскоки отрезаны | ничего: loop крутится дальше | на 30 мс позже — неощутимо |
- 1. Пять нажатий пальцем, сырой счётчик по спадам показал 13. Кто виноват?
- 2. Окно антидребезга поставили 2 мс. Что случится?
- 3. Сколько раз loop() успевает спросить пин за 15 мс дребезга на Uno?
- 4. Чем плоха защита delay(200) после детекции нажатия?
- 5. Почему сырой счётчик врёт только вверх?
- Дребезг — механика: упругие пластинки отскакивают 1-20 мс, старые тумблеры — до 50 мс; число плавает от кнопки к кнопке
- Оксидная плёнка при микротоках не пробивается: позолота контактов — выживание, а не маркетинг
- Сырой счётчик фронтов врёт только вверх: отскоки плодят события, а не съедают их
- delay(200) перекрывает дребезг вдесятеро и глушит петлю: быстрые нажатия теряются целиком
- Окно millis подбирают по своей кнопке: измерили самый длинный отскок — удвоили
Частые вопросы по теме урока
- Пять нажатий пальцем, сырой счётчик по спадам показал 13. Кто виноват?
- Пин докладывает то, что делает контакт: пластина после удара отскакивает 1-20 мс, каждое соударение — честный переход уровня. Код при этом может быть безупречным. Контакт: отскоки упругих пластинок после соударения
- Окно антидребезга поставили 2 мс. Что случится?
- Окно обязано перекрывать самый длинный отскок вашей кнопки: типично 10-20 мс, у старых тумблеров до 50. Два миллисекунды перекрывают только фабричные кнопки с отскоком до пары мс. Короткие отскоки проскочат окно — ложные нажатия вернутся
- Сколько раз loop() успевает спросить пин за 15 мс дребезга на Uno?
- Один digitalRead стоит единицы микросекунд, пустой loop крутится десятки тысяч раз в секунду. За 15 мс — две-три тысячи опросов, и каждый скачок уровня вход видит как настоящий. Тысячи раз
- Чем плоха защита delay(200) после детекции нажатия?
- Дребезгу хватает 10-20 мс, пауза перекрывает его вдесятеро: всё это время не опрашиваются датчики, не обновляется экран, а быстрые клики теряются целиком. Глушит всю петлю и теряет нажатия быстрее пяти в секунду
- Почему сырой счётчик врёт только вверх?
- Каждый отскок — дополнительный переход уровня, то есть лишний счёт. Режима, где дребезг прячет настоящее нажатие, у механического контакта нет. Отскоки плодят фронты, но не съедают нажатия