Счётчик деталей у сверлильного станка я повесил на прерывание, как и советовали на форуме: фронт на пине 2, в обработчике pulses++. Счётчик насчитывал вчетверо-впятеро больше, чем проходило деталей: кнопка-микрик дребезжала, и каждый отскок съедался фронтом RISING как отдельное событие. Вторая версия была наивнее: я добавил в обработчик delay(50) для паузы. Счётчик стал честным, зато плата сошла с ума — Serial замолчал, millis() застыл, мигалка встала: на время работы ISR плата делала вид, что её не существует.
В уроке про прерывания мы собирали attachInterrupt и окно фильтра, которое решало в основной петле. Здесь другой приём — отсечка по millis() прямо внутри обработчика: пять строк, при которых ISR остаётся коротким, а дребезг режется на самом входе. Плюс два правила, которые делают связку надёжной: флаг вместо работы в ISR и подтверждение digitalRead в loop().
Дребезг при обычном опросе мы глушим в уроке про дребезг — там delay и окно по millis() в основном коде. Прерывание меняет условия игры: функции под запретом, время замерзает, реакция нужна за микросекунды. И потому приём из опроса целиком не переносится — переносится только идея.
Зачем городить прерывание для кнопки, которую всё равно жмёт медленный человек? Причин три, и каждая из них реальная. Первая — сон: на батарейном питании плата спит большую часть времени и просыпается ровно на фронт — опрос тут не годится, ему нужен живой loop(). Вторая — очередь: loop() бывает занят длинными делами, и фронт, пойманный аппаратно в ISR, не потеряется между кадрами. Третья — честность отсчёта: метка millis() внутри обработчика фиксирует момент фронта с точностью до миллисекунды, тогда как опрос даёт момент опроса — с невязкой в круг loop().
Почему в ISR нельзя delay и Serial
Механика простая и беспощадная. На AVR вход в обработчик сам закрывает прерывания: пока ISR не вернётся, ни одно новое прерывание не обслуживается. А millis() тикает именно от прерывания таймера раз в миллисекунду. Долгий обработчик — и счётчик времени стоит. delay(50) внутри ISR на AVR не выходит вовсе: delay ждёт тики таймера, а таймерное прерывание закрыто, — то есть навсегда. Со стороны это выглядит как зависшая плата, хотя процессор честно крутится в одном месте.
Serial страдает по соседней причине: печать на 9600 бод стоит около миллисекунды за символ, аппаратный буфер заполняется — и печать начинает ждать освобождения места. Всё то же ожидание внутри кода, которому ждать нельзя. Правило, к которому я пришёл после станка: обработчик живёт микросекунды; в нём допустимы присваивание, взвод флага и запись millis() — короткое чтение счётчика, никакой блокировки. Всё, что тянет дольше, переезжает в loop(). Про переменные, которые пишутся в обработчике и читаются снаружи, — отдельный урок про volatile, там же история про кэш компилятора.
Оценка бюджета без бортового осциллографа: вход в ISR на 16 МГц занимает десятки тактов — единицы микросекунд, и всё остальное время — ваши строки. Отсечка из этого урока делает в обработчике три операции: чтение счётчика, сравнение, присваивание — считанные такты. Для сравнения: один Serial.print на 9600 бод съедает около миллисекунды — в тысячу раз больше всего обработчика. Такая таблица цен стоит на стене у каждого, кто однажды ловил плату с застывшим временем: сначала считаете такты, потом пишете строки.
Отсечка по millis внутри ISR
Идея отсечки: считать фронт настоящим, только если он отстоит от предыдущего принятого дальше окна. В обычном коде для этого берут millis() и сравнивают — но у нас ISR, и вопрос, законно ли там millis(). Законно: это чтение двух байтов счётчика, без ожиданий и без вызовов библиотек. Дальше вступает беззнаковая разность из урока про переполнение: now - lastFront даёт верный интервал даже через переход счётчика через максимум, поэтому отсечка не ломается на 49-е сутки — редкий случай, когда код можно не трогать годами.
Почему не переключать светодиод прямо в обработчике? Можно, но я против, и не из педантизма. Обработчик должен решать одну узкую задачу — событие это или отскок; всё остальное — подтверждение, переключение, печать, реакция приборов — делает loop(). Так обработчик остаётся в пяти строках, которые не страшно перечитать через год, а поведение всей системы видно из основной петли. Если однажды захотите поставить отладочную печать внутрь ISR — сначала вспомните этот абзац, потом вспомните станок.
const int BTN = 2; // кнопка между D2 и 5 В, резистор 10 кОм с D2 на GNDconst int LED = 12; volatile int flag = 0; // просим loop() сделать остальноеvolatile unsigned long lastFront = 0; // момент последнего принятого фронтаint ledState = 0; void onFront() { unsigned long now = millis(); // внутри ISR можно: короткое чтение if (now - lastFront < 50) return; // отсечка дребезга прямо здесь lastFront = now; flag = 1;} void setup() { pinMode(LED, OUTPUT); pinMode(BTN, INPUT); attachInterrupt(digitalPinToInterrupt(BTN), onFront, RISING); Serial.begin(9600); Serial.println("ISR s otsechkoi 50 ms, podtverzhdenie v loop");} void loop() { if (flag == 1) { flag = 0; if (digitalRead(BTN) == HIGH) { // контакт подтверждает: ещё нажат ledState = 1 - ledState; digitalWrite(LED, ledState); Serial.print("t="); Serial.print(lastFront); Serial.print(" led="); Serial.println(ledState); } }}Обратите внимание на связку флаг — подтверждение. Флаг говорит loop(): фронт был, отсечка его пропустила. Подтверждение digitalRead проверяет, что контакт ещё замкнут: если флаг взведён, а пин уже в нуле, нажатие оказалось короче одного прохода loop() — и оно потеряно честно, потому что палец держит кнопку сотню миллисекунд, а loop() делает круг за единицы миллисекунд. Отсюда бюджет: круг основной петли не должен быть длиннее нажатия, иначе события начнут теряться — та же цена, что и у чистого опроса. Если в проекте есть длинные дела, сначала разнесите их по кооперативному расписанию, потом прикручивайте ISR.
Тонкость для перфекционистов, которую я честно отложу в копилку: метка lastFront — четыре байта, а ядро восьмибитное, и чтение такой переменной в loop() идёт четырьмя инструкциями. Между ними может прилететь фронт, ISR обновит метку — и loop() соберёт из двух половин небывалое число. В учебном скетче мы метку только печатаем, и цена испорченной строки — странный лог; в серьёзном коде составные значения из ISR читают под прикрытием: noInterrupts(), копия, interrupts(). Однобайтовый флаг такими предосторожностями не нуждается — присваивание байта атомарно само по себе, и потому флаг для связи сторон я выбираю чаще, чем общие счётчики.
| Способ | Цена | Где живёт код | Кому подходит |
|---|---|---|---|
| Конденсатор 100 нФ вдоль кнопки | ноль строк кода, копеечная деталь | железо | кнопки, которые вы паяете сами |
| Окно в loop() по millis() | десяток строк, ISR максимально короткий | основной код | события, которых много и они не срочные |
| Отсечка прямо в ISR | пять строк в обработчике | прерывание | события, которые нельзя пропускать: счётчики, охранные кнопки |
| Библиотека Bounce2 | ноль своего кода, класс на каждую кнопку | основной код | прототипы и макетки с десятком кнопок |
Границы окна
Окно подбирается по источнику. Человеческая кнопка дребезжит 1-20 мс — окно 50 мс перекрывает запас и не режет честные нажатия: палец дважды за полсотни миллисекунд не нажмёт. Реле и старые пыльные контакты отскакивают до 20-40 мс — окно расширяется. А вот энкодер EC11 при том же дребезге требует окна 1-2 мс и таблицы состояний: широкое окно съест честные тики при быстром вращении ручки.
Есть и обратная сторона: окно молча отбрасывает события. Счёт импульсов с датчика потока, где фронты идут каждые 4 мс, окном 50 мс убивается наповал. Правило у меня такое: окно фильтрует механику человека, а быстрые источники фильтрует схема — конденсатор вдоль контакта гасит отскоки до фронта, не трогая частоту событий. Длинные нажатия, двойные клики и другие человеческие жесты — это уже автомат состояний кнопки: он надстраивается над отсечкой без единой правки в ISR.
Подобрать окно для нового источника помогают метки в журнале: печатайте lastFront каждого принятого события и нажимайте кнопку по-разному — быстро, медленно, с усилием. Ровный шаг между метками — окно в запасе; слипшиеся метки через 1-3 мс — окно прозревает отскоки, расширяйте. Пятнадцать минут с журналом заменяют сотню догадок, а заодно оставляют в коде комментарий с реальными цифрами вашей кнопки — не из даташита, а из жизни.
- В ISR запрещены delay, Serial и всё долгое: вход в обработчик закрывает прерывания, тики таймера останавливаются, delay() на AVR не выходит вовсе
- millis() внутри ISR разрешён — короткое чтение счётчика; отсечка now - lastFront < 50 режет дребезг на самом входе
- Беззнаковая разность корректна и через переход счётчика через максимум — окно не ломается на 49-е сутки
- Флаг плюс подтверждение digitalRead в loop(): обработчик решает, событие это или отскок, всю работу делает основная петля
- Окно 50 мс — для кнопки человека; реле шире, энкодеру 1-2 мс и таблица состояний; быстрые источники фильтрует конденсатор 100 нФ
- 1. Что происходит с millis(), пока крутится длинная ISR на Uno?
- 2. Кнопка на пине 2 подключена с внутренней подтяжкой INPUT_PULLUP, в скетче attachInterrupt RISING. На какое движение пальца сработает ISR?
- 3. Почему отсечка now - lastFront < 50 не боится перехода millis() через максимум?
- 4. В чём роль флага flag в скетче урока?
- 5. Сколько примерно длится дребезг типичной кнопки, которую жмёт человек?
Частые вопросы по теме урока
- Что происходит с millis(), пока крутится длинная ISR на Uno?
- Вход в ISR закрывает прерывания, а millis() тикает от прерывания таймера. Поэтому delay() внутри ISR на AVR не выходит вовсе: он ждёт тики, которых не будет. счётчик замирает: тики таймера не обслуживаются, пока обработчик не вернётся
- Кнопка на пине 2 подключена с внутренней подтяжкой INPUT_PULLUP, в скетче attachInterrupt RISING. На какое движение пальца сработает ISR?
- С INPUT_PULLUP пин в покое HIGH, нажатие роняет его в LOW: нажатие — FALLING, отпускание — RISING. Для RISING на нажатие нужна подтяжка вниз, резистор 10 кОм с пина на GND. на отпускание — фронт 0 к 1
- Почему отсечка now - lastFront < 50 не боится перехода millis() через максимум?
- Беззнаковая арифметика идёт по модулю 2 в 32-й степени: разность даёт верный интервал сквозь склейку, пока окно короче 49,7 суток. volatile тут ни при чём — он про видимость переменной компилятору. потому что беззнаковая разность корректна и через переполнение
- В чём роль флага flag в скетче урока?
- Обработчик решает ровно одну задачу: событие это или отскок, и взводит флаг. Подтверждение digitalRead, переключение светодиода и печать живут в основной петле — так ISR остаётся коротким. ISR сообщает «было событие», а всю работу — подтверждение и переключение — делает loop()
- Сколько примерно длится дребезг типичной кнопки, которую жмёт человек?
- Отскоки контакта укладываются в 1-20 мс, окно 50 мс перекрывает запас. Реле и пыльные контакты шумят до 20-40 мс, а энкодеру наоборот нужно окно 1-2 мс. 1-20 мс