Practical Zephyr - Devicetree basics (Part 3)

In the previous article, we configured software using the kernel configuration tool Kconfig, and we’ve silently assumed that there’s a UART interface on our board that is configurable and used for logging.

In this third article of the “Practical Zephyr” series, we’ll see how we configure and use hardware. For this, Zephyr borrows another tool from the Linux kernel: Devicetree.

In contrast to Kconfig, the Devicetree syntax and its use are more intricate. Therefore, we’ll cover Devicetree in two articles. In this article, we’ll see what Devicetree is and how we can write our own Devicetree source files. In the next article, we’ll look at so-called Devicetree bindings, which add semantics to our Devicetree. Be prepared for a fair bit of theory, but as usual, we’ll use an example project to follow along.

Disclaimer: This article covers Devicetree in detail and from first principles. If you’re already familiar with Devicetree or don’t want to know how it works in detail, this article may not be for you. Instead, consider tuning in for the next article!

Prerequisites

This article is part of an article series. If you haven’t read the previous articles, please have a look. The first article explains the required installation steps, and we’ll follow along with our running UART example from the previous article. If you’re already familiar with Zephyr and have a working installation at hand, it should be easy enough for you to follow along with your own setup without reading the previous articles.

Note: A full example application including all the Devicetree source files that we’ll see throughout this article is available in the 02_devicetree_basics folder of the accompanying GitHub repository.

We’ll again be using the development kit for the nRF52840 as a reference and explore its files in Zephyr, but you can follow along with any target - even virtual ones.

A short heads-up: We will not define our own custom board, and we will not describe memory layouts. Those are more advanced topics and go beyond this series. With this article, we want to have a detailed look and familiarize ourselves with Devicetree and the syntax of its input files.

What’s a Devicetree?

Let’s first deal with the terminology: In simple words, the Devicetree is a tree data structure you provide to describe your hardware. Each node describes one device, e.g., the UART peripheral that we used for logging via printk in the previous article. Except for the root note, each node has exactly one parent, thus the term devicetree.

Devicetree files use their own Devicetree Source (DTS) format, defined in the official Devicetree specification. For certain file types (bindings) used by Devicetree, Zephyr uses yet another file format - but fear not, it simply replaces the DTS format with simple .yaml files. We’ll cover bindings in the next article, so we’ll focus on Devicetree source files for now. There are also some subtle differences between the official Devicetree specification and the way it is used in Zephyr, but we’ll touch up on that throughout this article.

The build system takes the Devicetree specification and feeds it to its own compiler, which - in the case of Zephyr - generates C macros that are, in turn, used by Zephyr’s device drivers. For all the readers coming straight from Linux - yes, this approach is a little different than what you’re used to, but we’ll get to that.

The following is a snippet of Nordic’s Devicetree Source Include file of their nRF52840 SoC:

zephyr/dts/arm/nordic/nrf52840.dtsi

#include <arm/armv7-m.dtsi>
#include "nrf_common.dtsi"

/ {
  soc {
    uart0: uart@40002000 {
      compatible = "nordic,nrf-uarte";
      reg = <0x40002000 0x1000>;
      interrupts = <2 NRF_DEFAULT_IRQ_PRIORITY>;
      status = "disabled";
    };
  };
};

One could compare the Devicetree to something like a struct in C or a JSON object. Each node (or object) lists its properties and their values and thus describes the associated device and its configuration. From the above snippet, we can see that there’s a system on chip (soc) node that contains a UART instance with some configuration data.

Personally, I felt the details of the Devicetree specification or Zephyr’s official documentation on Devicetree a bit overwhelming, and I could hardly keep all the information in my head when reading straight through it (getting lost following links again), so in this article, I’m choosing a slightly different approach:

Instead of going into great detail about the DTS (Devicetree Source) format, we’ll start with a simple project, build it, and dive straight into the input and output files used or generated by the build process. Based on those files, one by one, we’ll try and figure out how this whole thing works. Finally, we’ll also create our own nodes, but we won’t fully specify the file format.

In case you’re looking for a more detailed description of the DTS (Devicetree Source) format, other authors provided much more detailed descriptions - much better than anything I could possibly come up with. Here are some of my favorites:

Devicetree and Zephyr

We create a new freestanding application with the files listed below to get started. If you’re not familiar with the required files, the installation, or the build process, have a look at the previous articles or the official documentation. As mentioned in the prerequisites, you should be familiar with creating, building, and running a Zephr application.

$ tree --charset=utf-8 --dirsfirst
.
├── src
│   └── main.c
├── CMakeLists.txt
└── prj.conf

The prj.conf can remain empty for now, and the CMakeLists.txt only includes the necessary boilerplate to create a Zephyr application with a single main.c source file. As an application, we’ll use the same old main function that outputs the string “Message in a bottle.” each time it is called, and thus each time the device starts.

#include <zephyr/kernel.h>
#define SLEEP_TIME_MS 100U

void main(void)
{
    printk("Message in a bottle.\n");
    while (1)
    {
        k_msleep(SLEEP_TIME_MS);
    }
}

As usual, we can now build this application for the development kit of choice, in my case, the nRF52840 Development Kit from Nordic.

$ west build --board nrf52840dk_nrf52840 --build-dir ../build

But wait, we didn’t “do anything Devicetree” yet, did we? That’s right, someone else already did it for us! After our first look into Zephyr and exploring Kconfig, we’re familiar with the build output of our Zephyr application, so let’s have a look! You should find a similar output right at the start of your build:

-- Found Dtc: /opt/nordic/ncs/toolchains/4ef6631da0/bin/dtc (found suitable version "1.6.1", minimum required is "1.4.6")
-- Found BOARD.dts: /opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts
-- Generated zephyr.dts: /path/to/build/zephyr/zephyr.dts
-- Generated devicetree_generated.h: /path/to/build/zephyr/include/generated/devicetree_generated.h
-- Including generated dts.cmake file: /path/to/build/zephyr/dts.cmake

CMake integration

The build output shows us that Devicetree - just like Kconfig - is a central part of any application build. It is added to your build via the CMake module zephyr/cmake/modules/dts.cmake. The first thing that Zephyr is looking for, is the Devicetree compiler dtc (see, zephyr/cmake/modules/FindDtc.cmake). Typically, this tool is in your $PATH after installing Zephyr or loading your Zephyr environment:

$ where dtc
/opt/nordic/ncs/toolchains/4ef6631da0/bin/dtc

Since we didn’t specify any so-called Devicetree overlay file (bear with me for now, we’ll see how to modify our Devicetree using overlay files in a later section), Zephyr then looks for a Devicetree source file that matches the specified board. In my case, that’s the nRF52840 development kit, which is supported in the current Zephyr version: The board and thus its Devicetree is fully described by the file zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts.

Note: If you’re using a custom board not supported by Zephyr, you’d have to provide your own DTS file for it. We won’t go into details about adding support for a custom board in this article (and maybe the next one), but at the end of it, you should have all the knowledge to do that - or to understand any other article showing you how to do it.

Let’s not dive into the file’s contents for now; instead, look at the build. Somehow, the build ends up with a generated zephyr.dts file. A couple of interesting things are happening in this process that are worth mentioning, so we’ll have a closer look.

Note: In this section, we’re really just brushing over the files to get a bit of a feeling for the syntax and how nodes are used. We’ll cover all of this in great detail still!

Devicetree includes and sources in Zephyr

If you’re familiar with the Devicetree specification, you might have wondered why the Devicetree snippet that we’ve seen before uses C/C++ style #include directives:

zephyr/dts/arm/nordic/nrf52840.dtsi

#include <arm/armv7-m.dtsi>
#include "nrf_common.dtsi"

The Devicetree specification introduces a dedicated /include/ compiler directive to include other sources in a DTS file. E.g., our include for nrf_common.dtsi should have the following format:

/include/ "nrf_common.dtsi"

The reason for this discrepancy is that Zephyr uses the C/C++ preprocessor to resolve includes - and for resolving actual C macros that are used within DTS files. This happens in the call to the CMake function zephyr_dt_preprocess in the mentioned Devicetree CMake module.

Let’s have a look at the “include” tree of the Devicetree source file of the nRF52840 development kit used in my build:

nrf52840dk_nrf52840.dts
├── nordic/nrf52840_qiaa.dtsi
│   ├── mem.h
│   └── nordic/nrf52840.dtsi
│       ├── arm/armv7-m.dtsi
│       │   └── skeleton.dtsi
│       └── nrf_common.dtsi
│           ├── zephyr/dt-bindings/adc/adc.h
│           ├── zephyr/dt-bindings/adc/nrf-adc.h
│           │   └── zephyr/dt-bindings/dt-util.h
│           │       └── zephyr/sys/util_macro.h
│           │           └── zephyr/sys/util_internal.h
│           │               └── util_loops.h
│           ├── zephyr/dt-bindings/gpio/gpio.h
│           ├── zephyr/dt-bindings/i2c/i2c.h
│           ├── zephyr/dt-bindings/pinctrl/nrf-pinctrl.h
│           ├── zephyr/dt-bindings/pwm/pwm.h
│           ├── freq.h
│           └── arm/nordic/override.dtsi
└── nrf52840dk_nrf52840-pinctrl.dtsi

Note: You might have noticed the file extensions .dts and .dtsi. Both file extensions are Devicetree source files, but by convention the .dtsi extension is used for DTS files that are intended to be included by other files.

That “include” tree makes a lot of sense, doesn’t it?

  • Since we didn’t specify anything external in an overlay file, our outermost Devicetree source file is our board nrf52840dk_nrf52840.dts.
  • Our board uses the nRF52840 QIAA microcontroller from Nordic, which is described in its own Devicetree include source file nrf52840_qiaa.dtsi.
  • That MCU is, in turn, a variant of the nRF52840, and therefore includes nrf52840.dtsi.
  • The nRF52840 is an ARMv7 core, which is described in armv7-m.dtsi.
  • In addition, the nRF52840 uses Nordic’s peripherals, specified in nrf_common.dtsi.

The included C/C++ header files .h are just a “bonus” since Zephyr uses the preprocessor and we’re therefore allowed to use C/C++ macros within Devicetree source files: All the macros will be replaced by their values before the actual compilation process. E.g., the macro NRF_DEFAULT_IRQ_PRIORITY in interrupts = <2 NRF_DEFAULT_IRQ_PRIORITY>; that we’ve seen before is expanded and thus replaced by its actual value before the Devicetree is compiled.

This type of include graph is very common for Devicetrees in Zephyr: You start with your board, which uses a specific MCU, which has a certain architecture and vendor-specific peripherals.

Each Devicetree source file can reference nodes to add, remove, or modify properties for each included file. E.g., for our console output we can find the following parts (the below snippets are incomplete!) in the Devicetree source file of the development kit:

zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts

/ {
  chosen {
    zephyr,console = &uart0;
  };
};

&uart0 {
  compatible = "nordic,nrf-uarte";
  status = "okay";
  current-speed = <115200>;
  /* other properties */
};

With zephyr,console we seem to be able to tell Zephyr that we want it to use the node uart0 for the console output and therefore printk statements. We’re also modifying the &uart0 reference’s properties, e.g., we set the baud rate to 115200 and enable it by setting its status to “okay”.

The uart0 node is originally defined in the included Devicetree source file of the SoC nrf52840.dtsi, and seems to be disabled by default:

zephyr/dts/arm/nordic/nrf52840.dtsi

/ {
  soc {
    uart0: uart@40002000 {
      compatible = "nordic,nrf-uarte";
      reg = <0x40002000 0x1000>;
      interrupts = <2 NRF_DEFAULT_IRQ_PRIORITY>;
      status = "disabled";
    };
  };
};

But that’s enough Devicetree syntax for now. After this step, we end up with a single zephyr.dts.pre Devicetree (DTS) source file in the build/zephyr output directory. In there, we can also find our uart@40002000 node (with the value of the expanded NRF_DEFAULT_IRQ_PRIORITY macro):

build/zephyr/zephyr.dts.pre

/ {
  soc {
    uart0: uart@40002000 {
      compatible = "nordic,nrf-uarte";
      reg = < 0x40002000 0x1000 >;
      interrupts = < 0x2 0x1 >;
      status = "okay";
      current-speed = < 0x1c200 >;
      pinctrl-0 = < &uart0_default >;
      pinctrl-1 = < &uart0_sleep >;
      pinctrl-names = "default", "sleep";
    };
  };
};

Compiling the Devicetree

At the beginning of this article, we mentioned that Zephyr uses the dtc Devicetree compiler to generate the corresponding source code. That is, however, not entirely true: While the official Devicetree compiler dtc is definitely invoked during the build process, it is not used to generate any source code. Instead, Zephyr feeds the generated build/zephyr/zephyr.dts.pre into its own GEN_DEFINES_SCRIPT Python script, located at zephyr/scripts/dts/gen_defines.py.

Now, why would you want to do that? The Devicetree compiler dtc is typically used to compile Devicetree sources into a binary format called the Devicetree blob dtb. The Linux kernel parses the DTB and uses the information to configure and initialize the hardware components described in the DTB. This allows the kernel to know how to communicate with the hardware without hardcoding this information in the kernel code. Thus, in Linux, the Devicetree is parsed and loaded during runtime and thus can be changed without modifying the application.

Zephyr, however, is designed to run on resource-constrained, embedded systems. It is simply not feasible to load a Devicetree blob during runtime: Any such structure would take up too many resources in both the Zephyr drivers and storing the Devicetree blob. Instead, the Devicetree is resolved during compile time.

Note: In case this article is too slow for you but you still want to know more about Devicetree, there is a brilliant video of the Zephyr Development Summit 2022 by Martí Bolívar on Devicetree.

So, if not a binary, what’s the output of this gen_defines.py generator? Let’s have another peek at the output of our build process:

-- Generated zephyr.dts: /path/to/build/zephyr/zephyr.dts
-- Generated devicetree_generated.h: /path/to/build/zephyr/include/generated/devicetree_generated.h
-- Including generated dts.cmake file: /path/to/build/zephyr/dts.cmake

The build process creates three important output files: The zephyr.dts that has been generated out of the preprocessed zephyr.dts.pre, a devicetree_generated.h header file, and a CMake file dts.cmake.

As promised, the original Devicetree dtc compiler is invoked during the build, and that’s where it comes into play: The zephyr.dts Devicetree source file is fed into dtc, but not to generate any binaries or source code: Instead, it is only used to generate warnings and errors; the output itself is discarded. This helps to reduce the complexity of the Python Devicetree script gen_defines.py and ensures that the generated Devicetree source file used in Zephyr is at least still compatible with the original specification.

The devicetree_generated.h header file replaces the Devicetree blob dtb. It contains macros for “all things Devicetree” and is included by the drivers and our application thereby strips all unnecessary or unused parts. “Macrobatics” is the term that Martí Bolívar used in his talk about the Zephyr Devicetree at the June 2022 developer summit, and it fits. Even for our tiny application, the generated header is over 15000 lines of code! In the next article, we’ll see what these macros look like and how they are used with the Zephyr Devicetree API. If you’re curious, have a look at zephyr/include/zephyr/devicetree.h already. In fact, let’s have a glimpse:

build/zephyr/include/generated/devicetree_generated.h

#define DT_CHOSEN_zephyr_console DT_N_S_soc_S_uart_40002000
// --snip---
#define DT_N_S_soc_S_uart_40002000_P_current_speed 115200
#define DT_N_S_soc_S_uart_40002000_P_status "okay"

Looks cryptic? With just a few hints, this becomes readable. Know that:

  • DT_ is just the common prefix for Devicetree macros,
  • _S_ is a forward slash /,
  • _N_ refers to a node,
  • _P_ is a property.

Thus, e.g., DT_N_S_soc_S_uart_40002000_P_current_speed simply refers to the property current_speed of the node /soc/uart_40002000. In Zephyr, this configuration value is set during compile time. You’ll need to recompile your application in case you want to change this property. The approach in Linux would be different: There, the (UART speed) property is read from the Devicetree blob dtb during runtime. You could change the property, recompile the Devicetree, exchange the Devicetree blob, and wouldn’t need to touch your application or the Kernel at all.

But let’s leave it at that for now, we’ll have a proper look at this later. For now, it is just important to know that we’ll resolve our Devicetree at compile time using lots of generated macros.

Finally, the generated dts.cmake is a file that basically allows to access the entire Devicetree from within CMake, using CMake target properties, e.g., we’ll find the current speed of our UART peripheral also within CMake:

dts.cmake

set_target_properties(
    devicetree_target
    PROPERTIES
    "DT_PROP|/soc/uart@40002000|current-speed" "115200"
)

That’s it for a peek into the Zephyr build. Let’s wrap it up:

  • Zephyr uses a so-called Devicetree to describe hardware.
  • Most of the Devicetree sources are already available within Zephyr, e.g., MCUs and boards.
  • We’ll use Devicetree source files mostly to override or extend existing DTS files.
  • In Zephyr, the Devicetree is resolved at compile time, using macrobatics.

Basic source file syntax

Now we finally take a closer look at the Devicetree syntax and its files. We’ll walk through it by creating our own Devicetree nodes. This section heavily borrows from existing documentation such as the Devicetree specification, Zephyr’s Devicetree docs and the nRF Connect SDK Fundamentals lesson on Devicetree.

Note: We won’t explain all syntactical aspects, but will focus on the most important ones. Have a look at the Devicetree specification for a full syntax description. Also, misplaced semicolons, etc., will also be caught by the Devicetree compiler in the future, so we won’t talk about trivial details.

Node names, unit-addresses, reg, and labels

Let’s start from scratch. We create an empty Devicetree source file .dts with the following empty tree:

/dts-v1/;
/ { /* Empty. */ };

The first line contains the tag /dts-v1/; which identifies the file as a version 1 Devicetree source file. Without this tag, the Devicetree compiler would treat the file as being of the obsolete version 0 - which is incompatible with the current major Devicetree version 1 used by Zephyr. The tag /dts-v1/; is therefore required when working with Zephyr. Following the version tag is an empty Devicetree: Its only node is the root node, identified by convention by a forward slash /.

Note: Devicetree source files use C/C++ style comments. You can use both, multi-line /* ... */ and single-line // comments.

Within this root node, we can now define our own nodes in the form of a tree, kind of like a JSON object or nested C structure:

/dts-v1/;

/ {
  node {
    subnode {
      /* name/value properties */
    };
  };
};

Nodes are identified via their node name. Each node can have subnodes and properties. In the above example, we have a node with the name node, containing a subnode named, well, subnode. For now, all you need to know about properties is that they are name/value pairs.

A node in the Devicetree can be uniquely identified by specifying the full path from the root node, through all subnodes, to the desired node, separated by forward slashes. E.g., our full path to our subnode is /node/subnode.

Node names can also have an optional, hexadecimal unit-address, specified using an @ and thus resulting in the full node name format node-name@unit-address. E.g., we could give our subnode the unit-address 0123ABC as follows:

/dts-v1/;

/ {
  node {
    subnode@0123ABC {
      reg = <0x0123ABC>;
      /* properties */
    };
  };
};

The unit-address can be used to distinguish between several subnodes of the same type. It can be a real register address, typically a base address, e.g., the base address of the register space of a specific UART interface, but also a plain instance number, e.g., when describing a multi-core MCU by using a /cpus node, with two instances cpu@0 and cpu@1 for each CPU core. Each node with a unit-address must also have the property reg - and any node without a unit-address must not have the property reg. It may seem redundant now, but we’ll learn more about the reg property later in this article.

Let’s finish up on the node name with a convention that ensures that each node in the Devicetree can be uniquely identified by specifying its full path. For any node name and property at the same level in the tree:

  • in the case of node-name without an unit-address the node-name must be unique,
  • or if a node has a unit-address, then the full node-name@unit-address must be unique.

Now we can address all of our nodes using their full path. As you might imagine, within a more complex Devicetree the paths become quite long, and if you’d ever need to reference a node (we’ll see how that works in practice later) this is quite tedious, which is why any node can be assigned a unique node label:

/dts-v1/;

/ {
  node {
    subnode_label: subnode@0123ABC {
      reg = <0x0123ABC>;
      /* properties */
    };
  };
};

Now, instead of using /node/subnode@0123ABC to identify the node, we can simply use its label subnode_label - which must be unique throughout the entire Devicetree. This form of the label syntax is known as a node label. This term is not explicitly defined in the Devicetree specification but is used extensively in Zephyr.

That’s it for the first theory lesson - let’s see how nodes look like in a real Devicetree source (include) file in Zephyr. The following is a snippet of the Devicetree source file for the nRF52840 microcontroller:

zephyr/dts/arm/nordic/nrf52840.dtsi

/ {
  soc {
    uart0: uart@40002000 {
      reg = <0x40002000 0x1000>;
    };
    uart1: uart@40028000 {
      reg = <0x40028000 0x1000>;
    };
  };
};

The System-On-Chip is described using the soc node. The soc node contains two uart instances, which match the UARTE instances that we can find in the register map of the nRF52840 datasheet (the E in UARTE refers to EasyDMA support):

ID Base address Peripheral Instance Description
0 0x40000000 CLOCK CLOCK Clock control
       
2 0x40002000 UARTE UARTE0 UART with EasyDMA, unit 0
3 0x40003000 SPIM SPIM0 SPI master 0
       
40 0x40028000 UARTE UARTE1 UART with EasyDMA, unit 1
41 0x40029000 QSPI QSPI External memory interface
       

As we can see, the unit-address of each uart node matches its base address. The reg property seems to be some kind of value list enclosed in angle brackets <...>. The first value of the property matches the node’s unit-address and thus the base address of the UARTE instance, but the property also has a second value and thus provides more information than the unit-address itself: Looking at the register map we can see that the second value 0x1000 matches the length of the address space that is reserved for each UARTE instance:

  • The base address of the UARTE0 instance is 0x40002000, followed by SPIM0 at 0x40003000.
  • The base address of the UARTE1 instance is 0x40028000, followed by QSPI at 0x40029000.

Finally, each UART instance also has a unique label:

  • uart0 is the label of the node /soc/uart@40002000,
  • uart1 is the label of the node /soc/uart@40028000.

Note: Throughout this article, we sometimes refer to node labels as just “labels”. The Devicetree specification also allows creating labels to reference properties and their values but Zephyr’s DTS generator ignores this feature. We therefore use the terms label and node label interchangeably.

Property names and basic value types

Let’s now have a look at properties. As we’ve already seen for the property reg, properties consist of a name and a value and are used to describe the characteristics of the node. Property names can contain:

  • digits 0-9,
  • lower and uppercase letters a-z and A-Z,
  • and any of the special characters .,_+?#-.

Most of the properties you’ll encounter in Zephyr simply use kebab-case names, though you’ll also encounter properties starting with a #, e.g., the standard properties #address-cells and #size-cells (also described in the Devicetree specification). The special character # is really just part of the property name, there’s no magic behind it.

Zephyr uses the type names summarized below. For details, refer to the “type” section in Zephyr’s documentation on Devicetree bindings or Zephyr’s Devicetree introduction on property values.

Note: If you’re planning on reading the Devicetree specification, skip ahead to the section “Devicetree Source (DTS) Format”, specifically the subsection “Node and property definitions”. In case something is unclear, try to find the information in the initial chapters. Reading the specification top to bottom can be quite confusing.

The syntax used for property values is a bit peculiar. Except for phandles, which we’ll cover separately, the following table contains all property types supported by Zephyr and their DTSpec equivalent.

Zephyr type DTSpec equivalent Example
boolean property with no value interrupt-controller;
string string status = "disabled";
array 32-bit integer cells reg = <0x40002000 0x1000>;
int A single 32-bit integer cell current-speed = <115200>;
64-bit integer 32-bit integer cells value = <0xBAADF00D 0xDEADBEEF>;
uint8-array bytestring mac-address = [ DE AD BE EF 12 34 ];
string-array stringlist compatible = "nordic,nrf-egu", "nordic,nrf-swi";
compound “comma-separated components” foo = <1 2>, [3, 4], "five"

The table deserves some observations and explanations, but we’ll repeat all the types just for the sake of completeness:

boolean

A boolean property is true if the property exists, otherwise false.

string

A string property is just like a C string literal: double-quoted, null-terminated text.

array

An array is a lists of values enclosed in < and >, separated by spaces. arrays could in theory contain mixed elements of any supported type but most commonly uses 32-bit integers. Just like in C/C++, the prefix 0x is used for hexadecimal numbers, and numbers without a prefix are decimals.

int

An integer is represented as an array containing a single 32-bit value (“cell”): It’s a single 32-bit value enclosed in < and >.

Note: The term “cell” is typically used to refer to the individual data elements within properties and can thus be thought of as a single unit of data in a property. The Devicetree specification v0.4 defines a “cell” as “a unit of information consisting of 32 bits” and thus explicitly defines its size as 32 bits. Meaning it is just another confusing name for a 32-bit integer.

64-bit integers do not have their own type but are instead represented by an array of two integers.

uint8-array

A uint-array consists of 8-bit hexadecimal values enclosed in [ and ], optionally separated by spaces.

In contrast to an array, a uint8-array always uses hexadecimal literals without the prefix 0x - which can be confusing at first. E.g., <11 12> represents the two 32-bit integers with the decimal values 11 and 12, whereas [11, 12] represents the two 8-bit integers with the decimal values 17 and 18.

The spaces between each byte in a uint8-array are optional. E.g., mac-address = [ DEADBEEF1234 ]; is equivalent to mac-address = [ DE AD BE EF 12 34 ];. It practice, you’ll rarely see uint8-arrays without spaces between each byte.

string-array

A string-array is just a comma-separated list of strings. The only thing worth mentioning is, that the value of a string-array property may also be a single string. E.g., compatible = "something" is still a valid value assignment for a string-array property.

compound

The compound type is essentially a “catch-all” for custom types. Zephyr does not generate any code for compound properties. Also, notice how compound value definitions are syntactically ambiguous, e.g., for foo = <1 2>, "three", "four": Is "three", "four" a single value of type string-array, or two separate string values?

When reading Zephyr DTS files, keep in mind that in Zephyr all DTS files are fed into the preprocessor and therefore Zephyr allows the use of macros in DTS files. E.g., you might encounter properties like max-frequency = <DT_FREQ_M(8)>;, which do not match the Devicetree syntax at all. There, the preprocessor replaces the macro DT_FREQ_M with the corresponding literal before the source file is parsed.

The following is a snippet of a test file of Zephyr’s Python Devicetree generator. It contains the node props that nicely demonstrate the different property types supported by Zephyr.

zephyr/scripts/dts/python-devicetree/tests/test.dts

/dts-v1/;

/ {
  props {
    existent-boolean;
    int = <1>;
    array = <1 2 3>;
    uint8-array = [ 12 34 ];
    string = "foo";
    string-array = "foo", "bar", "baz";
  };
};

There’s one more thing that is worth mentioning: Parentheses, arithmetic operators, and bitwise operators are allowed in property values, though the entire expression must be parenthesized. Zephyr’s introduction on property values provides the following example for a property bar:

/ {
  foo {
    bar = <(2 * (1 << 5))>;
  };
};

The property bar contains a single 32-bit value (“cell”) with the value 64. Notice that operators are not just allowed in Zephr, but also according to the Devicetree specification: You therefore don’t need to use macros for simple arithmetic or bit operations.

References and phandle types

We’ve already seen how we can create node labels as shorthand forms of a node’s full path, but haven’t really seen how such labels are used within the Devicetree. Tired of all the theory? I thought so. Now that we’re familiar with a good part of the Devicetree source file syntax, it’s time for a little hands-on, so let’s dive back into the command line.

We’ll practice using a Devicetree overlay file. In the next article, we’ll go more into detail about what an overlay is. For now, it is enough to know that an overlay file is simply an additional DTS file on top of the hierarchy of files that is included starting with the board’s Devicetree source file. We can specify an extra Devicetree overlay file using the CMake variable EXTRA_DTC_OVERLAY_FILE, and we’ll use a newly created props-phandles.overlay file for that:

$ mkdir -p dts/playground
$ touch dts/playground/props-phandles.overlay
$ rm -rf ../build
$ west build --board nrf52840dk_nrf52840 --build-dir ../build -- \
  -DEXTRA_DTC_OVERLAY_FILE=dts/playground/props-phandles.overlay

The build system’s output now announces that it encountered the newly created overlay file with the message Found devicetree overlay:

-- Found Dtc: /opt/nordic/ncs/toolchains/4ef6631da0/bin/dtc (found suitable version "1.6.1", minimum required is "1.4.6")
-- Found BOARD.dts: /opt/nordic/ncs/v2.4.0/zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts
-- Found devicetree overlay: playground/test.overlay
-- Generated zephyr.dts: /path/to/build/zephyr/zephyr.dts

phandle

So what is a phandle? The easy answer is: Devicetree source files need some way to refer to nodes, something like pointers in C, and that’s what phandles are. If we’re being picky about the terminology - it’s complicated. Why?

  • In the DTSpec, we’ve seen that a phandle is a base type.
  • In addition, the DTSpec also defines a standard property named phandle - ironically of type 32-bit integer, and not phandle. We’ll see this property in just a second.
  • In Zephyr, the term phandle is used pretty much only for node references in any format and never even mentions the same named property.

Why this ambiguity? Because in the end, any reference to a node is replaced by a unique, 32-bit value that identifies the node - the value stored in the node’s phandle property. The fact that phandle property is not intended to be set manually, but is instead created by the Devicetree compiler for each referenced node, makes mentioning the phandle property as such unnecessary. Thus, the approach chosen in Zephyr’s documentation - referring to any reference as phandle - makes a lot of sense.

But enough nit-picking. Let’s see how this looks in a real Devicetree source file. Let’s create two nodes node_a and node_refs in our overlay file, and have node_refs reference the node_a once by its path and once by a label label_a that we create for node_a. How do we do this? The syntax is specified in the DTSpec as follows:

“Labels are created by appending a colon (‘:’) to the label name. References are created by prefixing the label name with an ampersand (‘&’), or they may be followed by a node’s full path in braces.” [DTSpec]

Thus, in our Devicetree overlay file, we can create the properties as shown below. Notice how we’re missing the DTS version /dts-v1/;: The version is only defined Devicetree source files, but not in overlay files.

/ {
  label_a: node_a { /* Empty. */ };
  node_refs {
    phandle-by-path = <&{/node_a}>;
    phandle-by-label = <&label_a>;
  };
};

Let’s run a build to ensure that our overlay is sent through the preprocessor so that we can have a look at the generated zephyr.dts:

$ west build --board nrf52840dk_nrf52840 --build-dir ../build -- \
  -DEXTRA_DTC_OVERLAY_FILE=dts/playground/props-phandles.overlay

build/zephyr/zephyr.dts

/dts-v1/;

/ {
  /* Possibly lots of other nodes ... */
  label_a: node_a {
    phandle = < 0x1c >;
  };
  node_refs {
    phandle-by-path = < &{/node_a} >;
    phandle-by-label = < &label_a >;
  };
};

We can now see that the generator has created a phandle property for our referenced node_a. Your mileage may vary on the exact value for the property since it depends on the number of referenced nodes in the set of DTS files that are merged. What is this phandle property? The DTSpec defines phandle as follows:

Property name phandle, value type 32-bit integer.

The phandle property specifies a numerical identifier for a node that is unique within the Devicetree. The phandle property value is used by other nodes that need to refer to the node associated with the property.

Note: Most devicetrees […] will not contain explicit phandle properties. The DTC tool automatically inserts the phandle properties when the DTS is compiled […].

This is also what we see in the generated zephyr.dts: Since node_a is referenced by node_ref, Zephyr’s DTS generator has inserted the property phandle for node_a in our Devicetree. To see what happens to the reference within this phandle property, we need to jump back to the syntax section in the DTSpec, where we find the following:

“In a cell array, a reference to another node will be expanded to that node’s phandle.”

This means that the references &{/node_a} and &label_a in our properties phandle-by-path and phandle-by-label are essentially expanded to node_a’s phandle 0x1c. Thus, the reference is equivalent to its phandle. Zephyr’s documentation is right to refer to &{/node_a} and &label_a as “phandles”. Could we also define node_a’s phandle property by ourselves? Let’s find out:

/ {
  label_a: node_a {
    phandle = <0xC0FFEE>;
  };
  node_refs {
    phandle-by-path = <&{/node_a}>;
    phandle-by-label = <&label_a>;
  };
};

After executing west build with the previous parameters again, we indeed end up with the value 0xc0ffee for node_a’s phandle property:

build/zephyr/zephyr.dts

/dts-v1/;

/ {
  /* Possibly lots of other nodes ... */
  label_a: node_a {
    phandle = < 0xc0ffee >;
  };
  node_refs {
    phandle-by-path = < &{/node_a} >;
    phandle-by-label = < &label_a >;
  };
};

What’s the use of a phandle? A single phandle can be useful where a 1:1 relation between nodes in the Devicetree is required. E.g., the following is a snippet from the nRF52840 DTS include file, which contains a software PWM node with a configurable generator, which by default refers to timer2:

zephyr/dts/arm/nordic/nrf52840.dtsi

/ {
  /* ... */
  sw_pwm: sw-pwm {
    compatible = "nordic,nrf-sw-pwm";
    status = "disabled";
    generator = <&timer2>;
    clock-prescaler = <0>;
    #pwm-cells = <3>;
  };
};

We’ve seen the type of the phandle property, but what about the types of the properties phandle-by-path and phandle-by-label? Knowing that a phandle is expanded to a cell we might guess that phandle-by-path and phandle-by-label are of type int or array: Assuming that node_a’s phandle has the value 0xc0ffee, both <&{/node_a}> and <&label_a> essentially expand to <0xc0ffee>, which could either be a single int or an array of size 1 containing a 32-bit value.

In Zephyr, however, an array containing a single node reference has its own type phandle.

path, phandles and phandle-array

In addition to the phandle type, three more types are available in Zephyr when dealing with phandles and references:

Zephyr type DTSpec equivalent Example
path 32-bit integer cells zephyr,console = &uart0;
phandle phandle pinctrl-0 = <&uart0_default>;
phandles 32-bit integer cells cpu-power-states = <&idle &suspend>;
phandle-array 32-bit integer cells gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;

Let’s go through the remaining types one by one, starting with paths: In our previous example, we’ve enclosed the references in < and > and ended up with our phandle type. If we don’t place the reference within < and >, the DTSpec defines the following behavior:

“Outside a cell array, a reference to another node will be expanded to that node’s full path.”

So let’s try this out: Let’s first get rid of the properties phandle-by-path and phandle-by-label, and create two new properties path-by-path and path-by-label as follows:

/ {
  label_a: node_a { /* Empty. */ };
  node_refs {
    path-by-path = &{/node_a};
    path-by-label = &label_a;
  };
};

If we now execute west build, we can see something interesting in our generated zephyr.dts:

build/zephyr/zephyr.dts

/dts-v1/;

/ {
  /* Lots of other nodes ... */
  label_a: node_a {
  };
  node_refs {
    path-by-path = &{/node_a};
    path-by-label = &label_a;
  };
};

The phandle property for node_a is gone! The reason for this is simple - and also matches exactly what is stated in the DTSpec: The references used for the values of the properties path-by-path and path-by-label are really just a different notation for the path /node_a. They are not phandles, and therefore do not require a property phandle in the referenced node.

In Zephyr, you’ll encounter paths exclusively for properties of the standard nodes /aliases and /chosen, both of which we’ll see in a later section in this article. Meaning: You can’t actually use paths for your own nodes.

Next, phandles. This is not a typo: The plural form phandles of phandle is really a separate type in Zephyr, and it’s as simple as it sounds: Instead of supporting only a single reference - or phandle - in its value, it is an array of phandles. Let’s create another node_b and change node_refs to contain only a new property phandles and recompile:

/ {
  label_a: node_a { };
  label_b: node_b { };
  node_refs {
    phandles = <&{/node_a} &label_b>;
  };
};

build/zephyr/zephyr.dts

/dts-v1/;

/ {
  /* Lots of other nodes ... */
  label_a: node_a {
    phandle = < 0x1c >;
  };
  label_b: node_b {
    phandle = < 0x1d >;
  };
  node_refs {
    phandles = < &{/node_a} &label_b >;
  };
};

The output is not surprising: Both nodes are now referenced by node_ref’s phandles and therefore get their own phandle property. The phandles type is especially useful when we need to reference nodes of the same type, like the cpu-power-states example in the previous table.

This leaves us with the last type phandle-array, a type that is used quite a lot in Devicetree. Let’s go by example:

/ {
  label_a: node_a {
    #phandle-array-of-ref-cells = <2>;
  };
  label_b: node_b {
    #phandle-array-of-ref-cells = <1>;
  };
  node_refs {
    phandle-array-of-refs = <&{/node_a} 1 2 &label_b 3>;
  };
};

build/zephyr/zephyr.dts

/dts-v1/;

/ {
  /* Lots of other nodes ... */
  label_a: node_a {
    #phandle-array-of-ref-cells = < 0x2 >;
    phandle = < 0x1c >;
  };
  label_b: node_b {
    #phandle-array-of-ref-cells = < 0x1 >;
    phandle = < 0x1d >;
  };
  node_refs {
    phandle-array-of-refs = < &{/node_a} 0x1 0x2 &label_b 0x3 >;
  };
};

So what’s this phandle-array type? It is a list of phandles with metadata for each phandle. This is how it works:

  • By convention, a phandle-array property is plural and its name should thus end with “s”.
  • The value of a phandle-array property is an array of phandles, but each phandle can be followed by cells (32-bit values), sometimes also called a phandle’s “metadata”. In the example above, the two values 1 2 are &{/node_a}’s metadata, whereas 3 is &label_b’s metadata.
  • The new properties #phandle-array-of-ref-cells tell the compiler how many metadata cells are supported by the corresponding node. Such properties are called specifier cells: In our example, node_a specifies that the node supports two cells, node_b’s specifier cell only allows one cell after its phandle.

Specifier cells like #phandle-array-of-ref-cells have a defined naming convention: The name is formed by removing the plural ‘s’ and attaching ‘-cells’ to the name of the phandle-array property. For our property phandle-array-of-refs, we thus end up with phandle-array-of-refs-cells.

If you’ve been browsing Zephyr’s Devicetree files, you may have noticed that the property #gpio-cells doesn’t follow the specifier cell naming convention: Every rule has its exceptions, and in case you’re interested in details, I’ll leave you with a reference to Zephyr’s documentation on specifier cells.

Before we look at a real-world example of a phandle-array, let’s sum up phandle types. You’ll find this exact list also in Zephyr’s documentation on phandle types, but since it is a short one, I hope it’s OK to borrow it:

  • To reference exactly one node, use the phandle type.
  • To reference zero or more nodes, we use the phandles type.
  • To reference zero or more nodes with metadata, we use a phandle-array.

In case you’re still confused, we’ll look at a real-world example for a phandle-array just now.

Syntax and semantics for phandle-arrays

In case you’ve been experimenting with the above overlay, you may have noticed that I’ve been cheating a little: The project still compiles just fine even if you delete the #phandle-array-of-ref-cells properties, or give phandle-array-of-refs a different name that does not end in an s.

Why? Our Devicetree is still syntactically correct even if we do not follow the given convention. In the end, phandle-array-of-refs is simply an array of cells since every reference is expanded to the node’s phandle property’s value - even without the expansion, its syntax would still fit an array of 32-bit integer cells. A syntactically sound Devicetree, however, is only half the job: Eventually, we’ll have to define some schema and add meaning to all the properties and their values; we’ll have to define the Devicetree’s semantics.

Without semantics, the DTS generator can’t make sense of the provided Devicetree and therefore also won’t generate anything that you’d be able to use in your application. Once you add semantics to your nodes, you’ll have to strictly follow the previous convention, which is also why I’ve already included the notation in this article. The details, however, we’ll explore in the next article about Devicetree bindings.

phandle-array in practice

How are phandle-arrays used in practice? Let’s look at the nRF52840’s General Purpose input and outputs (GPIOs). In the datasheet, we can find the following table on the GPIO instances:

Base address Peripheral Instance Description Configuration
0x50000000 GPIO P0 GPIO, port 0 P0.00 to P0.31
0x50000300 GPIO P1 GPIO, port 1 P1.00 to P1.15

Thus, the nRF52840 uses two instances P0 and P1 of the GPIO peripheral to expose control over its GPIOs. In the nRF52840 DTS include file, we find the matching nodes gpio0 for port 0 and gpio1 for port 1:

zephyr/dts/arm/nordic/nrf52840.dtsi

/ {
  soc {
    gpio0: gpio@50000000 {
      compatible = "nordic,nrf-gpio";
      gpio-controller;
      reg = <0x50000000 0x200 0x50000500 0x300>;
      #gpio-cells = <2>;
      status = "disabled";
      port = <0>;
    };

    gpio1: gpio@50000300 {
      compatible = "nordic,nrf-gpio";
      gpio-controller;
      reg = <0x50000300 0x200 0x50000800 0x300>;
      #gpio-cells = <2>;
      ngpios = <16>;
      status = "disabled";
      port = <1>;
    };
  };
};

With this structure it is clear that we cannot just reference the entire gpio instance in case we want to control only one pin: We always have to choose either gpio0 or gpio1 and specify at least which exact pin of the port we need. Let’s see how this is solved in the nRF52840 development kit’s Devicetree source file.

The nRF52840 development kit connects LEDs to the nRF52840 MCU, which are described using the node leds in the board’s DTS file. Within this Devicetree, we now see how the led instances reference to the nRF52840’s GPIOs: E.g., gpio0 is used in led0’s property gpios using a phandle-array:

zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts

/ {
  leds {
    compatible = "gpio-leds";
    led0: led_0 {
      gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
      label = "Green LED 0";
    };
  };
};

There are no individual nodes for each pin in the nRF52840’s Devicetree. Therefore, when referencing the gpio0 node, we need to be able to tell exactly which pin we’re using for our LED. In addition, we also typically need to provide some configuration for our pin, e.g., set the pin to active low.

The nodes gpio0 and gpio1 both contain the specifier cells #gpio-cells which indicate that we need to pass exactly two cells to use the node in a phandle-array. In the led0’s property gpios of type phandle-array we can see that we do exactly that: We use the two cells to specify the pin and flags that we’re using for the LED. Now, how would we know that 13 is the pin number and GPIO_ACTIVE_LOW is a flag?

As we’ve seen before, without additional information all the DTS compiler can do is make sure the syntax of your file is correct. It doesn’t know anything about the semantics and therefore can’t really associate the values in the phandle-array to gpio0. It therefore also doesn’t care about any semantic requirements.

In the next article, we’ll see how to use the standard property compatible and so-called bindings to provide the semantics - and thus the information that we need to associate the first cell with the pin number and the second cell with the flags.

A complete list of Zephyr’s property types

Having explored phandle types and paths, we can complete the list of types that are used in Zephyr devicetrees. You can find the same information in Zephyr’s documentation on bindings and Zephyr’s how-to on property values.

Zephyr type DTSpec equivalent Example
boolean property with no value interrupt-controller;
string string status = "disabled";
array 32-bit integer cells reg = <0x40002000 0x1000>;
int A single 32-bit integer cell current-speed = <115200>;
64-bit integer 32-bit integer cells value = <0xBAADF00D 0xDEADBEEF>;
uint8-array bytestring mac-address = [ DE AD BE EF 12 34 ];
string-array stringlist compatible = "nordic,nrf-egu", "nordic,nrf-swi";
compound “comma-separated components” foo = <1 2>, [3, 4], "five"
path 32-bit integer cells zephyr,console = &uart0;
phandle phandle pinctrl-0 = <&uart0_default>;
phandles 32-bit integer cells cpu-power-states = <&idle &suspend>;
phandle-array 32-bit integer cells gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;

Again, it is worth mentioning that the compound type is only a “catch-all” for custom types. Zephyr does not generate any macros for compound properties.

About /aliases and /chosen

There are two standard nodes that are important to know when talking about Devicetree: /aliases and /chosen. We’ll see them again in the next article about Devicetree bindings, but since they are quite prominent in Zephyr’s DTS files, we can’t just leave them aside for now.

/aliases

Let’s have a look at /aliases first. The DTSpec specifies that the /aliases node is a child node of the root node. The following is specified for its properties:

Each property of the /aliases node defines an alias. The property “name” specifies the alias name. The property “value” specifies the full path to a node in the Devicetree. [DTSpec]

Simply put, /aliases are just yet another way to get the full path to nodes in your application. In the previous section, we’ve learned that outside of < and > a reference to another node is expanded to that node’s full path. Thus, for any alias, we can specify its value of type path using references or a plain string, as shown in the example below:

/ {
  aliases {
    alias-by-label = &label_a;
    alias-by-path = &{/node_a};
    alias-as-string = "/node_a";
  };
  label_a: node_a {
    /* Empty. */
  };
};

So what’s the point of having an alias? Can’t we use labels instead? Well, yes - but also no. As mentioned, for the application there is no real difference between referring to a node via its label, its full path - or using its alias. We’ll learn about the Devicetree API in Zephyr in the next article. For now, know that there are three macros DT_ALIAS, DT_LABEL, and DT_PATH in zephyr/include/zephyr/devicetree.h that you can use to get a node’s identifier.

Why the emphasis on “the application” and what exactly doesn’t work? It’s important to know that /aliases is just another node with properties that are compiled accordingly. You cannot use aliases in the Devicetree itself as you can do with labels. Thus, you can’t replace an occurrence of a label with its alias. E.g., the following does not compile:

/ {
  aliases {
    alias-by-label = &label_a;
  };
  label_a: node_a {
    /* Empty. */
  };
  node_ref {
    // This doesn't work. An alias cannot be used like labels.
    phandle-by-alias = <&alias-by-label>;
  };
};

Still wondering why you’d need an alias if you already have labels? Let’s have a look at how aliases are used in Zephyr’s DTS files. Say, we want to build an application that reads the state of a button. If we look at the nRF52840 development kit, we find the following nodes for the board’s buttons:

zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts

/ {
  buttons {
    compatible = "gpio-keys";
    button0: button_0 { /* ... */ };
    button1: button_1 { /* ... */ };
    button2: button_2 { /* ... */ };
    button3: button_3 { /* ... */ };
  };
};

In our application, we could refer to button_0 via its full path /buttons/button_0, or using its label button0. Let’s say we want to run the same application on a different board, e.g., STM’s Nucleo-C031C6. After all, Zephyr’s promise is that switching boards should be easily doable, right? Let’s have a look at its board’s DTS file:

zephyr/boards/arm/nucleo_c031c6/nucleo_c031c6.dts

/ {
  gpio_keys {
    compatible = "gpio-keys";
    user_button: button { /* ... */ };
  };
};

So, our STM board has only one button, which has the path /gpio_keys/button and the label user_button - both incompatible with the references we’d use for our nRF development kit. Notice that user_button is a perfectly fine label for the only available button on the board.

However, if we’d like to use this button in our application, we’d now have to change our sources - or even worse - adapt the DTS files. Instead of doing this, we can use aliases - and since buttons are commonly used throughout Zephyr’s example applications, the corresponding alias, in our case sw0, already exists:

zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts

/ {
  aliases {
    led0 = &led0;
    /* ... */
    pwm-led0 = &pwm_led0;
    sw0 = &button0;
    /* ... */
  };
};

zephyr/boards/arm/nucleo_c031c6/nucleo_c031c6.dts

/ {
  aliases {
    led0 = &green_led_4;
    pwm-led0 = &green_pwm_led;
    sw0 = &user_button;
    /* ... */
  };
};

Note: In case the aliases don’t exist in the board DTS files, you could use overlay files - just like we already did in our examples. We’ll see this again in the next article.

As you can see, there are also other examples where labels are not consistent. Instead, aliases are used. We could change our application to get the node using the commonly available alias sw0, and it’ll work with both boards.

/chosen

Now what about the /chosen node? If you’ve been following along, or if you’ve just had another look at the DTS file of the nRF52840 development kit, then you might find that some of the /chosen nodes look an awful lot like what you find in /aliases, just with a different property name format:

zephyr/boards/arm/nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts

/ {
  chosen {
    zephyr,console = &uart0;
    zephyr,shell-uart = &uart0;
    zephyr,uart-mcumgr = &uart0;
    zephyr,bt-mon-uart = &uart0;
    zephyr,bt-c2h-uart = &uart0;
    zephyr,sram = &sram0;
    zephyr,flash = &flash0;
    zephyr,code-partition = &slot0_partition;
    zephyr,ieee802154 = &ieee802154;
  };
};

So what’s the difference between a property in /chosen and in /aliases? Let’s first look at the definition of the /chosen node in the DTSpec:

The /chosen node does not represent a real device in the system but describes parameters chosen or specified by the system firmware at run time. It shall be a child of the root node. [DTSpec]

The first sentence can be a bit misleading: It doesn’t mean that we cannot refer to “real devices” using /chosen properties, it simply means that a device defined as a /chosen property is always a reference. Thus, in short, /chosen contains a list of system parameters.

Note: In Zephyr, /chosen is only used at build-time. There is no run-time feature.

According to the DTSpec, technically, /chosen properties are not restricted to the path type. The following are acceptable /chosen properties according to the specification, and Zephyr’s DTS generator does indeed accept them as input:

/ {
  chosen {
    chosen-by-label = &label_a;
    chosen-by-path = &{/node_a};
    chosen-as-string = "/node_a";
    chosen-foo = "bar";
    chosen-bar = <0xF00>;
    chosen-invalid = "/invalid/path/is/a/string";
  };
  label_a: node_a {
    /* Empty. */
  };
};

At the time of writing, Zephyr uses /chosen properties exclusively for references to other nodes and therefore the /chosen node only contains properties of the type path. Also, the DT_CHOSEN macro in the Devicetree API is only used to retrieve node identifiers. You can find a list of all Zephyr-specific /chosen nodes in the official documentation.

So when would you use /aliases and when /chosen properties? If you want to specify a node that is independent of the nodes in a Devicetree, you should use an alias rather than a chosen property. /chosen is used to specify global configuration options and properties that affect the system as a whole. In Zephyr, /chosen is typically only used for Zephyr-specific parameters and not for application-specific configuration options.

Also, think twice when considering adding /chosen properties - for some configuration options, Kconfig is probably better suited. Zephyr’s official documentation has a dedicated page on Devicetree vs. Kconfig, check it out!

Complete examples and alternative array syntax

Along with the complete list of Zephyr’s property types, we can now create two overlay files as complete examples for basic types, phandles, /aliases, and /chosen nodes.

Note: The full example application including all the Devicetree files that we’ve seen throughout this article is available in the 02_devicetree_basics folder of the accompanying GitHub repository.

The first example covers all the basic types that we’ve seen:

dts/playground/props-basics.overlay

/ {
  aliases {
    // Aliases cannot be used as a references in the Devicetree itself, but are
    // used within the applicaiton as an alternative name for a node.
    alias-by-label = &label_equivalent;
    alias-by-path = &{/node_with_equivalent_arrays};
    alias-as-string = "/node_with_equivalent_arrays";
  };

  chosen {
    // `chosen` describes parameters "chosen" or specified by the application. In Zephyr,
    // all `chosen` parameters are paths to nodes.
    chosen-by-label = &label_equivalent;
    chosen-by-path = &{/node_with_equivalent_arrays};
    chosen-as-string = "/node_with_equivalent_arrays";
    // Technically, `chosen` properties can have any valid type.
    chosen-foo = "bar";
    chosen-bar = <0xF00>;
  };

  node_with_props {
    existent-boolean;
    int = <1>;
    array = <1 2 3>;
    uint8-array = [ 12 34 ];
    string = "foo";
    string-array = "foo", "bar", "baz";
  };
  label_equivalent: node_with_equivalent_arrays {
    // No spaces needed for uint8-array values.
    uint8-array = [ 1234 ];
    // Alternative syntax for arrays.
    array = <1>, <2>, <3>;
    int = <1>;
  };
};

// It is not possible to refer to a node via its alias - aliases are just properties!
// &alias-by-label {... };

// It is possible to "extend" and overwrite (non-const) properties of a node using
// its full path or its label.
&{/node_with_equivalent_arrays} {
  int = <2>;
};
&label_equivalent {
  string = "bar";

There are two things that we have already seen but haven’t explicitly described throughout this article: We can use node references outside the root node to define additional properties for a node or change a node’s existing properties, and we can use a different syntax for specifying array values:

In the above example, the property int of the node /node_with_equivalent_arrays is overwritten with the value 2, and a value "bar" is added to the node’s property string using its label label_equivalent.

We also see that we don’t have to provide all values of an array within a single set of <..>, but can instead place each value into its own pair of <..> and separate the values via commas. We also see the alternative syntax for uint8-arrays. In the semantics article, we’ll see that the output is indeed the same, for now, you have to trust me on this.

The following is the generated output for the two nodes /node_with_props and /node_with_equivalent_arrays:

/build/zephyr/zephyr.dts

/ {
  node_with_props {
    existent-boolean;
    int = < 0x1 >;
    array = < 0x1 0x2 0x3 >;
    uint8-array = [ 12 34 ];
    string = "foo";
    string-array = "foo", "bar", "baz";
  };
  label_equivalent: node_with_equivalent_arrays {
    uint8-array = [ 12 34 ];
    array = < 0x1 >, < 0x2 >, < 0x3 >;
    int = < 0x2 >;
    string = "bar";
  };
};

The second overlay file shows the use of paths, phandle, phandles, and phandle-array, including the alternative array syntax that we’ve just seen. Here, for the property phandle-array-of-refs, you can see how this syntax can sometimes result in a more readable Devicetree source file.

dts/playground/props-phandles.overlay

/ {
  label_a: node_a {
    // The value assignment for the cells is redundant in Zephyr, since
    // the binding already specifies all names and thus the size.
    #phandle-array-of-ref-cells = <2>;
  };
  label_b: node_b {
    #phandle-array-of-ref-cells = <1>;
  };
  node_refs {
    // Properties of type `path`
    path-by-path = &{/node_a};
    path-by-label = &label_a;

    // Properties of type `phandle`
    phandle-by-path = <&{/node_a}>;
    phandle-by-label = <&label_a>;

    // Array of phandle, type `phandles`
    phandles = <&{/node_a} &label_b>;
    // Array of phandles _with metadata_, type `phandle-array`
    phandle-array-of-refs = <&{/node_a} 1 2 &label_b 1>;
  };

  node_refs_equivalents {
    phandles = <&{/node_a}>, <&label_b>;
    phandle-array-of-refs = <&{/node_a} 1 2>, <&label_b 3>;
  };

  node_with_phandle {
    // It is allowed to explicitly provide the phandle's value, but the
    // DTS generator does this for us.
    phandle = <0xC0FFEE>;
  };
};

You can add those files to your own sources and provide both files using the EXTRA_DTC_OVERLAY_FILE option by separating the paths using a semicolon:

$ west build --board nrf52840dk_nrf52840 --build-dir ../build -- \
  -DEXTRA_DTC_OVERLAY_FILE="dts/playground/props-basics.overlay;dts/playground/props-phandles.overlay"

Zephyr’s DTS skeleton and addressing

Ok - we’ve seen nodes, labels, properties, and value types including phandles, we know how Zephyr compiles the Devicetree and we’ve even seen the standard nodes /aliases and /chosen. Are we done yet? Almost. There’s the famous one last thing that is worth looking into before wrapping up on the Devicetree basics. Yes, these are the basics, there’s more coming in the next article!

Note: Technically, we’re again describing semantics even though I’ve promised that we’ll wait with that until the next article about bindings, but one of our goals was being able to read Zephyr Devicetree files. Without understanding the standard properties #address-cells, #size-cells, and reg, we’d hardly be able to claim that.

When exploring Zephyr’s build, we’ve seen that our DTS file “include” tree ends up including a skeleton file zephyr/dts/common/skeleton.dtsi. The following is a stripped-down version of the full “include” tree that we’ve seen in the introduction when building our empty application for the nRF52840 development kit:

nrf52840dk_nrf52840.dts
└── nordic/nrf52840_qiaa.dtsi
    └── nordic/nrf52840.dtsi
        └── arm/armv7-m.dtsi
            └── skeleton.dtsi

This skeleton.dtsi contains the minimal set of nodes and properties in a Zephyr Devicetree:

/ {
  #address-cells = <1>;
  #size-cells = <1>;
  chosen { };
  aliases { };
};

We’ve already seen the /chosen and /aliases nodes. The #address-cells and #size-cells properties look a lot like the specifier cells we’ve seen when looking at the phandle-array type, right?

Even though they match the naming convention, their purpose, however, is a different one: They provide the necessary addressing information for nodes that use a unit-addresses. Too many new terms in one article? Let’s quickly repeat what we’ve already seen in the section about node names:

  • Nodes can have addressing information in their node name. Such nodes use the name format node-name@unit-address and must have the property reg.
  • Nodes without a unit-address in the node name do not have the property reg.

Let’s first have a look at what we can find out about reg in the DTSpec:

Property name reg, value type 32-bit integer cells encoded as an arbitrary number of (address, length) pairs.

The reg property describes the address of the device’s resources within the address space defined by its parent […]. Most commonly this means the offsets and lengths of memory-mapped IO register blocks […].

The value consists of 32-bit integer cells, composed of an arbitrary number of pairs of address and length, <address length>. The number of 32-bit cells required to specify the address and length are […] specified by the #address-cells and #size-cells properties in the parent of the device node. If the parent node specifies a value of 0 for #size-cells, the length field in the value of reg shall be omitted.

Bit much? Let’s bring up our good old uart@40002000 node from the nRF52840’s DTS include file:

zephyr/dts/arm/nordic/nrf52840.dtsi

#include <arm/armv7-m.dtsi>
#include "nrf_common.dtsi"

/ {
  soc {
    uart0: uart@40002000 { reg = <0x40002000 0x1000>; };
    uart1: uart@40028000 { reg = <0x40028000 0x1000>; };
  };
};

Using the information from the DTSpec, we should now be able to tell for sure that 0x40002000 is the address and _0x1000 the length, right? Yes, but no: We’ve also learned that a 64-bit integer value is represented using two cells, thus <0x40002000 0x1000> could technically be a single 64-bit integer, and length could be omitted. To be really sure how uart@40002000 is addressed, we need to look at the parent node’s #address-cells and #size-cells properties. So what are those properties? Let’s look them up in the DTSpec:

Property names #address-cells, #size-cells, value type 32-bit integer.

The #address-cells and #size-cells properties […] describe how child device nodes should be addressed.

  • The #address-cells property defines the number of 32-bit integers used to encode the address field in a child node’s reg property.
  • The #size-cells property defines the number of 32-bit integers used to encode the size field in a child node’s reg property.

The #address-cells and #size-cells properties are not inherited from ancestors in the Devicetree. […] A DTSpec-compliant boot program shall supply #address-cells and #size-cells on all nodes that have children. If missing, a client program should assume a default value of 2 for #address-cells, and a value of 1 for #size-cells.

Ok, now we’re getting somewhere: We need to look at the parent’s properties to know how many cells in the reg property’s value are used to encode the address and the length. In the DTS file zephyr/dts/arm/nordic/nrf52840.dtsi, however, there are no such properties for soc.

zephyr/dts/arm/nordic/nrf52840.dtsi

/ {
  soc {
    /* Just nodes, no properties ... */
  };
};

So, do we need to assume the defaults defined in the DTSpec? We might have to, but we’ve learned that node properties don’t all need to be provided in one place, and therefore the #address-cells and #size-cells of the soc might be provided by some includes.

Instead of going through all includes, we’ve seen that in Zephyr all includes are resolved by the preprocessor, and therefore a single Devicetree file is available in the build directory. You can therefore find the combined soc’s properties in build/zephyr/zephyr.dts. Since our include graph is rather easy to traverse, though, we’ll also find the missing information in the CPU architecture’s DTS file:

zephyr/dts/arm/armv7-m.dtsi

#include "skeleton.dtsi"

/ {
  soc {
    #address-cells = <1>;
    #size-cells = <1>;
    /* ... */
  };
};

Now we really can tell for sure that for our uart@40002000 node, the reg’s value <0x40002000 0x1000> indeed refers to the address 0x40002000 and the length/size 0x1000. This is also not surprising since the nRF52840 uses 32-bit architecture.

We can also find an example of a 64-bit architecture in Zephyr’s DTS files:

zephyr/dts/riscv/sifive/riscv64-fu740.dtsi

/ {
  soc {
    #address-cells = <2>;
    #size-cells = <2>;

    uart0: serial@10010000 {
      reg = <0x0 0x10010000 0x0 0x1000>;
    };
  };
};

Here, uart0’s address is formed by the 64-bit integer value <0x0 0x10010000> and the length by the 64-bit integer value <0x0 0x1000>.

Addressing is not only used for register mapped devices but, e.g., also for bus addresses. For this, let’s have a look at Nordic’s Thingy:53, a more complex board with several I2C peripherals connected to the nRF5340 MCU. The I2C bus of the nRF5340 uses the #address-cells and #size-cells properties to indicate that I2C nodes are uniquely identified via their address, no size is needed:

zephyr/dts/arm/nordic/nrf5340_cpuapp_peripherals.dtsi

i2c1: i2c@9000 {
  #address-cells = <1>;
  #size-cells = <0>;
};

In the board DTS file, we find which I2C peripherals are actually hooked up to the I2C interface:

  • The BMM150 geomagnetic sensor uses the I2C address 0x10,
  • the BH1749 ambient light sensor uses the I2C address 0x38,
  • and the BME688 gas sensor the address 0x76.

zephyr/boards/arm/thingy53_nrf5340/thingy53_nrf5340_common.dts

&i2c1 {
  bmm150: bmm150@10 { reg = <0x10>; };
  bh1749: bh1749@38 { reg = <0x38>; };
  bme688: bme688@76 { reg = <0x76>; };
};

With this, we’re really done with our Devicetree introduction.

Conclusion

In this article, we’ve learned what a devicetree is, its most important syntax, and how it is used in Zephyr. We’ve seen:

  • What a Devicetree actually is,
  • how a Devicetree is structured and how it represents the hardware,
  • all basic property types and phandles, including phandle-arrays,
  • we’ve seen the standard nodes /chosen and /aliases,
  • and we’ve learned about addressing within Devicetree.

Even without looking at bindings yet, this is a lot to take in, especially if you’ve never worked with Devicetree before. If you feel overwhelmed, don’t worry, we’ll look into each property type again in the next article about bindings and thus semantics. Practice is key!

Note: The full example application including all the Devicetree files that we’ve seen throughout this article is available in the 02_devicetree_basics folder of the accompanying GitHub repository.

Just like Kconfig, in your future Zephyr projects, Kconfig will become especially important once you’re starting to build your own device drivers. With this article, I hope I can give you an easy start and I hope to see you in the next article about Devicetree bindings!

Further reading

The following are great resources when it comes to Zephyr and is worth a read or watch:

Finally, have a look at the files in the accompanying GitHub repository.

Martin Lampacher is a Freelance Embedded Engineer based in the Italian Alps.