Bei Zephyr werden Devices bereits zur Kompilierzeit aus dem Device-Tree, der Konfiguration und den Sourcen erstellt. Daher ist die Kenntnis des Build-Vorganges essentiell, wenn man strukturiert Devices anbinden und zudem in der Lage sein möchte, Fehler selber diagnostizieren zu können. Dieser Beitrag führt die hierbei wichtigen Zusammenhänge näher aus.
Devicetree Build Flow unter Zephyr.
(Bild: Zephyr Project Documentation)
Richtig bekannt wurde der Device-Tree zusammen mit Linux. Als es damals darum ging die HardwarebeschreibAung vom Betriebssystemcode auf nicht Intel-kompatiblen Systemen zu trennen, wurde die Open-Firmware-Implementierung auf ARM portiert und als Device-Tree weiter entwickelt. Dabei handelt es sich um eine vom Betriebssystem getrennte Beschreibung des Systems, auf dem der Linux-Kernel laufen soll. Angefangen von der CPU, dem Memory, den Bussystemen bis hin zu den daran angeschlossenen Devices ist alles enthalten, was zur Laufzeit nicht vom Betriebssystem erkundet werden kann. Diese Datenstruktur existiert die gesamte Laufzeit und wird von Treibern auch zur Instanziierung benötigt.
Aus dieser Sichtweise heraus mag der Ansatz von Zephyr überraschend wirken. Bei Zephyr steht der Device-Tree zur Laufzeit nicht zur Verfügung, er wird auch gar nicht beim Booten und bei der Instanziierung von Treibern benötigt. Er hat seine Rolle während der Kompilierzeit! Wobei er selber nicht einmal mit dem dtc zu einem BLOB kompiliert wird so wie wir das von Linux her kennen.
Der Device-Tree unter Zephyr
Bei Zephyr werden aus dem Device-Tree beim Kompillieren des Sourcecodes die fertigen Devices innerhalb des einen Zephyr-Binaries erstellt. Gegenüber dem Ansatz von Linux bringt das zwei wesentliche Vorteile: Ein wesentlich geringerer Overhead zur Laufzeit und vor allem der Umstand, dass dafür keine dynamische Speicherallokationen benötigt werden.
Dabei ist es nicht so, dass es nur einen einzigen Device-Tree gibt. Der Device-Tree wird aus mehreren Quellen heraus erstellt:
Device-Tree aus dem Board-Support, welcher wiederum den
Architektur-Support verwendet;
Device-Tree-Overlays aus dem Projektverzeichnis (Nicht zu verwechseln mit den Device-Tree-Overlays bei Linux, welche zur Laufzeit typischerweise durch den Bootloader angefügt werden); und
Device-Tree-Bindings (*.yaml-Dateien) dienen der Prüfung auf Korrektheit der erzeugten Nodes.
Beispiel für ein Device-Tree-Overlay
Bild 1: Verarbeitung vom Device-Tree.
(Bild: Andreas Klinger)
Im Projektverzeichnis befindet sich im Unterverzeichnis boards der Device-Tree-Overlay mit dem Dateinamen olimex_stm32_e407.overlay für das GNSS-Modem des Beispielprojektes:
Das Modem wird dabei als Subnode des seriellen Controllers mit dem Label usart6 des Device-Trees aus dem Board-Support erstellt. Falls der usart6 mit status = "disabled" angelegt wurde, muss er auf "okay" gesetzt werden.
In einem ersten Konfigurationsschritt wird der Device-Tree (*.dts-Datei) für das gewählte Board mit dem C-Preprozessor verarbeitet. Device-Tree-Includes (*.dtsi-Dateien) und Headerfiles werden inkludiert. Dem daraus entstehenden Device-Tree wird dann noch ein Device-Tree-Overlay mit Anpassungen für das eigene Projekt (*.overlay-Datei) übergelegt. Daraus entsteht die vorkompilierte Device-Tree-Datei namens zephyr.dts.pre.
Diese wird mit Python-Skripten weiter zum fertigen Device-Tree (zephyr.dts) sowie aus Performacegründen zu einer binären und serialisierten Darstellung namens edt.pickle verarbeitet. In diesem Schritt wird der Device-Tree auch auf Korrektheit geprüft. Die dafür notwendigen Informationen sind in YAML-Dateien hinterlegt und werden als Device-Tree-Bindings bezeichnet.
Der fertige Devicetree (zephyr.dts) wird im Build-Verzeichnis abgelegt und dient nur einem Check, ob er syntaktisch in Ordnung ist. Mehr passiert mit ihm nicht. Dennoch ist dieser fertige Device-Tree für die Fehleranalyse herzlich willkommen.
Im Folgenden sehen wir den Ausschnitt von oben zusätzlich mit dem Node des referenzierten seriellen Controller im generierten zephyr.dts.
Nun wird aus dem edt.pickle ein Headerfile namens devicetree_generated.h erzeugt. In ihm sind alle im Device-Tree vorhanden Nodes mit ihren Properties sowie Hilfsmakros für die weitere Verarbeitung durch den C-Compiler abgelegt. Nodes deren Status nicht "okay" ist, werden erst gar nicht angelegt und fallen daher schon bei diesem Schritt unter den Tisch.
Die Definitionen aus diesem Headerfile sind in weiteren Compiler-Aufrufen verarbeitbar und werden verwendet um die konkreten Instanzen der Treiber zur Compilierzeit anzulegen. Dies passiert in der Applikation oder auch in Treibern immer dann, wenn direkt oder indirekt das Include <zephyr/devicetree.h> verwendet wird.
Nachfolgend das Device-Tree-Binding aus der Datei dts/bindings/gnss/u-blox,m8.yaml:
description: U-BLOX M8 GNSS Modulecompatible: "u-blox,m8"include: - uart-device.yamlproperties: uart-baudrate: type: int description: | Baudrate for communication on the UART port. default: 115200 enum: [4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]
Ändert man obiges Overlay so, dass es nicht mehr der YAML-Definition entspricht, und startet man den Erstellvorgang erneut wird unmittelbar sichtbar, wie der Build mit einem Fehler abbricht.
Konfiguration
Bild 2: Verarbeitung der Konfiguration
(Bild: Andreas Klinger)
Bei der Software-Konfiguration gibt es zum einen die vom Zephyr-Projekt und auch -Modulen definierte Konfiguration in den Kconfig-Dateien. Dort ist hinterlegt, welche CONFIG_-Schalter es gibt und von welchen anderen Schaltern diese abhängen. Das ist ganz ähnlich wie beim Linux-Kernel oder vielen anderen Open-Source-Projekten.
Stand: 08.12.2025
Es ist für uns eine Selbstverständlichkeit, dass wir verantwortungsvoll mit Ihren personenbezogenen Daten umgehen. Sofern wir personenbezogene Daten von Ihnen erheben, verarbeiten wir diese unter Beachtung der geltenden Datenschutzvorschriften. Detaillierte Informationen finden Sie in unserer Datenschutzerklärung.
Einwilligung in die Verwendung von Daten zu Werbezwecken
Ich bin damit einverstanden, dass die Vogel Communications Group GmbH & Co. KG, Max-Planckstr. 7-9, 97082 Würzburg einschließlich aller mit ihr im Sinne der §§ 15 ff. AktG verbundenen Unternehmen (im weiteren: Vogel Communications Group) meine E-Mail-Adresse für die Zusendung von redaktionellen Newslettern nutzt. Auflistungen der jeweils zugehörigen Unternehmen können hier abgerufen werden.
Der Newsletterinhalt erstreckt sich dabei auf Produkte und Dienstleistungen aller zuvor genannten Unternehmen, darunter beispielsweise Fachzeitschriften und Fachbücher, Veranstaltungen und Messen sowie veranstaltungsbezogene Produkte und Dienstleistungen, Print- und Digital-Mediaangebote und Services wie weitere (redaktionelle) Newsletter, Gewinnspiele, Lead-Kampagnen, Marktforschung im Online- und Offline-Bereich, fachspezifische Webportale und E-Learning-Angebote. Wenn auch meine persönliche Telefonnummer erhoben wurde, darf diese für die Unterbreitung von Angeboten der vorgenannten Produkte und Dienstleistungen der vorgenannten Unternehmen und Marktforschung genutzt werden.
Meine Einwilligung umfasst zudem die Verarbeitung meiner E-Mail-Adresse und Telefonnummer für den Datenabgleich zu Marketingzwecken mit ausgewählten Werbepartnern wie z.B. LinkedIN, Google und Meta. Hierfür darf die Vogel Communications Group die genannten Daten gehasht an Werbepartner übermitteln, die diese Daten dann nutzen, um feststellen zu können, ob ich ebenfalls Mitglied auf den besagten Werbepartnerportalen bin. Die Vogel Communications Group nutzt diese Funktion zu Zwecken des Retargeting (Upselling, Crossselling und Kundenbindung), der Generierung von sog. Lookalike Audiences zur Neukundengewinnung und als Ausschlussgrundlage für laufende Werbekampagnen. Weitere Informationen kann ich dem Abschnitt „Datenabgleich zu Marketingzwecken“ in der Datenschutzerklärung entnehmen.
Falls ich im Internet auf Portalen der Vogel Communications Group einschließlich deren mit ihr im Sinne der §§ 15 ff. AktG verbundenen Unternehmen geschützte Inhalte abrufe, muss ich mich mit weiteren Daten für den Zugang zu diesen Inhalten registrieren. Im Gegenzug für diesen gebührenlosen Zugang zu redaktionellen Inhalten dürfen meine Daten im Sinne dieser Einwilligung für die hier genannten Zwecke verwendet werden. Dies gilt nicht für den Datenabgleich zu Marketingzwecken.
Recht auf Widerruf
Mir ist bewusst, dass ich diese Einwilligung jederzeit für die Zukunft widerrufen kann. Durch meinen Widerruf wird die Rechtmäßigkeit der aufgrund meiner Einwilligung bis zum Widerruf erfolgten Verarbeitung nicht berührt. Um meinen Widerruf zu erklären, kann ich als eine Möglichkeit das unter https://contact.vogel.de abrufbare Kontaktformular nutzen. Sofern ich einzelne von mir abonnierte Newsletter nicht mehr erhalten möchte, kann ich darüber hinaus auch den am Ende eines Newsletters eingebundenen Abmeldelink anklicken. Weitere Informationen zu meinem Widerrufsrecht und dessen Ausübung sowie zu den Folgen meines Widerrufs finde ich in der Datenschutzerklärung, Abschnitt Redaktionelle Newsletter.
Das besondere an Zephyr ist, dass auch Schalter verwendet werden können, welche aus dem vorhergehenden Schritt der Generierung vom Device-Tree erstellt wurden. Dazu ist das verarbeitende Python-Skript (kconfig.py) in der Lage, aus dem edt.pickle heraus Informationen des Device-Trees abzufragen.
So kann geprüft werden, ob im Device-Tree ein Device überhaupt vorhanden ist. Nur dann wird auch die zugehörige Konfiguration angezeigt und später erstellt. Das bedeutet: Wenn man in der Zephyr-Konfiguration einen Treiber nicht angezeigt bekommt, hat man diesen ggf. im Device-Tree gar nicht oder unter falschem Compatible angelegt. Die Vorgehensweise bei der Entwicklung muss also immer so sein, dass man zuerst den Device-Tree anlegt und dann die Konfiguration durchführt.
In den Kconfig-Dateien sind diese Abhängigkeiten vom Device-Tree an den Config-Schaltern mit dem Namen DT_HAS_<compatible-upper>_ENABLED zu erkennen. Dabei ist "<compatible-upper>" durch das Compatible des Treibers in Grossbuchstaben und "-" und "," durch "_" ersetzt zu verwenden. So wird beispielsweise aus compatible = "u-blox,m8" im Devicetree der Schalter DT_HAS_U_BLOX_M8_ENABLED.
Hier der Ausschnitt aus der Kconfig.u_blox_m8:
config GNSS_U_BLOX_M8 bool "U-BLOX M8 GNSS Module" default y depends on GNSS depends on DT_HAS_U_BLOX_M8_ENABLED depends on GNSS_REFERENCE_FRAME_WGS84 select MODEM_MODULES select MODEM_BACKEND_UART select MODEM_CHAT select MODEM_UBX select GNSS_PARSE select GNSS_NMEA0183 select GNSS_NMEA0183_MATCH select GNSS_U_BLOX_PROTOCOL select UART_USE_RUNTIME_CONFIGURE help Enable U-BLOX M8 GNSS modem driver.
Wie man sieht, ist dieser Config-Schalter vom Vorhandensein des compatible im Device-Tree abhängig.
Bei der Generierung der verwendeten Konfiguration wird nun die Konfiguration des Boards (<board>_defconfig) verwendet und mit der Projekt-Konfiguration (prj.conf) überlagert. Allein die Auswahl des Boards bewirkt eine Selektion des SoC bzw. der Architektur. Mittels Kconfig.defconfig-Dateien werden dadurch eine ganze Menge an Default-Schaltern für den betreffenden SOC eingeschaltet. Dies ist bei Mainline-Boards schon vorhanden und kann einfach verwendet werden.
Am Ende wird die projektspezifische Konfiguration in der Datei prj.conf überlagert und damit die Konfiguration abgeschlossen. Als Ausgabe entsteht die fertige Konfiguration in der Datei .config. Außerdem wird ein Headerfile namens autoconf.h mit Präprozessor-Defines für alle Config-Schalter erzeugt. Dieses wird bei der weiteren Erstellung des Systems verwendet.
Erstellvorgang von Devices
Bild 3: Erstellung der Applikation mit Devices.
(Bild: Andreas Klinger)
Nun werden Devices aus dem Treiber-Code heraus erstellt. Wenn jetzt die Konfiguration so ist, dass ein Treibermodul erstellt werden soll, ist es dem Präprozessor überlassen dieses zu generieren.
Am Ende des Treibercodes finden wir ein Makro DT_INST_FOREACH_STATUS_OKAY(). Dieses prüft, ob das Compatible, welches im Makro DT_DRV_COMPAT (im gleichen C-Modul) definiert ist, als Device-Tree-Node mit status = "okay" mindestens einmal existiert. Wenn ja, wird für jede vorhandene Instanz das übergebene Makros aufgelöst und damit die Datenstrukturen inklusive ihrer Einsprungfunktionen zur Compilierzeit angelegt.
Für diese Instanz wird eine struct device __device_dts_ord_<NN> angelegt. Genau diese Instanz wird dann wiederum im C-Code durch den Präprozessor eingesetzt, wo das betreffende struct device von einem Makro DEVICE_DT_GET() zugewiesen wird.
In der Applikation erfolgt die Referenzierung beispielsweise mit:
Das Makro DT_NODELABEL() holt den Device-Tree-Node mit der Labelkennung "gnss"; DEVICE_DT_GET() macht daraus das fertig verwendbare Device.
Kompilliert man mit:
west build [...] -DCONFIG_COMPILER_SAVE_TEMPS=y
werden die vom Präprozessor generierten C-Files nicht gelöscht und können zur Diagnose verwendet werden. Diese Dateien heissen *.c.i und existieren für die Dateien des Projektes (z. B. main.c.i) sowie für die Treiber (z. B. gnss_u_blox_m8.c.i). Gerade für die Fehlersuche kann dies sehr nützlich sein.
Die obige C-Zeile ist in der vom Präprozessor generierten Datei main.c.i zu finden:
Und genau nach dem automatisch vergebenen Device-Namen __device_dts_ord_102 im Beispiel kann man in den Precompilierten Treiber-Sourcen suchen und gelangt dadurch zur eigentlichen Treiberinstanz:
Für die Fehlersuche ein ganz wichtiger Zusammenhang. Wenn beispielsweise beim Bauen die Fehlermeldung entsteht, dass ein __device_dts_ord_XX nicht gefunden wird, sollte man prüfen, ob für die im Device-Tree verwendeten Compatibles auch entsprechende Treiber konfiguriert und mit dem Präprozessor erstellt wurden.
Auf den ersten Blick erscheint dies ziemlich viel Präprozesser-Magie zu sein. Bei genauerer Betrachtung des Makros DT_INST_FOREACH_STATUS_OKAY() zusammen mit einem beispielhaft generierten devicetree_generated.h sowie der devicetree.h aus den Includes im Repository werden die Zusammenhänge aber schnell logisch und nachvollziehbar.
Workflow für die Anbindung von Devices
Beim Einbinden von Devices in Zephyr empfiehlt sich folgender Workflow:
1. Einbinden des Devices in den Device-Tree: Dazu sollte man das Device-Tree-Overlay für das verwendete Board im Unterverzeichnis boards des Projektordners erstellen und den Node entsprechend der Device-Tree-Bindings (YAML-Files im Verzeichnis zephyr/dts/bindings/) anlegen. Ist das Device Teilnehmer eines Bussystems (z. B. I2C, SPI) ist darauf zu achten, dass auch das übergeordnete Bussystem mit status = "okay" angelegt wurde.
2. Konfiguration des Projektes im Menuconfig: Der Treiber muss in der Konfiguration eingeschaltet werden; sollte er gar nicht sichtbar sein empfiehlt es sich, alle Konfigurationsschalter (auch die nicht erreichbaren) sichtbar zu machen (Taste <a>) und mittels der eingebauten Hilfe (Taste <?>) nach unerfüllten Abhängigkeiten (depends) zu suchen und aufzulösen. Gefundene Config-Schalter müssen in die Projekt-Konfiguration prj.conf eingetragen werden.
3. Im C-Code das Device referenzieren: Mit dem Makro DEVICE_DT_GET() kann aus einem Device-Tree-Node ein struct device erhalten werden. Der Device-Tree-Node wiederum kann gut mittels Label oder Node-Name erhalten werden.
4a. Wenn beim Erstellen keine Fehler auftreten und das C-Modul kompilliert wurde, kann man davon ausgehen, dass das Device zur Kompillierzeit referenziert werden konnte und softwaretechnisch verfügbar ist.
4b. Sollten beim Erstellen Fehler auftreten, empfiehlt sich zunächst die Überprüfung der Device-Tree-Compatibles auf genaue Schreibweise, Prüfung der erstellten Konfiguration in .config, Prüfung des aus dem Device-Tree generierten C-Codes in devicetree_generated.h und ggf. die Erstellung unter Beibehaltung der temporären Dateien, um die vorverarbeiteten C-Dateien anschauen zu können. Dies geschieht mit dem Übergabeargument -DCONFIG_COMPILER_SAVE_TEMPS=y beim Aufruf von west. (sg)
* Andreas Klinger ist selbständiger Trainer und Entwickler. Als Spezialist für Linux beschäftigt er sich mit dem internen Aufbau des Kernels, den Systemmechanismen sowie vor allem mit deren Einsatz in Embedded Systemen. Contributor zum Linux-Kernel, Zephyr und einigen anderen Open-Source-Projekten.