RTOS vs callbacki

Callbacki w języku C to jeden z kluczowych mechanizmów dzięki któremu można zapewnić reużywalność bibliotek.

W dużym skrócie (ponieważ ten wpis nie tłumaczy podstaw tylko bardziej wysokopoziomowy problem) - dzięki temu możemy przekazać do uniwersalnego modułu funkcję z logiką aplikacyjną konkretnej aplikacji do wywołania. Dla mnie najlepszy przykład dla wykorzystania callbacków to biblioteka do obsługi przycisków. W module przycisków zamknięta jest cała logika związana z debouncingiem, wykrywaniem eventów itp - taki moduł nie powinien mieć w sobie żadnej wiedzy aplikacyjnej - jednak potrzeba jakoś wywołać reakcję na konkretne zdarzenia. Wtedy taka biblioteka może podczas inicjalizacji przyjmować kilka wskaźników do funkcji dla konkretnych eventów i je uruchamiać po ich wykryciu.

Podczas używania modułów z callbackami zazwyczaj można wyróżnić 3 składowe elementy:

  • funkcja która będzie callbackiem
    • zazwyczaj to biblioteka definiuje jakiego typu ma to być funkcja - jakie parametry ma przyjmować i/lub co też musi zwracać
  • metoda rejestracji callbacka; najczęściej spotykane:
    • podczas inicjalizacji modułu
    • podczas runtime - przez funkcję do rejestracji (wtedy też zazwyczaj można podmieniać w czasie pracy na różne callbacki)
    • metoda z “#define” nazw funkcji - ja to nazywam “przesłanianiem nazw”, ale osobiście jej nie używam, bo zdecydowanie zmniejsza czytelność
  • w bibliotece z zależności od jej logiki znajdzie się gdzieś wywołanie tego callbacka po wykryciu “eventu”

fasdf

Artykuł Jacob’a Beningo na temat callbacków - (to z niego jest powyższy obrazek): https://www.beningo.com/embedded-basics-callback-functions/ (generalnie bardzo polecam zapoznanie się z pracą Jacob’a).

I właśnie z tym wywołaniem może być pewien problem do przemyślenia jeśli jest wykorzystywany RTOS.

Na czym polega potencjalny problem?

Załóżmy “standardowe” podejście MAIN+ISR i bibliotekę przycisków.

Działanie aplikacji będzie wyglądać mniej więcej tak:

fasdf

Nie ma tutaj niczego dziwnego - klasyczne podejście spotykane w wielu bibliotekach - w głównym loopie sprawdzamy eventy z poszczególnych bibliotek. Ewentualnie jeśli w urządzeniu jest zastosowany jakiś prosty scheduler - funkcje z biblioteki przekazujemy do niego, a czasem w ten sposób można pozbyć się też funkcji sprawdzającej z poziomu ISR i przerzucić to też do schedulera.

Teraz odpowiednik tego, ale z wykorzystaniem RTOSa - tak samo używamy “zwykłego” callbacka, ale wszystko przeniesione do kontekstu oddzielnego taska.

fasdf

Powiedziałbym, że często taka forma do bibliotek będzie widoczna u osób, które dopiero przechodzą na RTOSa - czyli w prosty sposób obudowanie starej biblioteki z podejścia “main+ISR” na podejście z RTOSem.

Więc gdzie jest ukryty problem? Na diagramach oznaczone są konteksty - gdzie w MAIN+ISR będzie to tak naprawdę wspólny stos, natomiast w RTOSie task “buttons” uruchomi w swoim kontekście funkcję użytkownika korzystając ze swojego przypisanego stosu - o ile bardzo łatwo jest wyciągnąć informacje ile stosu (w gcc dodatkowa flaga -Wstack-usage wygeneruje komplet informacji do przeparsowania, a w CubeIDE dla STM32 jest narzędzie do analizy danych z tych plików i wizualizacji) potrzeba na funkcję danego taska o tyle w przypadku callbacków już nie jest to takie proste.

Czyli w przypadku “MAIN+ISR” używamy “aplikacyjnego/wspólnego” stosu, natomiast w wersji implementacji z wykorzystaniem RTOSa - callback użytkownika jest wołany z poziomu taska przycisków - czyli zmienne lokalne tego callbacka i wszystkich funkcji które będzie on potem wołał będą używać jego stosu.

Jakie to podejście ma wady:

  • na użytkownika przerzucono odpowiedzialność zadbania o to, aby wystarczyło miejsca na stosie w tym tasku
  • potencjalne źródło problemu ze stackoverflow dla tego taska
  • inną kwestią wartą też poruszenia jest czas wykonywania tego callbacka - zbyt długi może spowodować przesunięcie uruchomienia kolejnego “ticku” taska do obsługi przycisków - to też wtedy przerzucamy na użytkownika

Biblioteki powinny ułatwiać pracę, więc spychanie odpowiedzialności na użytkownika w tego typu kwestiach powinno być ostatecznością. Dodatkowo na tym etapie wiemy już z jakiego typu “klasą problemu” będzie trzeba się zmierzyć jeśli parametry będą źle ustawione - problemy z przepełnieniem się stosu - dość ciężkie do zdebugowania (choć w fazie dev w RTOSach polecam mieć zawsze włączony mechanizm detekcji).

W jaki sposób można to zrobić lepiej przy zachowaniu zbliżonej funkcjonalności?

Zmiana podejścia z callbacków na np. “kolejkę komend” wychodzącą z modułu - czyli wyniki eventów (to co w callbacku byłoby parametrami funkcji) obudować w strukturę i przesyłać kolejką - udostępniając interfejs do odebrania danych z tej kolejki (alternatywne rozwiązanie - można też przekazywać obiekt kolejki do użytkownika, aby sam to obudował). W sumie przypomina to trochę użycie patternu “active object” - gdzie interfejs wejściowy/komendy do modułu ubieramy w kolejkę - tutaj tak samo zrobimy w przypadku danych wyjściowych.

“wady” takiego rozwiązania (tzn. osobiście dla mnie te “wady” mają dość mały priorytet skoro zwiększamy dzięki temu reużywalność):

  • zwiększone zużycie zasobów
    • zasoby na kolejkę z komendami
    • zasoby na task użytkownika z poziomu którego eventy będą przetwarzane
  • co za tym idzie - trzeba napisać ten task itp - wcześniej wystarczyło zaimplementować tylko funkcję callbacka

Co zyskujemy:

  • nie trzeba uwzględniać wywołania callbacka przy wyliczaniu rozmiaru stosu dla taska!
  • czyli worst case dla użycia stosu biblioteki button będzie stały - nie będzie już zależał od tego poda użytkownik
  • użytkownik może wprost wykonywać z poziomu swojego taska operacje o większym czasie wykonywania - bez wpływu na interwał pomiarowy biblioteki button
  • dodatkowo “nie zgubimy” eventów przycisków w takiej sytuacji - kolejka może przechować kilka eventów (można wystawić interfejs do ustawiania jej długości) - wcześniej taki mechanizm użytkownik musiałby dopisać sam - potencjalny imporvement

Przykład implementacji - w kolejnym wpisie, będzie on modyfikacją biblioteki z postu: https://www.embedownik.pl/posts/012.html, która aktualnie to na użytkownika przerzuca dbanie o rozmiar stosu.

Podobny problem można napotkać przy implementacji np. systemu logowania - jeśli w urządzeniu występuje kilkanaście tasków i każdy będzie coś logował ze swojego kontekstu - należy w każdym tasku zwiększyć rozmiar stosu tak, aby umożliwić wywołanie funkcji typu snprinf itp (zakładam, że biblioteki do logów wołają je pod spodem do sformatowania/ustandaryzowania linijki logu) + zapewne jakiś bufor lokalny na sformatowany text (żeby zapewnić, że funkcje są reentrant) - zmarnujemy wtedy bardzo dużo pamięci RAM (chyba, że mamy jest sporo i nie trzeba się tym przejmować). Do obsługi logowania powstanie osobna biblioteka z osobnym wpisem ze szczegółami jak do tego podejść.