Krótka odpowiedź: nie próbuj „obsłużyć” najazdu Millis, zamiast tego napisz kod bezpieczny dla najazdu. Twój przykładowy kod z samouczka jest w porządku. Jeśli spróbujesz wykryć najazd w celu wdrożenia działań naprawczych, prawdopodobnie robisz coś złego. Większość programów Arduino musi zarządzać tylko zdarzeniami, które trwają stosunkowo krótko, np. Ogłaszanie przycisku na 50 ms lub włączanie grzejnika na 12 godzin ... Wtedy, nawet jeśli program ma działać przez lata, rollis Millis nie powinien stanowić problemu.
Prawidłowym sposobem zarządzania (a raczej unikania zarządzania) problemem najazdu jest przemyślenie unsigned long
liczby zwróconej
millis()
w postaci arytmetyki modułowej . Dla matematyków pewna znajomość tej koncepcji jest bardzo przydatna podczas programowania. Możesz zobaczyć matematykę w akcji w artykule Millis () przepełnienie artykułu Nicka Gammona ... zła rzecz? . Dla tych, którzy nie chcą przechodzić przez szczegóły obliczeniowe, oferuję tutaj alternatywny (miejmy nadzieję prostszy) sposób myślenia o tym. Opiera się na prostym rozróżnieniu między chwilami i czasem trwania . Tak długo, jak twoje testy polegają jedynie na porównywaniu czasów trwania, wszystko powinno być w porządku.
Uwaga na temat micros () : Wszystko, o czym tu mowa, millis()
dotyczy w równym stopniu micros()
, z wyjątkiem tego, że micros()
przewija się co 71,6 minut, a setMillis()
funkcja podana poniżej nie ma wpływu micros()
.
Instanty, znaczniki czasu i czasy trwania
Kiedy mamy do czynienia z czasem, musimy rozróżnić co najmniej dwa różne pojęcia: momenty i czasy trwania . Chwila to punkt na osi czasu. Czas trwania to długość przedziału czasu, tj. Odległość w czasie między instancjami, które określają początek i koniec przedziału. Rozróżnienie między tymi pojęciami nie zawsze jest bardzo wyraźne w języku potocznym. Na przykład, jeśli powiem „ wrócę za pięć minut ”, to „ pięć minut ” to szacowany
czas mojej nieobecności, podczas gdy „ za pięć minut ” to chwila
mojego przewidywanego powrotu. Ważne jest, aby pamiętać o tym rozróżnieniu, ponieważ jest to najprostszy sposób, aby całkowicie uniknąć problemu przewrócenia.
Zwracana wartość millis()
może być interpretowana jako czas trwania: czas, który upłynął od początku programu do chwili obecnej. Jednak ta interpretacja załamuje się, gdy tylko Millis się przepełni. Na ogół o wiele bardziej przydatne jest millis()
zwracanie
znacznika czasu , tj. „Etykiety” identyfikującej konkretny moment. Można argumentować, że interpretacja ta ma niejednoznaczny charakter, ponieważ są one ponownie wykorzystywane co 49,7 dni. Jest to jednak rzadko problem: w większości osadzonych aplikacji wszystko, co wydarzyło się 49,7 dni temu, to historia starożytna, na której nam nie zależy. Dlatego recykling starych etykiet nie powinien stanowić problemu.
Nie porównuj znaczników czasu
Próba ustalenia, który z dwóch znaczników czasu jest większy od drugiego, nie ma sensu. Przykład:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Naiwnie można oczekiwać, że warunek if ()
zawsze będzie prawdziwy. Ale tak naprawdę będzie to fałsz, jeśli Millis się przepełni
delay(3000)
. Myślenie o t1 i t2 jako etykietach nadających się do recyklingu jest najprostszym sposobem uniknięcia błędu: etykieta t1 została wyraźnie przypisana do chwili sprzed t2, ale za 49,7 dni zostanie ona ponownie przypisana do przyszłej chwili. Zatem t1 zachodzi zarówno przed jak i po t2. Powinno to wyjaśnić, że wyrażenie t2 > t1
nie ma sensu.
Ale jeśli są to zwykłe etykiety, oczywiste pytanie brzmi: w jaki sposób możemy wykonać z nimi użyteczne obliczenia czasu? Odpowiedź brzmi: ograniczając się do jedynych dwóch obliczeń, które mają sens dla znaczników czasu:
later_timestamp - earlier_timestamp
daje czas trwania, a mianowicie czas, który upłynął między wcześniejszą chwilą a późniejszą chwilą. Jest to najbardziej przydatna operacja arytmetyczna obejmująca znaczniki czasu.
timestamp ± duration
zwraca znacznik czasu, który jest jakiś czas po (jeśli używasz +) lub przed (jeśli -) początkowy znacznik czasu. Nie jest to tak przydatne, jak się wydaje, ponieważ wynikowy znacznik czasu można wykorzystać tylko w dwóch rodzajach obliczeń ...
Dzięki modułowej arytmetyki gwarantuje się, że obie z nich będą działały poprawnie w przypadku najazdu millis, przynajmniej tak długo, jak długo opóźnienia będą krótsze niż 49,7 dni.
Porównywanie czasów trwania jest w porządku
Czas trwania to po prostu ilość milisekund, które upłynęły w pewnym przedziale czasu. Tak długo, jak nie musimy obsługiwać czasów trwania dłuższych niż 49,7 dni, każda operacja, która fizycznie ma sens, również powinna mieć sens obliczeniowy. Możemy na przykład pomnożyć czas trwania przez częstotliwość, aby uzyskać liczbę okresów. Lub możemy porównać dwa czasy, aby wiedzieć, który jest dłuższy. Na przykład oto dwie alternatywne implementacje delay()
. Po pierwsze, błędny:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
unsigned long finished = start + ms; // finished: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
if (now >= finished) // comparing timestamps: BUG!
return;
}
}
A oto poprawny:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
unsigned long elapsed = now - start; // elapsed: duration
if (elapsed >= ms) // comparing durations: OK
return;
}
}
Większość programistów C napisałaby powyższe pętle w bardziej zwięzłej formie
while (millis() < start + ms) ; // BUGGY version
i
while (millis() - start < ms) ; // CORRECT version
Chociaż wyglądają na zwodniczo podobne, rozróżnienie znacznika czasu / czasu trwania powinno jasno wskazywać, który jest błędny, a który poprawny.
Co jeśli naprawdę muszę porównać znaczniki czasu?
Lepiej spróbuj uniknąć sytuacji. Jeśli jest to nieuniknione, nadal istnieje nadzieja, jeśli wiadomo, że odpowiednie momenty są wystarczająco blisko: bliżej niż 24,85 dni. Tak, nasze maksymalne opóźnienie wynoszące 49,7 dni zostało właśnie zmniejszone o połowę.
Oczywistym rozwiązaniem jest konwersja naszego problemu porównywania znaczników czasu na problem porównania czasu trwania. Powiedzmy, że musimy wiedzieć, czy natychmiastowe t1 jest przed czy po t2. Wybieramy chwile odniesienia w ich wspólnej przeszłości i porównujemy czasy trwania od tego odniesienia do obu t1 i t2. Moment odniesienia uzyskuje się, odejmując odpowiednio długi czas od t1 lub t2:
unsigned long reference_instant = t2 - LONG_ENOUGH_DURATION;
unsigned long from_reference_until_t1 = t1 - reference_instant;
unsigned long from_reference_until_t2 = t2 - reference_instant;
if (from_reference_until_t1 < from_reference_until_t2)
// t1 is before t2
Można to uprościć, ponieważ:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
Kuszące jest dalsze upraszczanie if (t1 - t2 < 0)
. Oczywiście to nie działa, ponieważ t1 - t2
obliczone jako liczba bez znaku, nie może być ujemne. To jednak, choć nie jest przenośne, działa:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
Powyższe słowo kluczowe signed
jest zbędne (zwykły znak long
jest zawsze podpisany), ale pomaga wyjaśnić zamiar. Konwersja na podpisany długi jest równoważny ustawieniu LONG_ENOUGH_DURATION
równemu 24,85 dni. Sztuczka nie jest przenośna, ponieważ zgodnie ze standardem C wynik jest zdefiniowany jako implementacja . Ale ponieważ kompilator gcc obiecuje zrobić coś dobrego , działa niezawodnie na Arduino. Jeśli chcemy uniknąć zachowania zdefiniowanego w implementacji, powyższe porównanie jest matematycznie równoważne z tym:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
z jedynym problemem, że porównanie wygląda wstecz. Jest również równoważny, o ile długości są 32-bitowe, z tym testem jednobitowym:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
Ostatnie trzy testy są w rzeczywistości kompilowane przez gcc do dokładnie tego samego kodu maszynowego.
Jak przetestować mój szkic w stosunku do najazdu Millis
Jeśli będziesz przestrzegać powyższych zasad, powinieneś być dobry. Jeśli mimo to chcesz przetestować, dodaj tę funkcję do swojego szkicu:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
i możesz teraz podróżować w czasie po swoim programie, dzwoniąc
setMillis(destination)
. Jeśli chcesz, aby przepełniało go millis w kółko, tak jak Phil Connors przeżywający Dzień Świstaka, możesz umieścić to w środku loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
Ujemny znacznik czasu powyżej (-3000) jest domyślnie konwertowany przez kompilator na niepodpisaną długość odpowiadającą 3000 milisekund przed najazdem (jest konwertowany na 4294964296).
Co jeśli naprawdę muszę śledzić bardzo długie czasy?
Jeśli potrzebujesz włączyć przekaźnik i wyłączyć go trzy miesiące później, naprawdę musisz wyśledzić przepełnienie millis. Można to zrobić na wiele sposobów. Najprostszym rozwiązaniem może być po prostu rozszerzenie millis()
do 64 bitów:
uint64_t millis64() {
static uint32_t low32, high32;
uint32_t new_low32 = millis();
if (new_low32 < low32) high32++;
low32 = new_low32;
return (uint64_t) high32 << 32 | low32;
}
Zasadniczo zlicza to zdarzenia najazdu i wykorzystuje tę liczbę jako 32 najbardziej znaczące bity z 64-bitowej liczby milisekund. Aby to zliczanie działało poprawnie, funkcja musi być wywoływana co najmniej raz na 49,7 dni. Jeśli jednak jest wywoływany tylko raz na 49,7 dni, w niektórych przypadkach może się zdarzyć, że sprawdzenie się (new_low32 < low32)
nie powiedzie i kod nie będzie mógł zostać policzony high32
. Użycie millis () do podjęcia decyzji, kiedy wykonać jedyne wywołanie tego kodu w pojedynczym „zawinięciu” millis (specyficzne okno 49,7 dni), może być bardzo niebezpieczne, w zależności od tego, jak ustawione są ramy czasowe. Dla bezpieczeństwa, jeśli używasz millis () do określenia, kiedy wykonać jedyne wywołanie do millis64 (), powinny być co najmniej dwa wywołania w każdym oknie 49,7 dnia.
Pamiętaj jednak, że 64-bitowa arytmetyka jest droga na Arduino. Warto obniżyć rozdzielczość czasową, aby pozostać na 32 bitach.