Device Tree

Introduction

Device tree is a way of describing hardware and configuration information for boards. Device tree was adopted for use in the Linux kernel for the PowerPC architecture. However, it is now in use for ARM and other architectures.

The device tree is a data structure for dynamically describing hardware using a Device Tree Source (DTS) data structure language, instead of hard coding every detail of a board’s hardware into the operating system.

In Linux, DTS is compiled into a compact Device Tree Blob (DTB) using a Device Tree Compiler (DTC), then the hardware-describing DTB is passed to the operating system at boot time. This allows the same compiled Linux kernel to support different hardware configurations within an architecture family (e.g., ARM, x86, PowerPC) and moves a significant part of the hardware description out of the kernel binary itself.

For larger systems, the flexibility this offers offsets the extra runtime memory overhead incurred. But the primary targets for Zephyr applications are small micro-controller systems with limited memory resources. So instead of requiring the additional runtime memory to store the DTB blob and the code to parse it, the DTS information is used at compile time.

Device tree uses a specific format to describe the device nodes in a system. This format is described in the Device Tree Specification.

More device tree information can be found at the device tree repository.

System build requirements

The Zephyr device tree feature requires a device tree compiler (DTC) and Python YAML packages. Refer to the installation guide for your specific host OS:

Zephyr and Device Tree

Device Tree provides a unified description of a hardware system used in an application. It is used in Zephyr to describe hardware and provide a boot-time configuration of this hardware.

In Zephyr, the device tree is also used to describe Zephyr-specific configuration information. This Zephyr-specific information augments the device tree descriptions and sits on top of it, rather than diverging from it. The main reason for this is to leverage existing device tree files that a SoC vendor may already have defined for a given platform.

The device tree files are compiled using the device tree compiler. The compiler runs the .dts file through the C preprocessor to resolve any macro or #defines utilized in the file. The output of the compile is another dts formatted file.

After compilation, a Python script extracts information from the compiled device tree file using rules specified in bindings (see the Bindings section). The extracted information is placed in a header file that is used by the rest of the code as the project is compiled.

Temporary fixup files are required for device tree support on most devices. These fixup files by default reside in the board and soc directories and are named dts_fixup.h. These fixup files map the generated include information to the current driver/source usage.

The Python code that deals with device tree and bindings is in scripts/dts/.

Device Tree vs Kconfig

As mentioned above there are several tools used to configure the build with Zephyr. The two main ones, Device Tree and Kconfig, might seem to overlap in purpose, but in fact they do not. This section serves as a reference to help you decide whether to place configuration items in Device Tree or Kconfig.

The scope of each configuration tool can be summarized as follows:

  • Device Tree is used to describe the hardware and its boot-time configuration.
  • Kconfig is used to describe which software features will be built into the final image, and their configuration.

Hence Device Tree deals mostly with hardware and Kconfig with software. A couple of noteworthy exceptions are:

  • Device Tree’s chosen keyword, which allows the user to select a particular instance of a hardware device to be used for a concrete purpose by the software. An example of this is selecting a particular UART for use as the system’s console.
  • Device Tree’s status keyword, which allows the user to enable or disable a particular instance of a hardware device in the Device Tree file itself. This takes precedence over Kconfig.

To further clarify this separation, let’s use a particular, well-known example to illustrate this: serial ports a.k.a. UARTs. Let’s consider a board containing a SoC with 2 UART instances:

  • The fact that the target hardware contains 2 UARTs is described with Device Tree. This includes the UART type, its driver compatibility and certain immutable (i.e. not software-configurable) settings such as the base address of the hardware peripheral in memory or its interrupt line.
  • Additionally, hardware boot-time configuration is also described with Device Tree. This includes things such as the IRQ priority and boot-time UART baudrate. These may also be modifiable at runtime later, but their boot-time default configuration is described in Device Tree.
  • The fact that the user intends to include software support for UART in the build is described in Kconfig. Through the use of Kconfig, users can opt not to include support for this particular hardware item if they don’t require it.

Another example is a device with a 2.4GHz, multi-protocol radio supporting both the Bluetooth Low Energy and 802.15.4 wireless technologies. In this case:

  • Device Tree describes the presence of a radio peripheral compatible with a certain radio driver.
  • Additional hardware boot-time configuration settings may also be present in the Device Tree files. In this particular case it could be a default TX power in dBm if the hardware does have a simple, cross-wireless technology register for that.
  • Kconfig will describe which protocol stack is to be used with that radio. The user may decide to select BLE or 802.15.4, which will both depend on the presence of a compatible radio in the Device Tree files.

Device tree file formats

Hardware and software is described inside of device tree files in clear text format. These files have the file suffix of .dtsi or .dts. The .dtsi files are meant to be included by other files. Typically for a given board you have some number of .dtsi include files that pull in common device descriptions that are used across a given SoC family.

Example: FRDM K64F Board and Hexiwear K64

These boards are defined in frdm_k64fs.dts and hexiwear_k64.dts. They are based on the same NXP Kinetis SoC family, the K6X.

Common definitions for K6X are stored in nxp_k6x.dtsi, which is included by both board .dts files. nxp_k6x.dtsi in turn includes armv7-m.dtsi, which has common definitions for ARMv7-M-based systems.

Since nxp_k6x.dtsi is meant to be generic across K6X-based boards, it leaves many devices disabled by default. For example, there is a CAN controller defined as follows (with unimportant parts skipped):

can0: can@40024000 {
     ...
     status = "disabled";
     ...
};

It is up to the board .dts files to enable devices (by setting status = "okay"). The board .dts files are also responsible for any board-specific configuration of the device.

For example, FRDM K64 (but not Hexiwear K64) enables the CAN controller, also setting the bus speed:

&can0 {
     status = "okay";
     bus-speed = <125000>;
};

The &can0 { ... }; syntax adds/overrides properties on the node with label can0, i.e. the can@4002400 node.

Other examples of board-specific customization is pointing properties in aliases and chosen to the right nodes (see aliases and chosen nodes), and making GPIO/PinMux assignments.

Currently supported boards

Device tree is currently supported on all embedded targets except posix (boards/posix) and qemu_x86_64 (boards/x86_64).

Adding support for a board

Adding device tree support for a given board requires adding a number of files. These files will contain the DTS information that describes a platform, the YAML descriptions that define the contents of a given Device Tree peripheral node, and also any fixup files required to support the platform.

When writing Device Tree Source files, it is good to separate out common peripheral information that could be used across multiple SoC families or boards. DTS files are identified by their file suffix. A .dtsi suffix denotes a DTS file that is used as an include in another DTS file. A .dts suffix denotes the primary DTS file for a given board.

The primary DTS file will contain at a minimum a version line, optional includes, and the root node definition. The root node will contain a model and compatible that denotes the unique board described by the .dts file.

Device Tree Source File Template

/dts-v1/;

/ {
        model = "Model name for your board";
        compatible = "compatible for your board";
        /* rest of file */
};

One suggestion for starting from scratch on a platform/board is to examine other boards and their device tree source files.

The following is a more precise list of required files:

  • Base architecture support
    • Add architecture-specific DTS directory, if not already present. Example: dts/arm for ARM.
    • Add target specific device tree files for base SoC. These should be .dtsi files to be included in the board-specific device tree files.
    • Add target specific YAML binding files in the dts/bindings/ directory. Create the yaml directory if not present.
  • SoC family support
    • Add one or more SoC family .dtsi files that describe the hardware for a set of devices. The file should contain all the relevant nodes and base configuration that would be applicable to all boards utilizing that SoC family.
    • Add SoC family YAML binding files that describe the nodes present in the .dtsi file.
  • Board specific support
    • Add a board level .dts file that includes the SoC family .dtsi files and enables the nodes required for that specific board.
    • Board .dts file should specify the SRAM and FLASH devices, if present.
      • Flash device node might specify flash partitions. For more details see Flash Partitions
    • Add board-specific YAML binding files, if required. This would occur if the board has additional hardware that is not covered by the SoC family .dtsi/.yaml files.
  • Fixup files
    • Fixup files contain mappings from existing Kconfig options to the actual underlying DTS derived configuration #defines. Fixup files are temporary artifacts until additional DTS changes are made to make them unnecessary.
  • Overlay Files (optional)
    • Overlay files contain tweaks or changes to the SoC and Board support files described above. They can be used to modify Device Tree configurations without having to change the SoC and Board files. See Device Tree Overlays for more information on overlay files and the Zephyr build system.

aliases and chosen nodes

Using an alias with a common name for a particular node makes it easier for you to write board-independent source code. Device Tree aliases nodes are used for this purpose, by mapping certain generic, commonly used names to specific hardware resources:

aliases {
   led0 = &led0;
   sw0 = &button0;
   sw1 = &button1;
   uart-0 = &uart0;
   uart-1 = &uart1;
};

Certain software subsystems require a specific hardware resource to bind to in order to function properly. Some of those subsystems are used with many different boards, which makes using the Device Tree chosen nodes very convenient. By doing, so the software subsystem can rely on having the specific hardware peripheral assigned to it. In the following example we bind the shell to uart1 in this board:

chosen {
   zephyr,shell-uart = &uart1;
};

The full set of Zephyr-specific chosen nodes follows:

chosen node name Generated symbol
zephyr,flash CONFIG_FLASH
zephyr,sram CONFIG_SRAM_SIZE/CONFIG_SRAM_BASE_ADDRESS (via DT_SRAM_SIZE/DT_SRAM_BASE_ADDRESS)
zephyr,ccm DT_CCM
zephyr,console DT_UART_CONSOLE_ON_DEV_NAME
zephyr,shell-uart DT_UART_SHELL_ON_DEV_NAME
zephyr,bt-uart DT_BT_UART_ON_DEV_NAME
zephyr,uart-pipe DT_UART_PIPE_ON_DEV_NAME
zephyr,bt-mon-uart DT_BT_MONITOR_ON_DEV_NAME
zephyr,uart-mcumgr DT_UART_MCUMGR_ON_DEV_NAME

Adding support for device tree in drivers

As drivers and other source code is converted over to make use of device tree generated information, these drivers may require changes to match the generated #define information.

Source Tree Hierarchy

The device tree files are located in a couple of different directories. The directory split is done based on architecture, and there is also a common directory where architecture agnostic device tree and YAML binding files are located.

Assuming the current working directory is the ZEPHYR_BASE, the directory hierarchy looks like the following:

dts/common/
dts/<ARCH>/
dts/bindings/
boards/<ARCH>/<BOARD>/

The common directory contains a skeleton.dtsi which provides device tree root node definition. The bindings subdirectory contains YAML binding files used to instruct how the python DTS parsing script should extract nodes information in a format that will be usable by the system.

Example: Subset of DTS/YAML files for NXP FRDM K64F (Subject to Change):

dts/arm/armv7-m.dtsi
dts/arm/k6x/nxp_k6x.dtsi
boards/arm/frdm_k64f/frdm_k64f.dts
dts/bindings/interrupt-controller/arm,v7m-nvic.yaml
dts/bindings/gpio/nxp,kinetis-gpio.yaml
dts/bindings/pinctrl/nxp,kinetis-pinmux.yaml
dts/bindings/serial/nxp,kinetis-uart.yaml

Bindings

.dts files describe the available hardware devices, but don’t tell the system which pieces of information are useful, or what kind of configuration output (#define’s) should be generated. Bindings provide this information. Bindings are files in YAML format.

Configuration output is only generated for devices that have bindings.

Nodes are mapped to bindings via their compatible string(s). Take the following node as an example:

bar-device {
     compatible = "foo-company,bar-device";
     ...
};

This node would get mapped to a binding with this in it:

...

properties:
    compatible:
        constraint: "foo-company,bar-device"

    ...

Bindings are stored in dts/bindings/. The filename usually matches the compatible string.

If a node has more than one compatible string, then the first binding found is used, going from the first string to the last. For example, a node with compatible = "foo-company,bar-device", "generic-bar-device" would get mapped to the binding for generic-bar-device if there is no binding for foo-company,bar-device.

If a node appears on a bus (e.g. I2C or SPI), then the bus type is also taken into account when mapping nodes to bindings. See the description of parent and child in the template below.

Below is a template that shows the format of binding files, stored in dts/bindings/binding-template.yaml.

title: Short description of the node

description: >
    Longer free-form description of the node.
    Can go over multiple lines.

# Bindings are often based on other bindings, which are given in 'inherits'.
# The resulting binding is the union of the inherited bindings and this binding
# (internally, it's a recursive dictionary merge).
#
# If a field appears both in this binding and in a binding it inherits, then
# the value in this binding takes precedence. This can be used to change a
# 'category: optional' from an inherited binding to a 'category: required' (see
# the 'properties' description below).
inherits:
    !include other.yaml # or [other1.yaml, other2.yaml]

# If the node describes a bus, then the bus type should be given, like below
parent:
    bus: <string describing bus type, e.g. "i2c">

# If the node appears on a bus, then the bus type should be given, like below.
#
# When looking for a binding for a node, the code checks if the binding for the
# parent node contains 'parent: bus: <bus type>'. If it does, then only
# bindings with a matching 'child: bus: <bus type>' are considered. This allows
# the same type of device to have different bindings depending on what bus it
# appears on.
child:
    bus: <string describing bus type, e.g. "i2c">

# 'sub-node' is used to simplify cases where a node has children that can all
# use the same binding. The contents of 'sub-node' becomes the binding for each
# child node.
#
# The example below is for a binding for pwm-leds where the child nodes are
# required to have a 'pwms' property.
sub-node:
    properties:
        pwms:
          type: compound
          category: required

# 'properties' describes properties on the node, e.g.
#
#   reg = <1 2>;
#   current-speed = <115200>;
#   label = "foo";
#
# This is used to check that required properties appear, and to
# control the format of output generated for them. Except for some
# special-cased properties like 'reg', only properties listed here will
# generate output.
#
# A typical property entry looks like this:
#
#   <property name>:
#     category: <required | optional>
#     type: <string | int | boolean | array | uint8-array | string-array | phandle | compound>
#     description: <description of the property>
#     enum:
#       - <item1>
#       - <item2>
#       ...
#       - <itemN>
#
# 'type: uint8-array' is for what the device tree specification calls
# 'bytestring'. Properties of type 'uint8-array' should be set like this:
#
#   foo = [89 AB CD];
#
# Each value is a byte in hex.
#
# 'phandle' is for properties that are assigned a single phandle, like this:
#
#   foo = <&label>;
#
# 'compound' is a catch-all for more complex types, e.g.
#
#   foo = <&label1 1 2 &label2 7>;
properties:
    # An entry for 'compatible' must appear, as it's used to map nodes to
    # bindings
    compatible:
        constraint: "foo-company,bar-device"

    # Describes a property like 'current-speed = <115200>;'. We pretend that
    # it's obligatory for the example node and set 'category: required'.
    current-speed:
        type: int
        category: required
        description: Initial baud rate for bar-device

    # Describes an optional property like 'keys = "foo", "bar";'
    keys:
        type: string-array
        category: optional
        description: Keys for bar-device

    # Describes an optional property like 'maximum-speed = "full-speed";
    # the enum specifies known values that the string property may take
    maximum-speed:
      type: string
      category: optional
      description: Configures USB controllers to work up to a specific speed.
      enum:
         - "low-speed"
         - "full-speed"
         - "high-speed"
         - "super-speed"

# If the binding describes an interrupt controller, GPIO controller, pinmux
# device, or any other device referenced via a phandle plus a specifier (some
# additional data besides the phandle), then the cells in the specifier must be
# listed in '#cells', like below.
#
# If the specifier is empty (e.g. '#clock-cells = <0>'), then '#cells' can
# either be omitted (recommended) or set to an empty array. Note that an empty
# array is specified as '"#cells": []' in YAML.
#
# For example, say that some node has 'foo-gpios = <&gpio1 1 2>'. The <1 2>
# part of the property value is the specifier, with two cells. The node pointed
# at by &gpio1 is expected to have '#gpio-cells = <2>', and its binding should
# have two elements in '#cells', corresponding to the 1 and 2 values above.
"#cells":
  - cell0    # name of first cell
  - cell1    # name of second cell
  - cell2    # name of third cell
  - and so on and so forth

Include files generation

At build time, after a board’s .dts file has been processed by the DTC (Device Tree Compiler), a corresponding .dts_compiled file is generated under the zephyr directory. This .dts_compiled file is processed by the python DTS parsing script and generates an include file named include/generated/generated_dts_board_unfixed.h that holds all the information extracted from the DTS file with the format specified by the YAML bindings. For example:

/* gpio_keys */
#define DT_GPIO_KEYS_0               1

/* button_0 */
#define DT_GPIO_KEYS_BUTTON_0_GPIOS_CONTROLLER       "GPIO_2"
#define DT_GPIO_KEYS_BUTTON_0_GPIOS_FLAGS    0
#define DT_GPIO_KEYS_BUTTON_0_GPIOS_PIN              6
#define DT_GPIO_KEYS_BUTTON_0_LABEL          "User SW2"

#define DT_GPIO_KEYS_SW1_GPIOS_CONTROLLER            DT_GPIO_KEYS_BUTTON_0_GPIOS_CONTROLLER
#define DT_GPIO_KEYS_SW1_GPIOS_FLAGS                 DT_GPIO_KEYS_BUTTON_0_GPIOS_FLAGS
#define DT_GPIO_KEYS_SW1_GPIOS_PIN                   DT_GPIO_KEYS_BUTTON_0_GPIOS_PIN
#define DT_ALIAS_SW1_GPIOS_CONTROLLE                 DT_GPIO_KEYS_BUTTON_0_GPIOS_CONTROLLER
#define DT_ALIAS_SW1_GPIOS_FLAGS                     DT_GPIO_KEYS_BUTTON_0_GPIOS_FLAGS
#define DT_ALIAS_SW1_GPIOS_PIN                       DT_GPIO_KEYS_BUTTON_0_GPIOS_PIN
#define DT_ALIAS_SW1_LABEL                           DT_GPIO_KEYS_BUTTON_0_LABEL

Additionally, a file named generated_dts_board_fixups.h is generated in the same directory concatenating all board-related fixup files.

The include file include/generated_dts_board.h includes both these generated files, giving Zephyr C source files access to the board’s device tree information.

Flash Partitions

Device tree can be used to describe a partition layout for any flash device in the system.

Two important uses for this mechanism are:

  1. To force the Zephyr image to be linked into a specific area on Flash.

    This is useful, for example, if the Zephyr image must be linked at some offset from the flash device’s start, to be loaded by a bootloader at runtime.

  2. To generate compile-time definitions for the partition layout, which can be shared by Zephyr subsystems and applications to operate on specific areas in flash.

    This is useful, for example, to create areas for storing file systems or other persistent state. These defines only describe the boundaries of each partition. They don’t, for example, initialize a partition’s flash contents with a file system.

Partitions are generally managed using device tree overlays. Refer to Device Tree Overlays for details on using overlay files.

Defining Partitions

The partition layout for a flash device is described inside the partitions child node of the flash device’s node in the device tree.

You can define partitions for any flash device on the system.

Most Zephyr-supported SoCs with flash support in device tree will define a label flash0. This label refers to the primary on-die flash programmed to run Zephyr. To generate partitions for this device, add the following snippet to a device tree overlay file:

&flash0 {
        partitions {
                compatible = "fixed-partitions";
                #address-cells = <1>;
                #size-cells = <1>;

                /* Define your partitions here; see below */
        };
};

To define partitions for another flash device, modify the above to either use its label or provide a complete path to the flash device node in the device tree.

The content of the partitions node looks like this:

partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        partition1_label: partition@START_OFFSET_1 {
                label = "partition1_name";
                reg = <0xSTART_OFFSET_1 0xSIZE_1>;
        };

        /* ... */

        partitionN_label: partition@START_OFFSET_N {
                label = "partitionN_name";
                reg = <0xSTART_OFFSET_N 0xSIZE_N>;
        };
};

Where:

  • partitionX_label are device tree labels that can be used elsewhere in the device tree to refer to the partition
  • partitionX_name controls how defines generated by the Zephyr build system for this partition will be named
  • START_OFFSET_x is the start offset in hexadecimal notation of the partition from the beginning of the flash device
  • SIZE_x is the hexadecimal size, in bytes, of the flash partition

The partitions do not have to cover the entire flash device. The device tree compiler currently does not check if partitions overlap; you must ensure they do not when defining them.

Example Primary Flash Partition Layout

Here is a complete (but hypothetical) example device tree overlay snippet illustrating these ideas. Notice how the partitions do not overlap, but also do not cover the entire device.

&flash0 {
        partitions {
                compatible = "fixed-partitions";
                #address-cells = <1>;
                #size-cells = <1>;

                code_dts_label: partition@8000 {
                        label = "zephyr-code";
                        reg = <0x00008000 0x34000>;
                };

                data_dts_label: partition@70000 {
                        label = "application-data";
                        reg = <0x00070000 0xD000>;
                };
        };
};

Linking Zephyr Within a Partition

To force the linker to output a Zephyr image within a given flash partition, add this to a device tree overlay:

/ {
        chosen {
                zephyr,code-partition = &slot0_partition;
        };
};

If the chosen node has no zephyr,code-partition property, the application image link uses the entire flash device. If a zephyr,code-partition property is defined, the application link will be restricted to that partition.

Flash Partition Macros

The Zephyr build system generates definitions for each flash device partition. These definitions are available to any files which include <zephyr.h>.

Consider this flash partition:

dts_label: partition@START_OFFSET {
        label = "def-name";
        reg = <0xSTART_OFFSET 0xSIZE>;
};

The build system will generate the following corresponding defines:

#define FLASH_AREA_DEF_NAME_LABEL        "def-name"
#define FLASH_AREA_DEF_NAME_OFFSET_0     0xSTART_OFFSET
#define FLASH_AREA_DEF_NAME_SIZE_0       0xSIZE
#define FLASH_AREA_DEF_NAME_OFFSET       FLASH_AREA_MCUBOOT_OFFSET_0
#define FLASH_AREA_DEF_NAME_SIZE         FLASH_AREA_MCUBOOT_SIZE_0

As you can see, the label property is capitalized when forming the macro names. Other simple conversions to ensure it is a valid C identifier, such as converting “-” to “_”, are also performed. The offsets and sizes are available as well.

MCUboot Partitions

MCUboot is a secure bootloader for 32-bit microcontrollers.

Some Zephyr boards provide definitions for the flash partitions which are required to build MCUboot itself, as well as any applications which must be chain-loaded by MCUboot.

The device tree labels for these partitions are:

boot_partition
This is the partition where the bootloader is expected to be placed. MCUboot’s build system will attempt to link the MCUboot image into this partition.
slot0_partition
MCUboot loads the executable application image from this partition. Any application bootable by MCUboot must be linked to run from this partition.
slot1_partition
This is the partition which stores firmware upgrade images. Zephyr applications which receive firmware updates must ensure the upgrade images are placed in this partition (the Zephyr DFU subsystem can be used for this purpose). MCUboot checks for upgrade images in this partition, and can move them to slot0_partition for execution. The slot0_partition and slot1_partition must be the same size.
scratch_partition
This partition is used as temporary storage while swapping the contents of slot0_partition and slot1_partition.

Important

Upgrade images are only temporarily stored in slot1_partition. They must be linked to execute of out of slot0_partition.

See the MCUboot documentation for more details on these partitions.

File System Partitions

storage_partition
This is the area where e.g. NFFS expects its partition.