EmbeddedRelated.com
Blogs

Getting Started With Zephyr: Devicetrees

Mohammed BillooJuly 18, 20232 comments

In the last blog post, (Getting Started With Zephyr: Kconfig), we saw how the "Kconfig" infrastructure can be used to enable and disable specific Zephyr subsystems. Specifically, we observed the three main elements of the Kconfig infrastructure. First, we saw how the Kconfig infrastructure is formulated and an example of a Kconfig file to enable the LED subsystem. Second, we saw how specific Zephyr subsystems can be enabled or disabled using a GUI-based interface of Kconfig when using the Nordic VS Code Extension. Finally, we saw a mechanism to customize Zephyr that can be incorporated into CI/CD-based systems.

Quick Links

Besides customizing an RTOS to enable or disable specific subsystems, a standard step when developing embedded software is to add support for communication buses and peripherals. As the previous blog post mentioned, other MCU vendors incorporate customization capabilities into their IDE, including enabling communication buses and setting their parameters. On the other hand, Zephyr borrows another construct from the Linux Kernel (remember that Kconfig originates from the Linux kernel as well), which is the "Devicetree." Devicetree, abbreviated DT, is a data structure with its own language for describing hardware. Devicetrees became popular in embedded software as ARM-based System-on-Chips (SoCs) and System-on-Modules (SoMs) running embedded Linux became popular. When Linux was initially developed on Intel-based CPUs, bus designs that supported automatically detecting the hardware that was connected to the CPU were used. Thus, there was no need to describe the hardware beforehand. However, when ARM exploded onto the scene, new communication protocols and buses were introduced that didn't have any detection mechanism. Thus, a tool was needed in the Linux kernel to identify these buses and the connected devices. However, it is essential to note that while Zephyr borrowed the concept of the devicetree from the Linux kernel, there are sufficient nuanced differences in Zephyr's use of the devicetree.

This article is available in PDF format for easy printing

BASIC SYNTAX

One of the most important things to remember about the devicetree, which is usually a source of anxiety for embedded software engineers from a non-Linux background, is that it is not magical or mysterious. It is simply a file with a loosely defined structure. The file consists of nodes, each containing a particular set of definitions. A node can be referenced by and include other nodes. For example, the following snippet describes the LEDs present on a Nordic nRF52840 development kit (https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk):

 leds {
        compatible = "gpio-leds";
        led0: led_0 {
            gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
            label = "Green LED 0";
        };
        led1: led_1 {
            gpios = <&gpio0 14 GPIO_ACTIVE_LOW>;
            label = "Green LED 1";
        };
        led2: led_2 {
            gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
            label = "Green LED 2";
        };
        led3: led_3 {
            gpios = <&gpio0 16 GPIO_ACTIVE_LOW>;
            label = "Green LED 3";
        };
};

The "leds" node describes the four LEDs onboard the development kit.

As an aside, you will recognize the "compatible" property above if you have a Linux background. You may immediately jump to the conclusion (as I initially did) that Zephyr "finds" a device driver with a corresponding "compatible" value and makes calls to the functions specified in the device driver to control the GPIOs. This is incorrect! Zephyr has its own mechanisms for associating entries in the devicetree with source code called "bindings," which we will cover in more detail in the next blog post. For now, it is essential to remember that Zephyr's use of devicetrees is sufficiently different from Linux.

Returning to our example, the four LEDs referenced in the "leds" node also reference nodes themselves! For example, the "led0" node specifies the exact GPIO that is used to control that particular LED in the following line:

 led0: led_0 {
        gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
        label = “Green LED 0”;
}

In the above example, GPIO0_13 controls LED0 on the board, an active low GPIO. This node consists of the following elements:

  • "led0" is the "node label", and is used to reference the Devicetree node.
  • “led_0” is the node's "full name." Typically, a node's full name consists of the node name and a unit address (for example, "my-node@12345678"), but a node without an address is acceptable as well (sometimes an address doesn't make sense, as is the case here).
  • ·“gpios” is an example of a "property" of the "led0" node. A property is ultimately used by source code to control hardware in some manner. In this instance, the "gpios" property defines a GPIO port, pin, and the active state. The device driver uses this information to control the GPIO.
  • ·“label” is also a property of the "led0" node. Specifically, the label property can retrieve a more descriptive name for a node. In this example, "Green LED 0" can reference the node instead of "led0".

DEVICETREE LOCATION AND HIERARCHY

In the above section, we saw an example of a devicetree node for the LEDs on the Nordic nRF52840 development kit. It can be helpful to browse different devicetrees in the Zephyr repository (we will see why in a future blog post). The devicetrees are in two central locations in the Zephyr repository. The devicetrees for the multitude of boards supported by Zephyr are located under the "boards" directory in the respective processor architecture directory. For example, the location of the top-level devicetree for the nRF52840 development kit is located under "boards/arm/nrf52840dk_nrf52840":

The "top-level" devicetree file always has a ".dts" extension, and intermediate devicetree files, which are referenced by the top-level devicetree, have a ".dtsi" extension. Intermediate devicetree files can include other intermediate devicetree files. In the above example, if we open "nrf52840dk_nrf52840.dts", which is the top-level devicetree file for the nRF52840 development kit, we see the node that corresponds to the LEDs on the board!



If we scroll to the top of this file, we see the following statements:

/dts-v1/;
#include <nordic/nrf52840_qiaa.dtsi>
#include "nrf52840dk_nrf52840-pinctrl.dtsi"

The first line is meant to instruct the parser of the devicetree schema version. The second and third lines include intermediate devicetree files. If you're familiar with C (and C++) programming, you can see that the format is the same as including header files, with a "#include" preprocessor macro. The second include file, "nrf52840dk_nrf52840-pinctrl.dtsi," is in the same directory as the top-level directory:

The first include file is a variant of the nRF52840 SoC itself and is located under the dts/arm/nordic directory of the Zephyr repository:

If we open "nrf52840_qiaa.dtsi", we see that there isn’t much there:

#include <mem.h>
#include <nordic/nrf52840.dtsi>
&flash0 {
    reg = <0x00000000 DT_SIZE_K(1024)>;
};
&sram0 {
    reg = <0x20000000 DT_SIZE_K(256)>;
};
/ {
    soc {
        compatible = "nordic,nRF52840-QIAA", "nordic,nRF52840", "nordic,nRF52", "simple-bus";
    };
};

For the most part, it simply references the "nrf52840.dtsi" intermediate devicetree file, which is located in the same directory. If we open "nrf52840.dtsi", we see that it describes the Nordic nRF52840 SoC in significant detail:

We see above that this file describes the details of the CPU inside the SoC, and we can tell that it’s an ARM Cortex-M4 running at 32MHz, which matches the datasheet of the SoC!

USING THE DEVICETREE IN SOURCE CODE

Now that we understand the structure of the devicetree and its hierarchy in describing hardware, we can see how to use it in source code. Let's take the relevant portions of the "blinky" example in Zephyr (which can be found in the samples/basic/blink/src directory):

Line 14 extracts the "led0" node from the devicetree. Line 20 retrieves the "gpios" property of the "led0" node from the devicetree and stores it in a data structure. The "led" data structure is then used to control the GPIO in the remainder of the main function. First, on line 26, we check whether the specified GPIO port is ready. If it is, then on line 30, we configure the GPIO pin to be an output. Finally, we toggle the GPIO pin on line 36 in the main while loop.We can see that the GPIO functions used above are appended with "_dt." This is because these functions' arguments are data structures retrieved from the devicetree, so the appropriate functions must be called.

CONCLUSION

In this blog post, we learned how the "Devicetree" can be used to add support for custom hardware in our Zephyr-based firmware. A "blinky" example taught us how LEDs can be described on a board and how the corresponding GPIOs can be specified. We also learned about the location of devicetree files in the Zephyr repository and how their organization allows for boards to be described, starting with the CPU itself. One of the most significant advantages when using devicetree files in Zephyr, as opposed to other mechanisms offered by MCU vendors in their IDEs, is that we can leverage device drivers written by others. Typically, in different MCU-specific environments and IDEs, we need to pull in libraries or write driver code from scratch. Instead, with Zephyr's use of devicetrees and the device driver model, we can leverage work done by others. We can focus on writing the business logic of our final application. In the next blog post, we will take a peak under the hood of Zephyr to understand how it uses devicetrees and how it differs from the Linux kernel.


[ - ]
Comment by fjrg76July 27, 2023

Why didn't use the cleanest and most concise YAML format for the device tree?

[ - ]
Comment by mabJuly 28, 2023

Good question! The YAML format is used for devicetree bindings in Zephyr, which we'll discuss in more detail in an upcoming blog post (coming real soon ;-)).

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: