Arduino millis() – jak zrobić opóźnienie bez delay()

Arduino UNO i analogowy stoper ilustrujące odmierzanie czasu funkcją millis()

Arduino millis() pozwala odmierzać czas bez zatrzymywania programu. Zamiast czekać w delay(), pętla loop() może nadal odczytywać przyciski i czujniki, obsługiwać komunikację oraz wykonywać kilka zadań w różnych odstępach czasu.

Ładowanie interaktywnego porównania millis() i delay()…

Najważniejsza zasada: zapamiętaj poprzedni czas i sprawdzaj różnicę: millis() - poprzedniCzas >= interwał. Nie czekasz na upływ czasu — tylko regularnie pytasz, czy zadanie należy już wykonać.

Co robi millis() w Arduino?

Funkcja millis() zwraca liczbę milisekund, które upłynęły od rozpoczęcia działania bieżącego programu. Nie zatrzymuje szkicu i nie tworzy opóźnienia. Jest jedynie zegarem, który można odczytać w dowolnym miejscu programu.

Wynik ma typ unsigned long. Na klasycznym Arduino UNO jest to 32-bitowa liczba bez znaku. Zamiast próbować przewidzieć przyszłą wartość zegara, zapisujemy aktualny odczyt, a następnie obliczamy, ile czasu minęło.

Kod_001 — podstawowy wzorzec millis()

Zmienna teraz jest znacznikiem aktualnego czasu, a poprzedniCzas przechowuje moment ostatniego wykonania zadania. Warunek staje się prawdziwy dopiero wtedy, gdy różnica osiągnie ustalony interwał.

delay() a millis() — na czym polega różnica?

Cechadelay()millis()
Działanie programuZatrzymuje dalsze wykonywanie kodu na podany czasPętla loop() działa cały czas
Obsługa przyciskuKrótkie naciśnięcie podczas oczekiwania może zostać pominiętePrzycisk może być sprawdzany w każdym obiegu pętli
Kilka zadańKolejne opóźnienia sumują się i wzajemnie blokująKażde zadanie może mieć własny licznik czasu
Poziom trudnościBardzo prostyWymaga zapamiętania czasu i stanu zadania
Typowe użycieKrótka sekwencja startowa lub prosty testPrzyciski, czujniki, wyświetlacze, komunikacja i automatyka

delay() nie jest błędem samym w sobie. Problem pojawia się, gdy urządzenie podczas czekania powinno zareagować na inne zdarzenie. Jeśli dioda ma migać, a Arduino jednocześnie obsługiwać przycisk, wyświetlacz lub dane z portu szeregowego, blokujące opóźnienie szybko staje się przeszkodą.

Miganie diodą LED bez delay()

Poniższy przykład zmienia stan wbudowanej diody co sekundę. Pętla nie zatrzymuje się — warunek czasowy jest sprawdzany wielokrotnie, ale kod wewnątrz if wykonuje się tylko po upływie interwału.

Kod_002 — Blink Without Delay po polsku

Zmienna stanLED jest potrzebna, ponieważ program musi pamiętać, czy dioda była ostatnio włączona. To ważna część programowania bez blokowania: oprócz czasu zapisujemy również aktualny stan urządzenia.

Kilka niezależnych timerów millis()

Każde zadanie otrzymuje własny interwał i własną zmienną poprzedniego czasu. Dzięki temu jedna dioda może zmieniać stan co 500 ms, druga co 1300 ms, a czujnik może być odczytywany jeszcze w innym rytmie.

Kod_003 — dwa zadania wykonywane niezależnie

To nie jest prawdziwa wielowątkowość. Mikrokontroler nadal wykonuje instrukcje kolejno, lecz robi to na tyle szybko, że zadania sprawiają wrażenie równoczesnych. Każdy fragment kodu powinien kończyć się szybko i oddawać sterowanie do loop().

Jak obsługiwać przycisk podczas odmierzania czasu?

Odczyt przycisku należy umieścić poza warunkiem timera, aby był wykonywany w każdym obiegu pętli. Samo zastosowanie millis() nie usuwa drgań styków — debounce wymaga osobnego krótkiego timera lub odpowiedniej biblioteki.

Kod_004 — LED miga, a przycisk nadal działa

Przy konfiguracji INPUT_PULLUP wciśnięty przycisk daje stan LOW. Kod powyżej służy do pokazania responsywności; w docelowym projekcie trzeba dodać wykrywanie pojedynczego naciśnięcia i eliminację drgań styków.

Przepełnienie millis() po około 50 dniach

Wartość millis() na typowych 8-bitowych płytkach Arduino wraca do zera po około 49,7 dnia. Poprawnie napisany warunek nadal działa po przepełnieniu, ponieważ odejmowanie dwóch wartości typu unsigned long zachowuje prawidłowy czas, który upłynął.

Nie porównuj przyszłego czasu w ten sposób: if (millis() > poprzedniCzas + interwał). Dodawanie może przekroczyć maksymalną wartość licznika. Stosuj odejmowanie: if (millis() - poprzedniCzas >= interwał).
Kod_005 — wzorzec odporny na przepełnienie

Wszystkie zmienne przechowujące czas oraz interwały powinny mieć typ unsigned long. Dla stałych można używać przyrostka UL, na przykład 60000UL.

poprzedniCzas = teraz czy poprzedniCzas += interwał?

Zapis poprzedniCzas = teraz jest prosty i dobry dla większości projektów. Następny interwał jest liczony od rzeczywistego momentu wykonania zadania, dlatego niewielkie opóźnienia pętli mogą stopniowo przesuwać rytm.

Zapis poprzedniCzas += interwał pomaga zachować stały harmonogram. Trzeba jednak zdecydować, co ma się wydarzyć, gdy program spóźni się o kilka interwałów: wykonać zadanie kilka razy, pominąć zaległości czy rozpocząć nowy okres od bieżącej chwili.

Najczęstsze błędy przy millis()

  • Typ int zamiast unsigned long: na UNO licznik typu int przepełnia się znacznie wcześniej.
  • Porównywanie z przyszłą wartością: zapis teraz > start + interwał nie jest odporny na przepełnienie.
  • Jedna zmienna czasu dla wielu zadań: każde niezależne zadanie powinno mieć własny poprzedni czas.
  • Nadal blokujący kod: długie pętle while, oczekiwanie na Serial lub powolna procedura mogą zablokować program podobnie jak delay().
  • Brak zmiennej stanu: timer mówi, kiedy działać, ale program musi jeszcze pamiętać, co zrobić w następnym kroku.
  • Używanie operatora modulo do harmonogramu: warunek millis() % 1000 == 0 może wykonać się wiele razy w tej samej milisekundzie albo zostać całkiem pominięty.

Kiedy delay() jest wystarczające?

Krótki delay() bywa rozsądny podczas inicjalizacji sprzętu, w prostym teście lub wtedy, gdy urządzenie naprawdę nie ma niczego innego do wykonania. W projekcie reagującym na użytkownika, odbierającym dane albo sterującym kilkoma elementami lepszym punktem wyjścia jest millis() i maszyna stanów.

Najczęściej zadawane pytania

Czy millis() zatrzymuje program?

Nie. millis() tylko zwraca czas od uruchomienia szkicu. Program zatrzymuje dopiero kod, który aktywnie czeka, na przykład delay() lub blokująca pętla while.

Jak zrobić opóźnienie 5 sekund bez delay()?

Ustaw interwał na 5000UL, zapisz poprzedni czas jako unsigned long i sprawdzaj warunek millis() - poprzedniCzas >= 5000UL.

Czy millis() działa po przepełnieniu?

Tak, jeżeli czas jest przechowywany w unsigned long, a warunek korzysta z odejmowania aktualnego i poprzedniego czasu. Nie trzeba ręcznie zerować licznika.

Czym różni się micros() od millis()?

micros() mierzy czas w mikrosekundach i przydaje się do krótszych zdarzeń. Przepełnia się szybciej, a jego rzeczywista rozdzielczość zależy od płytki. Do interwałów liczonych w milisekundach wygodniejsze jest millis().

Czy millis() tworzy wiele wątków?

Nie. Arduino wykonuje kod kolejno. Częste, krótkie sprawdzanie wielu warunków czasowych sprawia jednak, że zadania mogą postępować niezależnie bez wzajemnego blokowania.

Powiązane materiały


Źródła: oficjalna dokumentacja funkcji millis(), oficjalny przykład Blink Without Delay oraz dokumentacja delay(). Zachowanie i rozmiary typów należy potwierdzić dla używanej platformy.