Skip to main content

Device Tree Technical Overview

Introduction

This article provides information and guidance to developers on how to customize Device Trees for Toradex system on modules. This is the procedure required to enable your Operating System to support your particular hardware configuration.

Why Customize Device Trees

Quoting from Device Tree: Kernel Internals and Practical Troubleshooting, ELC Europe 2014 by Frank Rowand

A device tree is a tree data structure with nodes that describe the devices in a system. Each node has property/value pairs that describe the characteristics of the device being represented. Each node has exactly one parent except for the root node, which has no parent. - Power.org Standard for Embedded Power Architecture Platform Requirements (ePAPR)

In other words, it describes a system's hardware, including the SoC's internal memory-mapped peripherals, external hardware, and the carrier board. As Toradex provides reference designs for carrier boards and matching Device Trees for the modules and carrier boards, when customizing reference carrier board designs and connecting some peripherals, you may be required to modify the device tree from the carrier board you have used as base for your design.

Customization Using Device Tree Overlays

Device Tree Overlays are a mechanism that allows modification or extension of the device tree at runtime without modifying the original device tree source file, provide a flexible and non-intrusive way to customize the base device tree during runtime, allowing for on-the-fly customization without the need to rebuild and reboot the system.

Instead of modifying the Device Trees, you can support your hardware configuration by applying Device Tree Overlays on top of the base device tree. This is applicable in some scenarios when doing simple modifications such as disabling some peripheral, and is specially recommended when you have readily-available Device Tree Overlays to commonly used hardware configurations.

For quick evaluation and testings, make sure to understand and check Toradex-provided Device Tree Overlays. This will help you deciding which approach is the best suitable for your case. You can do this by:

Overview

To make an operating system portable across different devices, a description of the layout of each supported hardware configuration is required to ensure the correct drivers and configurations are used. The ARM world is very heterogeneous, each SoC vendor and each board vendor wire their hardware a bit differently.

Purpose

Device tree aims to provide a language for decoupling the hardware configuration from the board and device driver support in the Linux kernel (or any other operating system for that matter), overcoming the lack of hardware description. Using it allows board and device support to become data driven; to make setup decisions based on data passed into the kernel instead of on per-machine hard coded selections.

Ideally, data driven platform setup should result in less code duplication and make it easier to support a wide range of hardware with a single kernel image.

Device tree are used on the Linux Kernel for three main purposes:

  1. Platform identification: Identify the specific machine. The compatible property contains a sorted list of strings starting with the exact name of the machine, followed by an optional list of boards it is compatible with sorted from most compatible to least. For more information, refer to Linux and the Device Tree - Platform identification

    imx6q-apalis-ixora-v1.1.dts
    / {
    compatible = "toradex,apalis_imx6q-ixora-v1.1",
    "toradex,apalis_imx6q-ixora", "toradex,apalis_imx6q",
    "fsl,imx6q";
    ...
  2. Runtime configuration: The device trees specified a node (/chosen) to pass and describe parameter required by the device firmware at run time, since the device tree is the recommended method of communicating data between the device's firmware and the device driver. These data could be required configuration, kernel parameters or the location of an initrd image. For more information, refer to Linux and the Device Tree - Runtime configuration.

    imx8-apalis-v1.1.dtsi
        ...
    chosen {
    stdout-path = &lpuart1;
    };
    ...
  3. Device population: In Device Tree (DT), device population refers to the process of adding information about the hardware platform to the Linux device model, framework that manages devices, drivers, and their interactions within the Linux kernel. This information is defined using platform devices, which are registered based on their "compatible" property. Platform devices are typically device nodes located at the root or children of simple memory mapped bus nodes in the device tree. These platform devices represent devices directly connected to the processor bus or miscellaneous system devices, and they do not require a parent device or bus. Linux allocates and registers a platform device (platform_device) for each of these nodes. These platform devices can then be associated with a platform driver for further functionality. To learn more about device population and the usage of Device Tree, you can refer to the documentation on Linux and the Device Tree - Device population.

    imx6q-apalis-ixora-v1.1.dts
    / {
    model = "Toradex Apalis iMX6Q/D Module on Ixora Carrier Board V1.1";
    compatible = "toradex,apalis_imx6q-ixora-v1.1",
    "toradex,apalis_imx6q-ixora", "toradex,apalis_imx6q",
    "fsl,imx6q";

    ...
    gpio-keys {
    compatible = "gpio-keys";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_gpio_keys>;
    ...
    };

    lcd_display: disp0 {
    compatible = "fsl,imx-parallel-display";
    #address-cells = <1>;
    #size-cells = <0>;
    interface-pix-fmt = "rgb24";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ipu1_lcdif>;
    status = "okay";

    port@0 {
    reg = <0>;

    lcd_display_in: endpoint {
    remote-endpoint = <&ipu1_di1_disp1>;
    };
    };

    port@1 {
    reg = <1>;

    lcd_display_out: endpoint {
    remote-endpoint = <&lcd_panel_in>;
    };
    };
    };

Device Tree Levels

The DT is not only a data structure that describes the SoC's internal memory-mapped peripherals, but it also allows us to describe the whole carrier board hardware. Typically, device trees are defined in tree levels:

  1. SoC level: Include files (*.dtsi), generally containing SoC-level definitions.

  2. SoM level: Include files (*.dtsi), containing specific SoM-level definitions, modifications and assignments.

  3. Board level: files for board-level definitions, taking into consideration the custom carrier board design.

In this manner, a board-level device tree file (dts) generally includes a SoM level device tree file (dtsi), which already includes a SoC device tree.

To support the modular approach of Toradex products, our device tree files usually have three levels of inclusion: carrier board, module, and SoC. This is also reflected in the device tree file names, which are composed of the three levels: ${soc}-${module}-${board}.dtb.

You can learn about how the multiple levels of Device Trees on Toradex Modules are structured in the Device Tree on Toradex article.

Device Tree Structure and Conventions

First, keep in mind these concepts:

  • Nodes: Nodes represent the hierarchical structure of the device tree and can contain properties with their respective values as well as other nodes. Accordingly to Device Tree Specifications, nodes can be referenced in a DT in different ways:

    • Using its label: &node
    • Using its absolute node path: &{/node@0/child-node@0}
    • Using its relative path based on some other node's label: &{node0/child-node@1}
  • Properties: Properties consist of a name-value pair that provides information about a node. For example, the compatible property is used to match a driver for a generic node or to match a machine entry for the root node (/).

The device tree structure comprises nodes that represent different hardware components. Each node contains properties that describe the configuration of the corresponding hardware component.

Every supported hardware device has a compatible string associated with it. Along with the compatible property, device-specific properties must be specified. These properties are defined in the device tree bindings. The most important properties include compatible, reg, interrupts, status and others. The overall structure consists of nodes that represent various hardware components. Each (parent) node contains properties and other (child) nodes that describe the configuration of the hardware component.

The following figure show some pieces of the anatomy of a device tree, which will be explored in the next sections.

Device Tree anatomy

Nodes

Each node in the Device Tree follows a specific naming convention:

node-name@unit-address

The node-name component represents the name of the node. It should begin with either a lowercase or uppercase character and provide a descriptive indication of the general device class. The unit-address component of the name is specific to the bus type on which the node is located.

Device Nodes

Root Node

The Device Tree has a single root node from which all other device nodes descend. The root node is located at the full path /.

/aliases node

The Device Tree may include an /aliases node that defines one or more alias properties. The /aliases node should be positioned at the root node of the Device Tree, and each property within this node represents an alias, where the property name specifies the alias name and the property value specifies the full path to a node in the Device Tree. For example, the alias rtc0 in the imx8mm-verdin.dtsi defines an alias to the node &rtc_i2c.

imx8mm-verdin.dtsi
    aliases {
rtc0 = &rtc_i2c;
rtc1 = &snvs_rtc;
};

This is useful for RTCs, for instance, since the first RTC device is used as the primary time source for the system. However, /aliases can be used for other kinds of device types, such as ethernet.

aliases {
ethernet0 = &fec2;
ethernet1 = &fec1;
};
/memory node

A memory device node is mandatory for all Device Trees as it describes the physical memory layout of the system. If the system has multiple memory ranges, multiple memory nodes can be created, or the ranges can be specified using the reg property within a single memory node. The node name should include the unit name memory.

imx6qdl-apalis.dtsi
    memory@10000000 {
device_type = "memory";
reg = <0x10000000 0>;
};

The memory node is usually defined at a SoC level or in SoM level, but can be used at the carrier card level if you want to override the node with the same memory@unity-address, previously set at the SoC or SoM level, as happens in the imx6dl-colibri-eval-v3.dts.

imx6dl-colibri-eval-v3.dts
/ {
model = "Toradex Colibri iMX6DL/S on Colibri Evaluation Board V3";
compatible = "toradex,colibri_imx6dl-eval-v3", "toradex,colibri_imx6dl",
"fsl,imx6dl";

/* Will be filled by the bootloader */
memory@10000000 {
device_type = "memory";
reg = <0x10000000 0>;
};
...
/reserved-memory node

Reserved memory regions are specified as nodes under the /reserved-memory node. The operating system excludes reserved memory from normal usage. Child nodes can be created to describe specific reserved memory regions intended for special usage by various device drivers.

imx8-apalis-v1.1.dtsi
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;

decoder_boot: decoder_boot@84000000 {
no-map;
reg = <0 0x84000000 0 0x2000000>;
};
...
};

Regions within the /reserved-memory node can be referenced by other device nodes by adding a memory-region property to the device node.

imx8-apalis-v1.1.dtsi
&vpu_core0 {
memory-region = <&decoder_boot>, <&decoder_rpc>;
reg = <0x2d080000 0x10000>;
status = "okay";
};

The /reserved-memory is another case usually defined at a SoC or SoM level, in include files. However, can also be used at the carrier board level if you want to override the related node.

/chosen node

The /chosen node describes parameters specified by the system firmware during runtime, but does not represent an actual device in the system. It should be a child node of the root node.

imx8mm-verdin.dtsi
    chosen {
stdout-path = &uart1;
};
/cpus node

The /cpus node is mandatory for all Device Trees. It does not represent a physical device in the system but serves as a container for child cpu nodes, each representing a CPU within the system. The /cpus node may contain properties that are common across CPU nodes.

imx8mm.dtsi
cpus {
#address-cells = <1>;
#size-cells = <0>;

A53_0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
...
};

A53_1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53";
...
};
};

Properties

Each node in the Device Tree has properties that describe the characteristics of the node. Properties consist of a name and a value.

  • Property names: Property names are strings consisting of 1 to 31 characters. It is recommended to use standard characters for property names. If nonstandard names are necessary, they should include a unique string prefix, such as a stock ticker symbol, to identify the organization or company that defined the property. Examples:
fsl,imx8mm
toradex,verdin-imx8mm
  • Property values: A property value is an array of zero or more bytes containing information associated with the property. In some cases, properties may have an empty value when conveying true-false information. In such cases, the presence or absence of the property itself is sufficient to describe its meaning.
Standard Properties for Device Nodes

DTSpec specifies a set of standard properties for device nodes.

PropertyValue TypeDescriptionExample
compatible<stringlist>Define the specific programming model for the device. Used by a client program for device driver selection. Allow a device to express its compatibility with devices, potentially allowing a single-device driver to match against several devices.compatible = "toradex,verdin-imx8mm-nonwifi-dahlia";
model<string>Specifies the manufacturer’s model number of the device. The recommended format is: manufacturer,model. model = "Toradex Verdin iMX8M Mini WB on Verdin Development Board";
status<string>The status property indicates the operational status of a device.
- okay: device is operational
- disabled: device is not presently operational, but it might become.
#adress-cells
#size-cells
<u32>Used in any device node that has children in the device tree hierarchy
and describes how child device nodes should be addressed.
- #address-cells: number of cells used to encode the address field in a child node’s reg property.
- #size-cells: number of cells used to encode the size field in a child node’s reg property.
reg<prop-encoded-array>The reg property describes the address of the device’s resources within the address space defined by its parent bus.reg = <0x3000 0x20 0xFE00 0x100>;

*encoded as an arbitrary number of (address, length) pairs.

Miscellaneous

This section defines a list of helpful properties that might be applicable to many types of devices and device classes. They are defined here to facilitate standardization of names and usage.

PropertyValue TypeDescription
clock-frequency<prop-encoded-array>Specifies the frequency of a clock in Hz. The value is in one of two forms: a 32-bit integer consisting of one specifying the frequency of a 64-bit integer represented as a specifying the frequency
reg-shift<u32>The reg-shift property provides a mechanism to represent devices that are identical in most respects except for the number of bytes between registers. The reg-shift property specifies in bytes how far the discrete device registers are separated from each other. The individual register location is calculated by using following formula: “registers address” \<\< reg-shift. If unspecified, the default value is 0. For example, in a system where 16540 UART registers are located at addresses 0x0, 0x4, 0x8, 0xC, 0x10, 0x14, 0x18, and 0x1C, a reg-shift = \<2> property would be used to specify register locations.
label<string>The label property defines a human-readable string describing a device. The binding for a given device specifies the exact meaning of the property for that device.
Delete Properties or Nodes

It is also possible to delete properties or even nodes using /delete-property/ or /delete-node/. For example:

imx6q-apalis-ixora-v1.1.dts
...
/delete-node/ &reg_can1_supply;
...
&can1 {
/delete-property/ xceiver-supply;
};
caution

The instructions delete-node and delete-property should be used only on device tree files and not on device tree overlays. On device tree overlays they will not have the expected effect of deleting properties/nodes from the base device tree.

Interrupts

Within the Device Tree, a logical interrupt tree is established to represent the hierarchy and routing of interrupts in the hardware, from SoC level to carrier board level. Although commonly referred to as an "interrupt tree," it is technically a directed acyclic graph.

The physical mapping of an interrupt source to an interrupt controller is described in the Device Tree using the interrupt-parent property. Nodes representing devices that generate interrupts include an interrupt-parent property, which holds a phandle value pointing to the device responsible for routing the device's interrupts, typically an interrupt controller. If an interrupt-generating device does not have an interrupt-parent property, it is assumed that its interrupt parent is the parent node in the Device Tree.

The root of the interrupt tree is determined when the traversal of the interrupt tree reaches an interrupt controller node without an "interrupts" property, indicating the absence of an explicit interrupt parent for that node.

If you are looking for interrupts' related documentation for BSP 6 upstream, refer to Specifying interrupt information for devices.

Properties for Interrupt Generating Devices
PropertyValue TypeDescriptionExample
interrupts<prop-encoded-array>*Defines the interrupt or interrupts that are generated by the device. The value of the interrupts property consists of an arbitrary number of interrupt specifiers. The format of an interrupt specifier is defined by the binding of the interrupt domain rootinterrupts = <0xA 8>;
interrupt-parent<phandle>Make the definition of an interrupt parent explicit. The value is the phandle to the interrupt parent. If this property is missing from a device, its interrupt parent is assumed to be its Device Tree parent.

*encoded as arbitrary number of interrupt specifiers

Properties for Interrupt Controllers
PropertyValue TypeDescriptionExample
#interrupt-cells<u32>The #interrupt-cells property defines the number of cells required to encode an interrupt specifier for an interrupt domain.interrupts = <0xA 8>;
interrupt-controller<empty>The presence of an interrupt-controller property defines a node as an interrupt controller node.

File Formats

  1. Source (*.dts): The device tree structure is defined using a specialized language called Device Tree Source (DTS). The file containing structures in this format has the .dts extension. These files define reusable hardware components or configurations that can be included in other device tree files - .dts - using the #include directive.

  2. Includes (*.dtsi): By using .dtsi files, developers can organize and modularize their device tree code in a flexible and scalable way, making it easier to maintain and reuse common configurations across different device tree files.

  3. Binary Blob (*.dtb): The DTS file is compiled into a binary format called Device Tree Blob (DTB) that is loaded by the bootloader. The file containing this binary has the .dtb extension. The Linux kernel needs device tree binaries (*.dtb) to boot. These binaries are generated by the device tree compiler (dtc) from the device tree source files. The compiler is part of the Linux sources and is automatically built if needed. The kernel build system provides the dtbs target which compiles all device trees which are compatible with the current kernel configuration.

On ARM, all Device Tree Source files and includes are located in arch/arm/boot/dts, on the linux kernel source code. A tool, source code located in scripts/dtc, the Device Tree Compiler compiles the source into a binary form. The Device Tree Blob is produced by the compiler, and is the binary that gets loaded by the bootloader and parsed by the kernel at boot time. The arch/arm/boot/dts/Makefile lists which DTBs should be generated at build time.

Customizing and Deploying Device Trees

The process of customize and use a Device Tree comprises four steps:

  1. Choose the base Device Tree Source
  2. Customize it according to your particular hardware
  3. Compile the Device Tree Source
  4. Deploy the Device Tree to your custom OS image

Take a look at this process on the following sections. Don't worry about learning how to perform each steps, as you will be pointed to the right resources at the end.

Choose the Base Device Tree

When customizing a Device Tree, the first step is to chose a base device tree (DTS) file corresponding to your hardware platform. The DTS file contains the description of the hardware components and their configurations. It serves as the starting point for your customizations.

The Device Tree files are provided by Toradex and are available in the Linux kernel source code, mainline or downstream. You can usually find it in the arch/arm/boot/dts or arch/arm64/boot/dts directory. If you cannot find a pre-existing DTS file for your specific hardware, you may need to create one from scratch using the existing DTS files as a reference.

Choose the base Device Tree that matches the hardware platform. Toradex provides a set of base Device Trees for its hardware platforms that can be found in:

Customization

Quoting Linux and the Devicetree:

Conceptually, a common set of usage conventions, called 'bindings', is defined for how data should appear in the tree to describe typical hardware characteristics including data busses, interrupt lines, GPIO connections, and peripheral devices.

When creating a new Device Tree representation for a component, external hardware, or even entire carrier boards, it's important to define a complete binding that includes all the required properties and their corresponding values. These properties provide device drivers with the necessary information to effectively communicate with the hardware. Whenever possible, it's recommended to utilize existing bindings that describe similar hardware, leveraging the available support code. However, if needed, you can extend existing bindings or create new ones by defining additional nodes and properties using the flexible naming convention provided by Device Tree.

There are some common use cases that require customizations of the device tree:

  • Change and overwrite nodes and properties: Typically the higher layers (e.g. carrier board device tree) overwrite the lower layers (e.g. SoC device tree) since the higher layers include the lower layers at the very beginning (the sequence order of entries is what matters, hence the include order matters). Entire nodes can be overwritten by simply redefining them. The node needs to be defined using the ampersand & character and the label, and its properties changed in the node.
  • Activate and deactivate devices: One of the simplest cases is the activation or deactivation of nodes. Usually it's done at a carrier board level using the property status with the values okay or disabled, defining the status of the node recently defined at the carrier board *.dts or changing the property of the previously defined nodes at the SoC or SoM *.dtsi.
  • Pin Multiplexing: Another case is the using the multiplexing capabilities to assign different functions to your board pins, used to assign groups of pins to a specific device.
  • Add and configure a new device/peripheral: Add, describe and configure a new node representing a new external device (e.g. gyroscope, thermal sensor, etc) added to the custom carrier board.

To ensure a well-defined and standardized Device Tree representation, the following recommended practices can be followed:

  1. Define a compatible string following the guidelines outlined in the standard properties section.
  2. Utilize the relevant standard properties for the new device. At a minimum, this typically includes the reg and interrupts properties.
  3. Adhere to the conventions specified in the miscellaneous property guidelines.
  4. If the binding requires additional properties, it is recommended to use the following format for property names: <company>, <property-name>. In this format, <company> represents an Organizationally Unique Identifier (OUI) or a unique string such as a stock ticker symbol, which identifies the creator of the binding.
caution

It is important to configure not-used and non-connected pins. Some pins can be left non-connected by design. However, you should make sure it is not generating triggers or interrupts. Even if you don't actively use these signals on the software side, they still can demand resources just by being triggered.

Compile the Device Tree Source

After making changes to the device tree, it needs to be compiled into the *.dtb file that can be loaded by the bootloader. This can be done using the Device Tree Compiler (DTC) tool. The resulting compiled DTB file will be used to deploy the custom device tree to the device. The DTC tool is included in the kernel and the Toradex Linux BSP and can be used to compile the device tree.

  1. Compile from source code: If you want to do a standalone compilation, you need to configure the build environment for the Linux kernel, setting the right version and elements of your Linux distributions, and use the Device Tree Compiler tool. Refer to Building Linux Kernel to correctly configure your Linux kernel and compile the device trees using make freescale/<custom-device-tree>.dtb.

  2. Yocto Project: If you are working with Yocto Project to build your custom embedded Linux distribution, you might write *.bbappends modifying the kernel, to automatically compile and add the device tree to your image. Refer to Customize the Kernel.

  3. TorizonCore Builder: If you are using Torizon OS Linux-based operating system, you might use a single TorizonCore Builder command, the dt apply command, to select and apply your custom device tree as the base one for your Torizon OS image. It's important to highlight that TorizonCore Builder compile and apply the *.dts file to your image, without needing to previous compile the source file. Refer to TorizonCore Builder Tool - Customizing Torizon OS Images to understand the TorizonCore Builder and how to customize your Torizon OS image. Also check the build command documentation to understand how to customize your image in a single step.

Deploy the Device Tree to the OS image

U-boot loads the device tree during the boot process. And accordingly to U-boot Variables it uses the variable fdtfile to set the string with a device tree binary (*.dtb file) which the bootloader will search for in the boot partition.

Torizon OS overrides the default value of the U-Boot environment variable fdtfile in a file named uEnv.txt, which is evaluated during U-Boot runtime. Therefore, if you read the value of fdtfile using a tool like U-Boot fw-utils or directly from the bootloader, you will not be reading the value to which this variable evaluates, instead, you should check the contents of /boot/loader/uEnv.txt

For testing and development, you can change the variable directly on the bootloader or on the device at run time. For production, however, you might edit the u-boot-initial-env-sd on the related image on Toradex Easy Installer.

If you are using Torizon OS, as mentioned above, you might use TorizonCore Builder dt apply command to handle the selection and deployment of your custom device tree to your Torizon OS image during evaluation and development. You will be specifying the custom device tree as one customization in your production build. Refer to TorizonCore Builder Documentation to learn how to deploy the device tree.

Next Steps

Now that you understand the basics about Device Trees and how to customize it, you are ready to your First Steps With Device Trees, where you will put in practice what you learned here.

Extra Resources



Send Feedback!