Как заменить delay() на millis() в Arduino?
Обновлено Редакция GAW
delay() в ядре Arduino — это цикл: функция отсчитывает миллисекунды по micros() и не возвращается, пока пауза не кончится. Всё это время loop() стоит, кнопки не опрашиваются, а второй светодиод не может мигать в своём ритме. Решение — не ждать, а проверять: в каждом проходе loop() смотреть, не пора ли выполнить действие.
Шаблон
const unsigned long LED_PERIOD = 500; // ms
const unsigned long LOG_PERIOD = 2000; // ms
unsigned long ledLast = 0, logLast = 0;
void loop() {
unsigned long now = millis();
if (now - ledLast >= LED_PERIOD) { // overflow-safe: unsigned subtraction
ledLast += LED_PERIOD; // keeps the rhythm without drift
digitalWrite(13, !digitalRead(13));
}
if (now - logLast >= LOG_PERIOD) {
logLast += LOG_PERIOD;
Serial.println(analogRead(A0));
}
// buttons, sensors and other work run here without waiting
}
ledLast += LED_PERIOD держит ровный ритм: если проход loop() задержался на пару миллисекунд, следующий момент от этого не сдвинется. Если важнее «не чаще, чем раз в N мс», пишут ledLast = now.
Почему разность переживает переполнение
millis() возвращает 32-битное беззнаковое число, и через 2³² мс оно возвращается к нулю:
По стандарту C (N1570, 6.2.5) беззнаковые вычисления выполняются по модулю 2³², поэтому разность остаётся верной и через ноль:
| Момент | last | now | now − last |
|---|---|---|---|
| До переполнения | 4 294 967 000 | 4 294 967 200 | 200 |
| Через ноль | 4 294 967 000 | 200 | 496 |
Во второй строке прошло 296 мс до нуля и ещё 200 после — 496, ровно то, что вернула разность. А сравнение now > last + 500 во второй строке дало бы неверный ответ.
Как millis() устроена внутри и почему она иногда прибавляет сразу 2, разобрано в уроке «millis, micros и delay». Когда действий становится много, удобно оформить их конечным автоматом, а о переполнении маленьких типов — в ответе «Переполнение uint8_t».
Похожие вопросы
Почему нельзя написать if (millis() > start + 1000)?
Потому что start + 1000 само может переполниться и стать маленьким числом, и условие окажется истинным сразу. Разность millis() − start таких ловушек не имеет: по правилам C она вычисляется по модулю 2³², и результат — истинное число прошедших миллисекунд, пока интервал меньше 49,7 суток.
Какого типа должна быть переменная с моментом времени?
unsigned long, как у самой millis(). Если сохранить время в int, на Arduino Uno это 16 бит со знаком: уже через 32,8 с значение станет отрицательным, и сравнения сломаются.
Можно ли так же заменить delayMicroseconds()?
Да, тем же шаблоном с micros(): у неё тоже unsigned long, но она переполняется примерно через 71,6 минуты. Для пауз в единицы микросекунд проще оставить delayMicroseconds() — опрос в loop() не даст такой точности.
Источники
- Arduino. ArduinoCore-avr 1.8.8: cores/arduino/wiring.c — millis() возвращает unsigned long timer0_millis, читая его при запрещённых прерываниях; delay(ms) крутится в цикле, вызывая yield() и отсчитывая по 1000 мкс через micros() − start, — до конца паузы loop() дальше не идёт
- ISO/IEC 9899:201x (C11), Committee Draft N1570, 2011 — 6.2.5, п. 9: вычисления с беззнаковыми операндами не переполняются — результат приводится по модулю числа, на единицу большего наибольшего представимого значения типа
Нашли ошибку?
Смотрите также
- millis, micros и delayПрерывание Timer0, FRACT_INC, micros через TCNT0, delay и свой тик
- Конечные автоматы на Cswitch-case, таблица переходов, функции-состояния, таймауты, очередь событий
- Почему 255 + 1 даёт 0 в микроконтроллере?Перенос из старшего бита теряется, остаётся 0
- Как сделать millis() на AVR без Arduino?Timer0 CTC на 1 кГц, volatile uint32_t и чтение в ATOMIC_BLOCK
- Прерывание по переполнению таймера (TOV)Флаг и прерывание при переходе счётчика таймера через максимум