Неблокирующее время: millis() вместо delay()

До сих пор мы ставили delay() между действиями — и это работало. Но у delay() есть цена: пока он «спит», станция не реагирует ВООБЩЕ ни на что — ни на кнопку, ни на новое показание датчика. Сегодня разберём, как вести время без остановки программы. Это новая для тебя тема — в Python-курсе аналога не было.

Секундомер и две мигающие лампы с разным ритмом рядом с платой Arduino

После урока сможешь: объяснить, почему delay() блокирует программу, использовать millis() для отсчёта интервалов без остановки кода, вести два независимых процесса одновременно. В конце — станция с двумя независимыми ритмами.

Проблема delay(): станция «замирает»

Представь: в loop() стоит delay(1000) для мигания светодиода. Если в это время нажать кнопку — станция её не заметит: весь код внутри loop() ждёт, пока пройдёт секунда, и только потом снова доходит до digitalRead. Чем больше delay(), тем «глуше» станция к остальным событиям.

  Проблема delay() — окно «глухоты»:

  loop(): мигание -> delay(1000) -----------> проверка кнопки
                      ^
                      |
              кнопку нажали ЗДЕСЬ, но
              программа её ещё не видит —
              она "спит" в delay()

Представь повара, который одновременно варит суп и печёт пирог. Если он встанет у плиты с секундомером и будет ждать ровно 10 минут, ничего больше не делая, — это delay(): пирог подгорит, пока повар «глухой» ко всему остальному. Хороший повар вместо этого поглядывает на часы на стене — засекает время и продолжает заниматься обоими блюдами. Часы на стене, на которые можно посмотреть в любой момент, не останавливая работу, — это и есть millis().

millis() — часы платы с момента включения

Команда millis() возвращает, сколько миллисекунд прошло с момента включения платы — число, которое только растёт, никогда не останавливаясь и не ожидая. Хранить его нужно в типе unsigned long — это «длинное целое без знака», рассчитанное на очень большие растущие значения.

void setup() {
  Serial.begin(9600);
}

void loop() {
  unsigned long teper = millis();
  Serial.println(teper);
}
0
1
2
3
...

Паттерн неблокирующего интервала

Идея: помнить, КОГДА действие выполнялось в прошлый раз (переменная previousMillis), и на каждом проходе loop() (без ожидания!) сравнивать: прошло ли уже достаточно времени. Если да — выполнить действие и запомнить новое «прошлый раз». Если нет — ничего не делать и идти дальше, не останавливаясь.

unsigned long previousMillis = 0;
const long interval = 1000;
bool ledState = false;

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

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    ledState = !ledState;
    digitalWrite(13, ledState);
  }
  // сюда можно дописать ЛЮБОЙ другой код —
  // он выполнится немедленно, без ожидания
}
  Неблокирующий интервал (каждый проход loop() — мгновенный):

  currentMillis - previousMillis >= interval ?
        |                          |
        да                        нет
        v                          v
  сделать действие          ничего не делать,
  и запомнить новое          идти дальше по loop()
  previousMillis                   |
        |                          |
        +------> loop() продолжается, не останавливаясь <------+

Ключевое отличие от delay(): loop() выполняется снова и снова без остановки, много раз в секунду. Условие внутри if просто пропускает действие, пока не пришло время, — но остальной код (проверка кнопки, чтение датчика) выполняется на КАЖДОМ проходе.

Два независимых процесса одновременно

Главная сила паттерна — можно вести НЕСКОЛЬКО независимых интервалов сразу, каждый со своей переменной previousMillis и своим interval. Они не мешают друг другу, даже если их периоды не кратны один другому — а с delay() так сделать было бы невозможно.

  Два независимых ритма на одной шкале времени (мс):

  LED1 (interval=500):  |----500----|----500----|----500----|
  LED2 (interval=300):  |--300--|--300--|--300--|--300--|--300--

  Оба идут на своих переменных previousMillis — независимо друг от друга

Миссия: два независимых ритма

Подключи два светодиода: LED1 на пин 13 (интервал 500 мс), LED2 на пин 12 (интервал 300 мс). Заведи для каждого свою пару переменных previousMillis/ledState и переключай их независимо в одном loop() — без единого delay().

unsigned long prevLed1 = 0;
unsigned long prevLed2 = 0;
const long interval1 = 500;
const long interval2 = 300;
bool state1 = false;
bool state2 = false;

void setup() {
  pinMode(13, OUTPUT);
  pinMode(12, OUTPUT);
}

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

  if (teper - prevLed1 >= interval1) {
    prevLed1 = teper;
    state1 = !state1;
    digitalWrite(13, state1);
  }

  if (teper - prevLed2 >= interval2) {
    prevLed2 = teper;
    state2 = !state2;
    digitalWrite(12, state2);
  }
}

Проверь себя

Контрольный вопрос. Что возвращает команда millis()?

АТекущее время суток по часам
БНомер текущего прохода loop()
ВЧисло миллисекунд, прошедших с момента включения платы

Подсказка: Это разбиралось в блоке «millis() — часы платы с момента включения».

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Контрольный вопрос. Почему delay(1000) внутри loop() — проблема для отзывчивости станции?

АВо время delay() программа не выполняет никакой другой код — не заметит нажатие кнопки или новое показание
Бdelay() потребляет слишком много памяти платы
Вdelay() работает только с целыми секундами, не с миллисекундами

Подсказка: Обратись к блоку «Проблема delay(): станция замирает».

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Контрольный вопрос. Зачем для millis() используют тип unsigned long, а не обычный int?

АЭто просто традиция в примерах Arduino, разницы нет
Бunsigned long работает быстрее, чем int, на плате Arduino
Вmillis() возвращает большое постоянно растущее число — обычный int для него слишком мал

Подсказка: Число миллисекунд с момента включения платы очень быстро становится большим.

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Контрольный вопрос. previousMillis = 1000, interval = 500, currentMillis = 1600. Сработает ли условие currentMillis — previousMillis >= interval?

АНельзя определить без Serial Monitor
БДа — разность 600 не меньше 500
ВНет — разность меньше интервала

Подсказка: Посчитай разность 1600 — 1000 и сравни результат с 500.

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Контрольный вопрос. previousMillis = 1000, interval = 500, currentMillis = 1300. Сработает ли условие currentMillis — previousMillis >= interval?

АДа — время всегда растёт, значит условие истинно
БНет — разность 300 меньше 500
ВНельзя определить, не зная delay()

Подсказка: Посчитай разность 1300 — 1000 и сравни результат с 500.

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Контрольный вопрос. Программист забыл строку previousMillis = currentMillis; внутри if. Что произойдёт после того, как порог интервала будет пройден один раз?

Исходный код для этого задания:

if (currentMillis - previousMillis >= interval) {
  // строка previousMillis = currentMillis; ПРОПУЩЕНА
  digitalWrite(13, !digitalRead(13));
}
АПрограмма не соберётся — компилятор потребует эту строку
БУсловие останется истинным всегда, действие будет выполняться на каждом проходе loop() без пауз
ВНичего не изменится, previousMillis обновится автоматически

Подсказка: previousMillis никогда не меняется — значит разность currentMillis — previousMillis будет только расти и оставаться больше interval.

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Контрольный вопрос. currentMillis = 900. Для LED1 (prevLed1=500, interval1=300) и LED2 (prevLed2=700, interval2=300) — какой из них сработает?

Исходный код для этого задания:

if (currentMillis - prevLed1 >= interval1) { /* LED1 */ }
if (currentMillis - prevLed2 >= interval2) { /* LED2 */ }
АLED1
Бни один
ВLED2
Гоба

Подсказка: Посчитай разность для каждого отдельно: 900-500 и 900-700, сравни обе с 300.

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Задание. Напиши код сам. Допиши условие вместо ___, которое проверяет, что прошло не меньше interval миллисекунд с previousMillis.

Исходный код для этого задания:

unsigned long currentMillis = millis();
if (___) {
  previousMillis = currentMillis;
}

Подсказка: Разность текущего и предыдущего времени должна быть не меньше interval — сравнение через >=.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Задание. Напиши программу целиком (около 11 строк). Пин 8 — светодиод. Через каждые 400 миллисекунд переключай его состояние (было включено — выключи, и наоборот), используя millis(), без единого delay().

Исходный код для этого задания:

// объяви previousMillis, interval=400, ledState и напиши setup()+loop()

Подсказка: Собери из паттерна занятия: unsigned long previousMillis=0, const long interval=400, bool ledState=false снаружи; в loop() — currentMillis, сравнение, если сработало — обновить previousMillis, инвертировать ledState, digitalWrite.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Проверить ответ →

Реальная проверка — на нашей платформе, бесплатно и без регистрации. Открыть задачу и ввести ответ →

Контрольный вопрос. В программе используется millis() для интервалов, но внутри loop() всё равно стоит delay(50) «для стабильности». Что это значит для отзывчивости станции?

АСмысл millis() частично теряется — на каждые 50 мс программа всё равно перестаёт реагировать на события
БЭто обязательная практика — millis() без delay() вообще не работает
ВНичего не меняется — delay(50) слишком мал, чтобы иметь значение

Подсказка: Любой delay() внутри loop(), даже маленький, снова блокирует выполнение на это время.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Тип unsigned long, в котором хранится millis(), физически переполняется примерно через 50 дней непрерывной работы платы (счётчик доходит до предела и начинается заново с 0). Что произойдёт с вычислением currentMillis — previousMillis в этот момент, если оба значения — unsigned long?

АПрограмма аварийно остановится в момент переполнения
ББлагодаря особенностям арифметики беззнаковых чисел разность всё равно посчитается корректно
ВРазность станет отрицательным числом и сравнение сломается

Подсказка: Подумай, что физически происходит с unsigned-типом при переполнении — он не уходит в минус, а начинается заново с 0. Как это отражается на результате вычитания двух близких значений через границу переполнения?

Онлайн-проверка ответа появится позже

Задание. Напиши программу целиком (около 14 строк). Два независимых процесса без единого delay(): LED на пине 13 переключается каждые 500 мс, LED на пине 12 — каждые 350 мс. У каждого — своя пара previousMillis/ledState.

Исходный код для этого задания:

// заведи две независимые пары previousMillis/ledState и два interval, напиши setup()+loop()

Подсказка: Повтори паттерн одного процесса дважды с разными именами переменных (prevLed1/prevLed2, state1/state2) — оба блока if идут подряд в одном loop(), не мешая друг другу.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Онлайн-проверка ответа появится позже

Что дальше

Теперь станция умеет вести время, не останавливаясь. На следующем занятии подключим настоящий датчик среды — DHT22 — и научимся пользоваться внешней библиотекой.

Итог: ты понимаешь, почему delay() блокирует программу, умеешь вести интервал через millis() без остановки кода и вести несколько независимых процессов сразу. Дальше — библиотеки и реальный датчик DHT22.

Назад  ·  ↑ В начало урока  ·  ⌂ В начало курса  ·  Вперёд →

Школа Виктора Комлева