Skocz do zawartości

Wielozadaniowość cz. 1.5 - Ostrzeżenia i pułapki


Pomocna odpowiedź

Napisano

Ten wpis powinienem opublikować po części pierwszej, gdyż zawiera informacje niezbędne zarówno dla części 1, ( arytmetyka modulo i funkcja millis() ), jak i dla kolejnych części. Powstał on dzięki uwagom Kolegów @ethanak@Santiago @orb777  @_LM_.

1. Wbudowana funkcja millis()

 

Bezpośrednio po zainstalowaniu środowisko Arduino dostarcza funkcje wbudowane, dostępne w aplikacjach bez deklaracji włączania bibliotek (#include). Jedną z nich, jest funkcja millis(), która podaje czas od uruchomienia procesora ze średnią (uśrednioną w czasie np. 1 s) dokładnością równą 1 ms. Dostarcza ona aktualną zawartość wewnętrznego 32-bitowego licznika, który jest zwiększany najczęściej o 1 w wyniku obsługi przerwania związanego z licznikiem nr 1 procesora ATMega, zainstalowanego na płytce Arduino. Dlaczego najczęściej? Ponieważ przerwanie jest zgłaszane co 1,024 milisekundy. Aby skompensować błąd czasu wynikający z takiej częstotliwości zgłaszania przerwań, w obsłudze przerwania programiści dodali drugi (pomocniczy) licznik 8-bitowy, który jest zwiększany każdorazowo o 3. Po osiągnięciu przez niego wartości 125 lub większej licznik czasu jest dodatkowo zwiększany o 1, a od niego jest odejmowana liczba 125. Zatem na 1000 zmian zawartości licznika mamy 41 – 42 chwile, w których licznik jest zwiększany o 2. Skąd biorą się te liczby? Otóż przerwanie jest zgłaszane przez sprzętowy licznik Timer0 co 1,024 milisekundy. Zliczenie 1000 przerwań dałoby wartość 1000 w czasie 1,024 sekundy. Natomiast licznik powinien zawierać wartość 1024. Zatem w czasie zliczania 1000 przerwań należy zwiększyć dodatkowo wartość licznika o 24. Czyli 24/1000 po skróceniu daje 3/125. Dlatego licznik pomocniczy zwiększa się o 3, a po osiągnięciu lub przekroczeniu wartości 125 dodaje się 1 do licznika głównego i zmniejsza się licznik pomocniczy o 125.

Algorytm jest zaprojektowany dla zegara 16 MHz (Arduino zasilane 5V, zegar 16 MHz). W przypadku płytek zasilanych napięciem 3,3 V z zegarem 8 MHz sytuacja wygląda w ten sposób, że algorytm pozostaje bez zmian, zmieniają się natomiast jego parametry. Przerwanie zwiększa licznik główny o 2, zaś licznik pomocniczy o 6. Wynikiem jest także dokładny czas, lecz z różnicą wartości pomiędzy dwoma kolejnymi odczytami równą 2 lub 3.

Przy okazji funkcja mikros(). Zwraca ona wartość unsigned long, złożoną z aktualnej wartości rejestru licznika Timer0 (najmłodszy bajt) i licznika funkcji millis() przesuniętego o 8 bitów w lewo. Zatem funkcja mikros() dla zegara 16 MHz ma rozdzielczość 4 mikrosekundy i dodatkowo jest obciążona przeskokiem o 2 (patrz wyżej) funkcji millis(). Dla zegara 8 MHz odpowiednio rozdzielczość 8 mikrosekund i przeskok taki, jak dla millis() dla zegara 8 MHz.

 

Program demo. Kropki na konsoli wskazują zwiększenie wartości funkcji millis() o 2 w stosunku do wartości poprzedniej.

short aktualnyCzas = 0;
short zapamietanyCzas = 0;
short roznicaCzasu = 0;
short poprzedniCzas = 0;
void setup(){
Serial.begin(9600);
}
void loop(){
poprzedniCzas = aktualnyCzas;
aktualnyCzas = (short) millis(); // pobierz aktualny czas
roznicaCzasu = aktualnyCzas - zapamietanyCzas;
if (aktualnyCzas - poprzedniCzas > 1) Serial.print(".");
//Jeśli różnica wynosi ponad sekundę
if (roznicaCzasu >= 1000) {
//Zapamietaj aktualny czas
zapamietanyCzas = aktualnyCzas;
//Wyslij do PC
Serial.println("!"); // koniec testu
Serial.print(aktualnyCzas);
Serial.print(" - ");
Serial.println(roznicaCzasu);
}
}

obraz.png.c28fdb0c5d488cd8f34e59eedaf4ce5a.png

 

2. Operator porównywania „==”

Z powyższego wynika, że używanie operatora „==” do porównywania danych zawierających czas jest bardzo niewskazane, gdyż wynik takiego porównania zależy od niepewnej wartości bieżącej funkcji millis() i dla liczb 32-bitowych może powodować, że system będzie się zawieszał losowo na ok. 50 dni. Systemy wbudowane natomiast powinny pracować bezawaryjnie latami. Poza tym system operacyjny kooperujący nie gwarantuje, że funkcja zostanie wywołana dokładnie w zadanym momencie. Poprzednio wywołana funkcja mogła wykonywać się trochę dłużej z jakiegoś powodu. Stąd też znowu aktualna funkcja może zostać zablokowana. Do porównywania zmiennych zawierających czas wskazane jest używanie operatorów „>=” i „<=” . Stąd też wniosek do konstruowania watchdoga: trzeba sprawdzać działanie każdej funkcji, a nie tylko reagować na ogólne pójście w maliny.

 

3. Format zmiennych zawierających czas i pomiar odcinka czasu

Czas komputera nie jest czasem ciągłym, lecz czasem skwantowanym. Przechowywany jest w zmiennych o skończonej długości (16, 32, 64 bity) i system (sprzęt lub oprogramowanie) zwiększa licznik czasu o zdefiniowaną w systemie wartość (najczęściej 1) co zdefiniowany w systemie odcinek czasu. Powoduje to, że każda zmienna po pewnym czasie przekroczy wartość maksymalną, co powoduje, że czas zacznie być liczony od początku.

Dla kwantu czasu 1 ms przepełnienie następuje:

Dla 16 bitów – po 65,535 sekundy

Dla 32 bitów – po 49 dni, 17 godzin, 2 minuty, 47 sekund

Dla 64 bitów – po 584 542 046 lat (~584,5 mln lat)

2. Operator porównywania „==”

 

Z powyższego wynika, że używanie operatora „==” do porównywania danych zawierających czas jest bardzo niewskazane, gdyż wynik takiego porównania zależy od niepewnej wartości bieżącej funkcji millis() i dla liczb 32-bitowych może powodować, że system będzie się zawieszał losowo na ok. 50 dni. Systemy wbudowane natomiast powinny pracować bezawaryjnie latami. Poza tym system operacyjny kooperujący nie gwarantuje, że funkcja zostanie wywołana dokładnie w zadanym momencie. Poprzednio wywołana funkcja mogła wykonywać się trochę dłużej z jakiegoś powodu. Stąd też znowu aktualna funkcja może zostać zablokowana. Do porównywania zmiennych zawierających czas wskazane jest używanie operatorów „>=” i „<=” . Stąd też wniosek do konstruowania watchdoga: trzeba sprawdzać działanie każdej funkcji, a nie tylko reagować na ogólne pójście w maliny.

 

3. Format zmiennych zawierających czas i pomiar odcinka czasu

 

Czas komputera nie jest czasem ciągłym, lecz czasem skwantowanym. Przechowywany jest w zmiennych o skończonej długości (16, 32, 64 bity) i system (sprzęt lub oprogramowanie) zwiększa licznik czasu o zdefiniowaną w systemie wartość (najczęściej 1) co zdefiniowany w systemie odcinek czasu. Powoduje to, że każda zmienna po pewnym czasie przekroczy wartość maksymalną, co powoduje, że czas zacznie być liczony od początku.

 

Dla kwantu czasu 1 ms przepełnienie następuje:

Dla 16 bitów – po 65,535 sekundy

Dla 32 bitów – po 49 dni, 17 godzin, 2 minuty, 47 sekund

Dla 64 bitów – po 584 542 046 lat (~584,5 mln lat)

Dla liczb ze znakiem pierwsze przepełnienie nastąpi po upływie połowy podanego czasu. Kolejne po upływie podanego czasu. Bezpośrednio długość mierzonego odcinka czasu musi być krótsza od czasu przepełnienia. Dłuższe niż maksymalny odcinki czasu można mierzyć dzieląc go na krótsze odcinki i zliczając ilość takich odcinków.

Wbrew rozpowszechnionym poglądom (także w dokumentacji) znak (signed/unsigned) i długość (long/int) nie mają znaczenia podczas wyliczania odcinka czasu. Istotny jest sposób, w jaki to robimy. Znacznik czasu możemy ustawiać albo w danym momencie i testować, jak daleko bieżący czas oddalił się od znacznika, albo ustawić znacznik gdzieś w przyszłości i testować, czy bieżący czas minął znacznik. Każda z tych metod zachowuje się inaczej przy przepełnieniu licznika. Pierwsza metoda (znacznik w chwili bieżącej) jest niewrażliwa na przepełnienie licznika, druga metoda (znacznik w przyszłości) – nie. Przy okazji ostrzeżenie dotyczące liczb ze znakiem: o ile teoretycznie zachowują się bez zarzutu, to niektóre kompilatory potrafią tu namieszać. W językach C i C++ przepełnienie liczb ze znakiem (Signed Overflow) to tzw. Undefined Behavior (UB), czyli zachowanie niezdefiniowane przez standard języka. W praktyce (na 99% kompilatorów dla mikrokontrolerów, np. GCC dla Arduino) kompilator wygeneruje kod, który zadziała poprawnie, bo procesor operuje na kodzie uzupełnień do dwóch (U2). Jednak zaawansowane optymalizacje kompilatora (np. -O2 lub -O3) potrafią "wyciąć" lub zniekształcić warunek if (current - last >= interval), jeśli kompilator uzna, że logicznie czas nie może być ujemny. Dlatego standardem i dobrą praktyką w systemach operacyjnych jest trzymanie się typów unsigned.

DEMO

Dla liczb bez znaku (unsigned). Działa

unsigned short aktualnyCzas = 0;
unsigned short zapamietanyCzas = 0;
unsigned short roznicaCzasu = 0;
void setup(){
Serial.begin(9600);
}
void loop(){
//Pobierz liczbe milisekund od startu
aktualnyCzas = (short) millis();
roznicaCzasu = aktualnyCzas - zapamietanyCzas;
//Jeśli różnica wynosi ponad sekundę
if (roznicaCzasu >= 1000) {
//Zapamietaj aktualny czas
//Wyslij do PC
Serial.print(aktualnyCzas);
Serial.print("-");
Serial.print(zapamietanyCzas);
Serial.print("=");
Serial.println(roznicaCzasu);
zapamietanyCzas = aktualnyCzas;
}
}

obraz.png.5693dba6612d8d558f419da24154d3a4.png

Dla liczb ze znakiem (signed). Też działa, ale kompilator może sprawiać czasami kłopoty

short aktualnyCzas = 0;
short zapamietanyCzas = 0;
short roznicaCzasu = 0;
void setup(){
Serial.begin(9600);
}
void loop(){
//Pobierz liczbe milisekund od startu
aktualnyCzas = (short) millis();
roznicaCzasu = aktualnyCzas - zapamietanyCzas;
//Jeśli różnica wynosi ponad sekundę
if (roznicaCzasu >= 1000) {
//Zapamietaj aktualny czas
//Wyslij do PC
Serial.print(aktualnyCzas);
Serial.print("-");
Serial.print(zapamietanyCzas);
Serial.print("=");
Serial.println(roznicaCzasu);
zapamietanyCzas = aktualnyCzas;
}
}

obraz.png.0cceba9b1d84b78058e385acea71c21d.png

Znacznik czasu ustawiony w przyszłości. KATASTROFA, gdy zmienna przyszlyCzas przepełni się. Gdy zmienna aktualnyCzas osiągnie 0 (przepełnienie) program znowu zacznie działać normalnie (do następnego przepełnienia).

unsigned short aktualnyCzas = 0;
unsigned short przyszlyCzas = 1000;
unsigned short roznicaCzasu = 0;
void setup(){
Serial.begin(9600);
}
void loop(){
//Pobierz liczbe milisekund od startu
aktualnyCzas = (short) millis();
roznicaCzasu = aktualnyCzas - przyszlyCzas;
//Jeśli różnica wynosi ponad sekundę
if (aktualnyCzas >= przyszlyCzas) {
//Zapamietaj aktualny czas
//Wyslij do PC
Serial.print(aktualnyCzas);
Serial.print("-");
Serial.print(przyszlyCzas);
Serial.print("=");
Serial.println(roznicaCzasu);
przyszlyCzas = aktualnyCzas + 1000;
}
}

obraz.png.bb467cd96a2df16372437bbf3193706a.png

I ostatnie ostrzeżenie dotyczące bibliotek – użycie biblioteki, która wewnątrz używają delay() lub delaymicroseconds() - wprowadzają one niekontrolowane przez nas opóźnienie. Można używać funkcji delaymicroseconds(), ale trzeba pamiętać, żeby opóźnienia były krótkie. Pętla wykonująca się 10 razy na milisekundę ma opóźnienie 100 mikrosekund. Aktualnie pętla loop() w moich przykładach wykonuje się powyżej 50 razy na milisekundę. Moim zdaniem użycie delaymicroseconds() ma sens dla wygenerowania np. impulsu o długości 10 mikrosekund, ale dla impulsu o długości 1000 mikrosekund już nie. Dla przykładu funkcje wbudowane: – funkcja analogWrite() niczego nie blokuje (PWM generuje bezpośrednio sprzęt), funkcja analogRead() blokuje wykonanie na okres 104/208 mikrosekundy dla zegara 16/8 MH. Powód? Po włączeniu odczytu trzeba poczekać na wynik. Przetwornik ADC w Arduino jest typu SAR. Sam przetwornik potrzebuje 13 cykli zegara ADC na wykonanie jednej konwersji (pierwsza konwersja po uruchomieniu trwa 25 cykli), do tego narzut kompilatora.

PODSUMOWANIE

🛑 Nigdy nie używaj operatora == do badania czasu. Czas płynie asynchronicznie do Twojego kodu. Zawsze używaj porównań >= lub <= i odejmowania wstecznego.

🛑 Nigdy nie wyliczaj znaczników w przyszłości (obecny_czas + interwał). To pułapka, która przy przepełnieniu typu (szczególnie ze znakiem) wywoła fałszywy alarm i natychmiastowe wykonanie zadania.

🟢 Zawsze badaj przeszłość poprzez odejmowanie (obecny_czas - poprzedni_czas >= interwał). Ta konstrukcja (na liczbach bez znaku) opiera się na matematyce modularnej i jest w 100% odporna na skoki licznika, przepełnienia oraz błędy kompensacji mikrokontrolera.

 

Inne ostrzeżenia (np. użycie przerwań) będę sygnalizował w razie potrzeby.

Powyższy tekst napisałem wykorzystując sztuczną inteligencję (chatgpt). Jak do tej pory wykorzystuję ją jako sekretarza – przeszukanie zapisów w internecie dotyczących opisywanych tematów, podsumowanie zapytania, namiary na dokumenty źródłowe. Niestety, nie jest dokładna. Fragment o przejściu przez 0 dla liczb ze znakiem i implementacji języka C znalazł sam w trzecim podejściu. Ja bym go pewnie nie znalazł. Daje dużo informacji, ale trzeba je analizować. Testowanie pomysłów – czy mają sens. Pytania były bardzo szczegółowe. Tu dostaję ostrzeżenia dotyczące wykonalności i jeśli wykonalne – kody. Kody nie są najlepszej jakości – za każdym razem potrafiłem napisać krótsze i bardziej zrozumiałe. Nigdy też nie dostałem propozycji lepszego rozwiązania niż moje. Spróbuję teraz przetestować SI jako drugą parę oczu. Wnioski do tej pory: SI jest mnożnikiem moich możliwości – głównie oszczędza czas. Jednak jako mnożnik ma oczywistą własność – trzeba coś wiedzieć, im więcej, tym lepsze efekty. Zero pomnożone przez cokolwiek to dalej zero.

 

 

  • Lubię! 1
(edytowany)

Nie wiem kto programował, te funkcje dla Arduino, ale miał fantazję jak dla mnie. Skoro używamy 16-bitowy licznik i mamy zegar 16MHz, to dzielimy sobie to przez 64000, pomijam już preskaler, bo nie pamiętam jakie on tam miał ustawienia, i otrzymujemy bardzo wygodne 250 przerwań/ sek. Dla czego wygodne? Bo to wygodna wartość do odświeżania wyświetlaczy LED, aby nie migotały. Dla 8MHz dostaniemy 125Hz. Obie te wartości, są bardzo wygodne w tworzeniu różnych systemów, nie mówiąc że to wręcz "Historyczny standard", jeszcze z 8051,a pamiętajmy że pierwsze AVRy powstały właśnie aby zastąpić 51 pin to pin, i zaoferować większa wydajność.

Kolejny plus takiego podejścia, to fakt że niedokładność pomiaru, wynika tylko z niedokładności oscylatora taktującego MPU.

Powyższe wyjaśnienia kolegi, to dla mnie tylko dowód jak standardowe funkcje zaimplementowane w Arduino, niszczą wydajność i jakość tej platformy, w sumie nie wiadomo po co?

Reasumując. Jeżeli chcemy pisać dobre systemy wielozadaniowe, nie unikniemy implementacji własnych, zoptymalizowanych funkcji, wykorzystujące optymalnie zasoby sprzętowe i programowe. Nawet na upartego precyzyjne odmierzenie jest do zrealizowania na 8-bitowym liczniku 0, bo damy preskaler 64 i liczymy do 100 i uzyskujemy 64000 impulsy zegara, do przepełnienia. Jak widać można to optymalizować, praktycznie na dowolnym liczniku.

Edytowano przez BlackJack
2 godziny temu, BlackJack napisał:

Jeżeli chcemy pisać dobre systemy wielozadaniowe

...to z reguły omijamy Arduino szerokim łukiem. 🙂 

Arduino nie powstało do tego celu, jego API, stanowcza większość oprogramowania (driverów) jest non-thread-safe, i w wielu przypadkach nawet zabezpieczanie się przeróżnego rodzaju mutexami (np. na ESP32) zwyczajnie może nie pomóc, zwłaszcza gdy pisze się kod używający tej samej magistrali sprzętowej, bo wszelakie Wire, SPI itp po prostu z natury jest jednowątkowe, i prędzej czy później to (u)gryzie.

Ale jak najbardziej jestem za tym, by optymalizować swoje loop(), i takie układanie zadań za pomocą pomiaru diffu czasu uważam za bardzo dobry pomysł - dzięki temu na jednowątkowym loop() można ustawić kilka wzajemnie w miarę nieblokujących się zadań, i zyskać taką "wielozadaniowość kooperatywną", bez martwienia się o jakieś "prawdziwe" zależności wielowątkowe.

(edytowany)

Szanowni Koledzy, a w szczególności @BlackJack i @jaszczurtd. Jak zauważyliście, piszę tutorial na temat konstrukcji systemu operacyjnego kooperacyjnego, to znaczy takiego, w którym zadania współpracują ze sobą. Tak był konstruowany np Win3.1.  Zadania przełącza funkcja loop(), zaś licznik jest używany do odmierzania odcinków czasu. Piszę ten tutorial po to aby dostarczyć Wam szkielet. Wy uzupełnicie go według własnych potrzeb, dołączając własne zadania i wyrzucając to, co załączyłem jako przykłady użycia. Tak zresztą działają wszystkie systemy operacyjne - dostarczają środowisko dla programów użytkowych. Ten kawałek tutoriala pokazuje zagrożenia, na jakie można się natknąć pisząc własne zadanie włączane do schedulera kooperującego. W  tym fragmencie przykłady kodu włączyłem po to aby zademonstrować opisywane problemy - przy 16 bitach przepełnienia liczników zachodzą o wiele szybciej, niż przy 32 bitach. Pracując z systemami operacyjnymi z wywłaszczaniem (np RTOS) które przełączją zadania w dowolnym (z punktu widzenia zadania) momencie, ma się dużą czarną skrzynkę, która zadaniu dostarcza cały procesor i podobno nie musimy martwić się innymi zadaniami.  W efekcie ma się problemy z synchronizacją zadań (mutexy, sekcje krytyczne itp). Systemy kooperacyjne są znacznie mniejsze (to co napisałem do tej pory zajmuje ok. 10% zasobów Arduino, z czego połowa zajętego RAM to bufory COM-a). One też mają swoje specyficzne problemy, i o części z nich jest ten artykuł. Z drugiej strony system kooperacyjny ma tylko 1 wątek - zatem z założenia nie ma problemu z wątkami. Co do ESP32 - Jest to system z zaszytym RTOS-em, gdzie program napisany w Arduino jest tylko jednym z wątków o niskim priorytecie - głównym wątkiem jest ten, który obsługuje łączność radiową. Piszę na razie w środowisku Arduino na Arduino DueMilanove, ale system powinien być łatwo przenaszalny na inne rodziny procesorów (na wszystkich z Arduino jest blink) i środowiska (wszędzie jest funkcja main(), SDK i funkcje inicjujące system). Poza tym z wypowiedzi Kolegów wnioskuję, że nie doceniacie mocy płytek, które leżą na Waszych biurkach. Być może napiszę kolejny artykuł (o ile nikt tego nie zrobił) dotyczący porównania procesorów zainstalowanych w Arduino, BluePill czy BlackPill z profesjonalnymi stacjami roboczymi z okolic przełomu wieków.  

Edytowano przez Kogut007
Literówka
(edytowany)
47 minut temu, Kogut007 napisał:

Poza tym z wypowiedzi Kolegów wnioskuję, że nie doceniacie mocy płytek, które leżą na Waszych biurkach

Ja się z tym nie zgodzę, startowałem od 89C2051 (bascom) z popularnymi kursami w edw. Już wtedy nie podobało mi się wszechobecne używanie wait i waitms dość szybko zorientowałem się że takie opóźnienia jeśli są potrzebne - łatwo zrealizować na timerach i przerwaniach lub nawet na przerwaniach zewnętrznych. Na przykład kiedy urządzenie jest zasilane sieciowo łatwo wyciągnąć sygnał 100Hz za mostkiem prostowniczym. Także to jest kwestia podejścia konstruktora, i potrzeb projektu. Dodatkowo w małych mikrokontrolerach 8 bit system szyje się na miarę co nie oznacza że są one bezużyteczne, wręcz przeciwnie dobrze poznane mikrokontrolery AVR i 8051 nadal są szeroko stosowane w przemyśle konsumenckim militarnym a nawet kosmicznym.

A to, co mi się nie spodobało w twoich artykułach to że mało że nie chciało Ci się sprawdzić co wywalił model LLM to jeszcze uparłeś się bez sprawdzenia dokumentacji: moja racja jest mojsza, wielokrotnie poprawiania dokumentacja jest błędna.. No jak to nasz kolega napisał "Katowice leżą na północy Polski" 

 

Edytowano przez _LM_
  • Lubię! 1

@Kogut007 ależ jak dla mnie nie ma czego wyjaśniać, czy się tłumaczyć - ja doskonale rozumiem co chcesz przekazać, i w pełni popieram to podejście, zwłaszcza że sam zaczynałem od prostych AVRów, i spędziłem na nich pół embeddowego życia, i wiem że odpowiednio skonstruowane loop() bez wszędobylskiego w amatorskich projektach, marnującego cykle CPU delay() potrafi dużo zmienić, i stosunkowo sporo wyciągnąć z tych kostek. Tylko jeśli mogę mieć jakąś sugestię, to ja na Twoim miejscu zrobiłbym sobie z tego funkcje, helpery, żeby nie powtarzać tego bloku porównań (DRY). Np. coś takiego jak ja zrobiłem w moim prywatnym HAL: https://github.com/jaszczurtd/JaszczurHAL/blob/main/src/hal/system/hal_system.h

Może to być zestaw zwykłych funkcji inline (ale niekoniecznie inline), coś w rodzaju:

/**
 * @brief Check whether a non-blocking millis interval elapsed.
 *
 * This helper implements the established wrap-safe pattern:
 *
 * @code
 * if ((now_ms - last_ms) >= interval_ms) { ... }
 * @endcode
 *
 * On success it updates @p last_ms to @p now_ms so the next interval starts
 * from the current tick.
 *
 * @param now_ms      Current timestamp (typically @ref hal_millis()).
 * @param last_ms     Pointer to caller-owned last-fire timestamp.
 * @param interval_ms Interval in milliseconds (>0).
 * @return true when elapsed and @p last_ms was updated.
 */
static inline bool hal_millis_interval_elapsed(uint32_t now_ms,
                                               uint32_t *last_ms,
                                               uint32_t interval_ms) {
  if (last_ms == NULL || interval_ms == 0u) {
    return false;
  }
  if ((uint32_t)(now_ms - *last_ms) < interval_ms) {
    return false;
  }
  *last_ms = now_ms;
  return true;
}

/**
 * @brief Check whether a non-blocking millis interval elapsed using now().
 *
 * Equivalent to calling @ref hal_millis_interval_elapsed with
 * @ref hal_millis() as @p now_ms.
 */
static inline bool hal_millis_interval_elapsed_now(uint32_t *last_ms,
                                                   uint32_t interval_ms) {
  return hal_millis_interval_elapsed(hal_millis(), last_ms, interval_ms);
}

/**
 * @brief Fire callback when a non-blocking millis interval elapsed.
 *
 * Equivalent to:
 *
 * @code
 * if (hal_millis_interval_elapsed(now_ms, &last_ms, interval_ms)) {
 *   callback(user_data);
 * }
 * @endcode
 *
 * When elapsed, @p last_ms is updated and the callback is invoked when not
 * NULL.
 */
static inline bool hal_millis_interval_call(
    uint32_t now_ms, uint32_t *last_ms, uint32_t interval_ms,
    hal_millis_interval_callback_t callback, void *user_data) {
  if (!hal_millis_interval_elapsed(now_ms, last_ms, interval_ms)) {
    return false;
  }
  if (callback != NULL) {
    callback(user_data);
  }
  return true;
}

/**
 * @brief Fire callback when a non-blocking millis interval elapsed using now().
 */
static inline bool
hal_millis_interval_call_now(uint32_t *last_ms, uint32_t interval_ms,
                             hal_millis_interval_callback_t callback,
                             void *user_data) {
  return hal_millis_interval_call(hal_millis(), last_ms, interval_ms, callback,
                                  user_data);
}

I wtedy wołanie w kodzie robi się znacznie bardziej eleganckie, i możesz zrobić albo tak:

#include <hal/system/hal_system.h>

static uint32_t last_publish_ms = 0;

static void publish_cb(void *user_data) {
    (void)user_data;
    callback();
}

void app_task0(void) {
    uint32_t now = hal_millis();

    // Wrap-safe non-blocking interval pattern.
    (void)hal_millis_interval_call(now,
                                   &last_publish_ms,  
                                   PUBLISH_INTERVAL,  //time in ms
                                   publish_cb,
                                   NULL);
}

Albo tak:

uint32_t now = hal_millis();
if (hal_millis_interval_elapsed(now, &last_publish_ms, PUBLISH_INTERVAL)) {
    callback();
}

Oczywiście nazwy funkcji pochodzą z mojego projektu, więc musisz sobie je przetłumaczyć na Arduino.

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...