Se sei uno sviluppatore embedded e hai lavorato su una serie di schede basate su Linux, hai senza dubbio sentito parlare di 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 l’immagine del kernel in memoria e l’esecuzione passa a quest’ultima, a partire dal suo punto di ingresso. Il kernel, a questo punto, come qualsiasi altra applicazione “bare-metal”, deve eseguire alcune operazioni di inizializzazione e configurazione hardware, ad esempio:
- configurazione del processore
- configurazione della memoria virtuale
- configurazione della console
Tutte queste operazioni vengono eseguite scrivendo valori specifici in determinati registri, a seconda del dispositivo da inizializzare e/o configurare. In altre parole, si tratta di operazioni dipendenti dall’hardware: il kernel deve quindi conoscere gli indirizzi dei registri su cui scrivere e quali valori utilizzare, a seconda dell’hardware su cui viene eseguito.
Per rendere il kernel compatibile con una determinata piattaforma hardware, la soluzione più immediata è rappresentata dalle routine di inizializzazione “ad-hoc” contenute nei sorgenti e abilitate da specifici parametri di configurazione, selezionabili in fase di compilazione. Questo percorso è percorribile per tutto ciò che normalmente è “fisso” (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 diverso: la piattaforma ARM
Per la piattaforma ARM le cose si complicano: ogni SoC (System on a Chip), pur condividendo lo stesso processore, può avere registri posizionati a indirizzi diversi e la procedura di inizializzazione può differire leggermente da un SoC all’altro. Inoltre i SoC sono montati su schede che, a loro volta, hanno interfacce e periferiche diverse a seconda del produttore, del modello e anche della specifica revisione.
Il trattamento separato di ciascun hardware disponibile ha comportato un numero eccessivo di file di intestazione, patch specifiche e parametri di configurazione speciali che erano difficili da mantenere per la comunità di sviluppo del kernel. Inoltre, questo approccio hardcoded richiede la ricompilazione del kernel al minimo cambiamento hardware. Questo è particolarmente fastidioso per gli utenti ma soprattutto per chi progetta schede: durante lo sviluppo, dove vengono prodotte numerose revisioni che differiscono per piccoli dettagli, è necessario 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.
Albero dei dispositivi: una definizione
L’albero dei dispositivi è un linguaggio di descrizione hardware che può essere utilizzato per descrivere l’hardware del sistema in una struttura di 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.
Bootstrap “Device Tree Powered”.
In un dispositivo basato su ARM che utilizza l’albero dei dispositivi, il bootloader:
- carica l’immagine del kernel e il DTB in memoria
- carica l’indirizzo del DTB nel registro R2
- salta al punto di ingresso del kernel
Compilazione del BLOB dell’albero dei dispositivi
Per compilare l’albero dei dispositivi, utilizzare il compilatore dell’albero dei dispositivi. I sorgenti dell’albero dei dispositivi possono essere trovati insieme al kernel
scripts/dtc
oppure scaricabile separatamente:
git clone git://git.kernel.org/pub/scm/utils/dtc/dtc.git
Dopo aver compilato il compilatore dell’albero dei dispositivi, possiamo compilare l’albero dei dispositivi:
dtc -O dtb -o /path/to/my-tree.dtb /path/to/my-tree.dts
dove:
- my-tree.dtb è il nome Device Tree Blob generato
- my-tree.dts è la descrizione dell’hardware
Le descrizioni di diverse schede basate su ARM sono già presenti nei sorgenti del kernel. I file dell’albero dei dispositivi corrispondenti si trovano in:
arch/arm/boot/dts
Qui si distinguono 2 tipi di file:
- File .dts per le definizioni hardware a livello di scheda
- File .dtsi inclusi (e condivisi) da più file .dts e che generalmente contengono definizioni di livello SoC
Il Makefile in arch/arm/boot/dts/Makefile
elenca quali BLOB della struttura dei dispositivi devono essere compilati durante l’esecuzione del comando make per creare l’immagine del kernel.
Sintassi dell’albero dei dispositivi
Illustriamo un breve esempio sulla sintassi dell’albero dei dispositivi. Il frammento di codice seguente contiene una descrizione di un controller UART:
arch/arm/boot/dts/imx28.dtsi
auart0: [email protected] {
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 di seguito descritte:
auart0: [email protected]
: [email protected]compatible
una stringa che consente al kernel di identificare il driver di dispositivo in grado di gestire il dispositivoreg
Indirizzo di base e dimensione dell’area contenente i registri dei dispositiviinterrupts
Numero di interruzionedmas
edma-names
Descrizione del DMA con canali e nomi DMAclocks
riferimento (phandle) all’orologio utilizzato dal dispositivostatus
lo stato del dispositivo (non abilitato)
Vedremo più avanti perché il dispositivo in esame risulta essere disabilitato.
Nel codice del kernel, possiamo vedere come il valore associato alla compatible
proprietà consenta al kernel stesso di associare il driver di dispositivo 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 compatible
proprietà assume il valore “fsl, imx28-auart”, ovvero il primo dei valori presenti nell’elenco presente nell’albero dei dispositivi. Questa corrispondenza permette l’associazione del dispositivo con il driver.
Meccanismi di inclusione e sovrapposizione
Come accennato nei paragrafi precedenti, i file .dtsi contengono descrizioni hardware a livello di SoC e, come tali, sono comuni a più schede. Nei file .dts possiamo includere i file .dtsi con la sintassi:
#include "common.dtsi"
esattamente come nel caso del preprocessore del linguaggio C. L’inclusione avviene per sovrapposizione, ovvero:
- le coppie chiave-valore presenti nel .dts si sommano a quelle presenti nel .dtsi
- se la coppia chiave-valore da aggiungere è già presente, il valore del file include (il .dts, nel nostro esempio) viene sovrapposto a quello già presente.
Gli overlay servono quindi per abilitare l’hardware descritto ma normalmente disabilitato (come visto nell’esempio precedente, nel paragrafo relativo alla sintassi). Come esempio di inclusione, considera i seguenti frammenti:
arch/arm/boot/dts/am33xx.dtsi
uart0: [email protected] {
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"
...
Compreso am335x-bone-common.dtsi
dopo am33xx.dts
i, il valore di “stato”, inizialmente impostato su “disabilitato”, viene sovrapposto al valore di “okay”, attivando così il dispositivo.
Alternative all’uso dell’albero dei dispositivi
Prima dell’introduzione di Device Tree, l’approccio classico al supporto dell’hardware basato su ARM consisteva, come accennato in precedenza, nella scrittura di codice specifico da includere nel kernel. Per una scheda basata su ARM si trattava di scrivere un cosiddetto “board-file”: un insieme di strutture e funzioni per riconoscere l’hardware come un “dispositivo di piattaforma” connesso al “bus di piattaforma” ( https:/ /lwn.net/Articles/448499/ ), terminato da una “Descrizione MACCHINA” 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 frammento di codice è stato utilizzato per la scheda GTA04 basata su OMAP3. La stessa scheda è ora supportata dal kernel tramite una descrizione hardware che utilizza il Device Tree. Per confrontare le 2 soluzioni si può fare riferimento ai seguenti link:
- File della scheda GTA04-omap3
- File dell’albero del dispositivo GTA04-omap3
Pro e contro dell’albero dei dispositivi
I vantaggi dell’utilizzo dell’albero dei dispositivi sono:
- Maggiore semplicità nel modificare la configurazione del sistema, senza la necessità di ricompilare il kernel.
- Maggiore semplicità nell’aggiunta del supporto per hardware che presenta piccole modifiche rispetto ad una versione già supportata (es: nuova revisione della scheda).
- Riutilizzo del codice preesistente, a livello di device driver (che può essere scritto in maniera più generica, relegando nell’albero dei dispositivi le differenze specifiche tra hardware simile) e a livello di file dell’albero dei dispositivi (grazie ai meccanismi di inclusione e overlay ).
- Maggior possibilità di ottenere supporto dalla comunità del kernel per l’inclusione nella mainline del supporto per il nuovo hardware.
- Possibilità di fornire una descrizione dell’hardware più facilmente leggibile e con nomi più descrittivi (utile per chi ha la necessità di sviluppare applicazioni su scheda e deve conoscere i dettagli hardware).
A contrastare questi (numerosi) vantaggi, tuttavia, c’è una documentazione ancora incompleta o carente per alcune parti della sintassi dell’albero dei dispositivi. Ad oggi, l’approccio migliore da seguire per scrivere un nuovo file .dts è partire da uno preesistente e indubbiamente funzionante e introdurre delle modifiche seguendo un approccio per tentativi.
Contenuto ispirato a: https://www.develer.com/en/linux-kernel-the-device-tree/