База знаний · Язык скетчей
millis() и delay(): паузы, которые не останавливают программу
Почему delay() замораживает плату, как заменить его на millis() и правда ли, что через 50 дней всё сломается.
delay() — первая функция, которой учат новичка, и первая, от которой потом приходится избавляться. Причина в том, что она делает буквально: останавливает всю программу. Пока идёт пауза, плата не читает кнопки, не отвечает на команды и не следит за датчиками. millis() решает это иначе — она просто сообщает, сколько миллисекунд прошло с момента включения.
Что на самом деле делает delay()
Вопреки названию, никакого «сна» там нет. В исходнике ядра delay() — это цикл, который крутится и сравнивает показания micros(), пока не набежит нужное число миллисекунд. Процессор всё это время крутит цикл на полной тактовой частоте: в спящий режим он не уходит и энергию не экономит. Более того, функция yield(), которую delay() вызывает внутри, на AVR-платах пустая — то есть время уходит полностью впустую.
Важная оговорка: на ESP8266 и ESP32 всё наоборот. Там delay() отдаёт время фоновым задачам Wi-Fi и сторожевому таймеру, и убирать его из длинных циклов опасно — плата перезагрузится. Совет «выкинь все delay» верен для Uno и Nano, но не для ESP8266.
Откуда Arduino знает время
Отсюда неочевидное следствие: millis() ничего не «включает» и не добавляет накладных расходов — вы за неё уже заплатили. В пустом скетче на 444 байта обработчик этого прерывания занимает 148 байт, а все девять байт ОЗУ — три переменные того же счётчика.
Вторая неочевидная деталь: таймер переполняется не ровно раз в миллисекунду, а каждые 1024 микросекунды. Лишние 24 микросекунды ядро копит в дробном счётчике, поэтому долговременного ухода часов нет, но отдельные значения millis() иногда пропускаются — счётчик перескакивает сразу на два. Практический вывод: никогда не пишите if (millis() == 5000), только сравнение «больше или равно».
И ещё одна честная оговорка: арифметика счётчика точна, а вот сам тактовый генератор — нет. На Uno частоту задаёт керамический резонатор, его точность заметно ниже кварцевой, поэтому за сутки часы на millis() убегают на минуты. Для настоящих часов нужен отдельный модуль времени.
Мигание через delay() — как все начинают
void setup() {
// Настраиваем вывод со встроенным светодиодом на выход.
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // подали напряжение — светодиод горит
delay(500); // ждём полсекунды
digitalWrite(LED_BUILTIN, LOW); // сняли напряжение — светодиод погас
delay(500); // и снова ждём полсекунды
}
Собрано: 924 байта флеша, 9 байт ОЗУ. Пока в программе одно дело, всё отлично. Проблемы начинаются, когда появляется второе.
То же самое на millis()
unsigned long previous = 0; // когда переключали в прошлый раз
const unsigned long PERIOD = 500; // период мигания, миллисекунды
bool ledOn = false; // текущее состояние светодиода
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
// millis() — сколько миллисекунд плата работает с момента включения.
unsigned long now = millis();
// Пора переключать? Тогда переключаем и запоминаем момент.
if (now - previous >= PERIOD) {
previous = now;
ledOn = !ledOn;
digitalWrite(LED_BUILTIN, ledOn ? HIGH : LOW);
}
// Здесь плата свободна: можно читать кнопку, порт, датчики —
// в отличие от delay(), который останавливает всю программу.
}
Собрано: 832 байта флеша, 14 байт ОЗУ. Обратите внимание — версия на millis() оказалась на 92 байта легче, чем версия с delay(). Расхожее «millis тяжелее и сложнее» неверно как минимум по размеру: delay() тянет за собой micros() и собственный цикл ожидания, а скетчу на millis() они не нужны. Плата за таймер — 5 байт ОЗУ на каждый: 4 байта на unsigned long и байт на флаг состояния.
То, чего delay() не умеет вообще
Два светодиода с периодами 300 и 1100 миллисекунд через delay() напрямую не мигают. Свести их можно только вручную: взять общий шаг 100 мс, вести два счётчика проходов — и получить тот же конечный автомат, только с платой, глухой на каждой паузе. А при периодах вроде 300 и 1105 общего шага уже не будет вовсе. На millis() это просто два независимых блока, и подбирать шаг не нужно:
const byte LED_A = 12; // первый светодиод
const byte LED_B = 13; // второй светодиод
const unsigned long PERIOD_A = 300; // полупериод первого, мс
const unsigned long PERIOD_B = 1100; // полупериод второго, мс
unsigned long prevA = 0; // у каждого светодиода свои «часы»
unsigned long prevB = 0;
bool stateA = false;
bool stateB = false;
void setup() {
pinMode(LED_A, OUTPUT);
pinMode(LED_B, OUTPUT);
}
void loop() {
unsigned long now = millis(); // время читаем один раз за проход
if (now - prevA >= PERIOD_A) { // пора переключать первый?
prevA = now;
stateA = !stateA;
digitalWrite(LED_A, stateA ? HIGH : LOW);
}
if (now - prevB >= PERIOD_B) { // пора переключать второй?
prevB = now;
stateB = !stateB;
digitalWrite(LED_B, stateB ? HIGH : LOW);
}
}
Собрано: 924 байта флеша и 19 байт ОЗУ — ровно столько же флеша, сколько у обычной мигалки на delay(), но дел вдвое больше.
Кнопка, которая не нажимается
Вот код, который приносят в поддержку чаще всего: светодиод мигает, кнопка «не работает».
const byte LED = 13;
const byte BUTTON = 2;
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
digitalWrite(LED, HIGH);
delay(1000); // целую секунду плата глухая
digitalWrite(LED, LOW);
delay(1000); // и ещё секунду
// Сюда программа доходит раз в 2000 мс — почти все нажатия пропущены.
if (digitalRead(BUTTON) == LOW) {
Serial.println("Кнопка нажата");
}
}
Кнопка здесь опрашивается ровно один раз за 2000 миллисекунд. Обычное нажатие длится около 200 мс, то есть будет замечено примерно в одном случае из десяти. Никакая «дребезгозащита» не поможет — плата просто спит в этот момент. Лечение: убрать оба delay(), мигать по схеме из предыдущего раздела, а digitalRead() оставить в loop() без пауз. Готовый пример с обработкой дребезга — в уроке про кнопку-выключатель.
Сколько это стоит: измеренные размеры
| Скетч | Флеш, байт | ОЗУ, байт |
|---|---|---|
| Пустой скетч (только setup и loop) | 444 | 9 |
| Мигание светодиодом через delay() | 924 | 9 |
| То же мигание на millis() | 832 | 14 |
| Два светодиода с разной частотой на millis() | 924 | 19 |
| Кнопка и мигание через delay() (пропускает нажатия) | 2144 | 214 |
Переполнение через 49,7 суток — и почему это не страшно
millis() возвращает unsigned long: 4 байта, максимум 4 294 967 295. Дальше счётчик обнуляется. Точное время — 49 суток 17 часов 2 минуты 47 секунд, а не «ровно 50 дней», как обычно пишут.
Половина статей пугает этим и советует «перезагружать плату раз в месяц». Это не нужно, если сравнение написано через разность. Беззнаковая арифметика считает по кругу, поэтому millis() - previous даёт верное «сколько прошло» даже когда счётчик перескочил через ноль. А вот привычная на вид запись millis() >= previous + interval ломается: сумма обрезается, и условие начинает срабатывать на каждом проходе. Вот трассировка у самой границы (previous = 4 294 967 196, интервал 250 мс):
| millis() | millis() − previous | Разность ≥ 250 | millis() ≥ previous + 250 |
|---|---|---|---|
| 4 294 967 196 | 0 | нет | ДА — ложное срабатывание |
| 4 294 967 271 | 75 | нет | ДА — ложное срабатывание |
| 0 | 100 | нет | нет |
| 150 | 250 | ДА — верно | ДА |
Вторая колонка справа — правильная запись, она сработала ровно на 250-й миллисекунде. Крайняя правая 100 миллисекунд подряд срабатывала на каждом проходе loop(), потому что previous + 250 обрезалось до 150.
Три ловушки, о которых редко пишут
int previous = millis();— ломает всё через 32,7 секунды работы. Вintна Uno помещается только 32 767, дальше число становится отрицательным, и разность оказывается больше настоящей на 65 536 — а после каждого следующего переполнения ещё на столько же. Момент времени хранят только вunsigned long.delayMicroseconds()больше 16 383 — врёт. Аргумент 16-битный, и внутри он умножается на четыре, поэтомуdelayMicroseconds(20000)даст вовсе не 20 миллисекунд. Больше 16 мс — толькоdelay(). Заодно: вызовы с 0 и 1 не ждут вообще.- Пока прерывания запрещены, millis() стоит на месте. Долгий обработчик прерывания,
noInterrupts()или блокирующая библиотека крадут время безвозвратно: плата помнит только одно отложенное переполнение таймера. Внутри обработчика прерыванияdelay()тоже не работает — он завершается почти мгновенно вместо запрошенной паузы.
Когда delay() всё-таки уместен
Когда программа делает одно дело и ждать ей не жалко: пауза между замерами датчика, задержка на запуск модуля, простое мигание в первом уроке. Плохим delay() становится там, где плата должна одновременно следить за чем-то ещё. Хорошее правило: как только в программе появилось второе дело — переходите на millis().
Проверено на нашей сборке
Все размеры получены 21 августа 2026 года: arduino-cli 1.5.2-rc.1, ядро arduino:avr 1.8.8, плата Arduino Uno. Устройство счётчика приведено по исходникам ядра cores/arduino/wiring.c и cores/arduino/hooks.c. Поведение во время работы мы не замеряли на живой плате — оно доказано кодом ядра и арифметикой.
Частые вопросы
Как заменить delay() на millis()?
Завести переменную unsigned long с моментом последнего действия и в каждом проходе loop() сравнивать разность: если millis() минус этот момент больше интервала — пора действовать и запомнить новое время. Готовый скетч мигания есть в статье; на Uno он занял 832 байта против 924 у версии с delay().
Через сколько переполняется millis()?
Через 4 294 967 296 миллисекунд — это 49 суток 17 часов 2 минуты 47 секунд, а не «ровно 50 дней». Если сравнение написано через разность (millis() − previous >= интервал), переполнение проходит незаметно. Ломается только запись вида millis() >= previous + интервал.
Как мигать светодиодом и читать кнопку одновременно?
Убрать все delay() и держать в loop() два независимых блока: один сравнивает millis() с моментом последнего переключения светодиода, второй каждый проход читает кнопку. Так плата опрашивает кнопку тысячи раз в секунду, а не раз в две секунды, как в версии с двумя delay(1000).
Можно ли вообще использовать delay()?
Да, если в программе одно дело: мигнуть, выдержать паузу между замерами датчика, дать модулю время на запуск. Более того, на ESP8266 и ESP32 delay() полезен — он отдаёт время фоновым задачам Wi-Fi и сторожевому таймеру. Вредным он становится там, где плата должна одновременно следить за чем-то ещё.
Читайте также
Обновлено: 21 августа 2026
