Rust w systemach wbudowanych – długoterminowa inwestycja czy moda?
Spis treści

Od czasów powstania Rust jako konkurencyjnego języka do znanych języków kompilujących się do kodu maszynowego, nie miał on początkowo szerokiego grona zwolenników. Jednak z każdym rokiem popularność rosła, a język ze swym charakterystycznym logo kraba zdobywał coraz bardziej znaczące miejsce w społeczności (zajmując pierwsze miejsca w ankiecie Stack Overflow w kategorii „ulubiony język”). Od 2020 roku Rust zwrócił uwagę gigantów technologicznych takich jak Microsoft czy Google, które zaczęły wdrażać go w swoich produktach.
W społeczności systemów wbudowanych widać rosnące wsparcie dla narzędzi i platform umożliwiających kompilowanie i uruchamianie programów w Rust. W tym artykule przedstawię dostępne rozwiązania, platformy, wady i zalety korzystania z Rust w systemach wbudowanych, porównam kod C++ i Rust, a także podzielę się własnymi doświadczeniami.
Co to systemy wbudowane?
Systemy wbudowane w dużym skrócie to specjalizowane komputery przeznaczone do konkretnych zadań. Zazwyczaj są to mikrokontrolery, które znajdują zastosowanie na przykład w:
- samochodach – czujniki temperatury, systemy zarządzania drzwiami
- urządzeniach IoT (ang. Internet of Things) – inteligentne systemy, np. taśmy LED, żarówki inteligentne, inteligentne gniazdka
- sprzęcie RTV/AGD – sterują poszczególnymi funkcjami, np. w pralce wyborem programu prania wybranego przez użytkownika
Takie systemy wbudowane służą do wykonywania prostych algorytmów, gdzie każda operacja musi być jak najbardziej zoptymalizowana, ponieważ te układy mają ograniczone zasoby: słaby procesor i niewielką pamięć RAM oraz ROM.
Platformy – klucz do rozwiązania problemu
Istnieje wiele rodzajów mikrokontrolerów tzw. platform, które mają zastosowanie w różnych dziedzinach. Oto kilka liderów branży:
- Arduino
- Espressif
- Raspberry Pi Pico
- STM32
Bardziej zaawansowane platformy mają większą moc obliczeniową i mogą uruchamiać wymagające systemy operacyjne, takie jak GNU/Linux:
- Raspberry Pi
- Orange Pi
- Banana Pi
Jak widać, wybór jest ogromny. Wszystkie te platformy mają wbudowane API (ang. Application Programming Interface), które umożliwia komunikację z GPIO (ang. General-Purpose Input/Output) – interfejsem odpowiedzialnym za komunikację z urządzeniami zewnętrznymi poprzez piny na płytce.
Dostępność bibliotek dla poszczególnych platform
Wsparcie różnych platform w Rust to temat, który można dyskutować długo. Dobrym przykładem jest Espressif – firma produkująca stosunkowo tanie mikrokontrolery z wbudowanym modułem Wi-Fi. Od kilku lat oferuje własne narzędzia ułatwiające tworzenie rozwiązań IoT dla swoich platform (https://github.com/esp-rs). W przypadku Arduino użytkownik Rahix utworzył abstrakcyjną warstwę sprzętową (HAL) do interakcji z komponentami na płytce, dostępną w repozytorium avr-hal. Sytuacja z Raspberry Pi jest bardziej złożona. Wystarczy użyć standardowego toolchaina Linux, ale komunikacja z GPIO wymaga odpowiedniej biblioteki. Niestety, najpopularniejszy crate rppal nie otrzymuje już wsparcia ze strony autora, co może prowadzić do braku poprawek błędów i nowych funkcji. Jednak ze względu na to, że projekt ma otwarte źródła, istnieje szansa na podtrzymanie go przez społeczność.
Biblioteki dla popularnych komponentów
Czy będą dostępne biblioteki do sterowania taśmami LED czy wyświetlaczami LCD? Na szczęście, dla popularnych komponentów takie biblioteki istnieją: blinksy, embedded-graphics, epd-waveshare. Obsługują one zarówno tryb no-std, jak i std.
Różnice między trybami std i no-std
Zgodnie z dokumentacją Rust – no-std oznacza, że biblioteka standardowa nie będzie dołączona do kompilacji, co powoduje, że nie możemy używać operacji I/O, asynchronicznych operacji, timingowania ani struktur alokujących pamięć dynamicznie (takich jak Vec, HashMap, String). W systemach wbudowanych jest to normalne, ponieważ zwykle brakuje systemu operacyjnego, który by obsługiwał te operacje. Istnieją jednak rozwiązania, jak systemy operacyjne FreeRTOS, które umożliwiają korzystanie z tych struktur. Programiści znający Arduino wiedzą, że inicjalizacja String wymaga podania długości, ponieważ allokuje pamięć dynamicznie na stercie, a nie na stosie.
Z kolei std jest całkowitym przeciwieństwem no-std i obsługuje zaawansowane struktury danych alokujące pamięć dynamicznie, takie jak Vec, HashMap aż po wspracie asynchronicznych operacji.
Poniżej porównanie trybów:
std | no-std |
|---|---|
| Dostęp do funkcjonalności systemu operacyjnego | Brak dostępu |
Dynamiczne struktury danych (Vec, HashMap, itd.) | Ograniczone – można zaimplementować własne algorytmy alokacji |
Automatyczna ochrona stosu i obsługa panic! | Ręczna obsługa wymagana |
| Wsparcie wielowątkowości | Brak wsparcia wielowątkowości |
Zalety i wady korzystania z Rust
Rust oferuje kilka istotnych zalet:
- Bezpieczeństwo pamięci – mechanizmy takie jak borrow checker i ownership zapobiegają powstawaniu tzw. Undefined Behavior przed uruchomieniem programu
- Bezpieczna współbieżność – system typów zapobiega data racingu już na etapie kompilacji
- Przyjazna składnia – system modułów, pattern matching i bezpieczne makra to tylko niektóre cechy
- „Kompilator to twój najlepszy przyjaciel” – komunikaty błędów kompilatora są jasne i klarowne, często zawierają sugestie rozwiązań
Nie obejdziemy się jednak bez pewnych wad:
- Duży rozmiar binarki – mimo postępów, skompilowany program zajmuje znacznie miejsca. W praktyce zwykle jest to akceptowalne (4 MB pamięci jest czasem wystarczające), ale na bardzo ograniczonych platformach może być problemem
- Ograniczona liczba bibliotek – nie wszystkie komponenty mają natywne sterowniki w Rust. Czasami konieczne jest wywoływanie zaimplementowanych funkcjonalności z C/C++
- Czas kompilacji – kompilacja i wgranie na płytkę może zająć więcej czasu
- Mniejsza społeczność – ekosystem jest mniejszy, ale z roku na rok się rozwija
Porównanie kodu C++ i Rust
Przykładowy kod został oparty na podstawowym programie „Hello, World!”, w którym zmieniamy stan wbudowanej diody LED co sekundę, aby uzyskać efekt migania. Poniżej znajduje się kod w C++, korzystający z frameworka Arduino:
const int LED_PIN = 13; // Pin diody LED wbudowanej w płytce
const int BLINK_DELAY = 1000; // 1000ms = 1s
void
void
A tutaj Rust, który używa crate'a arduino_hal:
use *;
use panic_halt as _;
const BLINK_DELAY: u32 = 1000; // 1000ms = 1s
!
Różnice między implementacjami są relatywnie niewielkie. W C++ inicjalizacja komponentów płytki jest obsługiwana przez framework Arduino, który ukrywa szczegóły implementacji. W Rust musimy sami zainicjalizować peryferia, ale jest to bardzo przejrzyste. Obie implementacje wymagają zdefiniowania głównej pętli programu i obsługi przerwań sprzętowych.
Moje doświadczenia
W moim przypadku wcześniej miałem styczność z frameworkiem Arduino, co ułatwiło mi przejście na narzędzia Espressifa. Dlatego przesiadka na narzędzia od Espressifa w ruście nie były dla mnie ciężkie do ogarnięcia. Nawet konfiguracja wyświetlaczy e-ink przebiegała gładko, ponieważ większość popularnych modeli ma już gotowe sterowniki napisane przez społeczność. Prawdziwym wyzwaniem jest borrow checker w źle zaprojektowanych projektach – struktura Peripherals może być pożyczona tylko raz, co może prowadzić do problemów przy dokładaniu kolejnych modułów. Czas kompilacji i wgrywania na płytkę może być znaczny ze względu na rozmiar binarki. Dlatego polecam testować grafikę na komputerze, korzystając z symulatora biblioteki embedded-graphics, co przyspiesza testowanie własnej funkcjonalności.
Słowem podsumowania
Zainteresowanie Rust w społeczności systemów wbudowanych rośnie z roku na rok. Rosnąca liczba bibliotek, narzędzi i platform wspiera dalszy rozwój ekosystemu. Korzyści oferowane przez Rust, takie jak bezpieczeństwo pamięci, bezpieczna współbieżność i przyjazna składnia mogą znacznie poprawić doświadczenie dewelopera (ang. Developer Experience) w porównaniu z tradycyjnymi językami. Rust rzeczywiście coraz bardziej staje się realną alternatywą dla systemów wbudowanych.