Devicetree HOWTOs

This page has step-by-step advice for getting things done with devicetree.

Get your devicetree and generated header

A board’s devicetree (BOARD.dts) pulls in common node definitions via #include preprocessor directives. This at least includes the SoC’s .dtsi. One way to figure out the devicetree’s contents is by opening these files, e.g. by looking in dts/<ARCH>/<vendor>/<soc>.dtsi, but this can be time consuming.

If you just want to see the “final” devicetree for your board, build an application and open the zephyr.dts file in the build directory.

Tip

You can build Hello World to see the “base” devicetree for your board without any additional changes from overlay files.

For example, using the ARM Cortex-M3 Emulation (QEMU) board to build Hello World:

# --cmake-only here just forces CMake to run, skipping the
# build process to save time.
west build -b qemu_cortex_m3 -s samples/hello_world --cmake-only

You can change qemu_cortex_m3 to match your board.

CMake prints the input and output file locations like this:

-- Found BOARD.dts: .../zephyr/boards/arm/qemu_cortex_m3/qemu_cortex_m3.dts
-- Generated zephyr.dts: .../zephyr/build/zephyr/zephyr.dts
-- Generated devicetree_unfixed.h: .../zephyr/build/zephyr/include/generated/devicetree_unfixed.h

The zephyr.dts file is the final devicetree in DTS format.

The devicetree_unfixed.h file is the corresponding generated header.

See Input and output files for details about these files.

Get a struct device from a devicetree node

When writing Zephyr applications, you’ll often want to get a driver-level struct device corresponding to a devicetree node.

For example, with this devicetree fragment, you might want the struct device for serial@40002000:

/ {
        soc {
                serial0: serial@40002000 {
                        status = "okay";
                        current-speed = <115200>;
                        /* ... */
                };
        };

        aliases {
                my-serial = &serial0;
        };

        chosen {
                zephyr,console = &serial0;
        };
};

Start by making a node identifier for the device you are interested in. There are different ways to do this; pick whichever one works best for your requirements. Here are some examples:

/* Option 1: by node label */
#define MY_SERIAL DT_NODELABEL(serial0)

/* Option 2: by alias */
#define MY_SERIAL DT_ALIAS(my_serial)

/* Option 3: by chosen node */
#define MY_SERIAL DT_CHOSEN(zephyr_console)

/* Option 4: by path */
#define MY_SERIAL DT_PATH(soc, serial_40002000)

Once you have a node identifier there are two ways to proceed. The classic way is to get the struct device by combining DT_LABEL() with device_get_binding():

const struct device *uart_dev = device_get_binding(DT_LABEL(MY_SERIAL));

You can then use uart_dev with UART API functions like uart_configure(). Similar code will work for other device types; just make sure you use the correct API for the device.

There’s no need to override the label property to something else: just make a node identifier and pass it to DT_LABEL to get the right string to pass to device_get_binding().

The second way to get a device is to use DEVICE_DT_GET():

const struct device *uart_dev = DEVICE_DT_GET(MY_SERIAL);

if (!device_is_ready(uart_dev)) {
        /* Not ready, do not use */
        return -ENODEV;
}

This idiom fetches the device pointer at build-time, which is useful when you want to store the device pointer as configuration data. But because the device may not be initialized, or may have failed to initialize, you must verify that the device is ready to be used before passing it to any API functions. (This check is done for you by device_get_binding().)

If you’re having trouble, see Troubleshoot devicetree issues. The first thing to check is that the node has status = "okay", like this:

#define MY_SERIAL DT_NODELABEL(my_serial)

#if DT_NODE_HAS_STATUS(MY_SERIAL, okay)
const struct device *uart_dev = device_get_binding(DT_LABEL(MY_SERIAL));
#else
#error "Node is disabled"
#endif

If you see the #error output, make sure to enable the node in your devicetree. If you don’t see the #error but uart_dev is NULL, then there’s likely either a Kconfig issue preventing the device driver from creating the device, or the device’s initialization function failed.

Find a devicetree binding

Devicetree binding YAML files document what you can do with the nodes they describe, so it’s critical to be able to find them for the nodes you are using.

If you don’t have them already, Get your devicetree and generated header. To find a node’s binding, open the generated header file, which starts with a list of nodes in a block comment:

/*
 * [...]
 * Nodes in dependency order (ordinal and path):
 *   0   /
 *   1   /aliases
 *   2   /chosen
 *   3   /flash@0
 *   4   /memory@20000000
 *          (etc.)
 * [...]
 */

Make note of the path to the node you want to find, like /flash@0. Search for the node’s output in the file, which starts with something like this if the node has a matching binding:

/*
 * Devicetree node:
 *   /flash@0
 *
 * Binding (compatible = soc-nv-flash):
 *   $ZEPHYR_BASE/dts/bindings/mtd/soc-nv-flash.yaml
 * [...]
 */

See Check for missing bindings for troubleshooting.

Set devicetree overlays

Devicetree overlays are explained in Introduction to devicetree. The CMake variable DTC_OVERLAY_FILE contains a space- or semicolon-separated list of overlays. If DTC_OVERLAY_FILE specifies multiple files, they are included in that order by the C preprocessor.

Here are some ways to set it:

  1. on the cmake build command line (-DDTC_OVERLAY_FILE="file1.overlay;file2.overlay")

  2. with the CMake set() command in the application CMakeLists.txt, before including zephyr’s boilerplate.cmake file

  3. using a DTC_OVERLAY_FILE environment variable (deprecated)

  4. create a boards/<BOARD>_<revision>.overlay file in the application folder for the current board revision. This requires that the board supports multiple revisions, see Multiple board revisions. The boards/<BOARD>_<revision>.overlay file will be merged with boards/<BOARD>.overlay if this file also exists.

  5. create a boards/<BOARD>.overlay file in the application folder, for the current board

  6. create a <BOARD>.overlay file in the application folder

Here is an example using west build. However you set the value, it is saved in the CMake cache between builds.

The build system prints all the devicetree overlays it finds in the configuration phase, like this:

-- Found devicetree overlay: .../some/file.overlay

Use devicetree overlays

See Set devicetree overlays for how to add an overlay to the build.

Overlays can override node property values in multiple ways. For example, if your BOARD.dts contains this node:

/ {
        soc {
                serial0: serial@40002000 {
                        status = "okay";
                        current-speed = <115200>;
                        /* ... */
                };
        };
};

These are equivalent ways to override the current-speed value in an overlay:

/* Option 1 */
&serial0 {
     current-speed = <9600>;
};

/* Option 2 */
&{/soc/serial@40002000} {
     current-speed = <9600>;
};

We’ll use the &serial0 style for the rest of these examples.

You can add aliases to your devicetree using overlays: an alias is just a property of the /aliases node. For example:

/ {
     aliases {
             my-serial = &serial0;
     };
};

Chosen nodes work the same way. For example:

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

To delete a property (in addition to deleting properties in general, this is how to set a boolean property to false if it’s true in BOARD.dts):

&serial0 {
     /delete-property/ some-unwanted-property;
};

You can add subnodes using overlays. For example, to configure a SPI or I2C child device on an existing bus node, do something like this:

/* SPI device example */
&spi1 {
     my_spi_device: temp-sensor@0 {
             compatible = "...";
             label = "TEMP_SENSOR_0";
             /* reg is the chip select number, if needed;
              * If present, it must match the node's unit address. */
             reg = <0>;

             /* Configure other SPI device properties as needed.
              * Find your device's DT binding for details. */
             spi-max-frequency = <4000000>;
     };
};

/* I2C device example */
&i2c2 {
     my_i2c_device: touchscreen@76 {
             compatible = "...";
             label = "TOUCHSCREEN";
             /* reg is the I2C device address.
              * It must match the node's unit address. */
             reg = <76>;

             /* Configure other I2C device properties as needed.
              * Find your device's DT binding for details. */
     };
};

Other bus devices can be configured similarly:

  • create the device as a subnode of the parent bus

  • set its properties according to its binding

Assuming you have a suitable device driver associated with the my_spi_device and my_i2c_device compatibles, you should now be able to enable the driver via Kconfig and get the struct device for your newly added bus node, then use it with that driver API.

Write device drivers using devicetree APIs

“Devicetree-aware” device drivers should create a struct device for each status = "okay" devicetree node with a particular compatible (or related set of compatibles) supported by the driver.

Note

Historically, Zephyr has used Kconfig options like CONFIG_I2C_0 and CONFIG_I2C_1 to enable driver support for individual devices of some type. For example, if CONFIG_I2C_1=y, the SoC’s I2C peripheral driver would create a struct device for “I2C bus controller number 1”.

This style predates support for devicetree in Zephyr and its use is now discouraged. Existing device drivers may be made “devicetree-aware” in future releases.

Writing a devicetree-aware driver begins by defining a devicetree binding for the devices supported by the driver. Use existing bindings from similar drivers as a starting point. A skeletal binding to get started needs nothing more than this:

description: <Human-readable description of your binding>
compatible: "foo-company,bar-device"
include: base.yaml

See Find a devicetree binding for more advice on locating existing bindings.

After writing your binding, your driver C file can then use the devicetree API to find status = "okay" nodes with the desired compatible, and instantiate a struct device for each one. There are two options for instantiating each struct device: using instance numbers, and using node labels.

In either case:

  • Each struct device‘s name should be set to its devicetree node’s label property. This allows the driver’s users to Get a struct device from a devicetree node in the usual way.

  • Each device’s initial configuration should use values from devicetree properties whenever practical. This allows users to configure the driver using devicetree overlays.

Examples for how to do this follow. They assume you’ve already implemented the device-specific configuration and data structures and API functions, like this:

/* my_driver.c */
#include <drivers/some_api.h>

/* Define data (RAM) and configuration (ROM) structures: */
struct my_dev_data {
     /* per-device values to store in RAM */
};
struct my_dev_cfg {
     uint32_t freq; /* Just an example: initial clock frequency in Hz */
     /* other configuration to store in ROM */
};

/* Implement driver API functions (drivers/some_api.h callbacks): */
static int my_driver_api_func1(const struct device *dev, uint32_t *foo) { /* ... */ }
static int my_driver_api_func2(const struct device *dev, uint64_t bar) { /* ... */ }
static struct some_api my_api_funcs = {
     .func1 = my_driver_api_func1,
     .func2 = my_driver_api_func2,
};

Option 1: create devices using instance numbers

Use this option, which uses Instance-based APIs, if possible. However, they only work when devicetree nodes for your driver’s compatible are all equivalent, and you do not need to be able to distinguish between them.

To use instance-based APIs, begin by defining DT_DRV_COMPAT to the lowercase-and-underscores version of the compatible that the device driver supports. For example, if your driver’s compatible is "vnd,my-device" in devicetree, you would define DT_DRV_COMPAT to vnd_my_device in your driver C file:

/*
 * Put this near the top of the file. After the includes is a good place.
 * (Note that you can therefore run "git grep DT_DRV_COMPAT drivers" in
 * the zephyr Git repository to look for example drivers using this style).
 */
#define DT_DRV_COMPAT vnd_my_device

Important

As shown, the DT_DRV_COMPAT macro should have neither quotes nor special characters. Remove quotes and convert special characters to underscores when creating DT_DRV_COMPAT from the compatible property.

Finally, define an instantiation macro, which creates each struct device using instance numbers. Do this after defining my_api_funcs.

/*
 * This instantiation macro is named "CREATE_MY_DEVICE".
 * Its "inst" argument is an arbitrary instance number.
 *
 * Put this near the end of the file, e.g. after defining "my_api_funcs".
 */
#define CREATE_MY_DEVICE(inst)                                       \
     static struct my_dev_data my_data_##inst = {                    \
             /* initialize RAM values as needed, e.g.: */            \
             .freq = DT_INST_PROP(inst, clock_frequency),            \
     };                                                              \
     static const struct my_dev_cfg my_cfg_##inst = {                \
             /* initialize ROM values as needed. */                  \
     };                                                              \
     DEVICE_DT_INST_DEFINE(inst,                                     \
                           my_dev_init_function,                     \
                           NULL,                                     \
                           &my_data_##inst,                          \
                           &my_cfg_##inst,                           \
                           MY_DEV_INIT_LEVEL, MY_DEV_INIT_PRIORITY,  \
                           &my_api_funcs);

Notice the use of APIs like DT_INST_PROP() and DEVICE_DT_INST_DEFINE() to access devicetree node data. These APIs retrieve data from the devicetree for instance number inst of the node with compatible determined by DT_DRV_COMPAT.

Finally, pass the instantiation macro to DT_INST_FOREACH_STATUS_OKAY():

/* Call the device creation macro for each instance: */
DT_INST_FOREACH_STATUS_OKAY(CREATE_MY_DEVICE)

DT_INST_FOREACH_STATUS_OKAY expands to code which calls CREATE_MY_DEVICE once for each enabled node with the compatible determined by DT_DRV_COMPAT. It does not append a semicolon to the end of the expansion of CREATE_MY_DEVICE, so the macro’s expansion must end in a semicolon or function definition to support multiple devices.

Option 2: create devices using node labels

Some device drivers cannot use instance numbers. One example is an SoC peripheral driver which relies on vendor HAL APIs specialized for individual IP blocks to implement Zephyr driver callbacks. Cases like this should use DT_NODELABEL() to refer to individual nodes in the devicetree representing the supported peripherals on the SoC. The devicetree.h Generic APIs can then be used to access node data.

For this to work, your SoC’s dtsi file must define node labels like mydevice0, mydevice1, etc. appropriately for the IP blocks your driver supports. The resulting devicetree usually looks something like this:

/ {
        soc {
                mydevice0: dev@0 {
                        compatible = "vnd,my-device";
                };
                mydevice1: dev@1 {
                        compatible = "vnd,my-device";
                };
        };
};

The driver can use the mydevice0 and mydevice1 node labels in the devicetree to operate on specific device nodes:

/*
 * This is a convenience macro for creating a node identifier for
 * the relevant devices. An example use is MYDEV(0) to refer to
 * the node with label "mydevice0".
 */
#define MYDEV(idx) DT_NODELABEL(mydevice ## idx)

/*
 * Define your instantiation macro; "idx" is a number like 0 for mydevice0
 * or 1 for mydevice1. It uses MYDEV() to create the node label from the
 * index.
 */
#define CREATE_MY_DEVICE(idx)                                        \
     static struct my_dev_data my_data_##idx = {                     \
             /* initialize RAM values as needed, e.g.: */            \
             .freq = DT_PROP(MYDEV(idx), clock_frequency),           \
     };                                                              \
     static const struct my_dev_cfg my_cfg_##idx = { /* ... */ };    \
     DEVICE_DT_DEFINE(MYDEV(idx),                                    \
                     my_dev_init_function,                           \
                     NULL,                                           \
                     &my_data_##idx,                                 \
                     &my_cfg_##idx,                                  \
                     MY_DEV_INIT_LEVEL, MY_DEV_INIT_PRIORITY,        \
                     &my_api_funcs)

Notice the use of APIs like DT_PROP() and DEVICE_DT_DEFINE() to access devicetree node data.

Finally, manually detect each enabled devicetree node and use CREATE_MY_DEVICE to instantiate each struct device:

#if DT_NODE_HAS_STATUS(DT_NODELABEL(mydevice0), okay)
CREATE_MY_DEVICE(0)
#endif

#if DT_NODE_HAS_STATUS(DT_NODELABEL(mydevice1), okay)
CREATE_MY_DEVICE(1)
#endif

Since this style does not use DT_INST_FOREACH_STATUS_OKAY(), the driver author is responsible for calling CREATE_MY_DEVICE() for every possible node, e.g. using knowledge about the peripherals available on supported SoCs.

Device drivers that depend on other devices

At times, one struct device depends on another struct device and requires a pointer to it. For example, a sensor device might need a pointer to its SPI bus controller device. Some advice:

Search existing bindings and device drivers for examples.

Applications that depend on board-specific devices

One way to allow application code to run unmodified on multiple boards is by supporting a devicetree alias to specify the hardware specific portions, as is done in the Blinky. The application can then be configured in BOARD.dts files or via devicetree overlays.

Troubleshoot devicetree issues

Here are some tips for fixing misbehaving devicetree code.

Try again with a pristine build directory

See Pristine Builds for examples, or just delete the build directory completely and retry.

This is general advice which is especially applicable to debugging devicetree issues, because the outputs are created at CMake configuration time, and are not always regenerated when one of their inputs changes.

Make sure <devicetree.h> is included

Unlike Kconfig symbols, the devicetree.h header must be included explicitly.

Many Zephyr header files rely on information from devicetree, so including some other API may transitively include devicetree.h, but that’s not guaranteed.

Make sure you’re using the right names

Remember that:

  • In C/C++, devicetree names must be lowercased and special characters must be converted to underscores. Zephyr’s generated devicetree header has DTS names converted in this way into the C tokens used by the preprocessor-based <devicetree.h> API.

  • In overlays, use devicetree node and property names the same way they would appear in any DTS file. Zephyr overlays are just DTS fragments.

For example, if you’re trying to get the clock-frequency property of a node with path /soc/i2c@12340000 in a C/C++ file:

/*
 * foo.c: lowercase-and-underscores names
 */

/* Don't do this: */
#define MY_CLOCK_FREQ DT_PROP(DT_PATH(soc, i2c@1234000), clock-frequency)
/*                                           ^               ^
 *                                        @ should be _     - should be _  */

/* Do this instead: */
#define MY_CLOCK_FREQ DT_PROP(DT_PATH(soc, i2c_1234000), clock_frequency)
/*                                           ^               ^           */

And if you’re trying to set that property in a devicetree overlay:

/*
 * foo.overlay: DTS names with special characters, etc.
 */

/* Don't do this; you'll get devicetree errors. */
&{/soc/i2c_12340000/} {
     clock_frequency = <115200>;
};

/* Do this instead. Overlays are just DTS fragments. */
&{/soc/i2c@12340000/} {
     clock-frequency = <115200>;
};

Validate properties

If you’re getting a compile error reading a node property, check your node identifier and property. For example, if you get a build error on a line that looks like this:

int baud_rate = DT_PROP(DT_NODELABEL(my_serial), current_speed);

Try checking the node by adding this to the file and recompiling:

#if !DT_NODE_EXISTS(DT_NODELABEL(my_serial))
#error "whoops"
#endif

If you see the “whoops” error message when you rebuild, the node identifier isn’t referring to a valid node. Get your devicetree and generated header and debug from there.

Some hints for what to check next if you don’t see the “whoops” error message:

Check for missing bindings

If the build fails to Find a devicetree binding for a node, then either the node’s compatible property is not defined, or its value has no matching binding. If the property is set, check for typos in its name. In a devicetree source file, compatible should look like "vnd,some-device"Make sure you’re using the right names.

If your binding file is not under zephyr/dts, you may need to set DTS_ROOT.

Errors with DT_INST_() APIs

If you’re using an API like DT_INST_PROP(), you must define DT_DRV_COMPAT to the lowercase-and-underscores version of the compatible you are interested in. See Option 1: create devices using instance numbers.