Co wyróżnia programistę embedded? Zarządzanie pamięcią?
Jeżeli zapytamy programistów, co wyróżnia programistę embedded, w odpowiedzi najczęściej dostaniemy dwa główne wyróżniki:
- interakcje z fizycznym sprzętem
- i kontrolę nad pamięcią.
Dzisiaj zajmiemy się tym drugim tematem. Czy na pewno aż tak kontrolujemy pamięć w naszych projektach? Kiedyś na pewno tak było. Już sama ilość pamięci to wymuszała. Mikrokontrolery często miały na przykład 16kB FLASH i 1kB RAM. Kontrolowaliśmy więc bardzo mocno zużycie pamięci. Ale również pamięć nie była zupełnie schowana za warstwą abstrakcji. Często bezpośrednio musieliśmy po adresach w pamięci. Korzystaliśmy z pamięci zewnętrznych. Mieliśmy pamięci specjalnego przeznaczenia. To wszystko trzeba było jakoś obsłużyć.

W dzisiejszych czasach często ktoś robi to za nas. Kiedy zaczynamy nowy projekt – IDE generuje nam skrypty linkera, gdzie wszystko jest już skonfigurowane. Możemy wybrać czy kod odpalamy z FLASHa, z RAMu, a od niedawna nawet konfiguruje dla nas sekcje secure/non-secure dla TrustZone. (Jeżeli interesuje Cię temat CRA – sprawdź prezentację w tym temacie z Gdańsk Embedded Meetup!)
No i super. Dzięki temu możemy szybko ruszyć z projektem. Dzięki temu możemy wręcz rozpocząć naukę embedded. Wyobraźcie sobie, co by było, gdybyśmy musieli zaczynać naukę od flag kompilacji, skryptów linkera, architektury procesora czy budowy pamięci. A dopiero potem byśmy mogli zaświecić LEDem, czy napisać Hello world na UART? Ja sobie wyobrażam, bo tak wyglądał mój pierwszy semestr programowania na studiach. Wtedy nie wiedziałem, o co chodzi. Dlatego nauczyłem się, żeby zdać bez zrozumienia. A potem kiedyś mi się przypomniało, że to przydatne i coś o tym było. Więc wróciłem do tematu dopiero jak coś już ogarniałem. No i super. Tak to powinno wyglądać – to jest naturalna progresja nauki.
Teraz wydaje mi się, że zaczynamy mieć problem w drugą stronę. Możemy dojść całkiem daleko kompletnie nie ruszając zarządzania pamięcią i skryptów linkera. Mamy dużo materiałów w internecie, producenci udostępniają gotowe przykłady. Teraz jeszcze doszło AI, które pomoże nam znaleźć błędy i wytłumaczy jak działają poszczególne elementy projektu.
Ale jak to zwykle bywa ze szczegółową, specjalistyczną i niskopoziomową wiedzą – to nie oznacza, że w dzisiejszych czasach jest niepotrzebna. Co więcej – w końcu dojdziemy do takiego momentu, że nie pójdziemy bez niej do przodu. Zarządzanie pamięcią dalej przydaje się w naszych projektach. Kiedyś będziemy musieli ręcznie dostosowywać skrypty linkera i wiedzieć, jakie to ma konsekwencje dla procesu kompilacji i samego działania programu. Inaczej wiecznie będziemy prosić o pomoc bardziej doświadczonych kolegów, wujka Google, czy czata GPT.
Omówię teraz kilka typowych przypadków.
Wykorzystanie całej pamięci na procesorze
Niby powinniśmy dostawać gotowe skrypty od producentów. To im powinno zależeć, żebyśmy mieli dostęp do wszystkich udostępnionych przez nich zasobów, szybko mogli ruszyć z projektem i żebyśmy nie uciekli do konkurencji. Jednak nie żyjemy w idealnym świecie. Nieraz widziałem jak producenci chwalili się wielkością RAMu, czy FLASHa, a potem okazywało się, że dostarczany skrypt wykorzystuje tylko połowę. A to mamy kilka banków ramu, a to specjalną pamięć z ograniczeniami użycia, a to jeszcze co innego. Ale problem zawsze sprowadzał się do jednego – dostawaliśmy prosty skrypt linkera, który nie obsługuje wszystkich pamięci, dlatego program ich nie widzi.

Co możemy wtedy zrobić? Albo piszemy do supportu i czekamy na pomoc czy nową wersję, albo sami modyfikujemy skrypty.
Pamięci zewnętrzne
Czasem zdarza się, że pamięć dostępna na procesorze nie jest systarczająca dla naszego projektu. Korzystamy wtedy z pamięci zewnętrznej. Możemy podłączyć pamięć równolegle i wtedy jest widoczna jako zwykły adres. Dokładnie tak samo jak wbudowany RAM czy FLASH. Wykorzystujemy je do wielu różnych celów. Możemy tam trzymać:
- Konfigurację
- Logi
- Backupy
- Lookup table
- Grafiki
- Animacje
- Fonty
- Próbki audio
- Wersje językowe
- Strony internetowe
- Czy nawet kod programu do uruchomienia

Konfiguracja pamięci zależy wtedy od konkretnego sprzętu. To my wiemy:
- Jaką pamięć podłączyliśmy?
- Jaki ma rozmiar?
- Jakie adresy na nią przeznaczyliśmy?
- Jakie dane tam chcemy trzymać
A skoro tak – nie możemy już liczyć na gotowce. Sami musimy ją skonfigurować, a następnie użyć w kodzie.
Bootloadery
To programy startowe wykonujące jakieś operacje, a następnie uruchamiające docelowy program. Mogą służyć do automatycznych aktualizacji Firmware’u, uruchamiania programu z RAM, czy wgrywania programu z zewnętrznej pamięci. W każdym razie wymagają od nas podzielenia pamięci na sekcje i odpowiedniego zarządzania tymi sekcjami. Bootloader to tak jakby druga aplikacja siedząca w tej samej pamięci. Musimy odpowiednio zdefiniować sekcje i przesunięcia, aby te dwa programy mogły współistnieć w naszym FLASHu.
Czasem wchodzą tu bardziej zaawansowane machinacje. Możemy mieć np. Kilka stopni bootloadera, albo kilka slotów na aplikację. Dzięki temu możemy wrócić do poprzedniej wersji, gdy coś pójdzie nie tak. Czasem mamy również procesory z bardzo skomplikowanymi procedurami bootowania, jak na przykład STM32N6.

Bootloadery to chyba obszar, gdzie zarządzanie pamięcią jest najważniejsze. A jednocześnie przypadki użycia mogą być najbardziej rozbudowane. Jednocześnie w dobie IoT, aktualizacji softu Over The Air, zarządzania całą konstelacją urządzeń coraz więcej projektów potrzebuje tej funkcji.
Funkcje safety-critical
Systemy safety-critical nakładają na nas dodatkowe obowiązki. Do ich obsługi potrzebujemy nieraz specjalnych obszarów pamięci. Możemy je podzielić na dwie kategorie:
- Self-checki
- Redundancja
Do pierwszej kategorii możemy zaliczyć ochronę stosu. Czyli specjalny region pamięci służący do wykrywania przepełnień stosu. Albo CRC wgrywane do obrazu programu po kompilacji i służące do walidacji, czy pamięć przechowująca program nie została uszkodzona lub zmodyfikowana.
Reprezentantem drugiej kategorii są na przykład zmienne zgodne z Klasą B. Czyli wartość jest jednocześnie przechowywana w dwóch regionach pamięci – jako zwykła wartość i jej negacja. Tylko jeżeli obie wartości się zgadzają – możemy użyć zmiennej. To kolejna ochrona przed uszkodzeniem pamięci i przypadkowym nadpisaniem ważnych zmiennych podczas działania programu.
Inną techniką spotykaną czasem w systemach safety-critical jest wypełnienie nieużywanej pamięci odpowiednim patternem. Chodzi o to, że jeśli program przypadkowo skoczy do nieistniejącej funkcji nie zaczął wykonywać jakiś dziwnych operacji. Najczęściej chcemy aby przeszedł wtedy do nieskończonej pętli i czekał na reset.

Własne sekcje pamięci
Już wszystkie wcześniejsze przypadki sprowadzały się do definiowania własnych sekcji w pamięci i przypisywania im konkretnych adresów. Ale tak naprawdę przydatnych scenariuszy z własnymi sekcjami mamy znacznie więcej:
- pamięć współdzielona – czasem mamy kilka rdzeni korzystających ze wspólnej pamięci. Możemy się wtedy wspomóc dodatkowymi sekcjami
- pamięć dla peryferiów – czasem chcemy jasno oznaczyć sektor pamięci wykorzystywany przez peryferia. Na przykład deskryptory do Ethernetu.
- pamięć persistent – czyli obszar pamięci, którego nie czyścimy w procedurze startowej. Oznacza to, że aplikacja jest w stanie odczytać dane zapisane tam przed resetem. W ten sposób możemy przekazać informacje między bootloaderem a docelową aplikacją, czy kod błędu z poprzedniego uruchomienia. Musimy jednak pamiętać tutaj o fizycznych ograniczeniach pamięci. Jeżeli uruchamiamy program pierwszy raz po włączeniu zasilania – mogą tam być śmieci. Pamięć zachowuje zawartość tylko jeśli była wcześniej poprawnie zasilana.
- Pamięć debugowa – możemy mieć sekcje debugowe za pomocą których odczytujemy dane po gdb, albo wstrzykujemy dane do bardziej zaawansowanych testów, czy debugu.
- Wymuszanie adresu zmiennej – Czasem zdarza się, że chcemy wymusić, aby zmienna, czy cała struktura lub tablica znalazła się pod konkretnym adresem. Możemy tego dokonać za pomocą skryptów linkera.
Podsumowanie
Jak widzisz – przypadków, kiedy musimy panować nad rozmieszczeniem danych w pamięci jest całkiem sporo i są dosyć różnorodne. Natomiast wszystkie realizują pewne zaawansowane funkcje potrzebne zwykle w bardziej rozbudowanych komercyjnych projektach. Istnieje więc duża szansa, że uda Ci się przez dłuższy czas realizować nawet skomplikowane projekty bez znajomości skryptów linkera. Ale w końcu będziesz potrzebować szczegółowej wiedzy.
Właśnie dlatego postanowiłem stworzyć kurs linkera dla programistów embedded. Omówię w nim wszystko od deski do deski od filozofii zarządzania pamięci i dodatkowych koncepcji wykorzystywanych przez linker, przez zawartość plików obiektowych i wykonywalnych w raz z narzędziami toolchaina do podglądania pamięci. Przejdziemy przez składnię skryptów linkera, która wyraża te koncepcje w kodzie. A następnie przerobimy dużą ilość przykładów. Od prostych przypadków i podstawowych konfiguracji do typowych scenariuszy zarządzania pamięcią w projekcie takich jak te omawiane przeze mnie wcześniej.
Kurs jest w trakcie tworzenia i również Ty możesz mieć wpływ na jego kształt. Koniecznie podziel się swoimi pomysłami w TEJ ANKIECIE. Możesz w niej napisać jakie tematy szczególnie Cię interesują, jakie przykłady chcesz zobaczyć jak Twoim zdaniem powinien wyglądać kurs.
Już niedługo rusza przedsprzedaż. Aby jej nie przegapić – zapisz się na mailing.
