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()…
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.
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?
| Cecha | delay() | millis() |
|---|---|---|
| Działanie programu | Zatrzymuje dalsze wykonywanie kodu na podany czas | Pętla loop() działa cały czas |
| Obsługa przycisku | Krótkie naciśnięcie podczas oczekiwania może zostać pominięte | Przycisk 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ści | Bardzo prosty | Wymaga zapamiętania czasu i stanu zadania |
| Typowe użycie | Krótka sekwencja startowa lub prosty test | Przyciski, 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.
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.
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.
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ął.
if (millis() > poprzedniCzas + interwał). Dodawanie może przekroczyć maksymalną wartość licznika. Stosuj odejmowanie: if (millis() - poprzedniCzas >= interwał).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
intprzepeł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 jakdelay(). - 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 == 0moż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
- Arduino UNO pinout — sprawdź piny cyfrowe, PWM, przerwania i zasilanie.
- Kalkulator rezystora do LED — dobierz rezystor do diody użytej w przykładach.
- Generator przerwań zegara Arduino — rozwiązanie sprzętowe dla precyzyjnych, cyklicznych zdarzeń.
- Wprowadzenie do Arduino — podstawy budowy szkicu, setup() i loop().
Ź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.