Getting Started With Zephyr: Devicetree Bindings
In a previous blog post, https://www.embeddedrelated.com/showarticle/1547.php, we learned how a devicetree in an embedded software application based on The Zephyr Project could describe the hardware on the device. We saw an example of how the four LEDs that are present on an nRF52840 development kit (https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk) can be described in the devicetree. We learned how multiple devicetree files can be combined to form a complete board. Finally, we reviewed some source code to understand how to reference elements in the devicetree. In this blog post, we will learn how the devicetree is used in The Zephyr Project.
Zephyr Is Not Linux!
One of the key points made in the previous blog post is that although the concept of the devicetree is borrowed from Linux, Zephyr’s use is quite different. In Linux, the kernel reads the devicetree in binary form located somewhere in RAM as part of the boot sequence and calls the appropriate driver functions depending on the devices that are present and enabled. However, in Zephyr, the devicetree is used to generate header files that are used in conjunction with the source code that make up the final application, along with the Zephyr kernel and drivers. Thus, whereas the Linux kernel uses the devicetree during run-time, Zephyr uses the devicetree during compile-time. Because of this distinction, Zephyr’s build infrastructure adds specific mechanisms to interface with the devicetree.
“Devicetree bindings” are the basis of Zephyr’s mechanism to allow the C portion of the application to reference the devicetree source file. The following graphic from The Zephyr Project’s documentation (https://docs.zephyrproject.org/latest/build/dts/intro-scope-purpose.html) demonstrates this mechanism:
The “Devicetree sources” are the traditional devicetree files discussed in the previous blog post. The “Devicetree bindings” describe the devicetree's contents, including the nodes' data types. Ultimately, the Zephyr build infrastructure combines the source files and bindings into a C header file. The contents of the generated header file are abstracted into the “devicetree.h” header file used by the source files in Zephyr and the application.
Through The Looking Glass
“custom_dts_binding” under samples/basic is an excellent example to help us understand how devicetree bindings work in Zephyr. Surprisingly, we only need to focus on the following three lines in main.c of the sample to navigate devicetree bindings:
#if !DT_NODE_EXISTS(DT_NODELABEL(load_switch)) #error “Overlay for power output node not properly defined.” #endif
First, we’ll need to build the application using the following invocation (assuming “zephyr_main” is where we cloned the latest west manifest):
$> west build -p always -b nucleo_l073rz zephyr_main/zephyr/samples/basic/custom_dts_binding
As shown in the image above, devicetree.h is ultimately what is used by the C source files in Zephyr and the applications that use it. If we open the devicetree.h under zephyr_main/zephyr/include/zephyr/, we see the inclusion of the generated header files near the top:
#ifndef DEVICETREE_H #define DEVICETREE_H #include <devicetree_generated.h> . . . #endif /* DEVICETREE_H */
Where is “devicetree_generated.h”? It’s not in the Zephyr repository but in the build directory!
If we return to the relevant lines in main.c and search for the “DT_NODELABEL” in devicetree.h, we find the following definition:
#define DT_NODELABEL(label) DT_CAT(DT_N_NODELABEL_, label)
If we further search for DT_CAT, we find the following definition:
#define DT_CAT(a1, a2) a1 ## a2
These two macros will convert “DT_NODELABEL(load_switch)” from main.c into “DT_N_NODELABEL_load_switch”. If we search for DT_NODE_EXISTS, we find the following definition:
#define DT_NODE_EXISTS(node_id) IS_ENABLED(DT_CAT(node_id, _EXISTS))
The IS_ENABLED macro is defined using the following clever macros in the util_macro.h and util_internal.h header files under zephyr_main/zephyr/include/zephyr/sys:
#define IS_ENABLED(config_macro) Z_IS_ENABLED1(config_macro) #define Z_IS_ENABLED1(config_macro) Z_IS_ENABLED2(_XXXX##config_macro) #define _XXXX1 _YYYY, #define Z_IS_ENABLED2(one_or_two_args) Z_IS_ENABLED3(one_or_two_args 1, 0) #define Z_IS_ENABLED3(ignore_this, val, ...) val
If we work through the macros using “DT_N_NODELABEL_load_switch,” the first one expands to the following:
IS_ENABLED(DT_N_NODELABEL_load_switch) --> IS_ENABLED(DT_N_S_load_switch_EXISTS)
Where is “DT_N_NODELABEL_load_switch_EXISTS” defined? It’s (ultimately) in devicetree_generated.h!
#define DT_N_S_load_switch_EXISTS 1 . . . #define DT_N_NODELABEL_load_switch DT_N_S_load_switch
The second macro will cause “DT_N_NODELABEL_load_switch” to expand to “DT_N_S_load_switch” and the first macro will result in the expansion to “DT_N_S_load_switch_EXISTS”! If we step through the expansion of the series starting with “IS_ENABLED” macros, we see the following (and remembering that DT_N_S_load_switch_EXISTS expands to “1”):
IS_ENABLED(DT_N_S_load_switch_EXISTS) --> Z_IS_ENABLED1(1) Z_IS_ENABLED1(1) --> Z_IS_ENABLED2(_XXXX1)
Now, since “_XXX1” expands to “_YYY,” (paying close attention to the comma), Z_IS_ENABLED2 expands to “Z_IS_ENABLED3(_YYY, 1, 0)”. Finally, the last macro expands to:
Z_IS_ENABLED3(_YYY, 1, 0, ...) --> 1
And there we have it! Let’s say the DT_N_S_load_switch_EXISTS macro was set to “0” instead. Then we wouldn’t be able to leverage the macro which expands “_XXX1” to “_YYY,” and rather, we’d have the following chain of macros:
IS_ENABLED(DT_N_S_load_switch_EXISTS) --> Z_IS_ENABLED1(0) Z_IS_ENABLED1(0) --> Z_IS_ENABLED2(_XXX0) Z_IS_ENABLED2(_XXX0) --> Z_IS_ENABLED3(_XXX0 1, 0)>p>And the lack of the comma in Z_IS_ENABLED3 will result in that macro expanding to 0 (since the macro is extracting the value after the first comma):
Z_IS_ENABLED3(_XXX0 1, 0, ...) --> 0
And ultimately, the original macro in main.c will result in a compilation error. You can try and see for yourself. Change the DT_N_S_load_switch_EXISTS macro in devicetree_generated.h from a “1” to a “0” and rebuild the application using the following invocation (take note that we’re not performing a pristine build since that will regenerate the header file):
In this blog post, we saw how the generated devicetree header files combined with a few header files in the core repository of The Zephyr Project could be leveraged in the application source to determine the presence of specific nodes during compilation. We saw how this differs from the Linux kernel, which uses this information during run-time. We used a sample from the Zephyr repository to trace the expansion of specific clever macros to get insight into this process. Then, we disabled the node's existence in the generated devicetree header file to confirm the expected consequences. In a future blog post, we will take a closer look at how the Zephyr build infrastructure combines the devicetree bindings and sources to formulate the generated devicetree header files.
To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.
Please login (on the right) if you already have an account on this platform.
Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: