Defining Hardware Capabilities: Devicetree Bindings
We discovered in a previous blog
post how the hardware on the device could be described by a devicetree in an
embedded software application based on The Zephyr Project. We saw an example of
how the devicetree can be used to describe the four LEDs found on a nRF52840
development kit
(https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk). We
discovered that a complete board can be created by combining several devicetree
files. In order to comprehend how to reference elements in the devicetree, we
lastly went over some source code. We will discover how The Zephyr Project uses
the devicetree in this blog post.
Zephyr Is
Not Linux!
One of the
main points of the previous blog post was that, despite the fact that the
devicetree concept is derived from Linux, Zephyr's application is very
different. As part of the boot process in Linux, the kernel reads the
devicetree in binary form from somewhere in RAM and, based on the devices that
are present and enabled, calls the relevant driver functions. But in Zephyr,
the devicetree is used to create header files that are used with the Zephyr
kernel and drivers, as well as the source code that makes up the finished
application. Therefore, Zephyr uses the devicetree during compile-time, while
the Linux kernel uses it during run-time. Zephyr's build infrastructure
incorporates particular mechanisms to interface with the devicetree as a result
of this distinction.
Devicetree Bindings
“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 conventional devicetree files
covered in the last blog post are the "Devicetree sources." The
contents of the devicetree, including the data types of the nodes, are
described by the "Devicetree bindings." The source files and bindings
are finally combined into a C header file by the Zephyr build infrastructure.
The "devicetree.h" header file, which is utilized by the application
and Zephyr source files, abstracts the contents of the generated header file.
Through
The Looking Glass
An outstanding illustration of how
devicetree bindings function in Zephyr is "custom_dts_binding" under
samples/basic. To navigate devicetree bindings, we surprisingly only need to
pay attention to the following three lines in the sample's main.c:
#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
The C source files in Zephyr and the applications that use
it ultimately use devicetree.h, as seen in the above image. The generated
header files are included close to the top of the devicetree.h file when we
open it under zephyr_main/zephyr/include/zephyr/:
#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):
Conclusion
This blog post demonstrated how the
application source could use the generated devicetree header files in
conjunction with a few header files from The Zephyr Project's core repository
to identify the existence of particular nodes during compilation. We observed
how the Linux kernel, which makes use of this data during run-time, is
different from this. To gain insight into this process, we traced the expansion
of certain clever macros using a sample from the Zephyr repository. To verify the
anticipated outcomes, we then disabled the node's existence in the devicetree
header file that was generated. We will examine in more detail how the Zephyr
build infrastructure integrates the devicetree sources and bindings to create
the generated devicetree header files in a subsequent blog post.
Understanding and implementing devicetree bindings is crucial for efficient
hardware configuration in embedded systems. At Silicon Signals, we specialize
in optimizing your hardware and software integration with Zephyr-based
projects. Whether you're tackling devicetree bindings or enhancing your embedded
system's performance, our team is equipped to support you through the
development cycle.
👉 Contact Us Today to get started and
take your embedded solutions to the next level with expert hardware design and
custom software development!

Comments
Post a Comment