Devicetree sous Zephyr : overlays, bindings et lecture des erreurs de build
Le devicetree de Zephyr ressemble à celui de Linux et se comporte différemment sur le point le plus important : sous Linux, il est chargé à l'exécution par le noyau ; sous Zephyr, il est entièrement résolu à la compilation. Il ne reste rien du devicetree dans le binaire, seulement des constantes. Cette différence explique la quasi-totalité des surprises rencontrées au début.
La chaîne de résolution
Quand vous lancez west build, la séquence est la suivante.
Le fichier <board>.dts de la carte est lu, avec les .dtsi qu'il inclut, décrivant le SoC. Les overlays viennent ensuite s'appliquer par-dessus, dans un ordre précis. L'ensemble est aplati en un seul arbre, validé contre les bindings, puis transformé en devicetree_generated.h, un fichier de macros C.
Ce fichier vaut le coup d'être ouvert au moins une fois, dans build/zephyr/include/generated/zephyr/devicetree_generated.h. On y voit exactement ce que le compilateur verra, et beaucoup de questions se règlent en le lisant plutôt qu'en devinant.
La règle de découverte des overlays est la source d'erreur la plus classique, et elle n'est pas cumulative. Le système cherche dans cet ordre :
socs/<SOC>_<qualificatifs>.overlayboards/<carte>.overlayboards/<carte>_<révision>.overlay<carte>.overlayapp.overlay
Dès qu'un ou plusieurs fichiers sont trouvés à une étape, la recherche s'arrête. C'est le point que tout le monde comprend de travers : votre app.overlay sera purement ignoré si un boards/<carte>.overlay existe. Il n'est pas fusionné, il n'est pas écrasé, il n'est jamais lu, et aucun avertissement ne le signale.
Les variables CMake échappent à cette règle. DTC_OVERLAY_FILE remplace la liste découverte, et EXTRA_DTC_OVERLAY_FILE s'ajoute après, donc avec une précédence supérieure.
En cas de doute, build/zephyr/zephyr.dts contient l'arbre final après fusion. C'est la seule source de vérité.
Les bindings, la partie que Linux n'impose pas de la même manière
Un binding est un fichier YAML qui décrit quelles propriétés un compatible accepte, leur type, et lesquelles sont obligatoires. Sans binding correspondant, un nœud est silencieusement ignoré : il ne génère aucune macro, et votre DEVICE_DT_GET échoue à la compilation avec un message qui ne mentionne pas le devicetree.
C'est le premier réflexe à acquérir : si un nœud semble inexistant alors qu'il est bien dans le .dts, cherchez le binding avant tout le reste.
Un binding minimal ressemble à ceci :
description: Capteur de température maison
compatible: "acme,temp-sensor"
include: [sensor-device.yaml, i2c-device.yaml]
properties:
sample-rate-hz:
type: int
required: true
enable-gpios:
type: phandle-array
Les bindings vivent dans dts/bindings/ de votre application ou d'un module. Le nom du fichier n'a aucune importance, c'est la valeur de compatible qui fait le lien.
Les macros, et le piège du statut
Chaque nœud activé produit un jeu de macros. Les plus utilisées :
#define MY_SENSOR DT_NODELABEL(temp0)
static const struct device *dev = DEVICE_DT_GET(MY_SENSOR);
static const uint32_t rate = DT_PROP(MY_SENSOR, sample_rate_hz);
static const struct gpio_dt_spec en = GPIO_DT_SPEC_GET(MY_SENSOR, enable_gpios);
Notez la transformation du nom de propriété : sample-rate-hz en devicetree devient sample_rate_hz en C. Les tirets deviennent des soulignés, tout passe en minuscules.
Le piège le plus courant concerne status. Un nœud dont le statut n'est pas "okay" existe dans l'arbre mais ne produit pas de macro exploitable. La vérification se fait ainsi :
#if !DT_NODE_HAS_STATUS(DT_NODELABEL(temp0), okay)
#error "temp0 est absent ou desactive dans le devicetree"
#endif
Ce #error explicite vaut infiniment mieux que le message de compilation par défaut, qui parle d'un identifiant introuvable sans jamais nommer le devicetree.
Écrire un overlay qui fonctionne
Pour ajouter un périphérique sur un bus existant, on cible le nœud du bus par son label et on lui ajoute un enfant :
&i2c1 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
temp0: temp-sensor@48 {
compatible = "acme,temp-sensor";
reg = <0x48>;
sample-rate-hz = <10>;
enable-gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>;
status = "okay";
};
};
Trois points qui font échouer les débutants. L'adresse après l'arobase doit correspondre exactement à la première valeur de reg, sinon la validation échoue. Le bus parent doit lui aussi être en status = "okay" : activer l'enfant seul ne suffit pas. Et les macros comme GPIO_ACTIVE_HIGH exigent l'inclusion des en-têtes correspondants en tête d'overlay :
#include <zephyr/dt-bindings/gpio/gpio.h>
Lire les erreurs
| Message | Cause réelle |
|---|---|
'DT_N_S_...' undeclared | Le nœud n'existe pas, ou son statut n'est pas okay |
no bindings found for ... | Aucun fichier YAML ne déclare ce compatible |
'reg' is marked as required | Le binding exige reg, absent du nœud |
'unit-address' does not match 'reg' | L'adresse après l'arobase diverge de reg |
dtc: Warning: unit_address_vs_reg | Même cause, en avertissement seulement |
La première ligne concentre la majorité des cas, et elle est trompeuse : le compilateur signale un identifiant C inconnu, jamais un problème de devicetree. Le réflexe correct est d'ouvrir build/zephyr/zephyr.dts et de vérifier que le nœud y figure avec le bon statut.
Les outils qui font gagner du temps
west build -t boards liste les cibles disponibles. Plus utile au quotidien, la cible de visualisation :
west build -t initlevels # ordre d'initialisation des devices
Et surtout, la lecture directe de l'arbre final. Face à un comportement inattendu, build/zephyr/zephyr.dts répond à la question « qu'est-ce que le système a réellement compris de ma configuration », ce que ni le .dts de la carte ni l'overlay ne peuvent dire séparément.
Références
- Zephyr Project, Devicetree HOWTOs, règle d'arrêt de la découverte des overlays et variables
DTC_OVERLAY_FILE - Zephyr Project, Devicetree bindings
- Zephyr Project, Devicetree API, macros
DT_ - devicetree.org, Specification, syntaxe de base
Pour aller plus loin
Ces mécanismes deviennent naturels une fois qu'on a écrit un binding, ajouté un périphérique sur une carte réelle, et débogué un nœud qui refusait d'apparaître. C'est ce que couvrent notre formation à la programmation avec Zephyr RTOS et notre cours sur West, le SDK et Kconfig.