Se siete sviluppatori embedded e avete lavorato su qualche board Linux-based, sicuramente avrete sentito parlare del Device Tree. In questo post introdurremo la suddetta tecnologia e il suo utilizzo con il kernel Linux.
Preambolo – Come tutto ha inizio
Durante la fase di boot, il bootloader carica in memoria l’immagine del kernel e l’esecuzione passa a quest’ultimo, a partire dal suo entry-point. Il kernel, a questo punto, come ogni altra applicazione “bare-metal”, necessita di compiere alcune operazioni di inizializzazione e configurazione dell’hardware, ad esempio:
- configurazione del processore
- setup della memoria virtuale
- setup della console
Tutte queste operazioni sono eseguite scrivendo appositi valori in determinati registri, a seconda del device da inizializzare e/o configurare. In altre parole, si tratta di operazioni hardware-dependent: il kernel deve quindi conoscere gli indirizzi dei registri sui quali scrivere e quali valori utilizzare, a seconda dell’hardware sul quale viene eseguito.
Per poter rendere il kernel compatibile con una data piattaforma hardware, la soluzione più immediata è rappresentata da delle routine di inizializzazione “ad-hoc” contenute nei sorgenti ed abilitate da specifici parametri di configurazione, selezionabili a tempo di compilazione. Questa strada è percorribile per tutto ciò che è normalmente “fissato” (o meglio ancora standardizzato), come i registri interni di un processore x86, o l’accesso alle periferiche di un PC tramite i servizi offerti dal BIOS.
Un caso differente: la piattaforma ARM
Per la piattaforma ARM, le cose si complicano: ciascun SoC (System on a Chip), pur condividendo lo stesso processore, può avere registri posizionati ad indirizzi diversi, e la procedura di inizializzazione può differire leggermente da un SoC all’altro.
Inoltre, i SoC sono montati su delle board che presentano, a loro volta, differenti interfacce e periferiche a seconda del produttore, del modello e addirittura della specifica revisione.
Trattare separatamente ciascun hardware disponibile ha portato ad
ottenere un numero eccessivo di header files, patches specifiche e parametri speciali di configurazione difficili da mantenere per la comunità di sviluppo del kernel.
Inoltre, questo approccio hard-coded richiede la ricompilazione del kernel ad ogni minima variazione dell’hardware. Questo è particolamente fastidioso per gli utenti ma soprattutto per chi progetta le board: in fase di sviluppo, dove si producono numerose revisioni che differiscono per piccoli dettagli, si è costretti a modificare e ricompilare ogni volta, indipendentemente dall’entità
della modifica.
La comunità di sviluppo ha quindi proposto un’alternativa migliore: l’utilizzo del Device Tree.
Device Tree – una definizione
Il Device Tree è un Hardware Description Language utilizzabile per descrivere l’hardware di sistema in una struttura dati ad albero. In questa struttura, ogni nodo dell’albero descrive un dispositivo. Il codice sorgente del Device Tree viene compilato dal Device Tree Compiler (DTC) per formare il Device Tree Blob (DTB), leggibile dal kernel all’avvio.
“Device Tree Powered” Bootstrap
In un dispositivo basato su ARM che utilizza il Device Tree, il bootloader:
- carica l’immagine del kernel e il DTB in memoria
- carica nel registro R2 l’indirizzo del DTB
- salta all’entry point del kernel
Compilazione del Device Tree Blob
Per compilare il Device Tree occorre usare il Device Tree Compiler. I sorgenti del Device Tree si possono trovare insieme ai sorgenti del kernel in:
scripts/dtc
oppure si possono scaricare separatamente:
git clone git://git.kernel.org/pub/scm/utils/dtc/dtc.git
Dopo aver compilato il Device Tree Compiler, possiamo compilare il Device Tree:
dtc -O dtb -o /path/to/my-tree.dtb /path/to/my-tree.dts
dove:
- my-tree.dtb è il nome Device Tree Blob che viene generato
- my-tree.dts è la descrizione dell’hardware
Nei sorgenti del kernel sono già presenti le descrizioni di numerose board basate su ARM. I device tree files corrispondenti si trovano in:
arch/arm/boot/dts
Qui si distinguono 2 tipi di file:
- file .dts per le definizioni hardware a livello di board
- file .dtsi che vengono inclusi (e condivisi) da più file .dts e che
contengono, generalmente, definizioni a livello di SoC
Il Makefile in arch/arm/boot/dts/Makefile
elenca quali Device Tree Blob devono essere costruiti quando si esegue il comando make per costruire l’immagine del kernel.
Sintassi del Device Tree
Illustriamo un breve esempio sulla sintassi del Device Tree. Il seguente snippet contiene la descrizione di un controller UART:
arch/arm/boot/dts/imx28.dtsi
auart0: serial@8006a000 {
compatible = "fsl,imx28-auart", "fsl,imx23-auart";
reg = <0x8006a000 0x2000>;
interrupts = <112>;
dmas = <&dma_apbx 8>, <&dma_apbx 9>;
dma-names = "rx", "tx";
clocks = <&clks 45>;
status = "disabled";
};
In particolare, le singole voci hanno la sintassi e la semantica descritte di seguito:
auart0: serial@8006a000
: nome_nodo@addresscompatible
stringa che consente al kernel di identificare il device driver capace di gestire il dispositivoreg
Indirizzo base e dimensione dell’area contenente i registri del dispositivointerrupts
Interrupt numberdmas
edma-names
Descrizione del DMA con canali DMA e nomiclocks
riferimento (phandle) al clock utilizzato dal dispositivostatus
stato del dispositivo (non abilitata)
Vedremo successivamente perchè il dispositivo in esame risulta essere
disabilitato.
Nel codice del kernel, possiamo osservare come il valore associato alla proprietà compatible
consenta al kernel stesso di associare il device driver corretto a questo dispositivo.
drivers/tty/serial/mxs-auart.c
static const struct of_device_id mxs_auart_dt_ids[] = {
{
.compatible = "fsl,imx28-auart",
.data = &mxs_auart_devtype[IMX28_AUART]
}, {
.compatible = "fsl,imx23-auart",
.data = &mxs_auart_devtype[IMX23_AUART]
}, { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mxs_auart_dt_ids);
static struct platform_driver mxs_auart_driver = {
.probe = mxs_auart_probe,
.remove = mxs_auart_remove,
.driver = {
.name = "mxs-auart",
.of_match_table = mxs_auart_dt_ids,
},
};
La proprietà compatible
assume il valore “fsl,imx28-auart”, cioè il primo dei valori nella lista presente nel device tree. Questo matching consente l’associazione del device con il driver.
Meccanismo di inclusione e overlay
Come accennato nei paragrafi precedenti, i file .dtsi contengono descrizioni hardware a livello di SoC, quindi comuni a più board. Nei file .dts possiamo includere i file .dtsi con la sintassi:
#include "common.dtsi"
esattamente come avviene per il preprocessore del linguaggio C. L’inclusione avviene per sovrapposizione (overlay), e cioè:
- le coppie chiave-valore presenti nel .dts vengono aggiunte a quelle presenti nel .dtsi
- se la coppia chiave-valore da aggiungere è già presente, il valore proveniente dal file che include (il .dts, nel nostro esempio) viene sovrapposto a quello già presente.
Gli overlay permettono quindi di abilitare hardware descritto ma normalmente disabilitato (come visto nell’esempio precedente, nel paragrafo relativo alla sintassi).
Come esempio di inclusione si considerino i seguenti snippet:
arch/arm/boot/dts/am33xx.dtsi
uart0: serial@44e09000 {
compatible = "ti,omap3-uart";
ti,hwmods = "uart1";
clock-frequency = <48000000>;
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
dmas = <&edma 26>, <&edma 27>;
dma-names = "tx", "rx";
};
arch/arm/boot/dts/am335x-bone-common.dtsi
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
status = "okay";
};
Entrambi questi file sono inclusi in:
arch/arm/boot/dts/am335x-bone.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
...
Includendo am335x-bone-common.dtsi
dopo am33xx.dtsi
, il valore di “status”, inizialmente posto a “disabled” viene sovrapposto con il valore “okay”, attivando la periferica.
Alternative all’utilizzo del Device Tree
Prima dell’introduzione del Device Tree, l’approccio classico per supportare un hardware basato su ARM consisteva, come accennato sopra, nella scrittura di codice specifico da includere nel kernel.
Per una board basata su ARM, si trattava di scrivere un cosiddetto “board-file”:
un insieme di strutture e funzioni per far riconoscere l’hardware come “platform device” connesso al “platform bus” (https://lwn.net/Articles/448499/), terminate da una “MACHINE description” come la seguente:
MACHINE_START(GTA04, "GTA04")
/* Maintainer: Nikolaus Schaller - http://www.gta04.org */
.atag_offset = 0x100,
.reserve = omap_reserve,
.map_io = omap3_map_io,
.init_irq = omap3_init_irq,
.handle_irq = omap3_intc_handle_irq,
.init_early = omap3_init_early,
.init_machine = gta04_init,
.init_late = omap3630_init_late,
.timer = &omap3_secure_timer,
.restart = omap_prcm_restart,
MACHINE_END
Questo snippet di codice veniva utilizzato per la board GTA04 basata su OMAP3.
La stessa board è adesso supportata dal kernel mediante una descrizione hardware che utilizza il Device Tree.
Per confrontare le 2 soluzioni, si può fare riferimento ai seguenti link:
Vantaggi e svantaggi del Device Tree
I vantaggi nell’utilizzo del Device Tree sono:
- Maggiore semplicità nel cambiare la configurazione del sistema, senza la necessità di ricompilare il kernel.
- Maggiore semplicità nell’aggiungere supporto per un hardware che presenta piccole modifiche rispetto ad una versione già supportata (es: nuova revisione di una board).
- Riutilizzo del codice preesistente, a livello di device driver (che possono essere scritti in modo più generico, relegando al device tree file le differenze specifiche fra hardware simili) e a livello di device tree file (grazie ai meccanismi di inclusione e overlay).
- Maggiori possibilità di ottenere supporto dalla kernel community per l’inclusione in mainline del supporto ad un nuovo hardware.
- Possibilità di fornire una descrizione dell’hardware più facilmente leggibile e con nomi più descrittivi (utile per chi deve sviluppare applicativi su una board e deve conoscerne i dettagli hardware).
A questi (numerosi) vantaggi, si contrappone, però, una documentazione ancora incompleta o carente per alcune parti della sintassi del device tree. Ad oggi, l’approccio migliore da seguire per scrivere un nuovo file .dts è partire da uno preesistente e sicuramente funzionante, e introdurre modifiche seguendo un approccio trial and error.