Перейти к содержанию

Как заменить 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³² мс оно возвращается к нулю:

232 мс=4 294 967 296 мс≈49,7 суток (наш расчёт).2^{32}\ \text{мс} = 4\,294\,967\,296\ \text{мс} \approx 49{,}7\ \text{суток (наш расчёт)}.

По стандарту C (N1570, 6.2.5) беззнаковые вычисления выполняются по модулю 2³², поэтому разность остаётся верной и через ноль:

Моментlastnownow − last
До переполнения4 294 967 0004 294 967 200200
Через ноль4 294 967 000200496

Во второй строке прошло 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() не даст такой точности.

Источники

  1. Arduino. ArduinoCore-avr 1.8.8: cores/arduino/wiring.c — millis() возвращает unsigned long timer0_millis, читая его при запрещённых прерываниях; delay(ms) крутится в цикле, вызывая yield() и отсчитывая по 1000 мкс через micros() − start, — до конца паузы loop() дальше не идёт
  2. ISO/IEC 9899:201x (C11), Committee Draft N1570, 2011 — 6.2.5, п. 9: вычисления с беззнаковыми операндами не переполняются — результат приводится по модулю числа, на единицу большего наибольшего представимого значения типа

Нашли ошибку?