Muszę czytać czujnik co pięć minut, ale ponieważ mój szkic ma również inne zadania do wykonania, nie mogę po prostu delay()
między odczytami. Istnieje samouczek „ Błysk” bez zwłoki, sugerujący kodowanie według następujących linii:
void loop()
{
unsigned long currentMillis = millis();
// Read the sensor when needed.
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
readSensor();
}
// Do other stuff...
}
Problem polega na tym, że millis()
po około 49,7 dniach nastąpi powrót do zera. Ponieważ mój szkic ma trwać dłużej, muszę upewnić się, że najazd nie spowoduje niepowodzenia mojego szkicu. Mogę łatwo wykryć warunek najazdu ( currentMillis < previousMillis
), ale nie jestem pewien, co wtedy zrobić.
Tak więc moje pytanie: jaki byłby właściwy / najprostszy sposób radzenia sobie z
millis()
najazdem?
programming
time
millis
Edgar Bonet
źródło
źródło
previousMillis += interval
zamiast tego,previousMillis = currentMillis
gdybym chciał określoną częstotliwość wyników.previousMillis += interval
jeśli chcesz mieć stałą częstotliwość i masz pewność, że przetwarzanie zajmie mniej niżinterval
, alepreviousMillis = currentMillis
dla zagwarantowania minimalnego opóźnienia wynoszącegointerval
.uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
Odpowiedzi:
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óconejmillis()
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 stopniumicros()
, z wyjątkiem tego, żemicros()
przewija się co 71,6 minut, asetMillis()
funkcja podana poniżej nie ma wpływumicros()
.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 jestmillis()
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:
Naiwnie można oczekiwać, że warunek
if ()
zawsze będzie prawdziwy. Ale tak naprawdę będzie to fałsz, jeśli Millis się przepełnidelay(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żeniet2 > 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:A oto poprawny:
Większość programistów C napisałaby powyższe pętle w bardziej zwięzłej formie
i
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:
Można to uprościć, ponieważ:
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:Powyższe słowo kluczowe
signed
jest zbędne (zwykły znaklong
jest zawsze podpisany), ale pomaga wyjaśnić zamiar. Konwersja na podpisany długi jest równoważny ustawieniuLONG_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: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:
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:
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 środkuloop()
: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: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ć policzonyhigh32
. 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.
źródło
TL; DR Wersja skrócona:
An
unsigned long
wynosi od 0 do 4 294 967 295 (2 ^ 32 - 1).Powiedzmy, że
previousMillis
wynosi 4 294 967 290 (5 ms przed najazdem), acurrentMillis
wynosi 10 (10 ms po przejściu). WtedycurrentMillis - previousMillis
rzeczywista liczba 16 (nie -4 294 967 280), ponieważ wynik zostanie obliczony jako długi bez znaku (który nie może być ujemny, więc sam się przewróci). Możesz to sprawdzić po prostu:Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16
Tak więc powyższy kod będzie działał idealnie. Sztuką jest zawsze obliczyć różnicę czasu i nie porównywać dwóch wartości czasu.
źródło
unsigned
, więc tutaj nie ma sensu!millis()
przewróci się dwukrotnie, ale jest bardzo mało prawdopodobne, że wystąpi w danym kodzie.previousMillis
musiała zostać zmierzona wcześniejcurrentMillis
, więc jeślicurrentMillis
jest mniejsza niżpreviousMillis
rolowanie, to miało miejsce. Zdarza się matematyka, że jeśli nie wystąpiły dwa najazdy, nie musisz nawet o tym myśleć.t2-t1
i jeśli możesz zagwarantować, żet1
jest mierzony wcześniej,t2
jest to równoważne z podpisem(t2-t1)% 4,294,967,295
, stąd automatyczne zawijanie. Miły!. Ale co jeśli są dwa najazdy, czyinterval
jest to> 4 294 967 295?Zawinąć
millis()
w klasie!Logika:
millis()
bezpośrednio.Śledzenie cofnięć:
millis()
. Pomoże to dowiedzieć się, czymillis()
się przepełniło.Kredyty czasowe .
źródło
get_stamp()
51 razy. Porównywanie opóźnień zamiast znaczników czasu z pewnością będzie bardziej wydajne.Podobało mi się to pytanie i wspaniałe odpowiedzi, które wygenerował. Najpierw krótki komentarz do poprzedniej odpowiedzi (wiem, wiem, ale nie mam jeszcze przedstawiciela, aby skomentować :-).
Odpowiedź Edgara Boneta była niesamowita. Koduję od 35 lat i dziś nauczyłem się czegoś nowego. Dziękuję Ci. To powiedziawszy, wierzę kodowi „Co, jeśli naprawdę muszę śledzić bardzo długie czasy trwania?” psuje się, chyba że wywołasz millis64 () przynajmniej raz na okres najazdu. Naprawdę dziwaczne i mało prawdopodobne, aby stanowiło problem we wdrażaniu w świecie rzeczywistym, ale proszę bardzo.
Teraz, jeśli naprawdę chciałeś znaczników czasu obejmujących dowolny rozsądny zakres czasu (64 bity milisekund to około pół miliarda lat, moim zdaniem), proste wydaje się rozszerzenie istniejącej implementacji millis () na 64 bity.
Te zmiany w attinycore / wiring.c (pracuję z ATTiny85) wydają się działać (zakładam, że kod dla innych AVR jest bardzo podobny). Zobacz wiersze z komentarzami // BFB i nową funkcją millis64 (). Oczywiście będzie on zarówno większy (98 bajtów kodu, 4 bajty danych), jak i wolniejszy, i jak zauważył Edgar, prawie na pewno możesz osiągnąć swoje cele, po prostu lepiej rozumiejąc matematykę całkowitą bez znaku, ale było to interesujące ćwiczenie .
źródło
millis64()
działa tylko wtedy, gdy jest wywoływane częściej niż okres odnowienia. Zredagowałem swoją odpowiedź, aby wskazać to ograniczenie. Twoja wersja nie ma tego problemu, ale ma jeszcze jedną wadę: wykonuje 64-bitową arytmetykę w kontekście przerwań , co czasami zwiększa opóźnienie w odpowiedzi na inne przerwania.