Основы · 9 мин чтения

Arduino millis() вместо delay(): тайминг без блокировки кода

Разбираем millis() в Arduino: почему delay() замораживает скетч, мигание без delay, несколько задач сразу, отсчёт без сбоя через 49 дней, micros(), ESP32 и Pico.

Функция millis() в Arduino возвращает время с момента запуска платы в миллисекундах, и это стандартный способ отсчитывать время без delay(). Когда в коде стоит delay(1000), Arduino целую секунду больше ничего не делает: нажатие кнопки остаётся незамеченным, датчики не опрашиваются, дисплей не обновляется. С millis() вопрос звучит иначе: «сколько времени прошло с последнего выполнения задачи?» Ты выполняешь задачи, которым пора, пропускаешь остальные, а loop() никогда не останавливается. Так одна плата Arduino может одновременно мигать светодиодами, читать кнопки, измерять показания датчиков и отправлять отчёты в последовательный порт.

В этом руководстве разберём, почему delay() блокирует программу, знаменитый пример blink without delay (мигание без delay), как запускать несколько задач одновременно, как безопасно пережить переполнение через 49,7 дня, функцию micros(), которая считает микросекунды, и готовые шаблоны таймеров. В последних разделах посмотрим, как та же логика пишется на ESP32 и на Raspberry Pi Pico (MicroPython).

Плата Arduino Uno и её встроенный светодиод L
Arduino Uno: благодаря встроенному светодиоду «L» на пине 13 для опытов с millis() не нужны дополнительные детали.

Что такое delay() и почему она останавливает программу?

delay(ms) заставляет процессор крутиться в цикле, пока не пройдёт заданное время. Всё это время следующая строка в loop() не выполняется; это называется блокирующим ожиданием. Для мигания одним светодиодом этого достаточно, но проблемы начинаются, как только в проекте появляется кнопка. Код ниже раз в секунду мигает светодиодом и заодно проверяет, нажата ли кнопка:

C++ (Arduino IDE)
1const int LED_PIN = 13;                   // dahili LED / built-in LED
2const int BUTON_PIN = 2;                  // buton, diğer ucu GND / button, other leg to GND
3
4void setup() {
5  pinMode(LED_PIN, OUTPUT);
6  pinMode(BUTON_PIN, INPUT_PULLUP);
7  Serial.begin(9600);
8}
9
10void loop() {
11  digitalWrite(LED_PIN, HIGH);
12  delay(1000);                            // 1 sn bekle, buton okunmaz / wait 1 s, button is not read
13  digitalWrite(LED_PIN, LOW);
14  delay(1000);
15  if (digitalRead(BUTON_PIN) == LOW) {    // 2 saniyede bir bakılır / checked once every 2 s
16    Serial.println(F("Butona basildi / Button pressed"));
17  }
18}

В этом коде кнопку замечают лишь раз в две секунды, и то только если её держат именно в этот момент; короткое нажатие теряется между двумя вызовами delay(). Прерывания во время delay() продолжают работать: millis() считает дальше, а байты, пришедшие в последовательный порт, складываются в 64-байтный буфер. Но твой код ждёт. Стоит добавить дисплей, датчики и зуммер, как каждая delay() тормозит всю остальную работу и устройство начинает казаться неповоротливым. Решение — перестать ждать и начать измерять время.

Что такое millis() и как она работает?

millis() возвращает число миллисекунд с момента включения или сброса платы в виде unsigned long (32 бита, без знака). На Arduino Uno этот счётчик обновляется в фоне прерыванием по переполнению аппаратного таймера Timer0. На частоте 16 MHz Timer0 переполняется каждые 1,024 мс; код ядра накапливает лишние 0,024 мс и примерно каждые 43 мс увеличивает счётчик не на 1, а на 2. Поэтому некоторые значения millis() просто пропускает, и проверки на равенство вроде millis() == 5000 ненадёжны; всегда используй >=. Учти также, что при отключённых прерываниях (например, внутри обработчика прерывания) millis() не растёт.

ФункцияЧто делаетРазрешение (Uno, 16 MHz)Переполнение черезБлокирует программу?
delay(ms)Ждёт заданное число миллисекунд1 мс-Да
delayMicroseconds(us)Короткое ожидание (точно от 3 до 16383 µs)Около 1 µs-Да
millis()Миллисекунды с момента запуска1 мс (иногда шаг 2)Около 49,7 дняНет
micros()Микросекунды с момента запуска4 µsОколо 71,6 минутыНет
  1. Заведи переменную типа unsigned long, в которой хранится момент последнего выполнения задачи (oncekiZaman).
  2. При каждом проходе loop() читай текущее время через millis().
  3. Проверяй, прошло ли достаточно времени, условием simdi - oncekiZaman >= ARALIK.
  4. Если время вышло, выполни задачу (переключи светодиод) и обнови oncekiZaman.
  5. Если нет, ничего не делай и дай loop() идти дальше; именно в этот момент работают остальные задачи.
C++ (Arduino IDE)
1const int LED_PIN = LED_BUILTIN;          // Uno'da 13 / pin 13 on the Uno
2const unsigned long ARALIK = 500;         // yanıp sönme aralığı (ms) / blink interval (ms)
3
4unsigned long oncekiZaman = 0;            // son değişim anı / time of the last toggle
5bool ledDurumu = false;
6
7void setup() {
8  pinMode(LED_PIN, OUTPUT);
9}
10
11void loop() {
12  unsigned long simdi = millis();         // şu anki zaman / current time
13  if (simdi - oncekiZaman >= ARALIK) {    // taşmaya karşı güvenli / overflow-safe
14    oncekiZaman += ARALIK;                // kaymayı önler / prevents drift
15    ledDurumu = !ledDurumu;
16    digitalWrite(LED_PIN, ledDurumu);
17  }
18  // burada başka işler beklemeden çalışır / other work runs here without waiting
19}

В коде стоит oncekiZaman += ARALIK, а не oncekiZaman = simdi. Разница небольшая, но важная: если loop() иногда опаздывает на несколько миллисекунд, второй способ каждый раз прибавляет это опоздание, и за несколько часов светодиод «уплывает», а первый сдвигает целевое время фиксированными шагами, поэтому средний период остаётся ровно 500 мс. Используй += для часов, метрономов и регулярных измерений. Если программа может надолго заблокироваться (например, пока ждёт Wi-Fi), += будет пытаться наверстать пропущенные шаги один за другим; в таком случае надёжнее = simdi. Если хочешь ещё и менять яркость светодиода, не останавливая код, загляни в статью PWM и analogWrite.

Несколько дел одновременно: многозадачность с millis()

У Arduino Uno одноядерный процессор, и по-настоящему делать два дела одновременно он не может. Но loop() способна выполняться десятки тысяч раз в секунду, поэтому если давать каждой задаче несколько микросекунд, когда подходит её очередь, со стороны всё выглядит «одновременным». Это называется кооперативной многозадачностью. Правило простое: у каждой задачи своя переменная времени и свой интервал, и ни одна задача не ждёт долго. В примере ниже бок о бок работают четыре задачи:

C++ (Arduino IDE)
1const int KIRMIZI_LED = 8;                // 300 ms'de bir yanıp söner / blinks every 300 ms
2const int YESIL_LED = 9;                  // 1000 ms'de bir yanıp söner / blinks every 1000 ms
3const int SARI_LED = 10;                  // butonla açılıp kapanır / toggled by the button
4const int BUTON = 2;                      // diğer ucu GND / other leg to GND
5
6unsigned long kirmiziZaman = 0, yesilZaman = 0, butonZaman = 0, seriZaman = 0;
7bool kirmizi = false, yesil = false, sari = false;
8int sonOkuma = HIGH;                      // son ham okuma / last raw reading
9int kararliDurum = HIGH;                  // titreşimsiz durum / debounced state
10
11void setup() {
12  pinMode(KIRMIZI_LED, OUTPUT);
13  pinMode(YESIL_LED, OUTPUT);
14  pinMode(SARI_LED, OUTPUT);
15  pinMode(BUTON, INPUT_PULLUP);
16  Serial.begin(9600);
17}
18
19void loop() {
20  unsigned long simdi = millis();
21
22  // Görev 1: kırmızı LED / Task 1: red LED
23  if (simdi - kirmiziZaman >= 300) {
24    kirmiziZaman = simdi;
25    kirmizi = !kirmizi;
26    digitalWrite(KIRMIZI_LED, kirmizi);
27  }
28
29  // Görev 2: yeşil LED / Task 2: green LED
30  if (simdi - yesilZaman >= 1000) {
31    yesilZaman = simdi;
32    yesil = !yesil;
33    digitalWrite(YESIL_LED, yesil);
34  }
35
36  // Görev 3: 50 ms debounce ile buton / Task 3: button with 50 ms debounce
37  int okuma = digitalRead(BUTON);
38  if (okuma != sonOkuma) {                // değişti, sayacı yeniden başlat / changed, restart the timer
39    butonZaman = simdi;
40    sonOkuma = okuma;
41  }
42  if (simdi - butonZaman >= 50 && okuma != kararliDurum) {
43    kararliDurum = okuma;
44    if (kararliDurum == LOW) {            // basıldı / pressed
45      sari = !sari;
46      digitalWrite(SARI_LED, sari);
47    }
48  }
49
50  // Görev 4: saniyede bir seri rapor / Task 4: serial report once per second
51  if (simdi - seriZaman >= 1000) {
52    seriZaman = simdi;
53    Serial.print(F("Calisma suresi (s) / Uptime (s): "));
54    Serial.println(simdi / 1000);
55  }
56}
ЗадачаПинИнтервалЧто делает
Красный светодиод8300 мсМигает быстро
Зелёный светодиод91000 мсМигает медленно
Кнопка + жёлтый светодиод2 и 10Антидребезг 50 мсПереключает жёлтый светодиод при каждом нажатии
Отчёт в порт-1000 мсВыводит время работы в секундах

Логика кнопки тоже построена на millis(): при каждом изменении сырого показания таймер запускается заново, и если показание держится стабильным 50 мс, нажатие считается настоящим. Так дребезг контактов кнопки не превращает одно нажатие в десятки, а код никогда не ждёт. Подробнее о подключении кнопки и встроенном подтягивающем резисторе читай в статье Как подключить кнопку к Arduino. Для светодиодов достаточно резистора 220 Ω; для других цветов и токов воспользуйся калькулятором резистора для светодиода.

Переполнение через 49,7 дня и сравнения, устойчивые к переполнению

unsigned long вмещает не больше 4 294 967 295. Когда millis() доходит до этого числа, примерно через 49 дней, 17 часов и 2 минуты, счёт снова начинается с 0. Если плата работает неделями (управление теплицей, аквариум, сигнализация), этот момент наступит. Хорошая новость: код с вычитанием вида simdi - onceki >= aralik переполнения не боится. Беззнаковое вычитание выполняется по модулю 2 в степени 32. Например, если onceki = 4 294 967 000, а после переполнения simdi = 200, то simdi - onceki даёт не отрицательное число, а правильный результат 496. Поэтому, пока ожидаемый интервал короче 49,7 дня, сравнение всегда работает.

КодРезультатПочему?
if (millis() - onceki >= aralik) (форма с вычитанием)ВерноБеззнаковое вычитание даёт правильную разницу даже через переполнение
if (millis() >= onceki + aralik) (форма со сложением)НеверноКогда onceki + aralik переполняется, сумма становится маленьким числом и условие срабатывает слишком рано
if (millis() == hedef) (проверка на равенство)Неверноmillis() иногда шагает на 2, поэтому точное значение может так и не появиться
int onceki = millis(); (16-битный int)НеверноНа Uno int 16-битный и после 32,7 секунды хранит неверные значения
long onceki = millis(); (знаковый long)РискованноЗнаковый тип через 24,8 дня уходит в минус; чтобы не путаться, используй unsigned long
Никогда не объявляй переменные времени как int. На Arduino Uno int 16-битный и вмещает не больше 32 767, поэтому начиная с 33-й секунды программа начинает вести себя странно. Каждая переменная, которая работает с millis() или micros(), должна быть unsigned long (или uint32_t). Большие интервалы тоже записывай в виде 60000UL: на Uno произведение вроде 60 * 1000 выходит за предел int и превращается в отрицательное число.

Измеряем микросекунды с micros()

micros() работает так же, но возвращает микросекунды с момента запуска. На платах с частотой 16 MHz, таких как Arduino Uno и Nano, её разрешение 4 µs (8 µs на платах 8 MHz), а переполняется она примерно каждые 71,6 минуты; правило вычитания действует и здесь. Её используют для точных задач: измерить, сколько выполняется фрагмент кода, засечь длительность коротких импульсов или генерировать звук. Пример ниже дважды в секунду измеряет, сколько длится вызов analogRead(); на Uno получится около 110–120 µs:

C++ (Arduino IDE)
1unsigned long raporZaman = 0;
2
3void setup() {
4  Serial.begin(9600);
5}
6
7void loop() {
8  if (millis() - raporZaman >= 500) {         // yarım saniyede bir ölç / measure every half second
9    raporZaman = millis();
10    unsigned long basla = micros();           // başlangıç (µs) / start (µs)
11    int deger = analogRead(A0);
12    unsigned long sure = micros() - basla;    // geçen süre (µs) / elapsed time (µs)
13    Serial.print(F("A0 = "));
14    Serial.print(deger);
15    Serial.print(F("  sure / time: "));
16    Serial.print(sure);
17    Serial.println(F(" us"));
18  }
19}

Для длинных интервалов выбирай millis(), а не micros(); период переполнения в 71 минуту слишком короткий для долгих ожиданий. delayMicroseconds() нужна только для коротких неизбежных пауз в несколько микросекунд, например чтобы подать импульс запуска 10 µs на пин TRIG ультразвукового датчика расстояния HC-SR04. Такие короткие паузы заметно не задерживают остальные задачи.

Шаблоны таймеров: периодический, однократный и конечный автомат

  • Периодическая задача: работа, которая повторяется через заданный интервал: мигающий светодиод, опрос датчика каждые 2 секунды или обновление дисплея раз в секунду. Этот шаблон уже разобран выше.
  • Однократный таймер (таймаут): срабатывает один раз через заданное время после события. Хороший пример — свет на лестнице: при движении включи свет, запомни время и выключи, если 30 секунд не было нового движения. Если при новом движении обновлять время, период продлевается.
  • Ожидание с таймаутом: когда ждёшь ответа или сигнала, задаётся верхний предел, чтобы никогда не зависнуть навечно; например, показать ошибку, если данные не пришли за 2 секунды.
  • Конечный автомат: текущий шаг устройства хранится в переменной, и когда время шага выходит, программа переходит к следующему. Так пишут светофоры, программы стиральной машины и уровни в играх.
  • Распознавание долгого нажатия: если измерять через millis(), сколько держат кнопку, коротким и долгим нажатиям можно назначить разные действия.
C++ (Arduino IDE)
1// Yeniden kullanılabilir zamanlayıcı / Reusable timer
2struct Zamanlayici {
3  unsigned long baslangic = 0;
4  unsigned long sure = 0;
5
6  void kur(unsigned long ms) {              // süreyi şimdiden başlat / start the period from now
7    baslangic = millis();
8    sure = ms;
9  }
10  bool doldu() const {                      // süre doldu mu? / has it expired?
11    return millis() - baslangic >= sure;
12  }
13};
14
15const int KIRMIZI = 11, SARI = 12, YESIL = 13;
16enum Durum { DUR, HAZIRLAN, GEC, YAVASLA }; // trafik lambası adımları / traffic light states
17Durum durum = DUR;
18Zamanlayici isik;
19
20void lambalar(bool k, bool s, bool y) {
21  digitalWrite(KIRMIZI, k);
22  digitalWrite(SARI, s);
23  digitalWrite(YESIL, y);
24}
25
26void setup() {
27  pinMode(KIRMIZI, OUTPUT);
28  pinMode(SARI, OUTPUT);
29  pinMode(YESIL, OUTPUT);
30  lambalar(true, false, false);
31  isik.kur(5000);                           // kırmızı 5 sn / red for 5 s
32}
33
34void loop() {
35  if (isik.doldu()) {
36    switch (durum) {
37      case DUR:      durum = HAZIRLAN; lambalar(true, true, false);  isik.kur(1500); break;
38      case HAZIRLAN: durum = GEC;      lambalar(false, false, true); isik.kur(5000); break;
39      case GEC:      durum = YAVASLA;  lambalar(false, true, false); isik.kur(2000); break;
40      case YAVASLA:  durum = DUR;      lambalar(true, false, false); isik.kur(5000); break;
41    }
42  }
43  // loop serbest: buton, sensör, ekran... / loop stays free: buttons, sensors, display...
44}

В этом коде маленькая структура (struct) Zamanlayici сводит запуск периода и проверку, истёк ли он, к одной строке. Светофор — это конечный автомат из четырёх шагов: на каждом шаге задаётся состояние ламп и запускается отсчёт до следующего перехода. Поскольку loop() никогда не ждёт, к той же плате легко добавить кнопку для пешеходов, датчик освещённости на фоторезисторе (LDR) для ночного режима или дисплей с оставшимся временем. Если таймеров нужно много, готовые библиотеки таймеров из «Менеджера библиотек» (Library Manager) работают по тому же принципу.

millis(), delay() и Ticker на ESP32

В ядре ESP32 для Arduino у millis() и micros() те же имена и тот же 32-битный тип unsigned long; их значения вычисляются из 64-битного счётчика esp_timer_get_time(). Значит, периоды переполнения 49,7 дня и 71,6 минуты действуют и на ESP32, и правило вычитания то же. Важное отличие — в delay(): на ESP32 delay() вызывает vTaskDelay() из FreeRTOS. Процессор не крутится вхолостую; пока идёт ожидание, работают другие задачи, например Wi-Fi и Bluetooth. Но твоя loop() всё равно ждёт, поэтому веб-сервер, MQTT или опрос кнопок запаздывают. Для периодических задач удобна и библиотека Ticker, которая входит в ядро ESP32:

C++ (Arduino IDE)
1#include <Ticker.h>
2
3const int LED1 = 2;                         // çoğu DevKit'te dahili LED / built-in LED on most DevKits
4const int LED2 = 4;
5
6Ticker ticker;
7bool led1Durum = false;
8bool led2Durum = false;
9unsigned long oncekiZaman = 0;
10
11void led1Degistir() {                       // kısa tut, uzun iş yapma / keep it short, no long work
12  led1Durum = !led1Durum;
13  digitalWrite(LED1, led1Durum);
14}
15
16void setup() {
17  Serial.begin(115200);
18  pinMode(LED1, OUTPUT);
19  pinMode(LED2, OUTPUT);
20  ticker.attach_ms(250, led1Degistir);      // her 250 ms'de çağır / call every 250 ms
21}
22
23void loop() {
24  unsigned long simdi = millis();
25  if (simdi - oncekiZaman >= 1000) {        // Arduino ile aynı desen / same pattern as on Arduino
26    oncekiZaman = simdi;
27    led2Durum = !led2Durum;
28    digitalWrite(LED2, led2Durum);
29    int64_t us = esp_timer_get_time();      // 64 bit, pratikte taşmaz / 64-bit, practically never overflows
30    Serial.printf("millis=%lu  esp_timer=%lld us\n", simdi, us);
31  }
32}

Функцию, переданную в Ticker, вызывает в фоне задача таймера; не делай в ней ничего медленного (HTTP-запросы, длинный вывод в порт, delay), только поставь флаг или переключи пин. 64-битный esp_timer_get_time() не переполнится сотни тысяч лет, так что его можно использовать для измерений с микросекундной точностью на устройстве, которое работает месяцами. Пин встроенного светодиода на разных платах может отличаться; карту пинов своей платы можно посмотреть в утилите Распиновка. Если ESP32 для тебя в новинку, для установки и выбора пинов хорошо начать со статьи Что такое ESP32?.

Raspberry Pi Pico (MicroPython): ticks_ms() и ticks_diff()

В MicroPython вместо delay() используются time.sleep(), time.sleep_ms() и time.sleep_us(), а вместо millis() — time.ticks_ms(). Но будь внимательнее: значения тиков сбрасываются не на 2 в степени 32, как на Arduino, а на меньшем пределе, который зависит от порта. На Pico этот предел — 2 в степени 30 мс, около 12,4 дня (проверить на своей плате можно через time.ticks_add(0, -1)). Поэтому никогда не вычитай и не сравнивай два значения тиков напрямую; разницу получай через time.ticks_diff(new, old), а время в будущем считай через time.ticks_add(). Код ниже мигает встроенным светодиодом Pico без блокировки, а кнопка на GP14 переключает светодиод на GP15:

MicroPython
1from machine import Pin
2import time
3
4led = Pin("LED", Pin.OUT)                       # dahili LED (Pico ve Pico W) / onboard LED (Pico and Pico W)
5dis_led = Pin(15, Pin.OUT)                      # GP15'e harici LED / external LED on GP15
6buton = Pin(14, Pin.IN, Pin.PULL_UP)            # GP14 buton, diğer ucu GND / GP14 button, other leg to GND
7
8ARALIK = 500                                    # ms
9onceki = time.ticks_ms()
10son_okuma = 1                                   # son ham okuma / last raw reading
11kararli = 1                                     # titreşimsiz durum / debounced state
12degisim = time.ticks_ms()                       # son değişim anı / time of the last change
13
14while True:
15    simdi = time.ticks_ms()
16    if time.ticks_diff(simdi, onceki) >= ARALIK:   # taşmaya karşı güvenli / wrap-safe
17        onceki = time.ticks_add(onceki, ARALIK)    # kaymasız / drift-free
18        led.toggle()
19    okuma = buton.value()
20    if okuma != son_okuma:                      # değişti, sayacı yeniden başlat / changed, restart the timer
21        degisim = simdi
22        son_okuma = okuma
23    if time.ticks_diff(simdi, degisim) >= 50 and okuma != kararli:   # 50 ms debounce
24        kararli = okuma
25        if kararli == 0:                        # basıldı / pressed
26            dis_led.toggle()
27

Для периодических задач в MicroPython есть и machine.Timer: строка Timer(period=500, mode=Timer.PERIODIC, callback=function) вызывает твою функцию каждые 500 мс. Делай такие обработчики короткими, как и с Ticker. Если программировать Pico из Arduino IDE (с ядром arduino-pico), millis() и micros() работают точно так же, как на Arduino. Как установить MicroPython на Pico и разобраться с её пинами, читай в статье Что такое Raspberry Pi Pico?.

Идеи проектов с millis()

  • Тест реакции: зажги светодиод в случайный момент, измерь через millis() время до нажатия кнопки и покажи результат в миллисекундах.
  • Свет на лестнице: включай свет, когда PIR-датчик замечает движение, и выключай через 30 секунд после последнего движения, а код тем временем занимается другими делами.
  • Парктроник с ускоряющимся писком: сокращай интервал сигналов зуммера по мере того, как расстояние, измеренное HC-SR04, уменьшается.
  • Мелодия без блокировки: переключай ноты на зуммере через millis(), а не через delay(), чтобы кнопки работали, пока играет мелодия.
  • Датчик и дисплей вместе: опрашивай датчик DHT11 каждые 2 секунды, а дисплей обновляй каждые 250 мс.
  • Секундомер: собери секундомер с кнопками «старт», «стоп» и «сброс», который показывает время на 7-сегментном индикаторе.

Частые ошибки и как их исправить

ОшибкаПризнакРешение
Переменная времени объявлена как intПримерно через 33 секунды программа сходит с умаСделай все переменные времени unsigned long
Запись millis() >= onceki + aralikЧерез 49,7 дня отсчёт времени ломаетсяПерейди на форму millis() - onceki >= aralik
Проверка millis() == hedefСобытие иногда так и не срабатываетИспользуй >= вместо равенства
onceki не обновляетсяКогда время вышло, задача повторяется на каждом проходеВнутри условия пиши onceki = simdi или onceki += aralik
delay(), оставшаяся в коде таймераОстальные задачи всё равно запаздываютРазбей долгие ожидания на конечный автомат
Ожидание через millis() внутри прерыванияПлата зависаетНикогда не жди внутри ISR; только ставь флаг
Произведение типа int, например 60 * 1000Интервал получается отрицательным или слишком короткимПиши 60000UL или 60UL * 1000
Простой способ проверить, что код действительно работает без блокировки, — посчитать, сколько раз в секунду выполняется loop(): увеличивай счётчик на каждом проходе, раз в секунду выводи его в последовательный порт и обнуляй. Если число внезапно падает с тысяч до однозначного, где-то что-то ждёт (delay, pulseIn или долгий цикл while).

Когда освоишься с millis(), проекты изменятся полностью: светодиоды, кнопки, датчики и дисплеи работают вместе и не ждут друг друга. Для следующего шага можно выбрать проект, где нужен отсчёт времени, из списка идей проектов на Arduino. В приложении BlueGrays можно пошагово, со схемой подключения и готовым кодом, изучить такие проекты, как «Светофор», «Полицейская мигалка», «Умное освещение лестницы и коридора» и «Тест реакции со звуковым сигналом», и попробовать их на своей плате.

Частые вопросы

Что такое millis() в Arduino?

millis() — функция, которая возвращает время с момента включения или сброса Arduino в миллисекундах. Её значение имеет тип unsigned long и примерно через 49,7 дня снова начинается с нуля. Её используют, чтобы отсчитывать время, не останавливая программу.

Чем millis() отличается от delay()?

delay() приостанавливает программу на заданное время, поэтому ничего другого, например опроса кнопок или обновления дисплея, в это время не происходит. millis() только сообщает, сколько прошло времени, и никогда не ждёт. Если проверять прошедшее время и выполнять задачи, которым пора, можно запускать несколько задач одновременно.

Сломается ли программа, когда millis() переполнится через 49 дней?

Нет, если проверять время вычитанием вида millis() - onceki >= aralik; беззнаковое вычитание даёт правильную разницу даже через переполнение. Проблемы появляются только у сравнений со сложением, вроде millis() >= onceki + aralik, и у переменных типа int.

Можно ли сбросить millis() на Arduino?

Официальной функции для сброса millis() нет, а менять внутреннюю переменную ядра не рекомендуется. Да это и не нужно: сохрани в переменную момент, от которого хочешь отсчитывать, и считай прошедшее время от него. При сбросе платы millis() естественным образом начинается с 0.

Может ли Arduino делать два дела одновременно?

У Uno одно ядро, поэтому физически выполнять две инструкции одновременно она не может, но если разбить каждую задачу на короткие шаги с millis(), loop() выполняется тысячи раз в секунду и даёт очередь каждой задаче. Со стороны кажется, что светодиод, кнопка, датчик и дисплей работают одновременно. У ESP32 к тому же два ядра и задачи FreeRTOS.

Достаточно ли точна millis(), чтобы сделать часы?

millis() зависит от генератора платы; из-за керамического резонатора на Arduino Uno она может уходить на несколько минут в сутки. Для измерения коротких интервалов этого хватает, но для часов, которые должны идти верно днями, точнее модуль RTC, например DS1307 или DS3231, или получение времени по NTP на ESP32.

Все проекты
Мигающий светодиод
#1Новичок

Мигающий светодиод

Самое первое «привет» в электронике: включение и выключение одного светодиода. Arduino включает и выключает светодиод через короткие промежутки, поэтому свет быстро мигает. Ты самым простым способом в

4.6(188)
1127
Светофор
#3Новичок

Светофор

Крошечная копия светофора, который ты каждый день видишь на дороге. Красный горит две секунды, жёлтый мигает на мгновение, зелёный горит две секунды, затем через жёлтый снова возвращается красный, и т

4.8(131)
397
Полицейская мигалка на светодиодах
#4Новичок

Полицейская мигалка на светодиодах

Как огни на полицейской машине: красный светодиод три раза быстро вспыхивает, затем три раза вспыхивает синий, и узор повторяется без перерыва. Нужны всего два светодиода и два резистора, подключённые

4.7(103)
258
Бегущие огни с регулируемой скоростью
#7Средний

Бегущие огни с регулируемой скоростью

Шесть светодиодов загораются друг за другом, и кажется, что свет бежит от одного края к другому. Поверни ручку потенциометра, и скорость этого бега сразу меняется: в одну сторону свет мчится, в другую

4.6(72)
146
Включение и выключение светодиода кнопкой (вкл/выкл)
#10Новичок

Включение и выключение светодиода кнопкой (вкл/выкл)

Нажми кнопку один раз — свет загорится, нажми ещё раз — погаснет, совсем как выключатель в твоей комнате. Arduino замечает каждое нажатие и переводит светодиод в противоположное состояние, выжидая кор

4.8(100)
167
Умное освещение лестницы и коридора
#82Средний
Premium

Умное освещение лестницы и коридора

Ступи на лестницу или в коридор, и свет загорается ступенька за ступенькой в ту сторону, куда ты идёшь. На каждом конце стоит датчик расстояния; мимо какого ни пройдёшь, восемь светодиодов загораются

5.0(35)
51
Тест времени реакции на Arduino со звуковым сигналом: измерь рефлексы в миллисекундах (LCD + зуммер + 3 светодиода)
#97Средний
Premium

Тест времени реакции на Arduino со звуковым сигналом: измерь рефлексы в миллисекундах (LCD + зуммер + 3 светодиода)

Насколько быстрые у тебя рефлексы? Нажми кнопку: красный светодиод говорит приготовиться, жёлтый — внимание, и начинается случайное ожидание от 1,5 до 3,5 секунды. Как только загорится зелёный светодиод и пискнет зуммер, нажми снова; время реакции в миллисекундах появится на I2C LCD, а под ним — лучший и средний результат сессии. Нажмёшь до зелёного — на экране TOO EARLY! и звук проигрыша. Удержание кнопки — новый игрок, и у всех одинаковый порядок ожиданий.

4.6(33)
50
Цифровые часы на 4-разрядном семисегментном индикаторе
#48Средний
Premium

Цифровые часы на 4-разрядном семисегментном индикаторе

Светящиеся цифровые часы из четырёх разрядов, почти как на кухонной духовке. Модуль часов DS1302 хранит время, а Arduino каждую секунду читает часы и минуты и показывает их на четырёхразрядном индикат

4.8(56)
73