Alokacja pamięci w systemach wbudowanych

Dzisiejszy temat to próba powrotu do stylu pierwszych artykułów, nieco bardziej teoretycznych i przegadanych 🙂 Tym razem chciałbym poruszyć temat szeroko pojętego zarządzania pamięcią w systemach wbudowanych.

Harward czy von Neumann?

Te dwie nazwy brzmią znajomo dla czytelników zaznajomionych nieco z działaniem procesorów. Architektura von Neumanna zakłada, że zarówno program jak i przetwarzane dane umieszczone są w tej samej pamięci. Można powiedzieć, że te słowa czytacie dzięki typowemu przedstawicielowi tej rodziny 🙂 Tak – komputery, tablety, telefony komórkowe – wszystkie te urządzenia należą właśnie do niej.

Architektura harwardzka wymusza z kolei rozdzielenie pamięci danych od pamięci programu – są to całkowicie odrębne przestrzenie adresowe. Pozornie może wydawać się ona przez to bardziej skomplikowana, jednak w rzeczywistości ów podział ról znacznie upraszcza budowę CPU. Najczęściej kod programu znajduje się w nieulotnej pamięci typu ROM/Flash, natomiast dane lądują w pamięci typu RAM/SDRAM. Z tego względu architektura ta dominuje w świecie embedded – zdecydowana większość najpopularniejszych mikrokontrolerów działa właśnie według tej zasady.

A jak to jest u nas?

W projekcie używamy procesora STM32F407, należącego do rodziny ARM Cortex-M4. Jego CPU stanowi połączenie obu idei – posiada on odrębne magistrale dla danych i kodu programu, jednak przechowywane są one we wspólnej przestrzeni adresowej. Tego rodzaju strukturę systemu nazywa się tzw. zmodyfikowaną architekturą harwardzką. Teoretycznie mamy zarówno możliwość uruchomienia kodu aplikacji z pamięci RAM, jak i odczytania danych z pamięci nieulotnej Flash. W naszym projekcie nie będziemy jednak korzystać z tej pierwszej opcji.

Dlaczego?

Najzwyczajniej w świecie nie ma takiej potrzeby – cały kod zostanie umieszczony we wbudowanej pamięci nieulotnej, co pozwoli uruchomić procesor bez konieczności korzystania z dodatkowych, zewnętrznych kości Flash czy karty SD.

Alokowanie pamięci RAM

W systemach wbudowanych zasadniczo korzysta się z tzw. statycznej alokacji. Innymi słowy wszystkie dane mają stałe i niezmienne położenie w pamięci RAM. Każda zmienna, tablica czy struktura istnieje “od początku do końca świata”, czyli od uruchomienia urządzenia aż do wyłączenia zasilania lub resetu. Pozwala to zaoszczędzić ogromną ilość czasu jaką pochłonęłaby tradycyjna alokacja pamięci na stercie.

Hmm… Czyli wszystkie widgety, okna, itd. będą stworzone statycznie?

Tu sytuacja nieco się komplikuje, gdyż do akcji wkracza C++. Oczywiście, możemy tworzyć każdą instancję klasy statyczne, jako np. zmienną globalną. Jest to jednak skrajnie nieefektywne podejście:

  • powstaje spora ilość kodu, definiującego całą chmurę obiektów oraz określająca relacje między nimi,
  • przy tworzeniu obiektu jesteśmy ograniczeni do zakresu parametrów przekazywanych statycznie do konstruktora klasy,
  • wszystkie obiekty są tworzone tuż przy starcie systemu, co wpływa na wydłużenie czasu uruchomienia aplikacji,
  • każdy obiekt trzeba nazwać (Brzmi absurdalnie? Droga Czytelniczko/Drogi Czytelniku, spróbuj zatem wymyślić kilkadziesiąt niepowtarzalnych nazw, jasno opisujących czym dany obiekt się zajmuje, a które nie będą dłuższe niż 20 znaków 🙂 )

W takiej sytuacji jedynym rozwiązaniem jest dynamiczne przydzielanie pamięci dla nowo tworzonych obiektów.

Jak zrobić to efektywnie i zgodnie ze sztuką?

Dobrą praktyką, spotykaną w systemach wbudowanych, jest tworzenie odrębnych stert (ang. heap) dla poszczególnych rodzajów obiektów tworzonych w systemie. Minimalizuje to ryzyko potencjalnego wzajemnego nadpisywania się sąsiadujących ze sobą obiektów czy struktur. Znacząco upraszcza to również debugowanie i ułatwia śledzenie zużycia pamięci przez poszczególne komponenty. Dodatkowo jeśli w systemie mamy więcej niż dwa banki pamięci (zazwyczaj szybszy, ale mniejszy i wolniejszy, za to o wiele bardziej pojemny), możemy niezależnie przydzielać ich kawałki, według realnych potrzeb danej aplikacji.

Gonimy własny ogon, czyli cykliczny alokator

Standardowa implementacja sterty pozwala zarówno alokować kawałki pamięci, jak i zwalniać je do ponownego użycia. Wprowadza to dodatkowy nakład obliczeniowy oraz wymaga poświęcenia obszaru pamięci na przechowywanie informacji o strukturze zaalokowanych fragmentów. Projektując system, warto zadać sobie pytanie, czy na pewno będziemy w jakikolwiek sposób zwalniać pamięć…

W naszym przypadku, alokacja zostanie przeprowadzona tylko raz, w czasie budowania interfejsu graficznego. Później wszystkie niezbędne obiekty będą istniały w pamięci aż do momentu wyłączenia urządzenia.

Czy w związku z tym można jakoś zoptymalizować zarządzanie pamięcią?

Oczywiście! Niezwykle prostym i popularnym rozwiązaniem jest tzw. cykliczny alokator. Na żądanie przydziela on kolejny fragment, o określonym rozmiarze, aż dojdzie do końca dostępnej pamięci. I tyle! 🙂 Jest to typowy przykład optymalizacji w systemach wbudowanych, które przyspieszają działanie systemu kosztem nieco większego nakładu pracy i uwagi developera. To w gestii programisty leży dbanie, aby zawsze wystarczyło miejsca, abyśmy nie weszli w szkodę sąsiadowi i nie “zjedli własnego ogona”… Co zarówno dosłownie jak i w przenośni nie oznacza nic dobrego.

Tyle rozważań na dziś! Do zobaczenia jutro 🙂

Posted in Sto dni w kolorze.