Таймеры и прерывания: реагируем мгновенно

Учимся ловить события и выполнять задачи по расписанию, не загружая основной цикл.

Прерывание (interrupt) — механизм, при котором аппаратура сама «дёргает» процессор при событии (нажатие, тик таймера), и тот немедленно выполняет специальную функцию-обработчик.

Проблема опроса (polling)

До сих пор мы читали кнопку в цикле: if button.value() == 0. Это опрос — процессор постоянно проверяет вход. Если в цикле есть долгие операции (отправка по сети, чтение датчика), нажатие можно пропустить. Прерывания решают проблему: процессор занят своими делами, но как только пин изменился — мгновенно вызывается обработчик.

Прерывание по фронту сигнала

from machine import Pin

presses = 0

def on_press(pin):
    global presses
    presses += 1

button = Pin(15, Pin.IN, Pin.PULL_UP)
button.irq(trigger=Pin.IRQ_FALLING, handler=on_press)

# основной цикл свободен для другой работы
while True:
    pass

IRQ_FALLING — реагировать на спадающий фронт (переход 1→0, то есть нажатие с pull-up). Есть также IRQ_RISING и их комбинация.

Таймер: задача по расписанию

from machine import Pin, Timer

led = Pin(2, Pin.OUT)

def blink(timer):
    led.value(not led.value())

tim = Timer(0)
tim.init(period=500, mode=Timer.PERIODIC, callback=blink)
# светодиод мигает каждые 500 мс сам по себе

Таймер мигает светодиодом «в фоне», а основной цикл свободен для другой логики.

Как работает под капотом

В обычном режиме процессор выполняет ваш код строка за строкой. У ESP32 есть контроллер прерываний: когда настроенное событие происходит (фронт на пине, переполнение таймера), он приостанавливает текущую работу, сохраняет состояние, выполняет ваш обработчик, а потом возвращается ровно туда, где остановился. Поэтому реакция почти мгновенная — не нужно ждать конца цикла.

Золотое правило обработчиков

Обработчик прерывания должен быть максимально коротким. Внутри нельзя делать долгие операции (сеть, печать, sleep) и создавать объекты. Правильный приём — выставить флаг или увеличить счётчик, а тяжёлую работу сделать в основном цикле:

flag = False
def handler(pin):
    global flag
    flag = True        # только флаг!

while True:
    if flag:
        flag = False
        # тут безопасно делать долгие дела
        print("Событие обработано")

Частые ошибки

  • Тяжёлый код в обработчике. Печать, сеть, выделение памяти внутри irq приводят к сбоям.
  • Забыть global. Без него обработчик не изменит внешнюю переменную.
  • Не учитывать дребезг. Прерывание от кнопки тоже срабатывает на дребезг — добавьте проверку времени.

Итог

  • Прерывания реагируют на события мгновенно, не загружая основной цикл.
  • pin.irq() ловит фронты, Timer выполняет задачу по расписанию.
  • Обработчик должен быть коротким: лучше выставить флаг.
  • Кнопочные прерывания тоже надо защищать от дребезга.
Проверьте себя
1. Чем прерывание лучше опроса (polling) кнопки?
AОно потребляет больше энергии
BПроцессор реагирует мгновенно и не пропускает событие при занятом цикле
CОно проще пишется всегда
DОпрос вообще не работает
2. Каким должен быть обработчик прерывания?
AКак можно длиннее
BМаксимально коротким — лучше только выставить флаг или счётчик
CСодержать сетевые запросы
DСодержать time.sleep
3. Что делает Timer в режиме PERIODIC?
AСрабатывает один раз
BВызывает callback регулярно через заданный период
CОстанавливает процессор
DЧитает АЦП