Урок 10. millis вместо delay — несколько дел одновременно
delay удобна в первых скетчах, но у неё есть большой недостаток: пока идёт пауза, плата не делает ничего — не читает кнопки, не принимает команды из монитора порта, не следит за датчиками. Скетч мигает светодиодом с delay(1000) — и нажатие кнопки в эту секунду просто теряется. Решение — функция millis().
Часы платы: millis
millis() возвращает число миллисекунд с момента запуска платы. Тип результата — unsigned long: целое без знака от 0 до 4 294 967 295. Такое число переполнится и начнёт счёт с нуля примерно через 49,7 суток работы (2³² мс ≈ 49,7 дня). Как это не сломает скетч — ниже.
Идея простая. Вместо «подожди секунду» скетч каждый проход loop спрашивает: «прошла ли секунда с прошлого раза?». Если нет — идёт дальше и занимается другими делами. Если да — выполняет действие и запоминает время. loop при этом повторяется тысячи раз в секунду и нигде не стоит.
Мигание без delay
Это переработанный пример File → Examples → 02.Digital → BlinkWithoutDelay:
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 не останавливается
}Три правила этого приёма:
- время хранят только в
unsigned long; - сравнивают разность
now - previousMillisс интервалом, а не сами моменты времени; - после срабатывания запоминают новое время.
Почему не 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:
#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 — нет: в ней перед переполнением сумма уйдёт «за круг» и условие сработает раньше времени.
Несколько дел одновременно
Два светодиода мигают каждый в своём ритме, а кнопка в любой момент печатает время нажатия. С delay так не получится, с millis — легко: у каждого дела своё «время прошлого раза».
Подключение: светодиоды с резисторами — к выводам 9 и 10, кнопка — между выводом 2 и GND.
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(" мс");
}
}
}Кнопка здесь тоже обходится без 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 скетча, который должен реагировать на кнопки, датчики или команды.
Коротко
delayостанавливает весь скетч;millis()даёт время с запуска и ничего не останавливает.- Приём:
if (millis() - прошлое >= интервал) { прошлое = millis(); действие; }. - Время — только
unsigned long:intна Uno вмещает лишь 32,7 секунды. - Через 49,7 суток
millisпереполняется, но разность беззнаковых чисел остаётся верной. - У каждого дела своя переменная «время прошлого раза» — так скетч делает несколько дел одновременно.