UBOOT

Ed eccoci qua, il vero cuore del bootloader. U-BOOT è l’ ultimo stage della nostra bootchain ed è responsabile dell’ inizializzazione delle altre periferiche che useremo, ma soprattuto è quello che prepara l’ ambiente e poi passa l’ esecuzione al kernel linux. E’ un progetto molto modulare e con una valanga di funzionalità. Più funzionalità incorporiamo e più la dimensione del binario finale aumenta. Questo può essere un problema se non abbiamo chissà quanta memoria (RAM) disponibile. Nel nostro caso ci va di lusso, perchè il TFA ha già inizializzato la RAM DDR e quindi non abbiamo problemi di spazio.

Una altra cosa interessante riguardo la repository è l’utilizzo di Kconfig per poter gestire la presenza e il valore dei vari #define presenti dentro al codice. Kconfig è un sistema molto utilizzato nel kernel linux perchè permette di gestire dipendenze anche complicate tra molteplici #define. Per configurarlo solitamente si utilizzano degli applicativi grafici o da terminale come menuconfig. Questi poi generano come output un file .config che contiene tutte le definizioni necessarie al progetto. In Yocto esistono .bbclass apposite per poter interagire con tali applicativi. Questo per dire che nel recipe UBOOT non punteremo ad utilizzare variabili make per configurare il progetto, ma a configurare questo file .config.

Per configurare UBOOT ci basiamo su:

dal recipe originale

Il recipe per UBOOT è particolarmente difficile, perchè presenta molti meccanismi per poter gestire la configurazione dell’ applicativo. Vengono usate molteplici .bbclass e una marea di variabili. La maggior parte delle variabili bitbake disponibili sono definite in uboot-config.bbclass.

meta/classes-recipe/uboot-config.bbclass
23# Some versions of u-boot use .bin and others use .img.  By default use .bin
24# but enable individual recipes to change this value.
25UBOOT_SUFFIX ??= "bin"
26UBOOT_BINARY ?= "u-boot.${UBOOT_SUFFIX}"
27UBOOT_BINARYNAME ?= "${@os.path.splitext(d.getVar("UBOOT_BINARY"))[0]}"
28UBOOT_IMAGE ?= "${UBOOT_BINARYNAME}-${MACHINE}-${UBOOT_VERSION}.${UBOOT_SUFFIX}"
29UBOOT_SYMLINK ?= "${UBOOT_BINARYNAME}-${MACHINE}.${UBOOT_SUFFIX}"
30UBOOT_MAKE_TARGET ?= "all"

Una variabile interessante è UBOOT_MACHINE, che viene usata come target del Makefile:

meta/classes-recipe/uboot-config.bbclass
23uboot_configure () {
24    if [ -n "${UBOOT_MACHINE}" ]; then
25        oe_runmake -C ${S} O=${B} ${UBOOT_MACHINE}
26    else
27        oe_runmake -C ${S} O=${B} oldconfig
28    fi
29    merge_config.sh -m .config ${@" ".join(find_cfgs(d))}
30    cml1_do_configure
31}

Notiamo anche la presenza di merge_config.sh e l’utilizzo di cml1. Non entriamo nei dettagli, ma entrambe sono legate alla configurazione del progetto tramite Kconfig.

dalla documentazione

La versione UBOOT utilizzata non supporta stm32mp2 e la documentazione tratta solo la serie stm32mp1, quindi non abbiamo granchè. Parliamo quinid un attimo del processo generico di build di UBOOT. Ogni board o configurazione desiderata è definita da un file chiamato defconfig. Configuriamo il progetto passando tale file come target di make:

repo
23make <defconfig_file>

Facciamo partire la compilazione usando il target all e definendo eventuali altre variabili make:

repo
23make DEVICE_TREE=<name> all

dai sorgenti

Spieghiamo un attimo come è organizzata la repo:

uboot repo layout
u-boot
├── Makefile
├── Kconfig
├── ...
├── configs
│    ├── ...
│    └── stm32mp25_defconfig
├── board
│    ├── ...
│    └── st
│         ├── ...
│         ├── common
│         │    ├── ...
│         │    └── Kconfig
│         └── stm32mp2
│              ├── ...
│              └── Kconfig
└── arch 
     ├── Kconfig
     ├── x86
     ├── xtensa
     ├── riscv
     ├── ...
     └── arm
          ├── Makefile
          ├── config.mk
          ├── Kconfig
          ├── dts
          │    ├── ...
          │    └── stm32mp257f-dk.dts
          ├── ...
          └── mach-stm32mp
               ├── Makefile
               ├── Kconfig
               ├── Kconfig.13x
               ├── ...
               ├── Kconfig.25x
               └── stm32mp2
                    ├── ...
                    └── Makefile
     

I file Kconfig definiscono i vari menù e scelte che possiamo fare e ci permettono di dare una struttura gerarchica ai classici #define. Un Kconfig può includere un altro Kconfig. I Makefile allora possono focalizzarsi sulla compilazione dei file a seconda di quali definizioni sono presenti.

Una particolarità del meccanismo, è che i simboli che nei file Kconfig vediamo come STM32MP25X, li ritroviamo nel codice e nei Makefile con il prefisso CONFIG_, quindi come CONFIG_STM32MP25X:

arch/arm/mach-stm32mp/Kconfig
23config STM32MP25X
24	bool "Support STMicroelectronics STM32MP25x Soc"
25	select ARM64
26	select CLK_STM32MP25
27	select OF_BOARD
28	select OF_BOARD_SETUP
29	select PINCTRL_STM32
30	select STM32_RCC
arch/arm/mach-stm32mp/Makefile
15obj-$(CONFIG_STM32MP25X) += stm32mp2/
16
17obj-$(CONFIG_STM32MP_TAMP_NVMEM) += nvram.o
18obj-$(CONFIG_MFD_STM32_TIMERS) += timers.o
19
20obj-$(CONFIG_STM32_ECDSA_VERIFY) += ecdsa_romapi.o
21ifndef CONFIG_SPL_BUILD
22obj-y += cmd_stm32prog/
23obj-$(CONFIG_CMD_STM32KEY) += cmd_stm32key.o
24obj-$(CONFIG_TFABOOT) += boot_params.o
25endif
arch/arm/mach-stm32mp/include/mach/stm32.h
15#ifdef CONFIG_STM32MP25X
16#define STM32_UART9_BASE		0x402C0000
17#endif

Alcune variabili Kconfig assumono un valore, mentre altre sono definite come booleani e vengono espanse in y oppure n.

Generalmente ogni Kconfig si occupa di includere tutti i Kconfig da lui dipendenti, anche se non necessari alla build attuale, e i simboli specifici per un target saranno condizionati dalla presenza o meno di altri simboli. In questo modo vengono attivati i possibili sottomenù per la famiglia una specifica famiglia, come stm32mp.

Nel nostro caso la catena di inclusione dei Kconfig è questa:

  Kconfig
    ↓
  arch/Kconfig
    ↓
  arch/arm/Kconfig
    ↓
  arch/arm/mach-stm32mp/Kconfig
    ↓ if ARCH_STM32MP
  arch/arm/mach-stm32mp/Kconfig.25x
    ↓ if STM32MP25X
  board/st/stm32mp2/Kconfig
    ↓ if TARGET_ST_STM32MP25X
  board/st/common/Kconfig

Notiamo che alcune inclusioni sono condizionate dalla presenza di simboli come:

  • ARCH_STM32MP
  • STM32MP25X
  • TARGET_ST_STM32MP25X

e non è necessario che questi vengano effettivamente utilizzati nel codice, ma possono rimanere delle variabili dedicate solo a Kconfig.

L’insieme delle definizioni e dei valori di questi simboli rappresentano la configurazione per un particolare target e vengono solitamente chiamati defconfig (default config). Passando stm32mp25_defconfig come target di make viene generato un file .config a partire dal file configs/stm32mp25_defconfig. Il file .config contiene la configurazione attuale ed è quello che verrà utilizzata per la compilazione. Per apportare modifiche a questo è consigliato utilizzare dei comandi specifici come make menuconfig. Infine ci basterà usare make all per ottenere i file binari desiderati.

Esempio di file defconfig:

u-boot/configs/stm32mp25_defconfig
1CONFIG_ARM=y
2CONFIG_USE_ARCH_MEMCPY=y
3CONFIG_ARCH_STM32MP=y
4CONFIG_SYS_MALLOC_F_LEN=0x500000
5CONFIG_CUSTOM_SYS_INIT_SP_ADDR=0x90000000
6CONFIG_ENV_OFFSET=0x900000
7CONFIG_ENV_SECT_SIZE=0x40000
8CONFIG_DEFAULT_DEVICE_TREE="stm32mp257f-ev1"
9CONFIG_STM32MP25X=y

Alcuni dei binari generati:

  • u-boot (binario in formato ELF per debugging su host)
  • u-boot.bin (binario da flashare sul target)
  • u-boot-nodtb.bin (binario senza devicetree da flashare)
  • u-boot-dtb.bin(identico a u-boot.bin)
  • u-boot.dtb (devicetree utilizzato)

recipe finale

Anche per uboot utilizziamo le repo di STM, quindi definiamo:

recipes-bsp/u-boot/u-boot_%25.bbappend
1SRC_URI = "git://github.com/STMicroelectronics/u-boot.git;protocol=https;nobranch=1"
2# SRCREV = "v2023.10-stm32mp-r2"
3SRCREV = "64beae9fed51f49b53f952d00d9918b9eb3b040b"
4
5PV := "2023.10-stm32mp"
6PR := "r2"

Se il *_defconfig per la nostra board è già presente nella repo, ci basta quindi definire UBOOT_MACHINE = "stm32mp25_defconfig". Già dal estratto di file che abbiamo qua sopra, notiamo che il device tree definito come default non è quello per la scheda stm32mp257f-dk ma per la stm32mp257f-ev1. Uboot permette di cambiare rapidamente la scelta del device tree utilizzando la variabile Make DEVICE_TREE, evitando quindi di dover cambiare il defconfig. Per il nostro recipe possiamo quindi utilizzare:

recipes-bsp/u-boot/u-boot_%25.bbappend
 7UBOOT_MACHINE = "stm32mp25_defconfig"
 8
 9# override CONFIG_DEFAULT_DEVICE_TREE
10EXTRA_OEMAKE:append = " \
11    DEVICE_TREE=stm32mp257f-dk \
12    "

Se abbiamo necessità di apportare modifiche alle definizioni possiamo invece creare una copia del defconfig, modificarla, posizionarla all’interno della cartella del recipe e utilizzare SRC_URI per aggiungere questo file alla nostra repo. Ad esempio:

recipes-bsp/u-boot/u-boot_%25.bbappend
 7# add a new defconfig and device-tree
 8SRC_URI += " \
 9    file://configs/custom_defconfig;subdir=git \
10    file://arch/arm/dts/custom_board_v1.dts;subdir=git \
11    " 
12
13# the defconfig must reference the new device-tree
14UBOOT_MACHINE = "custom_defconfig"

Inoltre, TFA richiede il binario di uboot senza devicetree (u-boot-nodtb.bin) e il binario del solo devicetree (u-boot.dtb), per poterli includere nel FIP. La ricetta originale di uboot prevede delle variabili per copiare determinati binari, però lo trovo tutto molto arzigogolato quindi rimpiazziamo completamente le funzioni di install e deploy e dichiariamo direttamente quello che ci interessa ottenere come risultato da questo recipe:

recipes-bsp/u-boot/u-boot_%25.bbappend
 1do_install(){
 2    install -d ${D}/boot/
 3    install -m 644 ${B}/u-boot-nodtb.bin ${D}/boot/
 4    install -m 644 ${B}/u-boot.dtb       ${D}/boot/
 5}
 6
 7do_deploy(){
 8    # install -D -m 644 ${B}/u-boot-nodtb.bin ${DEPLOYDIR}/
 9    # install -D -m 644 ${B}/u-boot.dtb       ${DEPLOYDIR}/ 
10}

Et voilà.