Software – czyli jak tchnąć życie w sprzęt

Język programowania

Zacznijmy dziś trochę nietypowo, od klasyka:

Chodzi mi o to, aby język giętki
Powiedział wszystko, co pomyśli głowa:
A czasem był jak piorun jasny, prędki,
A czasem smutny jako pieśń stepowa,
A czasem jako skarga nimfy miętki,
A czasem piękny jak aniołów mowa…
Aby przeleciał wszystka ducha skrzydłem.
Strofa być winna taktem, nie wędzidłem.

Co prawda wątpię, iż Słowacki miał na myśli język programowania, jednak ten fragment “Beniowskiego” dobrze wpisuje się w nasze dzisiejsze rozważania 🙂

Jaki język wybrać – to pytanie zadaje sobie każdy, kto zaczyna przygodę z programowaniem. W Sieci toczyły się, toczą się i będą się toczyły zażarte boje i gorące dyskusje, na temat wyższości danego języka nad innym. I w zasadzie, każda strona sporu… ma rację. Każdy język jest dobry w konkretnym zastosowaniu. Parsowanie tekstu – Perl. Przenośność? Java! Lubisz liczyć wskaźniki na palcach? C. A może jesteś milczącym samotnikiem? Proszę bardzo, Whitespace.

Jaki język wybrać dla naszego projektu?

Ponieważ operujemy w świecie systemów wbudowanych, dostępność narzędzi i zdrowy rozsądek ogranicza nas tak na prawdę do dwóch języków: C oraz C++. Oba mają tyluż zwolenników co przeciwników i często programiści jednego z nich nie darzą szczególną sympatią tego drugiego. Jak zatem mają się one do siebie w przypadku mikrokontrolerów? Spójrzmy:

C:

Zalety:

  • lekkość,
  • podatność na optymalizację w trakcie kompilacji,
  • łatwość debugowania,

Wady:

  • trudne instancjonowanie obiektów,
  • brak dziedziczenia,
  • brak wbudowanych bibliotek standardowych (kontenery, algorytmy),

C++:

Zalety:

  • trywialnie proste instancjonowanie,
  • enkapsulacja (ograniczenie zakresu widzialności danych),
  • dziedziczenie (zwłaszcza interfejsów),
  • biblioteka standardowa,

Wady:

  • automatyczna alokacja na stercie,
  • utrudnione debugowanie na sprzęcie,
  • większa liczba odwołań do pamięci (dereferencje przy dostępie do składowych klas, funkcje wirtualne) ,
  • nieco niższa wydajność,

Patrząc na to krótkie zestawienie trudno jest wybrać zwycięzcę. W przypadku niewielkiego, prostego systemu werdykt byłby prosty – C. Ale to co planujemy zbudować absolutnie nie zalicza się do tej kategorii.

Dany język warto wybrać, jeśli mamy w perspektywie szansę wykorzystać to, co sobą oferuje. Nie potrzebujemy ekspresji języka C++, jeśli wszystko w naszym projekcie jest statycznie zdefiniowane, a wszystkie operacje można łatwo wyrazić programowaniem funkcyjnym. Równie nieodpowiedni będzie czysty C w przypadku, w którym chcemy dynamicznie tworzyć chmurę obiektów w trakcie działania systemu i odwoływać się do nich za pomocą uniwersalnego interfejsu. Użycie danego języka powinno więc być podyktowane nie subiektywnymi odczuciami, upodobaniami czy modą, a jasną i konkretną analizą: czego na prawdę potrzebujemy.

No więc czego na prawdę potrzebujemy?

Krótko: niskopoziomowy kod obsługujący peryferia, przerwania jest domena języka C. Bardziej wysokopoziomowe elementy, jak interfejs użytkownika to pole do popisu dla C++. Ponieważ w naszym projekcie spotykają się oba te światy, nie pozostaje nam nic innego jak… użyć obu.

Takie mieszanie dwóch języków jest nieeleganckie. Wprowadza tyle niespójności…

Wręcz przeciwnie! Naszym celem jest przede wszystkim użycie danego języka tam, gdzie będzie najbardziej efektywny, gdzie możliwości jego ekspresji będą stanowić zaletę. Przykład?

Każdy, kto ma trochę zacięcia do majsterkowania wie, że dla każdego rozmiaru śruby mamy osobny klucz. Owszem, możemy próbować odkręcić każdą śrubę jednym kluczem – francuskim. Ale na pewno nie będzie to ani łatwe, ani eleganckie.

 

System operacyjny czy bare-metal

Wiele osób, zwłaszcza posiadających już pewne doświadczenie w programowaniu, rozpoczyna przygodę z mikrokontrolerami od postawienia na nich niewielkiego systemu operacyjnego. Na pierwszy rzut oka jest to rozwiązanie idealne – przy stosunkowo niewielkim nakładzie pracy powstaje wygodne środowisko, zapewniające wielowątkowość, komunikację międzyprocesową, zarządzanie pamięcią, często nawet gotowe drivery sprzętowe.

Istnieje również dość powszechny (i niestety błędny) pogląd, iż bez systemu operacyjnego czy nawet prostego schedulera zadań, nie da się stworzyć jakiejkolwiek złożonej aplikacji na systemie wbudowanym. Bo maks co można zrobić w “pętli” to migać diodą LED… 😉

Wszystkie zewnętrzne zdarzenia, które nasz system musi obsłużyć mogą trafić do niego jedynie przez peryferia. Każde z nich jest w stanie zazwyczaj wygenerować przerwanie, które efektywnie jest w stanie zastąpić wątek w systemie operacyjnym.

Może jakiś przykład?

Ok, wyobraźmy sobie prosty terminal: wyświetlacz i port szeregowy (RS-232). Naszym zadaniem jest wyświetlanie bajtów które napływają po lini Rx. Rozwiązanie z systemem operacyjnym jest dość intuicyjne. Spójrzmy zatem na “goły metal”:

  1. W pętli sprawdzamy czy pojawił się nowy znak w buforze portu szeregowego, jeśli tak pobieramy go i wyrzucamy na wyświetlacz.
  2. Każdy odebrany bajt generuje przerwanie, bajt wrzucany jest do programowej kolejki FIFO. W pętli procesor sprawdza jedynie czy cokolwiek znajduje się w kolejce i wypisuje ciurkiem wszystko co ewentualnie w niej znajdzie.

Rozwiązanie 1. ma jedną, zasadniczą wadę – jeśli czas potrzebny na wypisanie znaku na ekran jest dłuższy niż czas przesłania jednego znaku po RS-232, będziemy gubić bajty. Rozwiązanie 2. wolne jest od tego problemu, no może poza przypadkiem przepełnienia się kolejki FIFO. Dla bardziej zaznajomionych z tematem: można również pójść krok dalej i zaprząc do pracy transfer DMA, który wyręczy procesor w obsłudze przerwania.

Na tym prostym przykładzie widzimy, że umiejętnie delegując zadania między przerwaniami a główną pętlą programu można skutecznie obsługiwać wiele “pseudo-współbieżnych” zadań. Należy przy tym pamiętać, iż mimo braku systemu operacyjnego wciąż obowiązuje nas konieczność zapewnienia synchronizacji i spójności danych (patrz wspomniana kolejka FIFO).

Kiedy więc system operacyjny na mikrokontrolerze ma sens?

Na to pytanie nie da się udzielić jednoznacznej odpowiedzi, można natomiast wskazać pewien obszar zastosowań,  w którym używanie schedulera ułatwia życie. Są nim wszelkie systemy, w których występuje współbieżne przetwarzanie asynchronicznie nadchodzących żądań, co do których trudno określić jest czas wymagany na ich obsłużenie.

Brzmi zagadkowo…

Wyobraźmy sobie zadania A, B i C. Zadanie A jest relatywnie duże, ale ma niski priorytet. Zadania B oraz C są mniejsze i mają równy priorytet, różnią się jednak między sobą czasem potrzebnym na ich wykonanie. Mamy więc trzy zadania/wątki: A – toczący się powoli w “tle” oraz B i C, które walczą między sobą o dostęp do czasu CPU. W podany przykład świetnie wpisują się m.in. urządzenia sieciowe, przetwarzające dane powyżej warstwy drugiej (routery). Przy braku systemu operacyjnego, implementacja przełączania się kontekstu przetwarzania “w locie”, w obrębie pojedynczej jednostki wykonania byłaby niezwykle złożona.

Czy zatem w naszym projekcie będzie system operacyjny?

Odpowiedź jest krótka – nie.

Jak to “nie”? Przecież jest tyle różnych zadań: magistrala CAN, diagnostyka, karta pamięci, dźwięk, przyciski no i przecież grafikę trzeba rysować!

No właśnie o tym sceptycyzmie pisałem na początku…. Postaram się zatem udowodnić, że nawet w przypadku tak rozbudowanego systemu da się znośnie żyć bez systemu.

Posted in Sto dni w kolorze.