Добавить в избранное

millis() и неблокирующие таймеры на Arduino: как избавиться от delay

Чтение ≈ 4 мин

delay() блокирует весь скетч. Учимся делать всё параллельно через millis(): паттерн «прошло N мс», многозадачность без ОС, мигание несколькими светодиодами, опрос датчиков и debounce без блокировок.

Почему 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 — читайте снаружи.
Поделиться
Понравился материал?

Комментарии

Добавить комментарий