millis() против delay(): почему скетч замирает и как это лечить
Кнопка подключена к пину 2, светодиод мигает ровно по туториалу — а нажатия срабатывают через раз: жмёшь во время вспышки, и тишина. Отпускаешь, пробуешь ещё — иногда ловится. Это не плохие контакты и не брак кнопки: пока в скетче стоит delay(1000), плата половину жизни проводит без сознания и физически не успевает заглянуть на пин. На этом затыке ломается большая часть первых проектов сразу после мигалки.
Разберём, что delay() на самом деле делает с программой (не «пауза», а сон всего скетча), сведём delay() и millis() в таблицу по шести критериям, разберём канонический BlinkWithoutDelay построчно и приложим живой стенд, где потерянные нажатия видны в цифрах. Оба скетча со стенда запускаются в симуляторе — плата не нужна.
Что происходит во время delay(): скетч не висит, он спит
loop() с двумя delay(1000) заглядывает на digitalRead раз в две секунды; нажатие пальцем длится условные 150 мс, значит, поймать его можно в среднем в 7,5% попыток — остальные 92,5% начинаются и заканчиваются между проверками, и плата их не видит вовсе. Отсюда и формулировка запроса «arduino delay зависает»: зависает не плата, а логика скетча, которая всё свободное время проводит в пустом цикле ожидания. При этом счётчик millis() во время delay() продолжает тикать: на Uno и Nano им заведует аппаратный Timer0 через прерывание, а delay() — лишь плотная петля ожидания, которую прерывание обходит стороной. Всё для починки уже есть внутри чипа — вопрос в том, чтобы loop() перестал спать. Полный разбор шаблона с двумя светодиодами и живой кнопкой — в уроке «millis() вместо delay()».
delay() против millis(): сравнение по-честному
| Критерий | delay() | millis() |
|---|---|---|
| Читаемость | Код-распис: сделал — подождал — сделал | Переменные prev и interval; в первый день непривычно |
| Кнопки, датчики, Serial | Молчат всю задержку | Опрашиваются каждый проход loop(), тысячи раз в секунду |
| Несколько таймеров | Каждый новый период ломает структуру задержек | Своя пара prev + interval на процесс, таймеры не мешают |
| Потерянные события | Нажатия короче задержки пропадают целиком | Событие ловится на первом же проходе loop() |
| Переполнение счётчика | Времени не хранит — вопрос не стоит | Потолок 49,7 суток; вычитание now - prev переживает перескок через ноль |
| Точность периода | Плюс время всего остального кода | prev = prev + interval держит ровную сетку меток |
Два пункта таблицы обычно вызывают вопросы. Переполнение: unsigned long вмещает 4 294 967 295 мс — это 49,7 суток непрерывной работы, после чего millis() стартует с нуля. Прямое сравнение millis() > prev + interval в эту секунду провалится, а разность now - prev переживает перескок: в арифметике беззнаковых чисел она всегда корректна. Одно правило держите крепко: prev и now — только unsigned long; метка, сохранённая в int, ломает шаблон уже через 32,8 секунды. Что до читаемости — она возвращается на дистанции: когда таймеров становится три-четыре, пары prev + interval перерастают в планировщик задач на millis(), а loop() складывается в кооперативный цикл без единой блокирующей строки.
Ниже — живой стенд: понажимай кнопку на delay-версии и посмотри, сколько событий съедает блокировка. Слева мигалка на delay(1000) с кнопкой-ловушкой, справа — та же схема на millis(); счётчики под платами показывают потерянные и пойманные нажатия, а лента времени рисует «пробку» задержки против коротких штрихов неблокирующего кода. Попробуйте одно и то же: сначала зажать кнопку на секунду, потом щёлкать коротко — разница в счётчиках скажет за себя.
Блокирующая пауза против неблокирующего таймера: нажмите кнопку и сравните.
Подгружаем интерактив…
Канонический BlinkWithoutDelay: мигалка без единой задержки
const int ledPin = 13; // встроенный светодиод Lint ledState = LOW; unsigned long previousMillis = 0;const unsigned long interval = 1000; // период, мс void setup() { pinMode(ledPin, OUTPUT);} void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; if (ledState == LOW) { ledState = HIGH; } else { ledState = LOW; } digitalWrite(ledPin, ledState); } // сюда loop() попадает тысячи раз в секунду: // место для кнопок, датчиков и Serial — без единого delay()}Три вещи решают всё. Метка previousMillis — момент прошлого переключения; interval — сколько ждать; условие currentMillis - previousMillis >= interval проверяется на каждом проходе loop(), который без задержек крутится тысячи раз в секунду. Захотелось второй светодиод с другим периодом — заводите отдельную пару previousFast и fastInterval, и таймеры живут независимо. А когда в схеме возникают события короче миллисекунды — щелчки энкодера, пакеты ИК-пульта, импульсы счётчика Гейгера, — опрос не успевает, и наступает пора прерываний attachInterrupt(): процессор реагирует на фронт на пине мгновенно, занят loop() или нет.
- Кнопка срабатывает через раз или требует длинного удержания
- Ответы в Serial Monitor приходят пачками, раз в несколько секунд
- Сервопривод ходит ступенями вместо плавного движения
- Дисплей обновляется рывками, в «передышках» между задержками
- Второй период проще «добавить», разбив delay() на куски, — верный знак, что паттерн выбран неверно