RisalDash RisalDash

Гайд · Прерывания · Тайминг

Забудь про delay() — прерывания и millis() на Arduino и ESP32

Первое, что все учат, — delay(). Второе, во что все упираются, — его стена: пока идёт delay(1000), плата не делает ничего — не читает кнопку, не обновляет экран, не поддерживает Wi-Fi. Вот выход, в два шага: неблокирующий тайминг через millis() и ловля событий в момент их появления через прерывания.

Прерыванияdelay vs millisДребезгESP32Arduino

Шаг 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 — обработчик прерывания) и возвращается ровно туда, где был. Берёшь его, когда нельзя пропустить событие и нельзя позволить себе его опрашивать:

Схема: кнопка, притянутая к земле

Самой чистой кнопке не нужны внешние детали: одна нога на GPIO, другая на GND, а внутренний pull-up чипа включается через INPUT_PULLUP. Пин в покое HIGH и читается LOW при нажатии — поэтому триггер по FALLING.

Экспорт из BoardLab: кнопка подключена между GPIO ESP32 (пином прерывания) и GND, с внутренним pull-up
Кнопка между GPIO и GND — 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, которые реально кусаются

Вместе: millis() убивает бытовой delay(), а прерывания ловят события, которые нельзя пропустить. Когда оба в мышечной памяти, плата перестаёт заикаться и начинает делать несколько дел разом — а на ESP32 можно пойти дальше и вынести работу на второе ядро.

Подключаешь кнопку или датчик к нужному пину? Сначала проверь, что он свободен и умеет прерывания.