Własny moduł Linux na Raspberry Pi

programowanie modulow linux raspberry pi

W tym artykule zajmiemy się przygotowaniem modułu Linux „Hello World” na Raspberry Pi. Przy okazji przejdziemy przez proces kompilacji natywnej oraz kompilacji skrośnej. Jeśli jednak nie czytałeś wcześniej artykułu Programowanie sterowników urządzeń w Linux – jak zacząć?, a dopiero zaczynasz przygodę z programowaniem modułów/sterowników Linux to zacznij od niego.

Kompilacja natywna vs kompilacja skrośna

Jeśli nie słyszałeś wcześniej tych pojęć to może to brzmieć odrobinę groźnie. Jak się jednak zaraz okaże nie ma w tym nic trudnego. Kompilacja natywna to taka, podczas której kod generowany jest na platformę, na której wykonywana jest kompilacja. Najprostszym przykładem będzie kompilacja i uruchomienie programu na swoim komputerze.

W przypadku kompilacji skrośnej, proces budowania wykonywany jest na jednej platformie, ale wygenerowany kod przeznaczony jest do wykonywaniana na innej. Przykładem może być kompilacja kodu na PC, który potem jest wykonywany na mikrokontrolerze. Kompilacja skrośna jest szczególnie często stosowana dla systemów wbudowanych. W przypadku mikrokontrolerów nie mamy nawet możliwości zbudowania na nich kodu.

Na większych systemach wbudowanych, np. Raspberry Pi, często mamy możliwość przeprowadzenia kompilacji natywnej. Dla utrwalenia – w takim wypadku kompilujemy kod na Raspberry i uruchamiamy go na Raspberry. Mimo to, w większości przypadków wykorzystywana jest kompilacja skrośna. Jest to spowodowane tym, że stacja robocza ma po prostu dużo większe zasoby od prostego SBC i taka kompilacja trwa na niej znacznie krócej. W przypadku bardzo prostej aplikacji nie robi to aż tak dużej różnicy, ale już np. kompilacja jądra Linuxa na Raspberry zajęłaby od kilku do kilkunastu godzin, podczas gdy na zwykłym komputerze zajmuje to zazwyczaj kilkanaście minut.

Kompilacja natywna

Zacznijmy od prostszego wariantu, czyli kompilacji natywnej. Będzie to wyglądać niemal identycznie jak w artykule Programowanie sterowników urządzeń w Linux – jak zacząć?. Jedyną różnicą będzie platforma – tam robiliśmy to na desktopie, tutaj użyjemy Raspberry Pi. Większości opisów nie będę tutaj powtarzał dlatego jeśli jeszcze nie czytałeś tego artykułu to zacznij proszę od niego. Zakładam też, że Raspberry nie jest Ci obce i już wcześniej z niego korzystałeś, dlatego pominę tutaj opis platformy, instalacji systemu czy konfiguracji. Jeśli jednak wcześniej nie miałeś z nią do czynienia to myślę, że fajnym miejscem na początek będzie kurs Raspberry Pi kurs od podstaw z forbota. W swojej konfiguracji wykorzystałem system operacyjny Raspberry Pi OS Lite i skonfigurowałem ssh, aby móc wygodnie pracować bez konieczności podłączania UART czy monitora. Należy też pamiętać o zapewnieniu dostępu do internetu.

Kompilacja natywna modułu

Wykorzystamy tutaj ten sam kod modułu i Makefile, który przedstawiałem już we wspomnianym artykule:

Pierwszym krokiem będzie instalacja kernelowych headerów odpowiadającym wersji kernela. W przypadku raspberry pakiet z headerami nazywa się raspberrypi-kernel-headers:

Wszystkie niezbędne narzędzia do kompilacji są domyślnie dostępne w systemie, więc nie musimy nic więcej doinstalowywać i możemy od razu przejść do budowania modułu, wywołując make w folderze zawierającym kod źródłowy modułu hello_world.c i Makefile.

Jeśli wszystko się powiedzie powinniśmy widzieć plik hello_world.ko.

Uruchomienie modułu

Pozostało już tylko uruchomić moduł. Jak już wiesz, do ładowania i odładowywania modułów korzystamy z narzędzi insmod i rmmod. Sprawdźmy czy wszystko działa jak należy:

Po wykonaniu powyższych operacji w dmesgu powinny pojawić się wiadomości wypisane przez nasz moduł:

Jak widzisz, prosta sprawa 🙂 Przejdźmy teraz do kompilacji skrośnej

Kompilacja skrośna

W tej części moduł skompilujemy na komputerze, a potem przeniesiemy i uruchomimy go na Raspberry Pi. Na swojej maszynie mam zainstalowane Ubuntu 20.04, ale na każdej dystrybucji opartej na Debianie powinno to wyglądać tak samo lub bardzo podobnie. W przypadku innych, część komend może się trochę różnić, ale dostosowanie ich nie powinno być kłopotliwe.

Pobieranie kodu jądra

Jak już wiesz, do zbudowania modułu będą nam potrzebne narzędzia do kompilacji oraz headery odpowiadające wersji jądra. Ale uwaga: wersji jądra, do którego moduł będzie ładowany, czyli tego z Raspberry Pi. W moim wypadku jest to 5.10.63:

Pamiętaj, że u Ciebie wersja jądra na RPi może być inna. Możliwe, że w momencie kiedy Ty pobrałeś Raspbiana był tam kernel w innej wersji.

Na Ubuntu nie znajdziemy pakietu z headerami kernela wykorzystywanego na Raspberry Pi, dlatego w tym wypadku będziemy potrzebowali całego kodu źródłowego jądra. Możemy go znaleźć w repozytorium github.com/raspberrypi/linux:

Podczas klonowania można dodać flagę --depth=1. Jej dodanie spowoduje, że sklonowany zostanie wyłącznie aktualnie aktywny branch bez żadnej historii. Nie potrzebujemy całego repo ze wszystkimi branchami i historią, a zaoszczędzi to dużo czasu i miejsca na dysku.

Niestety w repozytorium nie ma tagów dla konkretnych wersji jądra. W momencie gdy klonowałem to repozytorium dostępne źródła były dla kernela w wesji 5.10.78:

Jak widać wersja różni się od tej, którą mamy na platformie (5.10.63). W związku z tym, jeśli zbudujemy moduł dla wersji 5.10.78 nie będziemy w stanie go załadować. Podczas takiej próby pojawi nam się komunikat:

Natomiast w dmesgu znajdziemy:

Najłatwiejszym sposobem rozwiązania tego problemu będzie zbudowanie całego kernela i zainstalowanie go na Raspberry. Napisałem na ten temat oddzielny artykuł (Kompilacja jądra dla Raspberry Pi), więc zajrzyj proszę do niego. Ja już to u siebie zrobiłem – jak widać poniżej, aktualnie wersja jądra na Raspberry jest taka sama jak wersja źródeł, którą sprawdzaliśmy wcześniej. Możemy więc przejść do kompilacji skrośnej modułu.

Kompilacja skrośna modułu

Tutaj sprawa też będzie wyglądało trochę inaczej, bo potrzebujemy kompilatora, który wygeneruje kod wykonywalny do uruchomienia na innej architekturze. Jeśli przechodziłeś ścieżkę cross kompilacji jądra z wyższej wspomnianego artykułu to już go instalowałeś:

Kod źródłowy modułu pozostanie taki sam, natomiast w Makefile musimy wprowadzić małą zmianę. Konieczne jest ustawienie odpowiedniej ścieżki do źródeł jądra. W moim wypadku wygląda to tak jak poniżej. Jeśli sklonowałeś repozytorium Linuxa w inne miejsce to będziesz musiał odpowiednio dostosować ścieżkę.

Aby skompilować moduł wystarczy już tylko wykonać make ze zdefiniowanymi zmiennymi środowiskowymi ARCH oraz CROSS_COMPILE. Są one niezbędne w przypadku kompilacji skrośnej. Zmienna ARCH określa dla jakiej architektury docelowej ma być wykonywana kompilacja, natomiast zmienna CROSS_COMPILE definiuje przedrostek dla narzędzi do kompilacji, które mają być wykorzystane podczas budowania.

Uruchomienie modułu

Po udanej kompilacji wystarczy już tylko przenieść skompilowany moduł hello_world.ko na Raspberry Pi. Można go przesłać np. za pomocą narzędzia scp lub bezpośrednio skopiować na kartę SD. Pozostałe kroki (ładowanie, odładowywanie modułu) wyglądają już tak samo jak zawsze. Korzystamy z narzędzi insmod i rmmod podając im ściężkę do skompilowanego modułu.

Podsumowanie

Kolejny artykuł z kategorii Linuxa za nami 🙂 Tym razem zrobiliśmy coś na platformie embedded. Zdecydowałem się na Raspberry Pi, ze względu na jej dużą popularność. Jest spore prawdopodobieństwo, że ze wszystkich platform posiadasz akurat ją. Jeśli masz jakąś inną platformę to bardzo fajnym ćwiczeniem będzie przejście całego procesu także dla niej.

W artykule udało się wpleść trochę informacji na temat kompilacji skrośnej. Jest ona bardzo często wykorzystywana w pracy z systemami wbudowanymi. Jeśli skupiłeś się tylko na pierwszej części z kompilacją natywną to zdecydowanie zachęcam do przejścia także tej trochę bardziej złożonej ścieżki 🙂