Watchdog w środowisku wielowątkowym

W poprzednim artykule omówiłem działanie watchdoga i zastosowanie w prostych aplikacjach zawierających całą obsługę w pętli głównej. Dzisiaj zajmiemy się częściej spotykanym, ale i trudniejszym, problemem – implementacją watchdoga w aplikacjach wielowątkowych.

Problem wielowątkowości

Większość programów pisanych na mikrokontrolery realizuje więcej niż jedno zadanie. Architekturę takiego systemu można oprzeć na przerwaniach, albo wykorzystać RTOSa. W obu tych przypadkach, jeśli implementujemy watchdoga, pytanie brzmi tak samo – jak kontrolować, czy wszystkie zadania się poprawnie wykonują?

Przedstawiona poprzednio metoda obsługiwania watchdoga w pętli głównej ma oczywiste ograniczenia. To, że pętla główna działa nie daje nam gwarancji, że poszczególne taski również działają. Aby więc zaimplementować watchdoga w środowisku wielowątkowym potrzebujemy procedury, która sprawdzi poprawność działania wszystkich krytycznych wątków i na tej podstawie podejmie decyzję, czy wszystko jest w porządku i można nakarmić watchdoga.

Jakby tego było mało, w środowisku wielowątkowym taski mogą pozostawać w uśpieniu, a także zdarzają się problemy charakterystyczne dla współbieżności takie jak deadlocki, czy zagłodzenie. Poza tym mogą istnieć problemy w RTOSie takie jak błędy kernela, czy zbugowanie się pojedynczego tasku.

Rozwiązanie dla tasków cyklicznych

Pierwsze rozwiązanie, które zaprezentuje sprawdza się w systemach, gdzie działa wiele tasków synchronicznych. Wykonują one pojedynczą iterację co pewien czas nadzorowany przez RTOS, albo przerwanie od timera. Nie są one blokowane w oczekiwaniu na zewnętrzne dane.

Interfejs zewnętrzny modułu obsługującego watchdog mógłby wtedy wyglądać następująco:

typedef enum wdg_task_id
{
    WDG_ID_TASK1,
    WDG_ID_TASK2,
    WDG_ID_TASK3,
};

void wdg_task_running(wdg_task_id task_id);
void wdg_check(void);

Funkcja wdg_function_called jest wywoływana na początku iteracji przez każdy kontrolowany task. Każdy task wywołuje ją z innym argumentem zawierającym identyfikator. W niej moduł watchdoga zapisuje informację o wywołaniu tasku. W prostym przypadku może to być jedynie ustawienie flagi na wartość ACTIVE. Jeżeli natomiast chcemy mieć również informację o ilości iteracji, można inkrementować licznik.

Druga funkcja jest wywoływana cyklicznie w tasku diagnostycznym i jej zadaniem jest sprawdzenie, czy wszystkie kontrolowane taski wykonują się poprawnie. Jeśli tak to karmi watchdoga i zmienia wartości flag z ACTIVE na UNKNOWN, albo zeruje liczniki wywołań. Jeśli stosujemy licznik wywołań, ilość iteracji powinna zawierać się w jakimś zdefiniowanym zakresie. Warto wtedy zapisać do celów debugowych również minimalną i maksymalną ilość iteracji. W przypadku, gdy obsługa watchdoga wykryje błąd, wchodzimy w nieskończoną pętlę i czekamy aż watchdog spowoduje reset procesora.

Rozwiązanie dla tasków asynchronicznych

Niestety czasem rozwiązanie opisane wyżej nie jest wystarczające. Niektóre taski mogą wykonywać się asynchronicznie – być wyzwalane przez jakieś zewnętrzne wydarzenie jak wciśnięcie przycisku, czy otrzymanie ramki przez protokół komunikacyjny. Oznacza to, że w danym cyklu watchdoga dany task cały czas pozostanie uśpiony i będzie to oczekiwane zachowanie.

Musimy więc dokonać pewnych modyfikacji w naszym module watchdoga:

typedef enum wdg_task_id
{
    WDG_ID_TASK1,
    WDG_ID_TASK2,
    WDG_ID_TASK3,
};

void wdg_task_active(wdg_task_id task_id);
void wdg_task_asleep(wdg_task_id task_id);
void wdg_check(void);

Tym razem zamiast funkcji wdg_task_running mamy dwie funkcje – wdg_task_active i wdg_task_asleep. Pierwszą z nich wołamy na początku iteracji tak samo jak poprzednią. W momencie, kiedy dochodzimy do funkcji blokującej (czyli takiej, która będzie czekać na jakieś zdarzenie) musimy obsłużyć ją w następujacy sposób:

wdg_task_asleep(WDG_ID_TASK1);
blocking_call();
wdg_task_alive(WDG_ID_TASK1);

Zaraz przed blokującą funkcją informujemy watchdoga, że task zostanie uśpiony i od razu po blokującej funkcji informujemy o wybudzeniu.

Funkcja wdg_check obsługuje teraz 3 flagi:

  • UNKNOWN – nie wiadomo, czy task działa poprawnie.
  • ALIVE – task działa poprawnie i jest wybudzony.
  • ASLEEP – task działa poprawnie i jest uśpiony.

Stany te obsługujemy w następujący sposób:

  • UNKNOWN – task się nie wywołał od poprzedniego checka – wykryliśmy błąd i wchodzimy w nieskończoną pętlę czekając na reset watchdoga.
  • ALIVE – task działa poprawnie, zmieniamy flagę na UNKNOWN.
  • ASLEEP – task działa poprawnie, nie ruszamy tej flagi.

Jeżeli flagę ASLEEP zmienilibyśmy na UNKNOWN, to w następnym checku task mógłby się nie wybudzić i nie zaktualizować flagi. Przy kolejnym checku wykrylibyśmy fałszywy alarm.

Możliwe pułapki

Wadą rozwiązania 2 jest możliwość pozostania tasku na zawsze w stanie ASLEEP, albo powstanie deadlocka z udziałem dwóch lub więcej tasków. Aby unikać podobnych sytuacji, zależności między poszczególnymi taskami nie powinny tworzyć pętli np. task1 czeka na 2, a 2 na 1, albo w bardziej skomplikowanym przypadku 1 na 2, 2 na 3, 3 na 1.

Operacja aktualizacji stanu tasku musi być atomowa. Należy albo wykorzystywać pojedynczą instrukcję, albo sekcję krytyczną. Szczególnie należy uważać na rozmiar zmiennych np. przypisanie do zmiennej 16-bitowej na procesorze 8-bitowym. Operacje bitowe też często nie są wykonywane w pojedynczej instrukcji, dlatego jeśli na jednej zmiennej chcemy obsłużyć wiele flag – musimy uważać.

Operacja czyszczenia flagi również musi być sekcją krytyczną. Task zawsze wpisuje jedną wartość, a kontrola wdg inną. Jednak może zdarzyć się sytuacja, że task przejdzie w stan ASLEEP, a za chwilę kontrola nadpisze UNKNOWN i w następnej iteracji wykryje błąd.

Dodatkowe wskazówki

W procedurze watchdoga można wykonać więcej testów, niż tylko sprawdzenie, czy taski żyją. Możemy na przykład dodać monitorowanie stacka, heapa, zadeklarowanych buforów, czy zewnętrznego sprzętu. Testy te mogą być elementem procedury watchdoga, a mogą również zostać przeniesione do oddzielnego tasku wspólnie z testami RAM, czy CPU.

Testy RAM – wprowadzenie

W trakcie debugowania włączony watchdog będzie pewnym problemem. W przypadku zatrzymania programu na breakpoincie może nam zresetować procesor. Dlatego mając procedurę decydującą o resecie watchdoga, można tam postawić instrukcję breakpointu programowego. Przy okazji jeśli implementujemy liczniki wywołań poszczególnych tasków, niosą one często informacje o potencjalnych błędach. Szczególnie jeśli monitorujemy także wartości minimalne i maksymalne tych liczników. Dlatego warto zaimplementować tą funkcjonalność dosyć wcześnie i monitorować timingi przy dodawaniu kolejnych elementów.

Podsumowanie

W tym wpisie przedstawiłem praktyczne podejście do implementacji watchdoga w aplikacji wielowątkowej. Dzięki takiemu podejściu jesteśmy w stanie wykrywać błedy związane z zawieszeniem całego programu, pojedynczych tasków, przerwań, czy nawet problemów z timingiem. Jesteśmy w stanie również w ten sposób wykryć błędy związane z RTOSem takie jak problemy w kernelu, deadlocki, czy zagłodzenia.

Źródła

https://www.embedded.com/electronics-blogs/beginner-s-corner/4023849/Introduction-to-Watchdog-Timers

http://www.ganssle.com/watchdogs.htm

https://barrgroup.com/Embedded-Systems/How-To/Advanced-Watchdog-Timer-Tips

https://betterembsw.blogspot.com/2014/05/proper-watchdog-timer-use.html

1 Comment

  1. Debugery działające przez JTAG mają możliwość zatrzymania wybranych peryferiali np. watchdoga. Wtedy problem resetu przez watchdoga gdy zatrzymujemy się na breakpoincie nie występuje.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *