Wstęp
Po co jest bootloader? Bootloader służy do wstępnej inicjalizacji niezbędnych peryferiów i do załadowania systemu operacyjnego. To w zasadzie tyle co trzeba wiedzieć aby przejść dalej, ale rozwinę nieco temat aby wprowadzić pewne teoretyczne podstawy. Więc co się dzieje krok po kroku?
Po uruchomieniu płytki pierwszym programem, który się uruchamia jest tzw. boot ROM a.k.a. IPL(initial program loader). Kod IPLa jest wgrywany na płytkę podczas produkcji, nie może on zostać podmieniony i jest on charakterystyczny dla każdego urządzenia. IPL może załadować inny program do pamięci RAM z pamięci nieulotnej jak np. pamięć flash więc musi on również umieć zainicjalizować jakiś interfejs komunikacyjny jak np. SPI. IPL ładuje do RAMu tzw. SPL czyli secondary program loader.
SPL może mieć dwa zastosowania- albo załadowanie rozbudowanego bootloadera(tzw. tretiary program loader, TPL) jak np. U-Boot, który umożliwia bardzo rozbudowaną konfiguracje uruchamiania systemu albo może załadować kernel systemu operacyjnego. Z pierwszym przypadkiem mamy do czynienia w przypadku BBB, natomiast w przypadku RPi4 SPL może załadować bezpośrednio kernel. SPL w przeciwieństwie do IPL może zostać podmieniony. Oczywiście SPL musi zainicjalizować niezbędne peryferia aby załadować TPL lub kernel.
No i ostatni krok czyli TPL. Na tym etapie mamy już dostępny prosty interfejs użytkownika. Tutaj mamy możliwość wczytania interesującego nas systemu operacyjnego czy przekazania odpowiednich parametrów systemowi operacyjnemu.
I to tyle tytułem wstępu, zainteresowani tematyką bootloaderów mogą rzucić okiem na rozdział poświęcony bootloaderom książki „Linux inside”.
A teraz przechodzimy do tego co tygryski lubią najbardziej czyli do zabawy z płytkami. W tej lekcji użytkownicy BBB będą mieli najwięcej pracy do wykonania, użytkownicy RPi4 będą musieli zrobić niewiele, a użytkownicy QEMU nic nie będą musieli zrobić, ale mimo to zachęcam ich do rzucenia okiem na tę lekcje.
U-Boot i BeagleBone Black
U-Boot, a właściwie Das U-Boot jest jednym z popularniejszych, jeśli nie najpopularniejszym bootloaderem na systemy wbudowane. Kod U-Boota jest otwarty i jest on dostępny na githubie. Udostępnia on interfejs w postaci prostej konsoli. Został on stworzony przez firmę Denx, zainteresowanych odsyłam na stronę projektu po więcej informacji.
A teraz do rzeczy, jak zbudować i uruchomić tego całego U-Boota? Przede wszystkim musimy pobrać jego kod źródłowy:
git clone --depth 1 https://github.com/u-boot/u-boot.git
Parametr –depth 1 informuje gita, że ma pobrać jedynie najświeższą wersje plików z repozytorium, nie pobiera on wtedy historii zmian. W tym przypadku zastosowaliśmy tę opcje aby przyspieszyć proces pobierania kodu.
Gdy już mamy kod źródłowy to możemy go przebudować. Katalog configs/ zawiera konfiguracje dla dostępnych architektur sprzętowych. Nas interesuje konfiguracja am335x_evm_defconfig. Aby ustawić tę konfigurację sprzętową wykonujemy następującą komendę:
make am335x_evm_defconfig
Po wykonaniu tej komendy program make będzie wiedział co ma zbudować.
Zanim przejdziesz do budowania przypominam, że ścieżka do kompilatorów arm-linux-gnueabihf musi się znajdować w zmiennej PATH. Jeśli nie wiesz o co chodzi to zapraszam do lekcji pierwszej- przygotowanie środowiska pracy.
Aby zbudować U-Boota wykonujemy komendę:
make CROSS_COMPILE=arm-linux-gnueabihf- -j4
Zmienna CROSS_COMPILE informuje, która rodzina kompilatorów ma zostać użyta, zwróć uwagę na myślnik na końcu, jest on bardzo ważny, bez niego kompilacja nie przejdzie. Wynika to z tego, że program make po prostu skleja zawartość zmiennej CROSS_COMPILE z nazwą kompilatora jak np. gcc, g++ albo linkera np. ld.
Parametr -j informuje program make ile wątków może jednocześnie uruchomić i dzięki temu przyspieszyć proces kompilacji. Są różne podejścia do tego ile ta liczba powinna wynosić, ja osobiście ustawiam tyle wątków ile mam fizycznych rdzeni w procesorze. Kompilacja U-Boota trwa bardzo krótko.
Po zbudowaniu zobaczysz kilka nowych plików, nas interesują pliki MLO i u-boot.img. MLO jest naszym SPL, a U-Boot to TPL. Oba pliki musimy umieścić na karcie uSD używanej przez BBB. Pliki te muszą się znaleźć na pierwszej FATowskiej partycji karty. My tej partycji nadaliśmy etykietę uboot. Skopiuj więc te pliki na kartę:
cp MLO /media/user/uboot/
cp u-boot.img /media/user/uboot/
Pamiętaj aby MLO było zawsze zapisane jako pierwszy plik na tę partycję po sformatowaniu karty, inaczej mogą wystąpić problemy z działaniem. Gdy już zapiszesz wymagane pliki na karcie to odmontuj ją i przełóż ją do BBB:
sudo umount /media/user/*
Zanim uruchomimy nasz bootloader musimy mieć możliwość komunikacji z nim. Do tego celu użyjemy interfejsu UART. BBB posiada specjalnie wyprowadzone piny i dedykowany konwerter UART-USB, ale jeśli posiadasz zwykłą przejściówkę UART-USB to podłącz ją do następujących pinów:
Pamiętaj aby podłączyć pin TX konwertera do pinu RX płytki oraz pin RX konwertera do pinu TX płytki, w skrócie wykonaj połączenie krzyżowe.
Potrzebujemy jeszcze programu do komunikacji z BBB, możesz w tym celu użyć programu screen. Po podłączeniu konwertera do płytki i komputera(nie uruchamiaj jeszcze BBB) możesz wykonać komendę:
sudo screen /dev/ttyUSB0 115200
Tak, musisz uruchamiać ten program jako użytkownik root. Podłącz teraz BBB do prądu, w konsoli powinieneś zobaczyć logi podobne do poniższych:
U-Boot SPL 2021.10-rc3 (Sep 03 2021 - 20:40:26 +0200)
Trying to boot from MMC1
U-Boot 2021.10-rc3 (Sep 03 2021 - 20:40:26 +0200)
CPU : AM335X-GP rev 2.1
Model: TI AM335x BeagleBone Black
DRAM: 512 MiB
WDT: Started with servicing (60s timeout)
NAND: 0 MiB
MMC: OMAP SD/MMC: 0, OMAP SD/MMC: 1
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1... <ethaddr> not set. Validating first E-fuse MAC
Net: eth2: ethernet@4a100000
Hit any key to stop autoboot: 2
Naciśnij jakikolwiek klawisz aby przerwać proces automatycznego startowania systemu, który się nie powiedzie w tym przypadku przede wszystkim z powodu braku systemu operacyjnego do użycia.
Po przerwaniu procesu automatycznego bootowania powinieneś ujrzeć znak zachęty powszechnie zwany promptem, który wygląda następująco:
=>
Aby zobaczyć jakie są dostępne komendy wpisz help, jak widzisz trochę ich, daje to duże pole do konfiguracji uruchamiania systemu. Nas w kolejnych lekcjach będą interesowały komendy:
- bootz- startuje system operacyjny wykorzystując obraz zImage
- ext4ls- sprawdza zawartość partycji z systemem plików ext4
- ext4load- ładuje plik z partycji z systemem plików ext4 do pamięci RAM
- printenv- wyświetla zmienną lub zmienne środowiskowe
- setenv- ustawia zmienną środowiskową
- saveenv- zapisuje wartości zmiennych środowiskowych na karcie uSD
- help- wyświetla opis dla wszystkich komend lub pomoc dla przekazanej komendy
Kolejnym ważnym elementem U-Boota są zmienne środowiskowe. Jeśli chcesz wyświetlić wszystkie zmienne środowiskowe wpisz komendę printenv. Znowu wyświetliło się sporo tekstu, ale nas interesować będą następujące zmienne:
- bootcmd- zmienna przechowująca sekwencje komend potrzebną do automatycznego uruchomienia systemu
- bootargs- zmienna przechowująca parametry przekazywane do systemu operacyjnego podczas jego uruchamiania
- fdt_addr_r- zmienna przechowująca adres pod który należy wczytać device-tree
- kernel_addr_r- zmienna przechowująca adres pod który należy wczytać kernel
- ramdisk_addr_r- zmienna przechowująca adres pod który należy wczytać ramdisk(initramfs)
I to tyle co nas interesuje ze zmiennych. Jako zadanie bojowe możesz poeksperymentować ze zmiennymi środowiskowymi za pomocą komend printenv, setenv oraz saveenv. Pamiętaj, że jeśli namieszasz coś ze zmiennymi środowiskowymi to możesz przywrócić ich domyślną wartość poprzez usunięcie pliku uboot.env, który powstaje po wykonaniu polecenia saveenv na tej samej partycji na której znajduje się obraz U-Boota.
I to tyle z najważniejszych funkcjonalności U-Boota, będziemy korzystać z tej wiedzy w kolejnej lekcji podczas uruchamianiu kernela.
Raspberry Pi 4
W przypadku RPi4 nie będziemy używać U-Boota pomimo że istnieje plik konfiguracyjny dla niej w źródłach U-Boota. Wynika to z tego, że jest problem z konfiguracją UARTa i nie można się dostać do konsoli U-Boota(a przynajmniej ja nie ogarnąłem jak to zrobić póki co).
W przypadku RPi4 użyjemy tylko SPL, który to będzie ładował bezpośrednio system. Firmware RPi4 jest dostępny na githubie, pobierzmy zatem odpowiednie repozytorium:
git clone --depth 1 https://github.com/raspberrypi/firmware.git
Jeśli nie czytałeś sekcji o BBB to tam w trzecim akapicie jest wytłumaczone co oznacza opcja –depth.
Stety bądź niestety kod firmware’u RPi4 jest zamknięty i nie mamy do niego dostępu, repozytorium to przechowuje pliki binarne. W katalogu boot/ wewnątrz repozytorium znajduje się kilkanaście plików, nas interesują pliki start4.elf oraz fixup4.dat. Skopiuj oba te pliki na FATowską partycje karty uSD używanej przez RPi4(dla przypomnienia w poprzedniej lekcji nadaliśmy tej partycji etykietę boot):
cp boot/{start4.elf,fixup4.dat} /media/user/boot
Moglibyśmy już uruchomić naszą płytkę jednak byśmy nie mogli nic zobaczyć na konsoli, jedynie byśmy widzieli świecącą się diodę na płycie i migający kursor w konsoli. Aby móc odebrać dane z bootloadera musimy utworzyć plik z konfiguracją o nazwie config.txt o następującej zawartości:
arm_64bit=1
enable_uart=1
uart_2ndstage=1
Plik ten informuje RPi4, że ma ona pracować w trybie 64-bitowym, ma włączyć UART i przekierować logowanie SPL na UART. Oczywiście umieść ten plik na pierwszej partycji karty pamięci:
cp config.txt /media/user/boot
Gdy już wgrałeś wymagane pliki na kartę uSD to odmontuj ją i przełóż ją do RPi4:
sudo umount /media/user/*
Teraz podłączymy konwerter UART-USB do RPi4. Podłącz konwerter do pinów wskazanych przez czerwoną ramkę:
Tak samo jak w przypadku BBB pamiętaj aby pin TX RPi4 podłączyć do pinu RX konwertera, a pin RX płytki do pinu TX konwertera. Po podłączeniu konwertera do RPi4 podłącz go do komputera, ale nie uruchamiaj jeszcze płytki. Następnie wydaj komendę:
sudo screen /dev/ttyUSB0 115200
Teraz uruchom RPi4. W konsoli powinieneś ujrzeć sporo logów z błędami odnośnie HDMI, a na samym dole powinny być widoczne błędy o braku możliwości znalezienia pliku z device-tree, cmdline.txt oraz kernela:
recover4.elf not found (6)
recovery.elf not found (6)
Read start4.elf bytes 2240352 hnd 0x00000003 hash 'd298436679a008f8'
Read fixup4.dat bytes 5407 hnd 0x00000226 hash '33dee5d007b097b2'
0x00d03114 0x00000000 0x00000fff
MEM GPU: 76 ARM: 948 TOTAL: 1024
Starting start4.elf @ 0xfec00200 partition 0
MESS:00:00:04.834808:0: arasan: arasan_emmc_open
MESS:00:00:05.123531:0: brfs: File read: /mfs/sd/config.txt
MESS:00:00:05.126212:0: brfs: File read: 42 bytes
MESS:00:00:05.180392:0: HDMI0:EDID error reading EDID block 0 attempt 0
MESS:00:00:05.189902:0: HDMI0:EDID error reading EDID block 0 attempt 1
MESS:00:00:05.199417:0: HDMI0:EDID error reading EDID block 0 attempt 2
MESS:00:00:05.208927:0: HDMI0:EDID error reading EDID block 0 attempt 3
MESS:00:00:05.218441:0: HDMI0:EDID error reading EDID block 0 attempt 4
MESS:00:00:05.227948:0: HDMI0:EDID error reading EDID block 0 attempt 5
MESS:00:00:05.237462:0: HDMI0:EDID error reading EDID block 0 attempt 6
MESS:00:00:05.246972:0: HDMI0:EDID error reading EDID block 0 attempt 7
MESS:00:00:05.256486:0: HDMI0:EDID error reading EDID block 0 attempt 8
MESS:00:00:05.265994:0: HDMI0:EDID error reading EDID block 0 attempt 9
MESS:00:00:05.270499:0: HDMI0:EDID giving up on reading EDID block 0
…
MESS:00:00:06.891971:0: hdmi: HDMI1:EDID error reading EDID block 0 attempt 3
MESS:00:00:06.902009:0: hdmi: HDMI1:EDID error reading EDID block 0 attempt 4
MESS:00:00:06.912039:0: hdmi: HDMI1:EDID error reading EDID block 0 attempt 5
MESS:00:00:06.922076:0: hdmi: HDMI1:EDID error reading EDID block 0 attempt 6
MESS:00:00:06.932109:0: hdmi: HDMI1:EDID error reading EDID block 0 attempt 7
MESS:00:00:06.942147:0: hdmi: HDMI1:EDID error reading EDID block 0 attempt 8
MESS:00:00:06.952177:0: hdmi: HDMI1:EDID error reading EDID block 0 attempt 9
MESS:00:00:06.957206:0: hdmi: HDMI1:EDID giving up on reading EDID block 0
MESS:00:00:06.962804:0: hdmi: HDMI:hdmi_get_state is deprecated, use hdmi_get_display_state instead
MESS:00:00:06.971568:0: HDMI0: hdmi_pixel_encoding: 300000000
MESS:00:00:06.977039:0: HDMI1: hdmi_pixel_encoding: 300000000
MESS:00:00:06.987239:0: dtb_file 'bcm2711-rpi-4-b.dtb'
MESS:00:00:06.989313:0: Failed to load Device Tree file '?'
MESS:00:00:06.994609:0: Failed to open command line file 'cmdline.txt'
MESS:00:00:07.001148:0: No compatible kernel found
No i fajnie, RPi4 jest już gotowe aby wystartować kernel Linuksa, a tym zajmiemy się już w kolejnej lekcji.