RTO Lab 7
Software Timers
Przyszedł na mnie czas
Wprowadzenie
Do tej pory układaliśmy nasze aplikacje FreeRTOS z klocków takich jak:
- taski o różnych priorytetach
vTaskDelay()- kolejki
- semafory i mutexy
- Task Notifications
- Dzięki nim potrafimy na przykład „budzić” taski na sygnał od innych tasków / ISR, przekazywać dane między wątkami albo po prostu czekać na zdarzenia bez marnowania zasobów CPU.
Pozostaje jednak jeden, bardzo częsty problem praktyczny:
„Chcę, żeby co X ms coś się działo” albo „Jeśli w ciągu Y ms nie stało się Z, to wykonaj jakąś akcję (timeout)”.
Oczywiście można to posklejać z vTaskDelay() i pętli wewnątrz taska, ale szybko może się to skończyć np. taskami, które istnieją tylko po to, żeby liczyć czas, albo patologicznym spaghetti-code z vTaskDelay() w kilku miejscach kodu taska, a przede wszystkim komplikuje zmianę interwałów (czasem co ćwierć sekundy, a czasem co dwie).
I aby pomóc nam rozwiązać te problemy w sposób elegancki na scenę wkraczają - całe na biało - Software Timers
Software timer to obiekt FreeRTOS, który odlicza ticki systemowe, a po upływie zadanego czasu wywołuje wskazaną funkcję. Alternatywnie, timery software’owe mogą również wykonywać zadaną funkcję okresowo. Funkcja wykonywana przez software timer nazywana będzie software timer’s callback function
Każdy timer ma:
- okres (period) – ile ticków mija między startem a wywołaniem callbacka,
- tryb:
- one-shot – strzela jeden raz i koniec,
- auto-reload – po wywołaniu callbacka automatycznie startuje od nowa;
- callback – funkcja wywoływana przy wygaśnięciu timera.
Wewnątrz jądra działa sobie timer service (daemon) task, który:
- trzyma listę aktywnych timerów,
- odbiera komendy z wewnętrznej kolejki (start, stop, reset, zmiana okresu, delete),
- gdy któryś timer „wygaśnie”, wywołuje jego callback.
A prościej?
Wyobraź sobie budzik:
- Nastawiasz go na 7:00 (bo zajęcia o 8:00)
- Kładziesz się spać
- Budzik sam pilnuje czasu
- Punktualnie o 7:00 - dzwoni
My nie musimy dzięki zastosowaniu tego budzika co chwila otwierać oczu i patrzeć na zegar, ani tym bardziej liczyć w głowie czasu od zaśnięcia. To byłoby dość trudne - a właśnie tak (w pewnym uproszczeniu) tworzyliśmy do tej pory systemy.
Porównanie z budzikiem jest w zasadzie wielowarstwowe i można na nim wytłumaczyć również zasadę działania trybów timerów:
- one-shot to budzik jednorazowy, odpalany na jeden dzień (np. piątek, kiedy zaczynam dwie godziny wcześniej niż każdego innego dnia). Uruchomi się raz i już nigdy więcej - chyba że go przeładujemy manualnie.
- auto-reload będzie budzikiem ustawianym od poniedziałku do piątku na tę samą godzinę - dzwoni co 24 godziny i sam się przeładowuje.
W FreeRTOS przy tworzeniu Software Timera określać będziemy trzy rzeczy: okres, tryb, funkcję, którą ma wykonać.
No i po co to wszystko?
Zastosowań jest mnogość, przejdziemy dziś przez kilka z nich, ale najprościej jest to wyjaśnić obrazowo:
1. One-shot Timer
- Opóźniona reakcja na przycisk (debouncing) - użytkownik naciska przycisk, a my chcemy zareagować dopiero, gdy przycisk był stabilnie wciśnięty przez 50 ms. ISR od przycisku tylko startuje / resetuje one-shot timer na 50 ms. Jeśli po 50 ms timer „zadzwoni”, w callbacku sprawdzamy stan pinu – jeśli wciąż wciśnięty - dopiero wtedy uznajemy, że klik był prawdziwy. Dzięki temu nie reagujemy na drgania styków, a logika siedzi w ładnym miejscu (callback), a nie w brzydkich pętlach z opóźnieniami.
- Jednorazowe „opóźnione” zdarzenie w UI - użytkownik coś klika, a my chcemy po 3 sekundach pokazać komunikat („Zapisano ustawienia”, „Restarting”, “Jajco”). Po kliknięciu przycisku: start timer one-shot na 3000 ms. W callbacku wyświetlamy komunikat na ekranie, mrugamy diodą, przełączamy stan urządzenia itp. Doskonałe, jeśli nie chcemy np. blokować interfejsu
vTaskDelay().
2. Auto-reload Timer
- Okresowe odświeżanie ekranu / UI - np. mamy mały wyświetlacz (np. TFT, OLED), który ma się odświeżać np. co 200 ms. Ustawiamy timer auto-reload co 200 ms. W callbacku ustawiamy flagę „odśwież UI” albo od razu odświeżamy licznik, godzinę, poziom sygnału, etc. Dzięki temu UI jest „na zegarku”, a logika w taskach może spokojnie robić swoje.
- Regularne zapisywanie danych do pamięci (logi, statystyki) - co 10 sekund zapisujemy do flasha lub na kartę SD np. temperaturę, licznik błędów, stan urządzenia. Timer auto-reload z okresem 10 000 ms. Callback wrzuca dane do kolejki dla „taska od zapisu” albo sam robi prosty zapis.
Stany egzystencji timerów.
Timer programowy w FreeRTOS może znajdować się w jednym z dwóch stanów: 1. Dormant (uśpiony / nieaktywny) Timer w stanie Dormant:
- istnieje w systemie - został utworzony i ma ważny uchwyt (
TimerHandle_t), - ale nie jest uruchomiony - system nie odlicza jego okresu,
- jego funkcja callback nie będzie wywoływana, dopóki timer nie zostanie wystartowany. Przykładowo: świeżo po utworzeniu timer jest właśnie w stanie Dormant – dopiero wywołanie
xTimerStart()lubxTimerReset()(w zależności od konfiguracji) przełącza go w stan Running.
2. Running (aktywny / działający) Timer w stanie Running:
- jest aktywnie odliczany przez FreeRTOS (na podstawie ticków systemowych),
- po upływie czasu równego jego okresowi:
- od momentu wejścia w stan Running
- lub od ostatniego resetu (
xTimerReset()), wywołana zostanie jego funkcja callback.
- W zależności od typu timera:
- dla timera one-shot – po wywołaniu callbacka timer zwykle wraca do stanu Dormant,
- dla timera auto-reload – po wywołaniu callbacka jego licznik jest automatycznie zerowany i timer pozostaje w stanie Running odliczając kolejny okres.
Przejście między stanami (Dormant <-> Running) odbywa się poprzez wywołania API (xTimerStart(), xTimerStop(), xTimerReset()), dopóki timer jest w stanie Running, możemy go w każdej chwili zatrzymać, zmienić jego okres albo zresetować odliczanie.
Podsumowując zatem
Software Timery są dla nas takim wbudowanym budzikiem FreeRTOS-a: zamiast upychać vTaskDelay() po taskach i ręcznie liczyć czas, tworzymy obiekt timera, podajemy mu:
- okres (jak długo ma czekać),
- tryb (jeden strzał czy powtarzanie),
- callback (co ma się stać, kiedy „zadzwoni”). Timer może być:
- Dormant – istnieje, ale „leży w szufladzie”, niczego nie odlicza, callback się nie wykona,
- Running – aktywnie odlicza ticki i po upływie okresu wywoła callback (raz albo cyklicznie).
Cała „magia zegarka” dzieje się w timer service tasku, więc:
- nie potrzebujemy osobnych tasków, które tylko śpią i czekają na upłynięcie czasu,
- logika czasowa jest skupiona w jednym miejscu (callbacki timerów),
- łatwo jest zmieniać interwały (wystarczy zmienić parametr timera, a nie pół taska).
W dalszej części labu zobaczymy, jak taki timer stworzyć, wystartować, zatrzymać i zresetować, a potem wykorzystamy to do zbudowania konkretnych mechanizmów, których na tym etapie pisania laba jeszcze nie wymyśliłem.
To teraz technicznie
Każdy timer wykonuje funkcję callback, której prototyp musi być zgodny z typem TimerCallbackFunction_t:
typedef void (*TimerCallbackFunction_t)( TimerHandle_t xTimer );
Przykładowa definicja funkcji:
void vTimerCallback( TimerHandle_t xTimer )
{
// Kod wykonywany przy wygaśnięciu timera
}
- Funkcja nie zwraca żadnej wartości (
void). -
Jako parametr dostaje uchwyt timera, który ją wywołał – w callbacku możemy dzięki temu:
- sprawdzić jego okres (
xTimerGetPeriod()), - odczytać/ustawić ID (
pvTimerGetTimerID(),vTimerSetTimerID()), - rozróżnić kilka timerów używających tego samego callbacka.
- sprawdzić jego okres (
1) xTimerCreate() — tworzenie timera (dynamiczne)
TimerHandle_t xTimerCreate(
const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
BaseType_t xAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction
);
Tworzy nową instancję timera programowego i zwraca uchwyt (TimerHandle_t), przez który timer będzie później referencjonowany. Samo utworzenie nie uruchamia timera.
Argumenty:
| Argument | Znaczenie |
|---|---|
pcTimerName | Opcjonalna, „ludzka” nazwa timera (dla debugowania, np. w trace’u). |
xTimerPeriodInTicks | Okres timera w tickach (TickType_t). Typowo podajemy pdMS_TO_TICKS(ms). |
xAutoReload | pdTRUE - timer typu auto-reload; pdFALSE - one-shot |
pvTimerID | Każdy timer ma swoją wartość ID - jest to void pointer - może zatem być użyty do dowolnego celu. Np. jakiś storage. |
pxCallbackFunction | Funkcja callback wywoływana przy zakończeniu timera (TimerCallbackFunction_t). |
Zwraca:
- not-NULL (
TimerHandle_t) – timer utworzony poprawnie, - NULL – błąd (np. brak pamięci).
Timer można wywołać również statycznie xTimerCreateStatic() - w tym wypadku musimy w dodatkowym argumencie wywołania dostarczyć bufor na strukturę timera.
2) xTimerStart() — uruchomienie timera
BaseType_t xTimerStart(
TimerHandle_t xTimer,
TickType_t xTicksToWait
);
Wysyła do timer service tasku polecenie „uruchom timer”. Jeżeli timer był w stanie Dormant, przechodzi do stanu Running i zaczyna odliczać okres.
Argumenty:
| Argument | Znaczenie |
|---|---|
xTimer | Uchwyt do timera, który ma zostać uruchomiony. |
xTicksToWait | Maksymalny czas (w tickach), przez jaki wywołujący task może blokować się, czekając na miejsce w kolejce komend timera. Nie ma nic wspólnego z czasem odliczanym przez sam timer. Często po prostu 0 |
Zwraca:
pdPASS– komenda startu została poprawnie wysłana do kolejki (timer wystartuje),pdFAIL– nie udało się wysłać komendy (np. przepełniona kolejka timera).
xTimerStartFromISR()
BaseType_t xTimerStartFromISR(
TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken
);
Wersja do użycia w ISR. Jeżeli wywołanie powoduje wybudzenie taska timera o wyższym priorytecie niż aktualnie wykonywany task, ustawia *pxHigherPriorityTaskWoken = pdTRUE. Po wyjściu z ISR należy wtedy wymusić przełączenie kontekstu.
3) xTimerStop() — zatrzymanie timera
BaseType_t xTimerStop(
TimerHandle_t xTimer,
TickType_t xTicksToWait
);
Zatrzymuje timer (przechodzi do stanu Dormant, jeśli był Running). Komenda jest wysyłana do timer service tasku przez kolejkę.
Argumenty:
| Argument | Znaczenie |
|---|---|
xTimer | Uchwyt do timera, który ma zostać zatrzymany. |
xTicksToWait | Czas oczekiwania na miejsce w kolejce komend timera (jak w xTimerStart()). |
Zwraca:
pdPASS– komenda stop została poprawnie wysłana,pdFAIL– nie udało się (np. kolejka pełna).
xTimerStopFromISR()
BaseType_t xTimerStopFromISR(
TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken
);
Wersja do użycia w ISR, działa analogicznie do xTimerStartFromISR().
4) xTimerReset() — restart / start timera
BaseType_t xTimerReset(
TimerHandle_t xTimer,
TickType_t xTicksToWait
);
Restartuje timer:
- jeśli timer jest już Running → jego czas wygaśnięcia liczony jest od nowa (od momentu resetu),
- jeśli timer był Dormant → reset pełni rolę startu (timer przechodzi do Running).
Argumenty i wartości zwracane jak w xTimerStart() (xTimer, xTicksToWait, pdPASS / pdFAIL).
xTimerResetFromISR()
BaseType_t xTimerResetFromISR(
TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken
);
Wersja resetu do wywołania w ISR.
5) xTimerChangePeriod() — zmiana okresu timera
BaseType_t xTimerChangePeriod(
TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait
);
Zmienia okres istniejącego timera na nowy (xNewPeriod). Jeśli timer jest Running, jego nowy czas wygaśnięcia będzie liczony od momentu wywołania tej funkcji, z użyciem nowego okresu.
Argumenty:
| Argument | Znaczenie |
|---|---|
xTimer | Uchwyt do timera, którego okres chcemy zmienić. |
xNewPeriod | Nowy okres timera w tickach (TickType_t). |
xTicksToWait | Jak długo można czekać na miejsce w kolejce komend timera. |
Zwraca:
pdPASS– komenda zmiany okresu przyjęta,pdFAIL– nie udało się wstawić komendy do kolejki.
xTimerChangePeriodFromISR()
BaseType_t xTimerChangePeriodFromISR(
TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken
);
Wersja do użycia w ISR – analogicznie jak inne funkcje *FromISR().
6) xTimerDelete() — usunięcie timera
BaseType_t xTimerDelete(
TimerHandle_t xTimer,
TickType_t xTicksToWait
);
Usuwa timer utworzony wcześniej przez xTimerCreate() / xTimerCreateStatic(). Po usunięciu uchwyt staje się nieważny.
Argumenty:
| Argument | Znaczenie |
|---|---|
xTimer | Uchwyt do timera, który ma zostać usunięty. |
xTicksToWait | Czas oczekiwania na miejsce w kolejce komend timera. |
Zwraca:
pdPASS– komenda usunięcia została przyjęta,pdFAIL– nie udało się wysłać komendy.
7) xTimerIsTimerActive() — sprawdzenie stanu timera
BaseType_t xTimerIsTimerActive(
TimerHandle_t xTimer
);
Pozwala sprawdzić, czy timer jest aktualnie Running. Przydatne, gdy chcemy warunkowo startować/stopować timery lub diagnozować ich zachowanie.
Zwraca:
pdFALSE– timer NIE jest aktywny (np. w stanie Dormant lub wygasły one-shot, który nie został zrestartowany),pdTRUE– timer jest Running.
Przykładowy kod
#include <Arduino.h>
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
const int LED_PIN = 2; // dopasuj do swojej płytki
const int BUTTON_PIN = 4; // przycisk do GND, wejście z PULLUP
TimerHandle_t blinkTimer;
// Callback timera – co okres zmienia stan LED-a
void vBlinkCallback(TimerHandle_t xTimer)
{
(void)xTimer; // nieużywany parametr
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
void setup()
{
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP); // przycisk zwiera do masy
// Tworzymy timer: 500 ms, auto-reload, prosty callback
blinkTimer = xTimerCreate(
"Blink",
pdMS_TO_TICKS(500),
pdTRUE,
nullptr,
vBlinkCallback
);
// Na start timer jest zatrzymany – LED nie miga
}
void loop()
{
static int lastButton = HIGH;
int current = digitalRead(BUTTON_PIN);
// Wykrycie zbocza opadającego (puszczony -> wciśnięty)
if (lastButton == HIGH && current == LOW)
{
// Jeśli timer nieaktywny -> start; jeśli aktywny -> stop
if (xTimerIsTimerActive(blinkTimer) == pdFALSE)
{
xTimerStart(blinkTimer, 0);
}
else
{
xTimerStop(blinkTimer, 0);
digitalWrite(LED_PIN, LOW); // zgaś LED po zatrzymaniu
}
}
lastButton = current;
delay(20); // proste odfiltrowanie drgań styków
}
Zadania
Zadanka dzisiaj zaprojektowane są tak, żeby was skutecznie spacyfikować na dłużej - Pierwwsze dwa, podobnie jak tydzień temu dotyczą tylko bazowej implementacji wprowadzonego dziś materiału. Dwa kolejne, o rosnącym poziomie trudności będą wymagać trochę pracy i pokazują bardziej profesjonalne zastosowania timerów - pierwsze dwa zadania każdy zespół powinien rozliczyć na zajęciach. Zadania 3 i 4 można będzie oddać za tydzień. GPT porozbijał mi zadania na prostsze taski - jak coś będzie niejasne albo wygląda jak bełkot to proszę wołać, jest 3:15.
Zadanie 1 — „Mrugająca dioda na budziku” (podstawy timera)
Cel: Poznanie podstaw API software timerów: xTimerCreate(), xTimerStart(), xTimerStop(), callback, tryb auto-reload.
Opis:
- Utwórz jeden timer auto-reload z okresem np. 250–1000 ms.
-
Timer w callbacku ma przełączać stan jakiegoś wyjścia:
- może to być dioda LED,
- albo po prostu wypis na UART („TICK”).
- Na początku programu timer jest zatrzymany.
-
Jeden przycisk (albo komenda przez UART) ma:
- jeśli timer jest zatrzymany → uruchomić go,
- jeśli timer jest uruchomiony → zatrzymać i „posprzątać” (np. zgasić LED).
Wymagania techniczne:
- użyj
xTimerCreate()(lubxTimerCreateStatic()), - użyj
xTimerIsTimerActive()do sprawdzenia stanu, - logika w callbacku ma być krótka – żadnych
vTaskDelay().
Możliwe rozszerzenie (dla chętnych):
- po zatrzymaniu timera zmień okres (np. z 250 ms na 1000 ms) i wystartuj go ponownie; użyj
xTimerChangePeriod().
Zadanie 2 — „Przycisk, który przestaje drżeć” (one-shot + debounce)
Cel: Pokazanie praktycznego zastosowania timera one-shot do eliminacji drgań styków (debounce) i komunikacji z taskiem.
Opis:
-
Konfigurujesz przerwanie od przycisku (lub w prostszej wersji — polling w tasku, ale ISR jest mile widziany).
-
Tworzysz timer one-shot o okresie ok. 30–50 ms.
-
Każde wciśnięcie / zmiana stanu przycisku:
- w ISR wywołuje
xTimerStartFromISR()lubxTimerResetFromISR()na tym timerze, - timer startuje / restartuje od nowa.
- w ISR wywołuje
-
Jeżeli po tych 30–50 ms nikt nie „ruszył” timera, jego callback:
- czyta aktualny stan przycisku,
- jeśli wciąż jest wciśnięty → uznajemy to za „prawdziwe” wciśnięcie.
-
Informacja o ustabilizowanym wciśnięciu trafia do głównego taska:
- np. przez kolejkę,
- albo przez task notification.
Task po otrzymaniu zdarzenia może np.:
- przełączyć LED,
- policzyć liczbę kliknięć,
- wypisać coś na UART.
Wymagania techniczne:
- timer musi być typu one-shot (
xAutoReload = pdFALSE), - użyj wersji
*FromISR()przy wywołaniach z przerwania, - większość „grubszej” logiki (zmiana LED, obsługa UI) powinna odbywać się w tasku, a nie w callbacku timera.
Rozszerzenia (opcjonalnie):
-
rozróżnij krótkie i długie przyciśnięcie:
- krótkie kliknięcie → pojedyncze naciśnięcie (one-shot timer),
- długie przytrzymanie → drugi timer, który zlicza np. 1 s.
Zadanie 3 — „Timeout na komunikację” (one-shot + taski)
Cel: Zastosowanie timera jako mechanizmu timeoutu między taskami – integracja timera, kolejek / notification i prostego „protokołu”.
Opis:
Zbuduj prosty scenariusz z dwoma taskami:
- Task A – „zleceniodawca”,
- Task B – „pracownik”, który wykonuje zadanie z pewnym opóźnieniem.
Przebieg:
- Task A wysyła do Task B „zlecenie” (np. przez kolejkę).
-
W momencie wysyłania zlecenia Task A:
- uruchamia timer one-shot o okresie np. 2000 ms (
xTimerStart()lubxTimerReset()), - zapisuje gdzieś informację, że „czekamy na odpowiedź”.
- uruchamia timer one-shot o okresie np. 2000 ms (
- Task B po jakimś czasie (np. po
vTaskDelay(500)lub dłużej) odpowiada do Task A (kolejką / notification). -
Po otrzymaniu odpowiedzi Task A:
- zatrzymuje timer (
xTimerStop()), - melduje „OK, odpowiedź przyszła na czas”.
- zatrzymuje timer (
-
Jeżeli odpowiedź nie przyjdzie w ciągu 2 s:
- timer wywołuje callback,
- callback informuje Task A o timeout (np. notification lub wrzucenie specjalnej wartości do kolejki),
- Task A przechodzi w stan „błąd” (zapala czerwoną diodę, wypisuje komunikat, resetuje B, itd.).
Wymagania techniczne:
- jeden timer one-shot, współpracujący z Task A,
- przynajmniej jedna kolejka lub task notifications między A i B,
-
rozróżnienie dwóch przypadków:
- odpowiedź na czas (timer zatrzymany),
- timeout (timer wywołał callback).
Rozszerzenia (opcjonalnie):
- Task A może co jakiś czas ponawiać próbę (kolejna komenda + restart timera);
- sygnalizuj różne stany systemu diodami / buzzerem.
Zadanie 4 — „Softowy scheduler / watchdog mini-system” (wiele timerów, stan, FromISR)
Cel: Zbudowanie małego, wieloelementowego „systemu czasowego” opartego na kilku timerach: watchdog, okresowe zadanie, zmiana okresu, reakcja na „zawieszony” task.
Opis (propozycja scenariusza):
Masz system z jednym „głównym taskiem” obsługującym np. logikę urządzenia. Chcemy:
- Co 500 ms wykonywać prostą akcję okresową (np. status LED, log na UART) → timer auto-reload A.
- Pilnować, że główny task „żyje” i melduje się regularnie → timer one-shot B pełniący rolę watchdog’a.
- Zmieniać okres jednego z timerów w locie w reakcji na zdarzenia (np. po naciśnięciu przycisku / otrzymaniu komendy) →
xTimerChangePeriod().
Szczegóły:
-
Timer A (auto-reload)
- okres np. 500 ms,
- callback: przełącza LED statusu albo wysyła powiadomienie do taska, że „minęło 500 ms”.
-
Timer B (one-shot, watchdog)
- okres np. 2000 ms,
- główny task w każdej iteracji pętli / po wykonaniu ważnego kroku wywołuje
xTimerReset()na tym timerze („karmi psa”), -
jeśli główny task się zawiesi lub „zapomni nakarmić”:
- Timer B dojdzie do końca → callback,
- w callbacku ustawiasz flagę „system w stanie awaryjnym”, włączasz czerwoną diodę, może wysyłasz komunikat, ew. restartujesz MCU.
-
Wejście zewnętrzne (przycisk / ISR / UART)
-
np. przycisk zmienia tryb pracy:
- w trybie normalnym Timer A ma okres 500 ms,
- w trybie „oszczędzania energii” → zwiększasz okres do 2000 ms (
xTimerChangePeriod()),
-
jeśli używasz ISR, skorzystaj z
xTimerChangePeriodFromISR()/xTimerStartFromISR().
-
Wymagania techniczne:
- min. dwa różne timery (one-shot + auto-reload),
- zmiana okresu timera w locie (
xTimerChangePeriod()lubxTimerChangePeriodFromISR()), - zastosowanie timera jako watchdog’a: brak resetu → callback sygnalizuje awarię,
-
czysta separacja odpowiedzialności:
- timery zajmują się „kiedy”,
- taski zajmują się „co”.
Rozszerzenia (opcjonalnie):
- rozbij logikę na kilka tasków (np. UI, logika, komunikacja) i daj każdemu własny watchdog (kilka timerów);
- dodaj stanową maszynę (np. „INIT → RUN → ERROR”) i część przejść wyzwalaj przez timery.