Qu'est-ce qu'un Device Tree ?

August 7, 2026 · visiteurs

Si vous avez déjà essayé de faire tourner Linux sur une carte embarquée, vous finissez tôt ou tard par tomber sur un fichier en .dts. En général au pire moment : la carte ne démarre pas, rien ne sort à l'écran, et quelqu'un vous dit d'« aller voir votre device tree ». Cet article explique ce qu'est ce fichier, pourquoi il existe et ce que vous y cherchez réellement.

Le Problème Qu'il Résout

Sur un ordinateur de bureau, le noyau retrouve le matériel pratiquement tout seul. PCI et USB sont des bus formellement spécifiés et énumérables (1). Les périphériques s'annoncent avec un identifiant de fabricant et de produit ; un pilote liste dans une table les identifiants qu'il gère, et le noyau appelle la fonction probe() de ce pilote pour chaque périphérique correspondant à une entrée (2). Côté USB, le mécanisme fonctionne de la même façon avec sa propre table d'identifiants (3). Personne n'a besoin de rien annoncer à l'avance.

Les cartes embarquées n'ont pas ce luxe. La documentation du noyau pose la distinction sans détour : les périphériques intégrés à un system-on-chip sont raccordés à des bus dotés d'une infrastructure minimale, « par opposition aux grands bus formellement spécifiés comme PCI ou USB », et ce que ces périphériques ont en commun, c'est un adressage direct depuis un bus CPU (1). Un capteur I²C ou un contrôleur d'écran branché en SPI ne s'annonce pas ; il est simplement là, à cette adresse, câblé sur cette ligne d'interruption. La seule façon pour le noyau de le savoir, c'est que quelqu'un le lui dise.

Pendant longtemps, ce « quelqu'un » fut le code lui-même. Chaque carte avait un fichier dans l'arbre du noyau décrivant en C quel périphérique était câblé où — le noyau documente toujours ce chemin, où du code d'initialisation spécifique à la carte enregistre les périphériques un par un (1). Sur ARM, ces fichiers se sont multipliés au point qu'en 2011 Linus Torvalds a démonté la situation dans un e-mail, jugeant que le code ARM allait droit dans le mur. La sortie de crise adoptée fut le device tree.

L'Idée

Un device tree est un fichier de données décrivant le câblage du matériel. Ce n'est pas du code — il ne s'exécute pas et n'est pas compilé dans le noyau. C'est une description : il y a un contrôleur à cette adresse, il utilise cette ligne d'interruption, il est alimenté par cette horloge, et ces périphériques sont accrochés en dessous.

Le format lui-même repose sur une spécification formelle (4). On l'appelle arbre parce que c'en est vraiment un. Il part d'un nœud racine, les bus s'y ramifient, et les périphériques rattachés à ces bus s'y ramifient à leur tour — reflétant la façon dont le matériel est physiquement connecté.

Plutôt qu'un exemple inventé, en voici un réel. Le nœud ci-dessous provient de k3-am62p-main.dtsi, dans l'arbre U-Boot de la famille J722S/AM67A de TI — la description que lit effectivement une carte AM67A au démarrage :

main_i2c0: i2c@20000000 {
    compatible = "ti,am64-i2c", "ti,omap4-i2c";
    reg = <0x00 0x20000000 0x00 0x100>;
    interrupts = <GIC_SPI 161 IRQ_TYPE_LEVEL_HIGH>;
    #address-cells = <1>;
    #size-cells = <0>;
    power-domains = <&k3_pds 102 TI_SCI_PD_EXCLUSIVE>;
    clocks = <&k3_clks 102 2>;
    clock-names = "fck";
    status = "disabled";
};

Chaque ligne ici veut dire quelque chose. reg donne l'adresse du contrôleur dans la carte mémoire — 0x20000000, sur une longueur de 0x100. Les adresses s'écrivent par paires de nombres parce que l'espace d'adressage est en 64 bits ; le premier nombre porte les 32 bits de poids fort. interrupts indique quelle ligne d'interruption est utilisée ; des noms comme GIC_SPI et IRQ_TYPE_LEVEL_HIGH viennent d'en-têtes inclus — donc même si la source d'un device tree n'est que de la donnée, elle passe tout de même par le préprocesseur C.

Il vaut la peine de remarquer que compatible porte deux valeurs : "ti,am64-i2c" et "ti,omap4-i2c". La liste se lit du plus spécifique au plus général. Le noyau cherche un pilote propre à am64 et, à défaut, retombe sur le pilote omap4, plus ancien et plus générique (5). Une puce récente peut ainsi fonctionner sans que personne ait écrit un pilote pour elle.

clocks et clock-names indiquent d'où le périphérique tire son horloge : la sortie 2 du périphérique 102 de &k3_clks, que le pilote retrouvera sous le nom "fck". power-domains raccorde le domaine d'alimentation selon la même logique. Ce ne sont pas des réglages de fréquence — ils décrivent le câblage, quelle horloge va où.

La dernière ligne compte : status = "disabled". Le fondeur décrit le périphérique mais le laisse éteint, car il ne peut pas savoir si ce bus I²C est utilisé sur votre carte. L'allumer relève du .dts de la carte. Dans le fichier de carte du J722S EVM, cela donne :

&main_i2c0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&main_i2c0_pins_default>;
    clock-frequency = <400000>;

    exp1: gpio@23 {
        compatible = "ti,tca6424";
        reg = <0x23>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

Le &main_i2c0 en tête est une référence au nœud précédent — il ne crée pas un nouveau nœud, il écrit par-dessus l'existant. status est activé, les broches sont assignées, la vitesse est fixée à 400 kHz, et un vrai périphérique est ajouté en dessous : un expandeur GPIO TCA6424 à l'adresse 0x23 sur le bus I²C. Comme le contrôleur a déclaré #address-cells = <1>, cette adresse s'écrit avec un seul nombre, contrairement au reg du contrôleur lui-même.

Voilà comment fonctionne un device tree en pratique : le fondeur décrit le SoC une fois, et vous dites ce qui est câblé sur votre carte.

La propriété qui compte vraiment, c'est compatible. C'est là que le noyau regarde pour apparier un pilote et un morceau de matériel. Un pilote dit « je suis compatible avec ti,tca6424 », un nœud du device tree porte la même chaîne, et le noyau réunit les deux. D'où des fautes de frappe qui échouent en silence : si la chaîne ne correspond pas, il n'y a pas d'erreur — le périphérique n'apparaît tout simplement jamais.

Un point à garder en tête : un device tree décrit le matériel, pas la configuration d'un pilote. Le guide d'écriture des bindings du noyau insiste particulièrement sur cette frontière (6). La ligne se brouille constamment en pratique, et c'est le sujet le plus disputé sur les listes du noyau — « est-ce vraiment une propriété du matériel, ou est-ce votre préférence ? »

Les Fichiers

Vous verrez trois extensions, et il est facile de les confondre (11).

.dts est la source que vous écrivez, la forme lisible par un humain. .dtsi est un fragment que d'autres fichiers peuvent inclure — le « i » est pour include. Un fondeur décrit typiquement tout le SoC dans un .dtsi ; vous écrivez un court .dts pour votre carte, vous l'incluez et vous n'ajoutez que ce qui lui est propre.

.dtb est la forme compilée — le device tree blob. C'est ce que le noyau lit réellement. Le compilateur est dtc (7) :

dtc -I dts -O dtb -o my-board.dtb my-board.dts

Il fonctionne aussi dans l'autre sens, ce qui est précieux quand on traque un problème. Si vous avez un .dtb qui marche, vous pouvez l'ouvrir et regarder dedans :

dtc -I dtb -O dts -o decode.dts my-board.dtb

Sur un système en marche, vous pouvez aussi lire le device tree directement depuis le système de fichiers. Le noyau y expose l'arbre qu'il utilise réellement :

ls /proc/device-tree/

C'est la réponse la plus rapide à « le fichier que j'ai modifié est-il bien celui qui a été chargé ? » Étonnamment souvent, la réponse est non.

Ce Qui Se Passe au Démarrage

L'ordre est le suivant : le bootloader — le plus souvent U-Boot (8) — charge le .dtb en mémoire, charge le noyau, puis lui passe l'adresse mémoire du blob dans un registre au moment de le lancer. Sur ARM, ce registre et les conditions que doit remplir le bootloader sont fixés dans le document de démarrage du noyau (9). Avant de démarrer le moindre pilote, le noyau lit cet arbre, en extrait les périphériques et apparie les pilotes selon les chaînes compatible.

La conséquence pratique, c'est que le noyau et le device tree peuvent être mis à jour séparément. Ajouter un capteur impose rarement de recompiler le noyau ; mettre à jour le .dtb suffit généralement. Mais cela coupe dans les deux sens — étant indépendants, ils peuvent diverger. Démarrez un noyau récent avec un vieux .dtb et la carte se lève silencieusement incomplète, et il faut un moment pour comprendre pourquoi.

Il y a aussi la notion d'overlay : des fragments appliqués sur un arbre existant à l'exécution. Les overlays sont des morceaux que l'on superpose à l'arbre principal (10). C'est ce qui se produit quand vous branchez un HAT sur un Raspberry Pi — l'arbre principal reste tel quel et un petit fragment vient se poser par-dessus. Utile quand la carte de base est toujours la même et que seul le module enfiché change.

Quand Quelque Chose Ne Marche Pas

Les défauts de device tree ont une désagréable particularité : ils ne produisent en général aucune erreur. Le périphérique est simplement absent. Procéder dans cet ordre fait gagner du temps.

Commencez par vérifier si le périphérique a seulement été créé. Le nœud est-il présent sous /proc/device-tree ? Sinon, le problème n'est pas dans le fichier que vous avez modifié — il y a de fortes chances que le mauvais .dtb soit chargé, ou que le nœud soit éteint un niveau plus haut par status = "disabled". Ce second cas arrive sans arrêt : les périphériques arrivent le plus souvent désactivés dans les .dtsi du fondeur, et c'est à vous de les allumer un par un avec status = "okay" dans le .dts de votre carte.

Si le nœud est là mais qu'aucun pilote ne s'y est accroché, l'œil doit se porter sur la chaîne compatible. Trouvez la table of_device_id dans la source du pilote et comparez les chaînes caractère par caractère. Une virgule, un tiret suffisent.

Et puis ceci : les avertissements de dtc arrivent désactivés dans la plupart des projets, et ils disent des choses réellement utiles. Une incohérence entre l'adresse et reg, un #address-cells manquant — exactement le genre de défaut qui se transforme en panne silencieuse au démarrage. Cela vaut la peine de garder les avertissements activés à la compilation.

Pour Finir

Ce qui rend un device tree difficile à saisir, c'est que personne ne dit d'emblée ce que c'est réellement. Cela ressemble à un fichier de configuration, mais ça n'en est pas un ; cela ressemble à un pilote, mais ça n'en est pas un non plus. Le cadrage le plus utile est d'y voir le contrat entre le matériel et le logiciel : la façon dont la carte est câblée, mise par écrit dans une langue que le noyau comprend.

Une fois posé dans ce cadre, le reste suit. Pourquoi compatible est si critique, pourquoi le .dtb vit séparément du noyau, pourquoi /proc/device-tree est le premier endroit où regarder quand quelque chose manque — tout découle de la même idée.

À Lire Aussi

Références

  1. Platform Devices and Drivers — pourquoi les périphériques d'un SoC ne sont pas énumérables comme PCI et USB, et comment le code propre à la carte les enregistre.
  2. How To Write Linux PCI Drivers — la table pci_device_id et l'appel à probe() pour les périphériques correspondants.
  3. USB Device Drivers — l'appariement des pilotes par identifiant côté USB.
  4. Devicetree Specification — la définition formelle du format.
  5. Linux and the Devicetree — comment le noyau lit l'arbre et apparie les pilotes.
  6. Writing Devicetree Bindings — la frontière entre décrire le matériel et configurer un pilote.
  7. dtc — Device Tree Compiler — le dépôt du compilateur et son utilisation.
  8. U-Boot: Devicetree Control — comment U-Boot traite le device tree.
  9. Booting ARM Linux — quel registre le bootloader utilise pour passer le blob au noyau.
  10. Devicetree Overlay Notes — comment les overlays sont appliqués à l'exécution.
  11. Device Tree Reference (eLinux) — référence communautaire avec exemples commentés.

Commentaires