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:

https://d23s79tivgl8me.cloudfront.net/user/163185/process%20flow_89493.png

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!

https://d23s79tivgl8me.cloudfront.net/user/163185/devicetree_generated_header_32575.png

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

Popular posts from this blog

How Android System Services Connect Apps and HAL: A Deep Dive

AOSP Passthrough HAL: Architecture, Use Cases & Performance Guide

Getting Started with AOSP: Build Custom Android Solutions