Zephyr RTOS
Disclaimer
Wszystko co tu opisuję może nie być w pełni poprawnym opisem z teoretycznego punktu widzenia, ale powinno nadawać się do skonfigurowania wszystkiego i rozpoczęcia zabawy. Potraktuj to jak moje osobiste notatki na wypadek jakbym zapomniał jak wszystko zainstalować na nowym komputerze.
Linki
Co to OS?
OS czyli system operacyjny (Operating System) to - w mocnym uproszczeniu - oprogramowanie kontrolujące hardware i oferujące różne przydatne rzeczy, jak na przykład wątki, wiele procesów i inne takie. Coś jak biblioteka do obsługi sprzętu.
Normalne systemy operacyjne przeważnie pozwalają na uruchomienie wielu procesów i przydzielają im czas procesora tak, aby każdy miał okazję chwilę podziałać.
Co to RTOS?
RTOS czyli system operacyjny czasu rzeczywistego (Real Time Operating System). Tak jak zwykły OS jest "biblioteką" do obsługi sprzętu, jednak różnica polega na tym, że stara się mieć minimalny narzut i możliwie dużo czasu poświęcić twojemu programowi. Stawia bardziej na działanie tylko jednego programu, który ma dostęp do wszystkiego i cały procesor na wyłączność. Skutkuje to szybszym działaniem programu, co jest przydatne przy programowaniu mikrokontrolerów, które powinny jak najszybciej reagować na sygnały (takie jak wciśnięcie przycisku lub dane z czujników).
Oczywiście RTOSy mogą mieć wsparcie dla wielowątkowości i innych rodzajów zrównoleglenia kodu, ale generalnie chodzi o to, żeby system robił jak najmniej i jak najkrócej.
Co to Zephyr?
Zephyr jest systemem operacyjnym czasu rzeczywistego, obsługującym różne rodzaje kontrolerów i płytek (Raspberry Pi, Arduino...). Zephyr korzysta z device tree - pliku konfiguracyjnego, który definiuje użycie pinów GPIO i innych takich, dzięki czemu można napisać uniwersalny kod, a dopasowanie do konkretnej płytki wymaga jedynie modyfikacji device tree. Ponieważ jest to system uruchamiany na mikrokontrolerach, które z reguły nie mają za dużo pamięci, system powinien zajmować jak najmniej miejsca. Wybór co ma być dołączone do Zephyra polega na ustawieniu odpowiednich zmiennych KConfiga.
Oprócz tego dostępne jest tam narzędzie west (biblioteka Pythonowa), które obsługuje pobieranie pakietów, sterowników, kompilację i takie tam.
Setup
Prawdopodobnie są różne konwencje, ale ja używam następującej:
- Wszystko co Zephyrowe - wspólne dla wielu projektów - znajduje się w folderze
D:\Dev\Zephyr
- Wszystko co dotyczy konkretnego projektu znajduje się gdzie indziej (np.
D:\Workspace\nazwa_projektu
)
Wszystkie wystąpienia tych ścieżek poniżej powinno dać się bez problemu zamienić na cokolwiek innego, więc nie należy ich traktować jako wymagane lokalizacje.
- Zainstaluj: Python, 7-zip, CMake, Ninja
- Wejdź na https://github.com/zephyrproject-rtos/sdk-ng/releases
- Pobierz "SDK Bundle" (minimal, pozostałe wersje mają wszystkie pozostałe toolchainy)
- SDK i toolchain
- SDK (Software Development Kit) zawiera narzędzia potrzebne do skompilowania wspólne dla różnych architektur
-
Toolchain zawiera kompilator i narzędzia pod konkretną architekturę procesora
- Pobierz toolchain pasujący do twojej płytki i systemu operacyjnego na którym pracujesz
Jeśli planujesz na Windowsie pisać na płytkę z ARMem, pobierz
arm-zephyr-eabi
z kolumny "Windows" - Rozpakuj SDK do folderu
D:\Dev\Zephyr\
(utworzy podfolder np.zephyr-sdk-<wersja>
- w momencie pisania dostępna jest wersja 0.17.0, czyli:zephyr-sdk-0.17.0
) - Rozpakuj toolchain do folderu
D:\Dev\Zephyr\zephyr-sdk-0.17.0
(w przypadku ARMowego, doda to podfolderarm-zephyr-eabi
) - Upewnij się, że masz folder
7-zip
w PATHu - Wejdź do folderu SDK:
D:\Dev\Zephyr\zephyr-sdk-0.17.0
i uruchomsetup.cmd
(w przypadku Windowsa) setup.cmd
może automatycznie pobrać różne toolchainy - wymagany masz już pobrany, więc na wszystkie pytania możesz odpowiedziećN
- poza ostatnim, dotyczącym zarejestrowania w CMake, na które warto odpowiedziećY
💬 W tym momencie struktura folderu powinna wyglądać mniej-więcej tak:
- Zephyr
- zephyr-sdk-0.17.0
- arm-zephyr-eabi
- setup.cmd
- ...oraz kilka innych plików i folderów
- Wejdź do folderu
D:\Dev\Zephyr
- Otwórz konsolę
- Utwórz venv:
python -m venv .venv
- Python Virtual Environment
- Pythonowy moduł venv pozwala tworzyć "wirtualne" środowiska. W uproszczeniu chodzi o to, żeby mieć kopię Pythona z własnym zestawem modułów i nie zaśmiecać głównej instalacji.
-
Virtual Environment (AKA venv) tworzy się uruchamiając:
python -m venv .venv
-
python
oznacza, że uruchamiamy Pythona
-
-m venv
mówi Pythonowi, że ma uruchomić moduł "venv"
-
.venv
mówi modułowi "venv", że ma stworzyć Virtual Environment w folderze ".venv"
-
Początkowo, otwierając nową konsolę, używany będzie Python zdefiniowany w PATHu. Aby przełączyć się na venva można użyć skryptu ustawiającego wszystkie zmienne:
.venv\Scripts\activate.bat
. Wykonanie go w konsoli spowoduje zmianę znaku zachęty zD:\Dev\zephyr>
na(.venv) D:\Dev\zephyr>
.
- Aktywuj venva:
.venv\Scripts\activate.bat
- Zainstaluj pakiet "west":
pip install west
- West
- West jest Zephyrowym narzędziem, które
-
- Pobiera kod i moduły Zephyra
-
- Ułatwia kompilację projektu,
-
- Pomaga we flashowaniu, debuggowaniu i kilku innych rzeczach
- Zainicjalizuj repozytorium Zephyra:
west init zephyrproject
- Przejdź do folderu
zephyrproject
- Pobierz wszystkie moduły:
west update
💬 Możliwe jest też pobranie tylko wymaganych rzeczy, ale później, w trakcie pisania, konieczne może być sprawdzenie czego brakuje i ręczne pobranie konkretnych modułów (np.
west update lvgl
) Jeśli nie brakuje ci miejsca na dysku, to pobierz wszystko (u mnie całość zajmuje ~8GB).
- Pobierz Pythonowe zależności Zephyra:
west packages pip --install
To powinno być na tyle jeśli chodzi o wspólną dla wszystkich projektów część.
Nowy projekt
Dobrym źródłem wiedzy oraz punktem wyjściowym nowego projektu może być Zephyrowy folder z przykładami. Na początek warto spróbować skompilować i uruchomić przykład "blinky", znajdujący się w D:\Dev\Zephyr\zephyrproject\samples\basic\blinky\
, który powoduje mruganie diody na mikrokontrolerze (o ile jakieś ma).
Przygotowanie
- Utwórz folder projektu, np.
D:\Workspace\nazwa_projektu
- Aktywuj venva:
D:\Dev\Zephyr\.venv\Scripts\activate.bat
- Dodaj Zephyrowe zmienne środowiskowe:
D:\Dev\Zephyr\zephyrproject\zephyr-env.cmd
- Opcjonalnie: Skopiuj pliki dowolnego przykładu do swojego folderu
Punkty 1. i 2. trzeba robić za każdym razem przy otwieraniu nowego okna konsoli.
Kompilacja
Kompilacja używa systemu budowania Ninja, więc upewnij się, że jest zainstalowany i dodany do PATHa.
west build -p -b <nazwa_mikrokontrolera>
gdzie:-p
oznacza pełne przebudowanie całego projektu - przydatne w razie jakichś błędów kompilacji, w większości przypadków lepiej pominąć.-b <nazwa_mikrokontrolera>
-<nazwa_mikrokontrolera>
trzeba zamienić na nazwę mikrokontrolera na który piszesz program, np.-b xiao_ble
Po skompilowaniu utworzony zostanie plik .\build\zephyr\zephyr.uf2
.
Flashowanie
Jeśli twój mikrokontroler obsługuje wgrywanie plików .uf2
, wystarczy skopiować .\build\zephyr\zephyr.uf2
.
W przeciwnym razie, można użyć west flash
⛔ TODO
Logi
Jeśli twój mikrokontroler to obsługuje, to Zephyr będzie wysyłał logi przez USB. Po pierwsze trzeba namierzyć na jakim porcie są nadawane. Na Windowsie można to zrobić używając "Menedżera Urzadzeń":
W tym przypadku kontroler wylądował na porcie COM7
.
Połączyć się z nim można przez putty. Jako "connection type" trzeba wybrać "Serial", a pod "Serial line" wpisać namierzony port:
Minusem takiego podejścia jest to, że po każdym zresetowaniu kontrolera trzeba ręcznie wszystko ponownie uruchomić. Osobiście wolę inne podejście - napisanie skryptu w Pythonie, który sam nawiązuje połączenie i wypisuje wszystkie logi. W uproszczeniu, jego kod wygląda tak:
import serial
with serial.Serial(port, baudrate=115200, timeout=0.5) as s:
line = s.readline().decode('ASCII').rstrip()
print(line)
Powyższy kod jest tylko przykładem - zamknięcie wszystkiego w pętli i dodanie ładnej obsługi błędów da bardzo przyjemny w użyciu nasłuchiwacz logów.
Przykład
Mój workflow wygląda następująco:
- Otwórz pierwsze okno konsoli
- Wywołaj w nim skrypt
init.bat
- Otwórz drugie okno konsoli
- Uruchom w nim nasłuchiwacz logów:
python serialwatch.py
- Po każdej zmianie w kodzie, w pierwszym oknie wywołaj
build.bat && flash.bat
init.bat
jest skryptem który uruchamia venva i ustawia Zephyrowe zmienne:
@echo off
call D:\Dev\zephyr\.venv\Scripts\activate.bat
call D:\Dev\zephyr\zephyrproject\zephyr-env.cmd
build.bat
uruchamia kompilację:
@echo off
cls
west build -b xiao_ble %*
flash.bat
kopiuje plik .uf2
na kontroler, który przeważnie w trybie flashowania pokazuje się u mnie jako dysk F:
@echo off
cp build\zephyr\zephyr.uf2 F:\
Struktura projektu
Foldery
Zephyr oczekuje pewnej konkretnej struktury folderów i plików. Poniżej znajduje się jej opis, a pod nim wyjaśnienie o co chodzi.
.\boards\
- folder zawierający pliki overlaye.\build\
- folder zawierający pliki wygenerowane przez kompilator (nie wrzucaj go do systemu kontroli wersji, najlepiej od razu dopisz do.gitignore
).\dts\bindings\
- folder zawierający bindingi.\src\
- folder z kodem źródłowym twojego projektu.\tests\
- folder z testami.\CMakeLists.txt
- konfiguracja projektu dla CMake - lista plików, opcje kompilacji....\prj.conf
- KConfigowy plik konfiguracyjny Zephyra
Device tree
Device Tree jest plikiem opisującym komponenty systemu i ich parametry. Dla przykładu weźmy płytkę której używałem: Seeed Studio XIAO nRF52840.
Device tree tej płytki wygląda następująco (niektóre fragmenty wycięte):
#include <nordic/nrf52840_qiaa.dtsi>
/ {
chosen {
zephyr,ieee802154 = &ieee802154;
};
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 26 GPIO_ACTIVE_LOW>;
label = "Red LED";
};
led1: led_1 {
gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
label = "Green LED";
};
led2: led_2 {
gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;
label = "Blue LED";
};
};
aliases {
led0 = &led0;
led1 = &led1;
led2 = &led2;
};
};
W pierwszej linii widać, że includowany jest plik nrf52840_qiaa.dtsi
co potwierdza, że ten kontroler bazuje na układzie nRF52840 (czyli jego nazwa nie kłamie).
Z zawartości device tree da się odczytać, że płytka ta ma trzy LEDy - czerwony, zielony i niebieski - wewnętrznie podpięte do GPIO kolejno 26, 30 i 6, które są włączone po podaniu stanu niskiego.
Przykładowy projekt "blinky" pokazuje jak użyć danych z device tree w kodzie:
struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
DT_ALIAS
konwertuje podany alias na identyfikator węzła device tree, używając aliasów zdefiniowanych w blokualiases
DT_ALIAS(led0)
konwertuje aliasled0
- w tym przypadku alias i nazwa węzła są takie sameGPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios)
pobiera informacje o pinie GPIO z noda na który wskazuje aliasled0
, które zapisane są w polugpios
gpio_dt_spec
przechowuje informacje o pinie GPIO takie jak jego numer, przy jakim stanie jest uznawany za aktywny...
Jeśli chcemy podpiąć do kontrolera jakieś inne urządzenia, lub przestawić parametry podpiętych (np. żeby przepiąć wbudowane LEDy na zewnętrzne) można zrobić overlay.
Overlay
Overlay jest nakładką na device tree wybranego kontrolera. Przykładowo: płytka ma swoje LEDy, ale w projekcie który piszemy możemy zamiast nich chcieć używać innych, lub dołożyć nowe diody. To, co dostępne jest w każdej płytce opisane jest w device tree ukrytym w Zephyrze, a to, co używane przez projekt będzie do niego doklejone podczas kompilacji.
Ponieważ używam płytki xiao_ble
, w folderze boards
musiałem stworzyć plik xiao_ble.overlay
. Jego zawartość wygląda następująco (fragmenty wycięte):
/ {
buzzers {
compatible = "gpio-leds";
buzzer0: buzzer0 {
gpios = <&xiao_d 6 GPIO_ACTIVE_LOW>;
label = "Buzzer";
};
};
qmbstarter: qmbstarter {
compatible = "qmbstarter";
relay-power-gpios = <&xiao_d 1 GPIO_ACTIVE_HIGH>;
relay-starter-gpios = <&xiao_d 7 GPIO_ACTIVE_LOW>;
relay-ignition-gpios = <&xiao_d 0 GPIO_ACTIVE_LOW>;
};
qmbstarter_charging_sensor: qmbstarter_charging_sensor {
compatible = "adc";
io-channels = <&adc 0>;
};
};
&adc {
status = "okay";
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_AIN3>;
zephyr,resolution = <12>;
};
};
- W
/
zdefiniowane są nowe komponenty device tree, nie występujące w bazowym - W bloku
buzzers
opisany jest jednotonowy głośnik (liczba dźwięków, nie waga), który:- Ma takie same parametry jak LED (
compatible = "gpio-leds"
) - Ma identyfikator
buzzer0
- Jest podpięty do GPIO zwanego w tej płytce "D6" (
&xiao_d 6
, bazując na dokumentacji układu jest to równoważne&gpio1 11
).
- Ma takie same parametry jak LED (
- W bloku
qmbstarter
opisane jest coś, co:- Ma parametry opisane w bindingu
qmbstarter
- Używa trzech przekaźników podpiętych do pinów "D1", "D7" oraz "D0"
- Pierwszy przekaźnik aktywowany jest stanem wysokim, pozostałe stanem niskim
- Ma parametry opisane w bindingu
- W bloku
qmbstarter_charging_sensor
mowa o czymś, co:- Jest ADC
- Używa ADC numer 0
- W
&adc
modyfikowane są wartości zdefiniowane w oryginalnym device treestatus
ustawiony jest na"okay"
(co oznacza, że ADC będzie dostępny do użycia - w przeciwnym razie pin działał by jako zwykłe GPIO),channel@0
definiuje parametry ADC numer 0zephyr,input-positive = <NRF_SAADC_AIN3>
mówi którego pinu GPIO użyć jako ADC, ale co wpisać w zamiastNRF_SAADC_AIN3
jest wiedzą tajemną (z pomocą przyszedł mi ChatGPT, bo nic lepszego nie znalazłem ಠ_ಠ )
Czyli, z programistycznego na ludzki, powyższy overlay mówi, że do tej płytki dodatkowo podłączony jest głośniczek, trzy przekaźniki oraz jeden z pinów GPIO używany jest jako ADC z pewnymi parametrami.
Tak jak w poprzednim przypadku, aby spod kodu dostać się do parametrów zdefiniowanych w device tree i overlayu, trzeba użyć odpowiednich makr:
GPIO_DT_SPEC_GET(DT_NODELABEL(qmbstarter), relay_power_gpios);
DT_NODELABEL(qmbstarter)
wyszukuje węzeł o nazwieqmbstarter
(bezpośrednio po jego nazwie, nie po aliasie)GPIO_DT_SPEC_GET(DT_NODELABEL(qmbstarter), relay_power_gpios)
pobiera parametry GPIO z polarelay_power_gpios
w węźle o nazwieqmbstarter
Oczywiście, możliwe jest zdefiniowanie wszystkiego ręcznie, bezpośrednio w kodzie - numer pinu, jakim stanem aktywowane, parametry ADC, ... - ale jest bardzo ważny powód używania device tree i overlayów: łatwo można przeportować kod na inną płytkę.
Jeśli postanowię skompilować swój projekt na czymś innym, na przykład xiao_rp2040
, wystarczy, że zrobię nowego overlaya xiao_rp2040.overlay
w którym opiszę gdzie co jest podpięte, oraz skompiluję z flagą -b xiao_rp2040
. Kod pozbawiony jest jakichkolwiek informacji o sprzęcie na jakim będzie działał co sprawia, że jest czytelniejszy i ułatwia kompilację na innym układzie.
Bindings
W device tree co chwilę pojawia się pole compatible
. Mówi ono jakie parametry ma dany węzeł i jakie są ich wartości domyślne. Konieczność napisania własnych bindingsów pojawia się jeśli chcemy podłączyć coś do płytki i zachować logiczne zgrupowanie tych rzeczy.
Przykładowo: chciałem podłączyć trzy przekaźniki, które razem są częścią systemu startującego silnik spalinowy. Napisałem więc binding dts\bindings\qmbstarter.yaml
:
description: binding for QMBStarter
compatible: "qmbstarter"
properties:
relay-power-gpios:
type: phandle-array
required: true
description: GPIO connected to the 'power' relay
relay-starter-gpios:
type: phandle-array
required: true
description: GPIO connected to the 'starter' relay
relay-ignition-gpios:
type: phandle-array
required: true
description: GPIO connected to the 'ignition' relay
compatible: "qmbstarter"
opisuje jaką wartość powinno mieć polecompatible
w device treeproperties
mówi jakie parametry ma węzeł device treerelay_power_gpios
to nazwa parametrutype: phandle-array
to typ parametrurequired: true
mówi, że trzeba zawsze zdefiniować wartośćdescription: ...
to zrozumiały dla człowieka opis
Możliwe jest też pójście na łatwiznę i ustawienie wszystkiemu compatible = "gpio-leds"
, ale wtedy nie widać, że niektóre rzeczy są częścią tego samego systemu (trzy niezależne przekaźniki VS trzy przekaźniki używane w starterze silnika).
Dokumentacja bindingów zawiera więcej informacji.
KConfig
System konfiguracyjny Zephyra, mówiący jakie komponenty systemu powinny być dołączone i skompilowane. Czemu nie kompilować wszystkiego? Bo kontroler może mieć za mało pamięci, żeby zmieścić Zephyra w jego pełnej okazałości. No i nie ma potrzeby marnować prądu na usługi, które nie będą wykorzystane, a kontroler może przecież być zasilany z małej baterii.
Plik konfiguracyjny znajduje się bezpośrednio w folderze projektu i nazywa się prj.conf
. Przykładowa zawartość:
CONFIG_LOG=y
CONFIG_GPIO=y
CONFIG_ADC=y
CONFIG_SPI=y
CONFIG_BT=y
CONFIG_BT_BAS=y
CONFIG_BT_DEVICE_NAME="QMB139 Starter"
CONFIG_LOG
- włącz w Zephyrze system logówCONFIG_GPIO
- będziemy używali GPIO, więc dołącz też wszystko co do tego potrzebneCONFIG_ADC
- o, ADC też nam się przyda...CONFIG_SPI
- ...i SPI...CONFIG_BT
- ...oraz Bluetooth, więc skompiluj wszystko co do niego potrzebneCONFIG_BT_BAS
- mój kontroler będzie nadawał stan baterii używając usługi bateriowejCONFIG_BT_DEVICE_NAME
- ...i tak ma się przedstawiać na liście urządzeń Bluetooth!
Proces kompilacji
Proces kompilacji przebiega, w dużym uproszczeniu, następująco:
- Znajdź device tree dla podanej płytki (zdefiniowanej flagą
-b
, np.-b xiao_ble
) - Nałóż overlay podanej płytki na device tree
- W trakcie przeglądania device tree, sprawdź czy wszystkie węzły spełniają wymagania podane w bindingach wskazanych przez
compatible
- Wygeneruj jeden wielki device tree i zapisz go w pliku
build\zephyr\zephyr.dts
- Przekonwertuj device tree do C i zapisz go w pliku
build\zephyr\include\generated\zephyr\devicetree_generated.h
- Sprawdź które komponenty Zephyra powinny być skompilowane bazując na zawartości
prj.conf
- Skompiluj projekt i Zephyra do pliku
build\zephyr\zephyr.uf2
TODO
- Sprawdzić jak używać
west flash
- Sprawdzić jak zachowuje się
west sdk install
Komentarze