Гайд · Прерывания · Тайминг
Забудь про delay() — прерывания и millis() на Arduino и ESP32
Первое, что все учат, — delay(). Второе, во что все упираются, — его стена: пока идёт
delay(1000), плата не делает ничего — не читает кнопку, не обновляет экран,
не поддерживает Wi-Fi. Вот выход, в два шага: неблокирующий тайминг через millis() и
ловля событий в момент их появления через прерывания.
Шаг 1 — millis() вместо delay()
Почти всякое «мне нужна задержка» на самом деле значит «делай это раз в N, но всё остальное пусть
работает». Ровно для этого и есть millis() — он возвращает миллисекунды с запуска, и ты
проверяешь, сколько прошло, вместо заморозки:
unsigned long prev = 0;
const unsigned long period = 1000; // ms
void loop() {
unsigned long now = millis();
if (now - prev >= period) {
prev = now;
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // blink
}
// ...everything else keeps running here, no freeze
} Цикл никогда не блокируется — он лишь смотрит на часы на каждом проходе. Этот один приём заменяет 90%
вызовов delay() и именно он позволяет плате мигать светодиодом, опрашивать датчик и отдавать
страницу одновременно. Но у опроса есть зазор — между двумя проверками можно пропустить быстрое событие.
Тут и выходят прерывания.
Шаг 2 — прерывания: не опрашивай, а получай уведомление
Прерывание — это аппаратный «хлопок по плечу»: когда пин меняется, CPU бросает своё дело, выполняет маленькую функцию (ISR — обработчик прерывания) и возвращается ровно туда, где был. Берёшь его, когда нельзя пропустить событие и нельзя позволить себе его опрашивать:
- Нажатие кнопки — реагируй в момент нажатия, чем бы ни был занят
loop(). - Энкодер — импульсы идут слишком быстро для надёжного опроса.
- Датчик расхода/импульсов — считай фронты, не теряя ни одного.
- Пробуждение из глубокого сна по пину (см. глубокий сон).
Схема: кнопка, притянутая к земле
Самой чистой кнопке не нужны внешние детали: одна нога на GPIO, другая на GND, а внутренний pull-up
чипа включается через INPUT_PULLUP. Пин в покое HIGH и читается LOW при нажатии — поэтому
триггер по FALLING.
INPUT_PULLUP означает «без внешнего резистора». Собрано в BoardLab.Прерыванием на ESP32 может быть почти любой GPIO; на классической Uno — только D2 и D3.
Всегда оборачивай пин в digitalPinToInterrupt() — он привязывает пин к нужной линии
прерывания. Нюанс ESP32: у input-only пинов GPIO34–39 нет внутренних подтяжек —
вот там кнопке реально нужен внешний резистор ~10 кΩ на 3V3.
Шаг 3 — дребезг, или одно нажатие считается за пять
Механическая кнопка даёт не один чистый фронт — контакты дребезжат, дёргая HIGH/LOW пару миллисекунд. Без обработки ISR срабатывает несколько раз за нажатие. Решение — игнорировать любой фронт, пришедший слишком скоро после предыдущего:
const byte BUTTON = 19; // your interrupt pin
volatile uint32_t presses = 0;
volatile uint32_t lastUs = 0;
void IRAM_ATTR onPress() { // ESP32: the ISR must live in IRAM
uint32_t now = micros();
if (now - lastUs > 25000) { // ignore bounces within 25 ms
presses++;
lastUs = now;
}
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON, INPUT_PULLUP); // button to GND, no external resistor
attachInterrupt(digitalPinToInterrupt(BUTTON), onPress, FALLING);
}
void loop() {
static uint32_t shown = 0;
if (presses != shown) { // do the slow work OUT here, not in the ISR
shown = presses;
Serial.printf("presses: %u\n", shown);
}
} Проверка по micros() глотает дребезг в пределах 25 мс, оставаясь отзывчивой к реальным
нажатиям. Хочешь аппаратно? Конденсатор 100 нФ на кнопку (или RC-фильтр) сглаживает фронт — но
программная проверка бесплатна и хороша почти для всего.
Правила ISR, которые реально кусаются
- Держи её короткой. Выстави флаг или увеличь счётчик, а обрабатывай в
loop(). Долгая ISR блокирует всё, включая Wi-Fi. - Дели через
volatile. Любая переменная, к которой лезут и ISR, иloop(), должна бытьvolatile— иначе компилятор может кэшировать устаревшую копию. - ESP32: помечай ISR
IRAM_ATTR. Она должна выполняться из RAM — без этого плата может упасть, если прерывание придёт во время операции с flash. - Никаких
Serial,delay()и тяжёлой работы внутри ISR. Печатай и считай вloop(), как в примере. - На 8-битном AVR многобайтные значения не атомарны. Читаешь
long, который обновляет ISR? Кратко выключи прерывания (noInterrupts()/interrupts()) вокруг чтения.
Вместе: millis() убивает бытовой delay(), а прерывания ловят события, которые
нельзя пропустить. Когда оба в мышечной памяти, плата перестаёт заикаться и начинает делать несколько
дел разом — а на ESP32 можно пойти дальше и вынести работу на
второе ядро.
Подключаешь кнопку или датчик к нужному пину? Сначала проверь, что он свободен и умеет прерывания.