Почему delay — зло
delay(1000) останавливает весь процессор на секунду: кнопка не реагирует, датчик не опрашивается, ШИМ не считается. Это нормально для blink-примера, но реальный проект — это всегда несколько задач одновременно: мигать, мерить, слушать, отвечать.
millis() возвращает миллисекунды с момента старта — и позволяет построить кооперативную многозадачность: loop крутится быстро, а каждая задача сама решает, «не пора ли».
Паттерн «прошло N мс»
uint32_t lastTime = 0;
const uint32_t INTERVAL = 1000;
void loop() {
uint32_t now = millis();
if (now - lastTime >= INTERVAL) {
lastTime = now;
// выполняем раз в секунду
Serial.println("tick");
}
// остальной код loop работает постоянно
}
Суть: вместо «поспи секунду» мы говорим «если прошла секунда — сделай». Пока не прошла — loop бежит дальше, ничего не блокирует.
Важный момент: вычитание now - lastTime корректно работает и при переполнении millis() (через ~49.7 дней счётчик обнулится) — это свойство беззнаковой арифметики uint32. Сравнение now >= lastTime + INTERVAL сломалось бы при переполнении.
Несколько задач одновременно
uint32_t tLed = 0, tDht = 0, tPrint = 0;
void loop() {
uint32_t now = millis();
if (now - tLed >= 500) { // мигание 2 раза в сек
tLed = now;
digitalWrite(13, !digitalRead(13));
}
if (now - tDht >= 2000) { // датчик раз в 2 с
tDht = now;
readDHT();
}
if (now - tPrint >= 5000) { // отчёт раз в 5 с
tPrint = now;
sendReport();
}
}
Три независимых «таймера» — три задачи, работающих со своей частотой, без блокировок.
Мигание без delay — пример
class Blinker {
uint8_t pin_;
uint32_t interval_;
uint32_t last_ = 0;
bool state_ = false;
public:
Blinker(uint8_t pin, uint32_t interval) : pin_(pin), interval_(interval) {}
void begin() { pinMode(pin_, OUTPUT); }
void update() {
if (millis() - last_ >= interval_) {
last_ = millis();
state_ = !state_;
digitalWrite(pin_, state_);
}
}
};
Blinker led1(13, 500); // быстро
Blinker led2(12, 1000); // медленно
Blinker led3(11, 2000); // очень медленно
void setup() { led1.begin(); led2.begin(); led3.begin(); }
void loop() { led1.update(); led2.update(); led3.update(); }
Класс оборачивает логику: каждый светодиод сам считает свои интервалы. Три светодиода мигают с разной частотой одновременно.
Антидребезг кнопки без delay
const int BTN = 2;
uint32_t lastChange = 0;
bool lastState = HIGH;
void loop() {
bool raw = digitalRead(BTN);
uint32_t now = millis();
if (raw != lastState && now - lastChange > 30) { // 30 мс фильтр
lastChange = now;
lastState = raw;
if (raw == LOW) { // INPUT_PULLUP — нажатие = LOW
onButtonPress();
}
}
}
Без delay(30) — фильтрация через сравнение времени, loop не замирает.
Периодический опрос датчика
class SensorPoller {
uint32_t interval_;
uint32_t last_ = 0;
public:
float temperature = NAN, humidity = NAN;
SensorPoller(uint32_t i) : interval_(i) {}
void update() {
if (millis() - last_ >= interval_) {
last_ = millis();
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t)) temperature = t;
if (!isnan(h)) humidity = h;
}
}
};
SensorPoller dhtSensor(2000); // DHT22 — не чаще раза в 2 с
void loop() {
dhtSensor.update();
// dhtSensor.temperature всегда свежая и доступна для вывода
}
micros() для микросекунд
millis() — для миллисекунд, micros() — для микросекунд (точность 4 мкс на Uno). Для таймингов быстрее ~мс или измерения длительности импульсов:
uint32_t t0 = micros();
fastOperation();
Serial.println(micros() - t0); // точное время выполнения
Типичные ошибки
if (millis() > last + N)— сломается при переполнении. Всегдаmillis() - last >= N.intдля времени — millis этоuint32_t,intпереполнится за 32 секунды. Толькоuint32_tилиunsigned long.- Смешивать delay и millis — лучше вообще без delay в loop.
last = millis()после работы — смещает фазу. Для ровной частотыlast += INTERVAL(но при пропусках накапливается).
Итог
Переход с delay на millis-паттерн — это главный шаг от «мигания в примере» к реальному устройству: несколько независимых задач, датчики, кнопки и вывод — всё параллельно. Формула одна: if (millis() - last >= interval) { last = millis(); действие(); } — и loop остаётся свободным.
Полный пример: три независимых задачи
unsigned long tLed = 0, tSensor = 0, tPrint = 0;
const int LED = 13;
bool ledState = false;
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
if (now - tLed >= 500) { // мигание 2 Гц
tLed = now;
ledState = !ledState;
digitalWrite(LED, ledState);
}
if (now - tSensor >= 2000) { // датчик раз в 2 с
tSensor = now;
float t = dht.readTemperature();
Serial.printf("T=%.1f\n", t);
}
if (now - tPrint >= 10000) { // статус раз в 10 с
tPrint = now;
Serial.printf("uptime: %lu s\n", now / 1000);
}
// здесь может жить остальной код — он не блокируется
}
Почему now - last >= interval, а не last + interval <= now
При переполнении millis() (~49.7 дней) last + interval может «перемахнуть» за границу и сравнение сломается. now - last всегда даёт правильную разность благодаря переполнению unsigned — работает корректно и через overflow.
Состояние кнопки без delay
unsigned long tBtn = 0;
bool lastBtn = HIGH;
void loop() {
unsigned long now = millis();
bool btn = digitalRead(BTN);
if (btn != lastBtn && now - tBtn > 30) { // антидребезг 30 мс
tBtn = now;
lastBtn = btn;
if (btn == LOW) onPress();
}
}
Таймеры по разным пинам
Не бойтесь множить unsigned long переменные — каждый таймер это одна переменная и один if. Пять независимых таймеров — пять переменных. На ESP32 для сложных систем есть аппаратные таймеры и FreeRTOS, но millis() покрывает 95 % задач.
Когда millis не хватает
- Точность лучше миллисекунд —
micros(). - События при отсутствии опроса — прерывания.
- Реальный многозадачный код — FreeRTOS (ESP32).
- Таймеры аппаратные — PWM/ICP/CTC на ATmega для жёстких таймингов.
Типичные ошибки
delayв смешанном коде: одинdelayломает все остальные таймеры.last = millis()вif: сброс при каждом заходе — правильноlast = now.- Сравнение
==вместо>=: может пропустить точное попадание. - Переполнение
int:unsigned longдля миллисекунд, неint. millis()в прерывании: счётчик не тикает в ISR — читайте снаружи.