Глиф Учебники

Урок 10. millis вместо delay — несколько дел одновременно

Курс «Arduino» · урок 10 из 10 · 4 минуты чтения

delay удобна в первых скетчах, но у неё есть большой недостаток: пока идёт пауза, плата не делает ничего — не читает кнопки, не принимает команды из монитора порта, не следит за датчиками. Скетч мигает светодиодом с delay(1000) — и нажатие кнопки в эту секунду просто теряется. Решение — функция millis().

Часы платы: millis

millis() возвращает число миллисекунд с момента запуска платы. Тип результата — unsigned long: целое без знака от 0 до 4 294 967 295. Такое число переполнится и начнёт счёт с нуля примерно через 49,7 суток работы (2³² мс ≈ 49,7 дня). Как это не сломает скетч — ниже.

Идея простая. Вместо «подожди секунду» скетч каждый проход loop спрашивает: «прошла ли секунда с прошлого раза?». Если нет — идёт дальше и занимается другими делами. Если да — выполняет действие и запоминает время. loop при этом повторяется тысячи раз в секунду и нигде не стоит.

нажатие кнопки delay(1000) ждёт — ничего не делает ждёт не замечено millis() проверка на каждом проходе прошла секунда — переключить замечено 0 1 с 2 с
С delay(1000) скетч секунду стоит и пропускает нажатие кнопки; с millis каждый проход loop проверяет, прошла ли секунда, и нажатие замечается сразу.

Мигание без delay

Это переработанный пример File → Examples → 02.Digital → BlinkWithoutDelay:

Arduino
const int LED = LED_BUILTIN;
const unsigned long INTERVAL = 1000;  // мс между переключениями

unsigned long previousMillis = 0;     // когда переключали в последний раз
bool ledOn = false;

void setup() {
  pinMode(LED, OUTPUT);
}

void loop() {
  unsigned long now = millis();
  if (now - previousMillis >= INTERVAL) {   // прошла секунда?
    previousMillis = now;                   // запомнить время
    ledOn = !ledOn;
    digitalWrite(LED, ledOn ? HIGH : LOW);
  }
  // здесь можно делать что-то ещё: loop не останавливается
}

Три правила этого приёма:

Почему не int

На Uno и Nano тип int занимает 2 байта и вмещает числа только до 32 767. В миллисекундах это всего 32,7 секунды: дальше значение в int искажается, и сравнения начинают врать. long (4 байта, со знаком) дотянет до 24,8 суток, а unsigned long — до 49,7. Для времени всегда берите unsigned long, как и возвращает millis().

На других платах (Uno R4, ESP32) int занимает 4 байта, но привычка писать unsigned long для времени работает везде.

Переполнение не страшно

Через 49,7 суток millis() снова начнёт с нуля. Кажется, что в этот момент now станет меньше previousMillis и условие сломается. Но вычитание беззнаковых чисел идёт «по кругу» и даёт верную разность. Пусть прошлое срабатывание было за 296 мс до переполнения, а сейчас 200 мс после него. Проверить арифметику можно и на компьютере — тип uint32_t там 32-битный, такой же, как unsigned long на Uno:

C++
#include <cstdint>
#include <iostream>

int main() {
    uint32_t previous = 4294967000u;  // за 296 мс до переполнения
    uint32_t now = 200;               // через 200 мс после него
    std::cout << now - previous << "\n";
}
Вывод
496

Разность — честные 296 + 200 = 496 мс. Поэтому запись now - previousMillis >= INTERVAL переживает переполнение, а запись now >= previousMillis + INTERVAL — нет: в ней перед переполнением сумма уйдёт «за круг» и условие сработает раньше времени.

время 4 294 967 295 → 0 previous now 296 + 200 = 496 мс не в масштабе 32 бита, без знака: по кругу previous = 4294967000u now = 200 now - previous → 496 разность верна и через переполнение millis()
Счётчик millis идёт по кругу: после 4 294 967 295 снова 0, но разность now − previous остаётся верной — 296 + 200 = 496 мс.

Несколько дел одновременно

Два светодиода мигают каждый в своём ритме, а кнопка в любой момент печатает время нажатия. С delay так не получится, с millis — легко: у каждого дела своё «время прошлого раза».

Подключение: светодиоды с резисторами — к выводам 9 и 10, кнопка — между выводом 2 и GND.

Arduino
const int LED_A = 9;      // мигает каждые 500 мс
const int LED_B = 10;     // мигает каждые 1300 мс
const int BUTTON = 2;

unsigned long lastA = 0, lastB = 0, lastChange = 0;
bool onA = false, onB = false;
int lastButton = HIGH;

void setup() {
  pinMode(LED_A, OUTPUT);
  pinMode(LED_B, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  unsigned long now = millis();

  if (now - lastA >= 500) {          // дело 1: светодиод A
    lastA = now;
    onA = !onA;
    digitalWrite(LED_A, onA ? HIGH : LOW);
  }

  if (now - lastB >= 1300) {         // дело 2: светодиод B
    lastB = now;
    onB = !onB;
    digitalWrite(LED_B, onB ? HIGH : LOW);
  }

  int button = digitalRead(BUTTON);  // дело 3: кнопка
  if (button != lastButton && now - lastChange >= 50) {   // 50 мс — защита от дребезга
    lastChange = now;
    lastButton = button;
    if (button == LOW) {
      Serial.print("Нажата на ");
      Serial.print(now);
      Serial.println(" мс");
    }
  }
}
светодиод A вывод 9, 500 мс светодиод B вывод 10, 1300 мс кнопка вывод 2 нажата сразу: «Нажата на … мс» 0 500 1000 1500 2000 2500 мс от запуска горит
Три дела в одном loop: светодиод A на выводе 9 переключается каждые 500 мс, светодиод B на выводе 10 — каждые 1300 мс, а нажатие кнопки печатается сразу.

Кнопка здесь тоже обходится без delay: первое изменение принимается сразу, а следующие 50 мс изменения игнорируются — за это время дребезг контактов успевает закончиться.

Так же строятся большие проекты: метеостанция раз в 2 секунды читает датчик, раз в минуту обновляет дисплей и всё время следит за кнопками. Каждое дело — короткий кусок кода со своей проверкой времени, и ни одно не задерживает остальные.

micros и точность

Для коротких интервалов есть micros() — микросекунды с запуска. Она тоже unsigned long и переполняется примерно через 71 минуту; правило с разностью работает и для неё. На Uno и Nano её значение кратно 4 мкс.

Сама millis() на платах с ATmega328P растёт неравномерно: внутренний таймер тикает раз в 1,024 мс, и примерно раз в 42 мс счётчик прибавляет сразу 2, чтобы догнать настоящее время. Для мигания и опроса датчиков это неважно. Точных часов на сутки и месяцы millis не заменит: кварц платы немного спешит или отстаёт, и для часов берут модуль реального времени (RTC).

Когда delay допустим

delay не запрещена. В setup подождать, пока прогреется датчик, или выдержать микросекундную паузу delayMicroseconds(10) для датчика HC-SR04 — нормально. Плохо, когда долгие delay стоят в loop скетча, который должен реагировать на кнопки, датчики или команды.

Коротко

Проверьте себя

1. Почему кнопка не срабатывает, если в loop есть delay(2000)?
2. Какой тип нужен переменной, в которой хранят значение millis()?
3. Какое условие правильно переживает переполнение millis()?
4. Через сколько примерно переполняется millis()?