W systemach embedded zwykle skupiamy się na niskopoziomowych interakcjach ze sprzętem. Poznajemy nowe interfejsy, wykorzystujemy kolejne zewnętrzne układy i wykorzystujemy nowe rodziny procesorów. Jednak to z czym sobie naprawdę nie radzimy to poskromienie rosnącej złożoności tworzonych przez nas systemów. W większości systemów prawdziwym problemem jest architektura, czyli systematyczne podejście do radzenia sobie z tą złożonością. Jako branża embedded nie potrafiliśmy do tej pory stworzyć jasnych zasad tworzenia architektury i utrzymywania jej w projekcie. Zwykle dominują dwa podejścia – albo architektury nie ma wcale, albo tworzona jest na początku przed rozpoczęciem kodowania i okazuje się niepraktyczna. W tym wpisie dzielę się swoimi spostrzeżeniami na temat problemów z architekturą w embedded.
Hardware Driven Design
Pierwszą rzeczą na jakiej skupiamy się w projekcie embedded jest hardware. Mając jakiś problem do rozwiązania patrzymy na peryferia, które musimy do tego wykorzystać i od razu bierzemy się do ich oprogramowania. Nic dziwnego – część HW jest uważana za najtrudniejszą i najbardziej niewiadomą. Musimy sprawdzić, czy wymyślona przez nas koncepcja ma szansę się sprawdzić w praktyce. Informacje o potrzebnym hardware są potrzebne również, aby stworzyć projekt PCB. Cała reszta projektu jest uznawana za prostszą i niegodną spędzania nad nią czasu. Dlatego wyższe warstwy architektury często nie są zbyt dobrze zdefiniowane. Co więcej jest ona podrzędna w stosunku do części sterującej peryferiami, musi się dostosować do API driverów, co często skutkuje skomplikowanym kodem i trudnościami w dalszym utrzymaniu.
A przecież wcale nie musi tak być. Wystarczy potraktować początkowe eksperymenty z hardwarem jako Proof Of Concept, który nie musi od razu stawać się kodem produkcyjnym. Na początku naszym celem powinno być udowodnienie, że jakieś konkretne działanie jest możliwe do uzyskania. A ostateczny kształt drivera powinien być skutkiem przemyślanej architektury całego systemu, w której w dodatku pełni zwykle funkcję szczegółu implementacyjnego.
Zależności od zewnętrznych bibliotek
W kodzie embedded bardzo często nie podejmujemy żadnych prób odseparowania logiki biznesowej od zewnętrznych bibliotek. Dobra architektura traktuje biblioteki jako szczegół implementacyjny ukryty za warstwą abstrakcji przy pomocy wzorców projektowych takich jak Adapter, Proxy, czy Bridge. W efekcie nasz kod bezpośrednio zależy bibliotek peryferiów (np. STM StdPeriph, Microchip Harmony), funkcji konkretnego RTOSa (np. FreeRTOSa), stosu TCP (np. lwIP), czy systemu plików (np. FatFS). Może nam się to wydawać niegroźne, ale tylko do momentu, kiedy będziemy chcieli taką bibliotekę zamienić na inną w połowie projektu.
Przyczyn takiego podejścia naley upatrywać w notach aplikacyjnych, przykładach kodu i różnych kursach. Tam biblioteki są wywoływane bezpośrednio, żeby nie zaciemniać obrazu dodatkowymi warstwami abstrakcji. W końcu te przykłady służą do celów edukacyjnych. Niestety są one później używane w ten sam sposób w kodzie produkcyjnym.
Ograniczenia języka C
Powszechne stosowanie języka C w embedded na pewno również nie ułatwia tworzenia przejrzystej architektury. Większość materiałów i technik tworzona jest z myślą o językach obiektowych. Osiągnięcie tego samego w C samo w sobie może być trudne i nieczytelne. W C i embedded brakuje jasno sprecyzowanych zasad pisania czystego kodu, czy SOLID.
Język C nie słynie również ze zbyt przyjaznej społeczności. Próg wejścia jest dość wysoki, zagadnienia trudne, a nauka wymaga czasu. Jednocześnie osoby, które już tę wiedzę posiadły, często niezbyt chętnie się nią dzielą. A czasem wręcz wyładowują się na nowicjuszach. Koronnym przykładem są tu listy mailowe Linuxa. W ten sposób nie dość, że ograniczamy sobie dopływ świeżej krwi, to jeszcze każemy nowym samemu dochodzić do wszystkiego, często z pominięciem dobrych praktyk.
Brakuje źródeł rzetelnej wiedzy
Owe dobre praktyki nie mają się za bardzo gdzie kształtować i rozprzestrzeniać. Konferencji dotyczących C i Embedded w Polsce praktyczne nie ma. Na świecie też jest ich raczej mało. Sytuację trochę ratują producenci sprzętu i oprogramowania organizujący warsztaty. Jednak zwykle służą one do reklamowania konkretnej rodziny procesorów, IDE, czy bibliotek. W Embedded brakuje nam wymiany wiedzy znanej z innych gałęzi oprogramowania. Mała jest również społeczność open source tworząca biblioteki i projekty niezależne od dużych producentów procesorów
Brakuje również fachowej literatury analizującej dokładnie podejście do architektury w systemach embedded. Mamy tutoriale pokazujące tworzenie mniejszych projektów, mamy opisy dobrych praktyk dotyczących przerwań, czy RTOSów jest nawet co nieco o wzorcach projektowych. Jednak brakuje zasad architektury całych dużych systemów łączących w sobie wiele różnych peryferiów i zaawansowanej logiki nad nimi.
Powolne przejmowanie technik z języków wyższego poziomu
Nie dość, że jako społeczność embedded sami nie tworzymy dobrych praktyki i wzorców, to jeszcze dosyć powoli adaptujemy odkrycia z innych gałęzi programowania. Minęło dużo czasu zanim zaczęliśmy przyswajać techniki takie jak Test Driven Development, czy Continuous Integration. Na szczęście zdążyły już na dobre zagościć w wielu projektach embedded poprawiając ich jakość. Jest jednak wiele innych koncepcji czekających na przeniesienie na grunt systemów embedded. Aktualnie na topie jest Domain Driven Design adresujące wiele problemów dotyczących projektowania dużych systemów webowych w językach obiektowych. Może nie da się przełożyć jeden do jednego do embedded, ale są duże części DDD, które mogłyby mocno ułatwić development systemów embedded.
Branżowe mity
Kolejnym problemem są pewne mity rozprzestrzenione wśród programistów embedded. Powstały one w dawnych czasach, kiedy kompilatory były dużo gorsze w tworzeniu optymalnego kodu maszynowego. Dzisiaj te reguły są już po prostu nieaktualne. Zabobony dotyczą najczęściej wydajności np. C++ nie nadaje się do embedded bo produkuje dużo nadmiarowego kodu, należy ograniczyć ilość wywołań funkcji, bo instrukcje skoku zajmują zbyt dużo czasu. Te stwierdzenia kiedyś może miały rację bytu, ale dawno już nie są prawdziwe. Ich popularność pokazuje inną niepokojącą tendencję – uczymy się pewnych rzeczy jeden raz i potem zawsze bierzemy je jako pewnik. Branża IT zmienia się bardzo szybko i po prostu musimy uczyć się nowych rzeczy żeby za nią nadążać.
Podsumowanie
Nie piszę tego wszystkiego tylko żeby sobie ulżyć i trochę ponarzekać. W najbliższym czasie mam zamiar podzielić się większą ilością przemyśleń dotyczących architektury systemów embedded. Chcę wskazać rozwiązania, które pomogą w efektywnym tworzeniu dużych systemów.
26 stycznia 2019 at 21:43
W końcu ktoś porusza tak istotny temat :). Co sądzisz o pisaniu bibliotek/driverów w taki sposób, że na początku pliku .c umieszczamy kilka funkcji hardware dependent Z odpowiednim komentarzem dotyczącym przerobienia pod danego procka lub alternatywnie z dyrektywą weak ? Czy coś takiego jest wystarczające ? Czy polecasz jakąś literaturę na ten temat ?
Keep up the good work 🙂
26 stycznia 2019 at 22:23
A teraz teraz na to popatrz tak – zrobiłeś plik i wiesz, że w środku będzie trzeba coś modyfikować przy zmianie procka. Czyli jeśli biblioteka będzie używana w dwóch projektach to się całkiem rozjedzie. Takie rzeczy z automatu wydzielaj do innych plików – wtedy masz jakiś core biblioteki niezależny od platformy i do niej osobne pliki do dołączenia do kompilacji dla różnych procków.
Osobiście jestem mega przeciwnikiem „dwuplikowych” bibliotek -> w postaci samego pliku c i h jeśli mają w sobie coś zależne od sprzętu, a to podejście w książkach krajowych jest chyba wszędzie stosowane niestety.
27 stycznia 2019 at 10:00
Dzięki za komentarz 🙂 Co do pisania bibliotek z driverami to ja widzę dwie opcje. Albo uznajemy, że driver jest nierozerwalnie związany z konkretną architekturą i nie zaśmiecamy kodu jakimiś definami itp. Wtedy trzeba pamiętać, żeby granica drivera była jak najniżej i nie umieszczać w driverze ogólnej logiki.
Druga opcja to uznanie, że nasza biblioteka jest ogólna. Wtedy części związane z konkretną architekturą wydzielamy do oddzielnego headera, który nazywamy np. port.h albo platform.h. I dajemy różne implementacje w .c dla wspieranych architektur. Mamy wtedy część ogólną taką samą w każdym porcie i dodatkowe pliki specyficzne dla konkretnego procka.
27 stycznia 2019 at 13:04
Dokładnie o to mi chodziło 🙂
U siebie pliki z konfiguracjami nazywam _settings.h i w projektach mam je w wydzielonych katalogach – tak, że każdy korzysta z tych samych plików „głównych” + plik ustawień per projekt.
26 stycznia 2019 at 22:16
Wydaje mi się, że względem webówki programiści embedded są leniwi jeśli chodzi o naukę nowych rzeczy/bibliotek – jedyną nowością często są rzeczy związane ze zmianą producenta mikrokontrolerów – i wtedy tylko praca polega na przepisaniu bibliotek na nie, ale w starym stylu, a nie na jakimś głębszym przemyśleniu/poprawieniu architektury.
Mega zgadzam się z mitami – coś się utrwaliło i jest powtarzane, że „tak i koniec” i potem powtarzane przez kolejne „pokolenia” programistów w firmie i ciężko z tego wyjść – w sumie powiązane jest to z tym co wspomniałem w pierwszym akapicie.
27 stycznia 2019 at 10:03
Po części problem polega na tym, że embedded wymaga trochę innej wiedzy niż webówka. Musisz znać się na elektronice, układach, algorytmach, architekturze itd. Nie starczy doby na śledzenie wszystkiego, trzeba się w czymś wyspecjalizować.
27 stycznia 2019 at 10:16
No ale z drugiej strony my – ludzie od embedded – możemy się jednak trochę usprawiedliwić z tego lenistwa (; Nie da się ukryć, że spora część (znacząca większość?) różnych „rozwiązań architektonicznych” czy design patterns opiera się na rzeczach które w embedded są niedostępne. Nie wspominam o rzeczach typu „napiszmy wszystko w javascript” czy o językach dynamicznie typowanych (np. Python), bo tu wiadomo na czym polega problem – chodzi mi po prostu o to, że bardzo wiele takich wzorców/architektur/pomysłów opiera się np. na użyciu pamięci dynamicznej i to raczej z założeniem, że jest ona wręcz nieskończona (bo na PC w praktyce tak właśnie jest), albo zgłaszania problemów przy użyciu wyjątków C++ (które niestety wymagają takich ilości RAM i flash, że są to „znaczące” wielkości dla mikrokontrolera). Nie żeby się nie dało używać C++ bez wyjątków czy bez pamięci dynamicznej (bo się oczywiście da), tyle że programiści PC biorą te rzeczy (i nie tylko te) jako pewnik, oczywisty jak wschód słońca, tymczasem w embedded już wcale takie oczywiste nie są, albo wręcz jest oczywiste że ich nie ma (; Do zrobienia bardzo rozbudowanego systemu wcale nie potrzeba jakiegoś super układu który ma SDRAM i kilkadziesiąt MB flash. Na układzie który ma ~128 kB flash i ze 32 kB RAMu można mieć projekt który ma sporo ponad 10 kloc samej aplikacji (bez liczenia driverów, RTOSa, frameworków, zewnętrznych bibliotek itd.), a to jest już (moim zdaniem) naprawdę dużo (zwłaszcza jeśli jest to projekt nad którym pracuje jedna czy dwie osoby). Z drugiej strony 128 kB flash i 32 kB RAM to nie są jakieś powalające wartości, więc nie jest też tak że można sobie na prawo i lewo stosować dowolne „fajne” ficzery wysokopoziomowe.
Inna sprawa jest też taka, że same języki programowania zwykle kompletnie ignorują istnienie embedded. Poza C i C++ czego „normalnego” można jeszcze użyć na embedded? Wiem tylko o pewnych przymiarkach w Rust, kiedyś były jakieś ruchy w języku D (ale obawiam się, że spaliło to na panewce przez same założenia języka, mocno opartego na dynamicznej alokacji), niby jest MicroPython czy Lua i w zasadzie koniec. Z 20 najpopularniejszych języków wg Tiobe Index na embedded realnie używalne są trzy.
Wg mnie nic więc dziwnego, że na embedded niestety wielu (większości?) fajnych wzorców architektury po prostu się nie da przenieść, ewentualnie nie jest to 1:1 i wymaga całkiem wielu zmian aby się pozbyć wymagań niepasujących do embedded.
27 stycznia 2019 at 11:49
Właśnie to miałem na myśli – tych ogólnych zasad architektury nie da się przenieść jeden do jednego i wymagane są jakieś zmiany. Każdy te zmiany robi według swojego zrozumienia ale brakuje dzielenia się przemyśleniami i jasnych zasad, które działają i które można by podać juniorom. Raczej każdy wypracowuje sobie własne zasady na bazie doświadczeń i w niektórych miejscach są one sprzeczne z zasadami kogoś innego. Przydałoby się to jakoś ustrukturyzować i wypracować wspólne stanowisko, ewentualnie kilka dobrze opisanych alternatyw.
28 stycznia 2019 at 10:59
Jestem ciekaw Twoich rozwiązań 🙂 Zdarzało mi się pracować w projekcie, gdzie każdy driver był z innej parafii i samo wejście w projekt zajmowało mnóstwo czasu.
28 stycznia 2019 at 17:05
super temat, zgadzam się w 100 % że w embedded jest to duży problem. Ja z swojej strony polecam poczytać o Autosar- architektura stosowana w automotive. Literatury niestety mało jest w sieci, a szkoda bo po zagłębieniu się można bardzo dużo wiedzy zdobyć.