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:

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:

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:

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:

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:

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è:

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:

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.