Многозадачность без delay: millis и автоматы
Главный секрет «живых» устройств: настоящая Arduino никогда не спит в delay. Она жонглирует десятком задач, не роняя ни одной.
delay() — это пауза, во время которой плата мертва. Она не видит кнопок, не читает датчики. Чтобы делать много дел сразу, мы заменяем delay на проверку времени через millis().
Сейчас заставим светодиод мигать и одновременно следить за кнопкой — без единого delay. Это переход от учебных скетчей к настоящим устройствам, которые «живут».
Почему delay вреден
С delay(1000): [мигнул]...замер на 1 сек (СЛЕПОЙ и ГЛУХОЙ)...[мигнул]...замер... За эту секунду нажатие кнопки ПОТЕРЯНО - плата спала.
delay замораживает всё. Если в это время нажать кнопку или прийти данным, Arduino их пропустит. Решение — не спать, а посматривать на часы.
Шаблон неблокирующего кода
const int LED = 9;
const int BUTTON = 2;
unsigned long lastBlink = 0;
const unsigned long INTERVAL = 500;
bool ledOn = false;
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
}
void loop() {
// Задача 1: мигаем по таймеру, не блокируя loop
if (millis() - lastBlink >= INTERVAL) {
lastBlink = millis();
ledOn = !ledOn;
digitalWrite(LED, ledOn ? HIGH : LOW);
}
// Задача 2: одновременно следим за кнопкой
if (digitalRead(BUTTON) == LOW) {
// мгновенная реакция - плата НЕ спала
}
}
Как работает под капотом
Идея: вместо «поспи 500 мс» мы говорим «если с прошлого мигания прошло 500 мс — мигни». loop() при этом крутится тысячи раз в секунду, успевая и моргать, и проверять кнопку, и читать датчик. Каждая задача хранит своё «время последнего действия» и сравнивает с millis(). Так одна loop() ведёт несколько дел — это кооперативная многозадачность.
# Та же логика на Python: две независимые задачи по таймерам в одном цикле
last_blink = 0
last_sensor = 0
led = False
log = []
for now in range(0, 1100, 100): # имитируем millis() с шагом 100мс
if now - last_blink >= 500: # мигаем раз в 500мс
last_blink = now
led = not led
log.append(f"{now}мс: LED -> {'ON' if led else 'OFF'}")
if now - last_sensor >= 300: # читаем датчик раз в 300мс
last_sensor = now
log.append(f"{now}мс: читаю датчик")
for line in log:
print(line)
Запусти — увидишь, как мигание (каждые 500 мс) и опрос датчика (каждые 300 мс) идут вперемешку в одном цикле, не мешая друг другу. Так и живёт настоящее устройство.
Частые ошибки
- Смешивают delay и millis-логику. Один delay в loop — и вся многозадачность рушится.
- Хранят время в int. Через ~32 секунды переполнение — только unsigned long.
- Сравнивают millis() напрямую с порогом вместо
millis() - last— ломается при переполнении счётчика.
Best practices
- Возьми за правило: в «живом» устройстве delay не место — только millis-таймеры.
- Каждой задаче — свою переменную времени (lastBlink, lastSensor, lastSend).
- Сложное поведение оформляй конечным автоматом (состояния: ОЖИДАНИЕ, ТРЕВОГА, СБРОС) — так в прошлом уроке про debounce.
Конечный автомат: думаем состояниями
Когда поведение устройства усложняется — не просто «мигать», а «ждать → при нажатии запустить отсчёт → по окончании поднять тревогу → сбросить» — на помощь приходит конечный автомат (state machine). Идея: завести переменную state, которая хранит, в каком режиме сейчас устройство, и в loop() решать, что делать, в зависимости от её значения. Переходы между состояниями происходят по событиям (нажали кнопку) или по времени (прошло 5 секунд по millis). Так сложное поведение раскладывается на простые, понятные кусочки.
Этот образ мышления — один из самых ценных во всём курсе, и он выходит далеко за пределы Arduino. Светофор, банкомат, торговый автомат, меню в игре — всё это конечные автоматы. Как только ты начинаешь спрашивать себя «в каких состояниях может быть моя система и как она между ними переходит?», запутанная задача превращается в аккуратную схему. В сочетании с неблокирующими millis-таймерами автоматы позволяют писать устройства, которые делают много дел сразу и при этом остаются понятными — а это и есть инженерная зрелость.
Итоги
delay усыпляет плату и крадёт события; millis() позволяет вести много задач сразу, сверяясь с часами. Каждая задача помнит своё время. Это и есть «секрет» отзывчивых устройств. Теперь соберём всё в один проект.