Having covered the Devicetree basics in the previous article, we now add semantics to our Devicetree using so-called bindings: For each supported type, we’ll create a corresponding binding and look at the generated output to understand how it can be used with Zephyr’s Devicetree API.

👉 Find the other parts of the Practical Zephyr series here.

Notice that we’ll only look at Zephyr’s basic Devicetree API and won’t analyze specific subsystems such as gpio in detail. We’re saving this for a practice round in the next article of this Practical Zephyr series.

🎬 Listen to Martin on Interrupt Live talk about the content and motivations behind writing this series.

Like Interrupt? Subscribe to get our latest posts straight to your inbox.

Prerequisites

This article is part of the Practical Zephyr article series. In case you haven’t read the previous articles, please go ahead and have a look. In this article we’re building up on what we’ve seen in the Devicetree basics: We’ll add bindings to the same nodes that we’ve created in the previous article. If you have some experience with Devicetree and Zephyr, you should be able to follow along just fine without reading the previous articles.

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

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

Warm-up

Before we get started, let’s quickly review what we’ve seen when we had a look at the UART nodes of the nRF52840 Development Kit from Nordic. We’re using the same old freestanding application with an empty prj.conf and the following file tree:

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

The CMakeLists.txt only includes the necessary boilerplate to create a freestanding Zephyr application. As an application, we’ll again 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

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

The following command builds this application for the nRF52840 Development Kit from Nordic that we’re using as a reference. As usual, you can follow along with any board (or emulation target) and you should see a similar output.

$ west build --no-sysbuild --board nrf52840dk/nrf52840 --build-dir ../build

Using the --board parameter, Zephyr selects the matching Devicetree source file zephyr/boards/nordic/nrf52840dk/nrf52840dk_nrf52840.dts, which specifies the properties status and current-speed for the node with the label uart0, as follows:

zephyr/boards/nordic/nrf52840dk/nrf52840dk_nrf52840.dts

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

Zephyr’s DTS generator script produces the matching output in devicetree_generated.h for the specified properties:

build/zephyr/include/generated/devicetree_generated.h

#define DT_N_S_soc_S_uart_40002000_P_current_speed 115200
#define DT_N_S_soc_S_uart_40002000_P_status "okay"

Can we get a similar output for Devicetree nodes that we define from scratch? In the previous article, we’ve been using overlay files to define custom nodes with their own properties to showcase Zephyr’s Devicetree types. We’ll see if we can get some output for the nodes and properties in these overlays, but before we go ahead and make blind use of overlays yet again, let’s finally have a look and learn how they work.

Devicetree overlays

Overlays are used to extend or modify the board’s Devicetree source file. Even though by convention overlay files use the .overlay file extension, they’re just plain old Devicetree source (DTS) files. The build system combines the board’s .dts file and any .overlay files by concatenating them, with the overlays put last. Thus, the contents of the .overlay file have priority over any definitions in the board’s .dts file or its includes.

Automatic overlays

The Zephyr build system automatically picks up additional overlays based on their location and file name. The following repeats the steps in Zephyr’s official documentation used by the build system to detect overlay files.

Before we have a look at the steps, there’s one detail that we haven’t seen yet. So far, we’ve specified the board as a command-line argument in the format --board <board>. Zephyr also supports building for a board revision in case you have multiple revisions or versions of a specific board. In such a case, you can use the format --board <board>@<revision>.

Note: As we’ve seen in previous articles, there are several ways to specify the board, but for the sake of simplicity we’re only using west and its --board argument here. You can specify the board in your path using the variable BOARD, you can provide it directly in your CMakeLists.txt file, or even pass it to an explicit cmake call.

With this detail out of the way, here’s the search performed by the CMake module zephyr/cmake/modules/dts.cmake:

  • In case the CMake variable DTC_OVERLAY_FILE is set, the build system uses the specified file(s) and stops the search.
  • If the file boards/<BOARD>.overlay exists in the application’s root directory, the build system selects the provided file as overlay and proceeds with the following step.
  • If a specific revision has been specified for the BOARD in the format <board>@<revision> and boards/<BOARD>_<revision>.overlay exists in the application’s root directory, this file is used in addition to boards/<BOARD>.overlay, if both exist.
  • If overlays have been encountered in any of the previous steps, the search stops.
  • If no files have been found and <BOARD>.overlay exists in the application’s root directory, the build system uses the overlay and stops the search.
  • Finally, if none of the above overlay files exist but app.overlay exists in the application’s root directory, the build system uses the overlay.

On top of the overlay files that have or haven’t been discovered by the build process, the CMake variable EXTRA_DTC_OVERLAY_FILE allows to specify additional overlay files that are added regardless of the outcome of the overlay search.

The important thing to remember is, that the Devicetree overlay files that were detected last have the highest precedence, since they may overwrite anything in the previously added overlay files. The precedence is always visible in the build output, where Zephr lists all overlay files using the output Found devicetree overlay: <name>.overlay in the order that they are detected and thus added. The precedence increases with the given list.

Overlays by example

Let’s try and visualize this list using an imaginary file tree and board “dummy_board”. I’ve annotated the files with precedence numbers, even though - as we’ve learned before - not all files will be used by the build:

$ tree --charset=utf-8 --dirsfirst.
.
├── boards
│   ├── dummy_board_123.overlay -- #3
│   └── dummy_board.overlay     -- #4
├── dts
│   └── extra
│       ├── extra_0.overlay -- #2
│       └── extra_1.overlay -- #1
├── src
│   └── main.c
├── CMakeLists.txt
├── app.overlay         -- #6
├── dummy_board.overlay -- #5
└── prj.conf

Let’s assume we would use the following command that doesn’t include the CMake variable DTC_OVERLAY_FILE:

$ west build --no-sysbuild --board dummy_board@123 -- \
  -DEXTRA_DTC_OVERLAY_FILE="dts/extra/extra_0.overlay;dts/extra/extra_1.overlay"

The overlay files would be detected and added as follows, depending on whether or not they exist. As mentioned, the precedence increases and thus the files listed last have the highest precedence:

  • boards/dummy_board.overlay
  • boards/dummy_board_123.overlay
  • if none of the previous files exist, dummy_board.overlay
  • if none of the previous files exist, app.overlay
  • dts/extra/extra_0.overlay
  • dts/extra/extra_1.overlay

Note: It is recommended to use the boards directory for board overlay files. You should no longer place your board’s overlay files in the application’s root directory.

If, instead, we specify the CMake variable DTC_OVERLAY_FILE as app.overlay in our command as follows, the automatic detection is skipped and the build process only picks the selected DTC overlay files:

$ west build --no-sysbuild --board dummy_board@123 -- \
  -DTC_OVERLAY_FILE="app.overlay" \
  -DEXTRA_DTC_OVERLAY_FILE="dts/extra/extra_0.overlay;dts/extra/extra_1.overlay"

The overlay files would thus be added as follows and with increasing precedence:

  • app.overlay
  • dts/extra/extra_0.overlay
  • dts/extra/extra_1.overlay

What does that mean in practice? If you provide the CMake variable DTC_OVERLAY_FILE in your build, the board overlays will no longer be picked up automatically. For quick builds that might be helpful, but in case you’re building an application that should run on multiple boards, you should not use DTC_OVERLAY_FILE but maybe rather list additional overlays using EXTRA_DTC_OVERLAY_FILE.

Towards bindings

Now that we finally know what overlays are and how we can use them in our build, let’s find out what Zephyr produces for our own overlays in its devicetree_generated.h file.

Extending the example application

We start by creating our own overlay file dts/playground/props-basics.overlay:

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

In the development kit’s Devicetree source file nrf52840dk_nrf52840.dts, the &uart0 node has two properties: A property current-speed of type int, and the property status of type string. We’ve seen that Zephyr creates some output in devicetree_generated.h for the UART node, so let’s try the same with a custom node:

dts/playground/props-basics.overlay

/ {
  node_with_props {
    int = <1>;
    string = "foo";
  };
};

When running a build whilst specifying the path to the overlay using the CMake variable EXTRA_DTC_OVERLAY_FILE, we can verify that the overlay is indeed picked up by the build system, since it informs us about the overlays it found using the output Found devicetree overlay:

$ west build --no-sysbuild --board nrf52840dk/nrf52840 --build-dir ../build -- \
  -DEXTRA_DTC_OVERLAY_FILE="dts/playground/props-basics.overlay"
-- Found Dtc: /opt/nordic/ncs/toolchains/322ac893fe/bin/dtc (found suitable version "1.6.1", minimum required is "1.4.6")
-- Found BOARD.dts: /opt/nordic/ncs/v3.2.1/zephyr/boards/nordic/nrf52840dk/nrf52840dk_nrf52840.dts
-- Found devicetree overlay: dts/playground/props-basics.overlay
-- 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

Checking the output file build/zephyr/zephyr.dts we also see that our node_with_props can be found in the Devicetree that Zephyr is using as input for its generator script, that in turn produces devicetree_generated.h:

build/zephyr/zephyr.dts

/ {
  /* ... */
  node_with_props {
    int = < 0x1 >;
    string = "foo";
  };
};

Let’s see if we can find anything in devicetree_generated.h by searching for the quite unique value foo of our string property:

$ grep foo ../build/zephyr/include/generated/devicetree_generated.h
$

The search yields no results! Did we miss something? One thing that we can easily verify, is that the devicetree_generated.h at least contains our node: We can search using the node’s full path /node_with_props. You should find a large comment, separating the macros generated for our node, containing a list of definitions:

build/zephyr/include/generated/devicetree_generated.h

/*
 * Devicetree node: /node_with_props
 *
 * Node identifier: DT_N_S_node_with_props
 */

/* Node's full path: */
#define DT_N_S_node_with_props_PATH "/node_with_props"
// ---snip ---
/* (No generic property macros) */

The omitted lines contain lots of generic macros for our node. Don’t worry, we’ll skim through these soon enough. However, we won’t find anything that is even remotely related to the two properties that we defined for our node. In fact, the comment “(No generic property macros)” seems to hint that the generator did not encounter the properties int and string in our node /node_with_props.

Understanding Devicetree macro names

Before we dig deeper, let’s try to gain a better understanding of the macro names in devicetree_generated.h. Once we understand those, we should be able to know which macro (or macros) Zephyr should generate for our node’s properties.

In Zephyr’s doc folder, you can find the “RFC 7405 ABNF grammar for Devicetree macros” zephyr/doc/build/dts/macros.bnf. This RFC describes the macros that are directly generated out of the Devicetree sources. In simple words, the following rules apply (we’ve seen some of those already in the previous article, but it really can’t hurt to repeat them):

  • DT is the common prefix for Devicetree macros,
  • S is a forward slash /,
  • N refers to a node,
  • P is a property,
  • all letters are converted to lowercase,
  • and non-alphanumerics characters are converted to underscores “_

Let’s look at our favorite /soc/uart@40002000 node, specified in the nRF52840’s DTS file, and modified by the nRF52840 development kit’s DTS file:

zephyr/dts/arm/nordic/nrf52840.dtsi

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

zephyr/boards/nordic/nrf52840dk/nrf52840dk_nrf52840.dts

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

Following the previous rules, we can transform the paths and property names into tokens:

  • The node path /soc/uart@40002000 is transformed to _S_soc_S_uart_40002000.
  • The property name current-speed is transformed to current_speed.
  • The property name status stays the same.

Since node paths are unique, by combining the node’s path with its property names we can create a unique macro for each property - and that’s exactly what Zephyr’s Devicetree generator does. The leading _N_ is used to indicate that it is followed by a node’s path, _P_ separates the node’s path from its property. For uart@40002000, we get the following:

  • /soc/uart@40002000, property current-speed becomes N_S_soc_S_uart_40002000_P_current_speed
  • /soc/uart@40002000, property status becomes N_S_soc_S_uart_40002000_P_status

Adding the DT_ prefix, we can indeed find those macros in devicetree_generated:

$ grep -sw DT_N_S_soc_S_uart_40002000_P_current_speed \
  ../build/zephyr/include/generated/devicetree_generated.h
#define DT_N_S_soc_S_uart_40002000_P_current_speed 115200

$ grep -sw DT_N_S_soc_S_uart_40002000_P_status \
  ../build/zephyr/include/generated/devicetree_generated.h
#define DT_N_S_soc_S_uart_40002000_P_status "okay"

For our little demo overlay we can do the same:

dts/playground/props-basics.overlay

/ {
  node_with_props {
    int = <1>;
    string = "foo";
  };
};

For /node_with_props’ properties, the generator should create the following macros:

  • DT_N_S_node_with_props_P_int for the property int,
  • DT_N_S_node_with_props_P_string for the property string.

Thus, in our devicetree_generated.h we should be able to find the above macros - but we don’t:

$ grep -sw DT_N_S_node_with_props_P_int \
  ../build/zephyr/include/generated/devicetree_generated.h

$ grep -sw DT_N_S_node_with_props_P_string \
  ../build/zephyr/include/generated/devicetree_generated.h

Something’s still missing.

Matching compatible bindings

What’s the difference between /soc/uart@40002000 and our /node_with_props? Admittedly, there are lots of differences, but the significant difference is that /soc/uart@40002000 has a compatible property. compatible is a standard property defined in the DTSpec, so let’s look it up:

Property name compatible, value type <stringlist>.

The compatible property value consists of one or more strings that define the specific programming model for the device. This list of strings should be used by a client program for device driver selection. The property value consists of a concatenated list of null-terminated strings, from most specific to most general. They allow a device to express its compatibility with a family of similar devices […].

The recommended format is "manufacturer,model". […] The compatible string should consist only of lowercase letters, digits and dashes, and should start with a letter. […]

Let’s rephrase this: compatible is a list of strings, where each string is essentially a reference to some model. The DTSpec uses a specific term for such models: They are called bindings.

The DTSpec defines bindings as “requirements […] for how specific types and classes of devices are represented in the Devicetree.” In very simple words, a binding defines the properties that a node (or its children) can or even must have, their exact type, and their meaning.

How is this any different from what we’ve been doing until now in our Devicetree source files? Well, without a binding, we can give a node any number of properties and can assign any property any value of any type, as long as all nodes, properties, and values are syntactically sound. Also, any property name must be considered random. Let’s take our own node_with_props:

dts/playground/props-basics.overlay

/ {
  node_with_props {
    int = <1>;
    string = "foo";
  };
};

The Devicetree compiler won’t complain if we assign a string value to the property int. In fact, it doesn’t even know whether or not node_with_props should have the int property at all. We could delete it and the compiler won’t complain. We also don’t know what the purpose of int or string is, what they mean, or what they’re used for.

While the property names int or string are not very revealing as such, the same is also true for the current-speed property of the /soc/uart@40002000 node in the nRF52840 Devicetree: Without any additional information, we can only assume that this is the baud rate in bits/s, but we can’t know for sure.

By providing compatible bindings, we’re telling the Devicetree compiler to check whether the given node really matches the properties and types defined in a binding, and we’re telling it what to do with the information provided in the Devicetree. Bindings also add semantics to a Devicetree by giving properties a meaning.

The DTSpec includes some standard bindings, e.g., bindings for serial devices such as our UART device. It thus defines how serial devices need to look like in a Devicetree. This binding includes the current-speed property:

Field Details
Property current-speed
Value type <u32>
Description Specifies the current speed of a serial device in bits per second. A boot program should set this property if it has initialized the serial device.
Example 115,200 Baud: current-speed = <115200>;

Note: In case you’re wondering why current-speed is not listed in the “Standard Properties” section in the DTSpec, but all of a sudden appears in the section about “Serial devices” in the “Device Bindings”, there’s a very short answer for that: current-speed is only relevant for certain node types. It isn’t a property that is globally defined and can be applied to any kind of node.

Finally, we’re getting somewhere! We now know what the current-speed property is all about: We know its type, its physical unit, and its meaning. But this is just some table in the DTSpec, how is this information represented in Zephyr?

Bindings live outside the Devicetree. The DTSpec doesn’t specify any file format or syntax that is used for bindings. Zephyr, like Linux, uses .yaml files for its bindings. I’m assuming you’re familiar with YAML - in case you’re not, have a quick look at its online documentation.

Bindings in Zephyr

Zephyr’s official documentation provides a comprehensive explanation of the syntax used for bindings. Unless you’re adding your own devices and drivers, you’ll hardly ever need to create bindings yourself. Therefore, in this section, we’ll walk through some existing bindings in Zephyr to gain a general understanding, and then create our own example bindings in the next section.

Let’s have a look at what we can find out about /soc/uart@40002000’s current-speed and status properties in Zephyr: Via its compatible property, the node /soc/uart@40002000 claims compatibility with a binding nordic,nrf-uarte. The comma is used to specify a vendor prefix (we’ll see cover this when looking at binding names), so we’re looking for nordic’s model (binding) for nrf-uarte devices:

zephyr/dts/arm/nordic/nrf52840.dtsi

/ {
  soc {
    uart0: uart@40002000 {
      compatible = "nordic,nrf-uarte";
      /* ... */
    };
  };
};

Zephyr recursively looks for Devicetree bindings (.yaml files) in zephyr/dts/bindings. Bindings are matched against the strings provided in the compatible property of a node. Thus, for our UART node, Zephyr looks for a binding that matches nordic,nrf-uarte. Conveniently, binding files in Zephyr use the same basename as the compatible string, and thus we can find the correct binding by searching for a file called nordic,nrf-uarte.yaml, which can be found in the serial bindings subfolder:

zephyr/dts/bindings/serial/nordic,nrf-uarte.yaml

description: Nordic nRF family UARTE (UART with EasyDMA)
compatible: "nordic,nrf-uarte"
include: ["nordic,nrf-uart-common.yaml", "memory-region.yaml"]

Checking the compatible key, we see that it indeed matches the node’s compatible property. This compatible key is what Zephyr really matches the Node’s compatible property against: In theory, the file could have a different name, but by convention, the filename matches what’s in its compatible key. Zephyr doesn’t care about the convention.

Apart from compatible, the binding also has a textual description and some includes. As the name suggests, the include key allows to include the content of other bindings. Files are included by filename without specifying any paths or directories - Zephyr determines the search paths, one of which includes all subfolders of zephyr/dts/bindings. For the exact syntax, have a look at the official documentation.

The contents of included files are essentially merged using a recursive dictionary merge. In short: Everything that’s declared in included bindings is available in the including file.

Note: In case you’re wondering what happens with duplicated keys and/or values, try it out based on what we’ll learn in the next section, where we’ll create our own bindings.

Since nordic,nrf-uarte.yaml doesn’t seem to define anything related to our properties - just like in the article about Kconfig - we once again have to follow the include tree. Let’s try nordic,nrf-uart-common.yaml:

zephyr/dts/bindings/serial/nordic,nrf-uart-common.yaml

include: [uart-controller.yaml, pinctrl-device.yaml]
properties:
  # --snip--
  current-speed:
    description: |
      Initial baud rate setting for UART. Only a fixed set of baud
      rates are selectable on these devices.
    enum:
      - 1200
      - 2400
      # --snip--
      - 921600
      - 1000000

This must be it! There’s now a key properties, with a child element that matches our current-speed property. And sure enough, the properties key is used to define all properties for nodes with the matching binding may or must contain.

Here, Nordic seems to restrict the allowed baud rates using an enumeration: We can only specify values from the given list. But how does the Devicetree compiler know the type of the property? Couldn’t we also use an enumeration to pre-define strings? Yes, we could, and there’s indeed something missing: A key for the property’s type.

To find out the property’s type, we need to step further down the include tree, into uart-controller.yaml. This is Zephyr’s base model for UART controllers, which is used regardless of the actual vendor:

zephyr/dts/bindings/serial/uart-controller.yaml

include: base.yaml

bus: uart

properties:
  # --snip--
  current-speed:
    type: int
    description: Initial baud rate setting for UART
  # --snip--

Now, we finally know that current-speed is of type int and is used to configure the initial baud rate setting for UART (though the description fails to mention that the baud rate is specified in bits per second).

Combining this with the information in nordic,nrf-uart-common.yaml, we know that we can only select from a pre-defined list of baud rates and cannot specify our own custom baud rate - at least not in the Devicetree. Given this binding, the Devicetree compiler now rejects any but the allowed values, and it is therefore not possible to specify a syntactically correct value that is not an integer of the given list.

Note: bus: uart is a special key/value pair that allows you to associate devices with a bus system, e.g., I2C, SPI or UART. This feature is especially useful if a device supports multiple bus types, e.g., a sensor that can be connected either via SPI or I2C. This is out of the scope of this article, though, but is explained nicely in the official documentation.

We now know about current-speed, but what about status? As you might have guessed, we need to take yet another step down the include tree and have a look at the base.yml binding. This binding contains common fields used by all devices in Zephyr. Here, we do not only encounter the status property but also compatible:

zephyr/dts/bindings/base/base.yaml

include: [pm.yaml]

properties:
  status:
    type: string
    description: indicates the operational status of a device
    enum:
      - "ok" # Deprecated form
      - "okay"
      - "disabled"
      - "reserved"
      - "fail"
      - "fail-sss"

  compatible:
    type: string-array
    required: true
    description: compatible strings
  # --snip--

status therefore is simply a property of type string with pre-defined values that can be assigned in the Devicetree. It indicates the operational status of a device, which we’ll see in action the practical example in the next article. You can, however, imagine that a device with the status “disabled” won’t be functioning.

While going through binding files may seem tedious, the include tree depth for bindings is usually quite limited, and once you know about the base bindings such as base.yaml, it typically comes down to a handful of files. There is, however, no conveniently “flattened” output file like build/zephyr/zephyr.dts, and therefore it is always necessary to walk through the bindings. We’ll have a quick look at Nordic’s plugin for Visual Studio Code in the next article, but thus far you’ll always need to look at the source files. The following shows the include tree of nordic,nrf-uarte.yaml at the time of writing:

nordic,nrf-uarte.yaml
├── nordic,nrf-uart-common.yaml
│   ├── uart-controller.yaml
│   │   └── base.yaml
│   │       └── pm.yaml
│   └── pinctrl-device.yaml
└── memory-region.yaml

There’s one more important thing about compatible and matching bindings: If a node has more than one string in its compatible property, the build system looks for compatible bindings in the listed order and uses the first match.

Bindings directory

Before we can go ahead and experiment using our own bindings, we need to know where to place them. Just like the dts/bindings in Zephyr’s root directory, the build process also picks up any bindings in dts/bindings in the application’s root directory.

In contrast to overlays, however, detected bindings are not listed in the build output.

Bindings by example

This is it, we’re finally there! We’ll now add bindings for our extended example application to get some generated output for our node’s properties. We’ve just seen that we can place our bindings in the dts/bindings directory.

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

Naming

Before we start, we need to solve one of the hardest problems in engineering: Finding a good name for our bindings. The Devicetree specification contains a guideline on the value for the compatible property of a node - and therefore the name of the binding:

“The compatible string should consist only of lowercase letters, digits, and dashes, and should start with a letter. A single comma is typically only used following a vendor prefix. Underscores should not be used.” DTSpec

Let’s try this with the binding name custom,props-basic, and thus vendor prefix custom. We’ll follow the convention and use the binding’s name as filename and create a new file custom,props-basics.yaml in the application’s dts/bindings directory:

$ tree --charset=utf-8 --dirsfirst.
├── dts
│   ├── bindings
│   │   └── custom,props-basics.yaml
│   └── playground
│       └── props-basics.overlay
├── src
│   └── main.c
├── CMakeLists.txt
└── prj.conf

In our binding, we set the compatible key as "custom,props-basics", and add the two properties int and string of the matching type, without providing any description:

dts/bindings/custom,props-basics.yaml

description: Custom properties
compatible: "custom,props-basics"

properties:
  int:
    type: int
  string:
    type: string

As we’ve seen before, bindings define a node’s properties under the key properties. The template given below is the simplest form for a property in a binding:

properties:
  <property-name>:
    type: <property-type>
    # required: false -> omitted by convention if false

Properties have a name and are therefore unique within the node, and each property is assigned a type using the corresponding key. Other keys such as required are optional.

There are several other keys and “features”, e.g., it is possible to define the properties for children of a node with the matching compatible property, but we’ll only have a look at the very basics. Definitely dive into Zephyr’s official documentation once you’re through with this article!

Finally, we create a new property compatible = "custom,props-basic" for our existing node_with_props

dts/playground/props-basics.overlay

/ {
  node_with_props {
    compatible = "custom,props-basic"
    int = <1>;
    string = "foo bar baz";
  };
};

… and recompile:

$ rm -rf ../build
$ west build --no-sysbuild --board nrf52840dk/nrf52840 --build-dir ../build -- \
  -DEXTRA_DTC_OVERLAY_FILE="dts/playground/props-basics.overlay"
-- Found devicetree overlay: dts/playground/props-basics.overlay
node '/node_with_props' compatible 'custom,props-basics' has unknown vendor prefix 'custom'
-- Generated zephyr.dts: /path/to/build/zephyr/zephyr.dts
-- Generated devicetree_generated.h: /path/to/build/zephyr/include/generated/devicetree_generated.h

Note: As mentioned, at the time of writing and in contrast to overlay files, bindings are not listed in the build output, not even for bindings in the application’s dts/bindings directory.

Even though the build passes, it seems that the Devicetree compiler is not too happy about our “custom” vendor prefix. Zephyr warns us here since it maintains a “Devicetree binding vendor prefix registry” zephyr/dts/bindings/vendor-prefixes.txt to avoid name-space collisions for properties and bindings. If you’re a vendor, you can of course add your name upstream, but for this article, we’ll simply skip the vendor prefix and use the binding name custom-props-basics instead.

$ mv dts/bindings/custom,props-basics.yaml dts/bindings/custom-props-basics.yaml
$ sed -i .bak 's/custom,/custom-/g' dts/bindings/custom-props-basics.yaml
$ sed -i .bak 's/custom,/custom-/g' dts/playground/props-basics.overlay
$ rm dts/**/*.bak

$ tree --charset=utf-8 --dirsfirst.
├── dts
│   ├── bindings
│   │   └── custom-props-basics.yaml
│   └── playground
│       └── props-basics.overlay
├── src
│   └── main.c
├── CMakeLists.txt
└── prj.conf

After recompiling, we can check whether the generator script has added our properties to devicetree_generated.h. The value foo is unique enough for a quick grep; for our property int we’ll use what we’ve learned and expect to find some macro containing node_with_props_P_int. And indeed, we finally have our generated output!

$ grep foo ../build/zephyr/include/generated/devicetree_generated.h
#define DT_N_S_node_with_props_P_string "foo bar baz"
#define DT_N_S_node_with_props_P_string_STRING_UNQUOTED foo bar baz
#define DT_N_S_node_with_props_P_string_STRING_TOKEN foo_bar_baz
$ grep node_with_props_P_int ../build/zephyr/include/generated/devicetree_generated.h
#define DT_N_S_node_with_props_P_int 1
#define DT_N_S_node_with_props_P_int_EXISTS 1

We’ll learn how to use those macros in the section about Zephyr’s Devicetree API. In this section, we just make sure that our bindings lead to some generated output for all supported types. In case you can’t wait and want to have a more detailed look instead, I suggest you have a look at Zephyr’s great introduction to Devicetree bindings.

Note: If you’re experimenting and don’t see any output, make sure that the compatible property is set correctly. The Devicetree compiler does not complain in case it doesn’t find a matching binding. E.g., in case you have a typo in your compatible property, the application builds without warnings, but devicetree_generated.h won’t have any content for the desired properties.

Basic types

In the previous article, we’ve seen all of the types supported by devicetrees in Zephyr. We’ll now use the same node, but extend it by two properties called enum-int and enum-string, since enumerations are represented differently in bindings. In the previous article, we’ve seen that Zephyr’s Devicetree generator ignores value labels. We’ll throw in two of those, named second_value and string_value, for good practice, and also add the label_with_props for /node_with_props:

dts/playground/props-basics.overlay

/ {
  label_with_props: node_with_props {
    compatible = "custom-props-basics";
    existent-boolean;
    int = <1>;
    array = <1 second_value: 2 3>;
    uint8-array = [ 12 34 ];
    string = string_value: "foo bar baz";
    string-array = "foo", "bar", "baz";
    enum-int = <200>;
    enum-string = "whatever";
  };
};

Now we need to extend our binding by adding an entry for each of the node’s properties under the properties key:

dts/bindings/custom-props-basics.yaml

description: Custom properties
compatible: "custom-props-basics"

properties:
  existent-boolean:
    type: boolean
  int:
    type: int
    required: true
  array:
    type: array
  uint8-array:
    type: uint8-array
  string:
    type: string
  string-array:
    type: string-array
  enum-int:
    type: int
    enum:
      - 100
      - 200
      - 300
  enum-string:
    type: string
    enum:
      - "whatever"
      - "works"

After recompiling …

$ rm -rf ../build
$ west build --no-sysbuild --board nrf52840dk/nrf52840 --build-dir ../build -- \
  -DEXTRA_DTC_OVERLAY_FILE="dts/playground/props-basics.overlay"

… we can now find macros for all of our properties in devicetree_generated.h for the node /node_with_props, and the comment block also indicates which binding was selected to generate the macros.

build/zephyr/include/generated/devicetree_generated.h

/*
 * Devicetree node: /node_with_props
 *
 * Node identifier: DT_N_S_node_with_props
 *
 * Binding (compatible = custom-props-basics):
 *   /path/to/dts/bindings/custom-props-basics.yaml
 */

/* Node's full path: */
#define DT_N_S_node_with_props_PATH "/node_with_props"
/* --snip-- */
/* Generic property macros: */
/* --snip-- */

Let’s have a look at the generated macros listed beyond the marker Generic property macros.

boolean

For the property existent-boolean of type boolean, the Devicetree generator produces the following macros:

#define DT_N_S_node_with_props_P_existent_boolean 1
#define DT_N_S_node_with_props_P_existent_boolean_EXISTS 1

Using what we’ve learned in the section about understanding Devicetree macro names, we can easily understand the basename DT_N_S_node_with_props_P_existent_boolean of the generated macros:

DT is the Devicetree prefix, N indicates that what follows is a node’s path, S is a forward slash /, and finally P indicates the start of a property. Thus DT_N_S_node_with_props_P_existent_boolean essentially translates to node=/node_with_props, property=existent_boolean.

Since the existent-boolean property is present in the node in our overlay, its value translates to 1. If we’d remove the property from our node, we’d end up with the following:

#define DT_N_S_node_with_props_P_existent_boolean 0
#define DT_N_S_node_with_props_P_existent_boolean_EXISTS 1

Thus, the value of booleans is 0 if the property is false, or 1 if it is true.

What about _EXISTS? Remember that any path or property name is transformed to its lowercase form in the Devicetree macros. _EXIST is all uppercase, which indicates that it isn’t something that we defined, but a macro that is generated for use with the Devicetree API.

For properties of any type but booleans, Zephyr’s Devicetree generator creates a matching _EXISTS macro only if the property exists in the Devicetree. If a property is not present, no macros are generated. Booleans are an exception where this macro is always generated since a missing property value means that the property is set to false.

Note: In case you’re wondering if it is possible to unset or delete a boolean that is defined somewhere else in the Devicetree - yes it is, and we’ll try it out in the section “deleting properties”.

int

For the property int of type int of our node /node_with_props, Zephyr’s Devicetree generator produces the following macros:

#define DT_N_S_node_with_props_P_int 1
#define DT_N_S_node_with_props_P_int_EXISTS 1

Unsurprisingly, for properties of type int the value of the macro is an integer literal (in decimal format). As mentioned in the previous section, the macro _EXISTS is created for every property that exists in the Devicetree. If we tried to remove the property from our Devicetree node, however, we’d get the following error when rebuilding since we specified int as a required property in our binding:

...
-- Found devicetree overlay: dts/playground/props-basics.overlay
devicetree error: 'int' is marked as required in 'properties:' in /path/to/dts/bindings/custom-props-basics.yaml, but does not appear in <Node /node_with_props in '/opt/nordic/ncs/v3.2.1/zephyr/misc/empty_file.c'>

If we’d remove required: true from the binding file and delete the node’s property in the overlay, both macros would indeed be removed from devicetree_generated.h; any search for N_S_node_with_props_P_int would fail.

Note: Knowing how macros work in C, you might be curious why Zephyr removes the _EXISTS macro instead of defining its value to 0. After all, without using the compile time switches #ifdef or #if defined() you can’t check a macro’s value if the macro is not defined - or can you? Turns out there is a neat trick that allows to do this, and Zephyr makes use of it, but we won’t go into detail about this trick in this article. If you still want to know how this works, have a look at the documentation of the IS_ENABLED macro in zephyr/include/zephyr/sys/util_macro.h and the macros it expands to. It is explained nicely in the macros’ documentation!

Note: In case you specify the value of a node’s property of type int in the Devicetree in the hexadecimal format, at the time of writing the integer literal is converted to its decimal value in devicetree_generated.h.

array and uint8-array

The following macros are produced for our array property of type array:

#define DT_N_S_node_with_props_P_array {10 /* 0xa */, 11 /* 0xb */, 12 /* 0xc */}
#define DT_N_S_node_with_props_P_array_IDX_0 10
#define DT_N_S_node_with_props_P_array_IDX_0_EXISTS 1
#define DT_N_S_node_with_props_P_array_IDX_1 11
#define DT_N_S_node_with_props_P_array_IDX_1_EXISTS 1
#define DT_N_S_node_with_props_P_array_IDX_2 12
#define DT_N_S_node_with_props_P_array_IDX_2_EXISTS 1
/* array_FOREACH_ ... */
#define DT_N_S_node_with_props_P_array_LEN 3
#define DT_N_S_node_with_props_P_array_EXISTS 1

For properties of the type array and uint8-array, the Devicetree generator produces initializer expressions in braces, whose elements are integer literals. In the Devicetree API section, we’ll use those expressions as, well, initialization values for our variables or constants. The macro with the suffix _LEN defines the number of elements in the array.

For each element and its position n within the array, the generator also produces the macros _IDX_n and _IDX_n_EXISTS. In addition, several _FOREACH macros (hidden in the snippet) are generated that expand an expression for each element in the array at compile time.

For the node’s uint8-array property with the corresponding type, a similar set of macros is generated. Only the _FOREACH macros are slightly different, but that doesn’t concern us right now (in case you’re still curious, check out the documentation of DT_FOREACH_PROP_ELEM in the Zephyr’s Devicetree API documentation).

#define DT_N_S_node_with_props_P_uint8_array {18 /* 0x12 */, 52 /* 0x34 */}
#define DT_N_S_node_with_props_P_uint8_array_IDX_0 18
#define DT_N_S_node_with_props_P_uint8_array_IDX_0_EXISTS 1
#define DT_N_S_node_with_props_P_uint8_array_IDX_1 52
#define DT_N_S_node_with_props_P_uint8_array_IDX_1_EXISTS 1
/* --snip-- uint8_array_FOREACH_ ... */
#define DT_N_S_node_with_props_P_uint8_array_LEN 2
#define DT_N_S_node_with_props_P_uint8_array_EXISTS 1

Just like for our int property, if we’d remove the array or uint8-array property from our node in the Devicetree, no macros (not even the _EXISTS macros) are generated for the property.

string

The following is a snipped of our devicetree_generated.h for the property string:

#define DT_N_S_node_with_props_P_string "foo bar baz"
#define DT_N_S_node_with_props_P_string_STRING_UNQUOTED foo bar baz
#define DT_N_S_node_with_props_P_string_STRING_TOKEN foo_bar_baz
#define DT_N_S_node_with_props_P_string_STRING_UPPER_TOKEN FOO_BAR_BAZ
/* --snip-- string_FOREACH_ ... */
#define DT_N_S_node_with_props_P_string_EXISTS 1

For properties of the type string, the Devicetree generator produces string literals. The generator also produces macros with a special suffix:

  • _STRING_UNQUOTED contains the string literals without quotes and thus all values as tokens.
  • _STRING_TOKEN produces a single token out of the string literals. Special characters and spaces are replaced by underscores.
  • _STRING_UPPER_TOKEN produces the same token as _STRING_TOKEN, but in uppercase letters.

In addition, the generator also produces _FOREACH macros, which expand for each character in the string. E.g., for our value “foo bar baz” with the string length 11, the _FOREACH macro would expand 11 times.

Note: The characters used b