If you are an embedded developer and have worked on a number of Linux-based boards, you have undoubtedly heard of Device Tree. In this post, we will introduce the afore-mentioned technology and its use with the Linux kernel.

Preamble – How it all begins

During the boot phase, the bootloader loads the kernel image into memory and execution passes to the latter, starting from its entry-point. The kernel, at this point, like any other “bare-metal” application, needs to perform certain hardware initialisation and configuration operations, for example:

All these operations are performed by writing specific values in certain registries, depending on the device to be initialised and/or configured. In other words, these are hardware-dependent operations: the kernel must therefore know the addresses of the registries on which to write and which values to use, depending on the hardware on which it is executed.

In order to make the kernel compatible with a given hardware platform, the most immediate solution is represented by the “ad-hoc” initialisation routines contained in the sources and enabled by specific configuration parameters, selectable at compilation time. This route is feasible for everything that is normally “fixed” (or better still standardised), such as the internal registries of an x86 processor, or access to the peripherals of a PC through the services offered by the BIOS.

A different case: the ARM platform

For the ARM platform, things get complicated: each SoC (System on a Chip), while sharing the same processor, can have registries positioned at different addresses, and the initialisation procedure may differ slightly from one SoC to another. Furthermore, the SoCs are mounted on boards which, in turn, have different interfaces and peripherals depending on the manufacturer, the model and even the specific revision.

Treating each available hardware separately resulted in an excessive number of header files, specific patches and special configuration parameters that were difficult to maintain for the kernel development community. Furthermore, this hard-coded approach requires kernel recompilation at the slightest hardware change. This is particularly annoying for users but especially for anyone who designs boards: during development, where numerous revisions are produced that differ in small details, it is necessary to modify and recompile each time, regardless of the extent of the change.

The development community has therefore proposed a better alternative: the use of the Device Tree.

Device Tree – a definition

The Device Tree is a Hardware Description Language that can be used to describe the system hardware in a tree data structure. In this structure, each tree node describes a device. The source code of the Device Tree is compiled by the Device Tree Compiler (DTC) to form the Device Tree Blob (DTB), readable by the kernel upon startup.

“Device Tree Powered” Bootstrap

In an ARM-based device that uses the Device Tree, the bootloader:

Compilation of the Device Tree Blob

To compile the Device Tree, use the Device Tree Compiler. Device Tree sources can be found together with the kernel

scripts/dtc

or can be downloaded separately:

git clone git://git.kernel.org/pub/scm/utils/dtc/dtc.git

After compiling the Device Tree Compiler, we can compile the Device Tree:

dtc -O dtb -o /path/to/my-tree.dtb /path/to/my-tree.dts

where:

Descriptions of several ARM-based boards are already present in the kernel sources. The corresponding device tree files are found in:

arch/arm/boot/dts

Here 2 types of files are distinguished:

The Makefile in arch/arm/boot/dts/Makefile lists which Device Tree Blobs should be built when performing the make command to create the kernel image.

Syntax of the Device Tree

Let’s illustrate a brief example on the syntax of the Device Tree. The following snippet contains a description of a UART controller:

arch/arm/boot/dts/imx28.dtsi

auart0: serial@8006a000 {
    compatible = "fsl,imx28-auart", "fsl,imx23-auart";
    reg = <0x8006a000 0x2000>;
    interrupts = <112>;
    dmas = <&dma_apbx 8>, <&dma_apbx 9>;
    dma-names = "rx", "tx";
    clocks = <&clks 45>;
    status = "disabled";
};

In particular, the individual entries have the syntax and semantics described below:

We will see later why the device under examination turns out to be disabled.

In the kernel code, we can see how the value associated with the compatible property allows the kernel itself to associate the correct device driver to this device.

drivers/tty/serial/mxs-auart.c

static const struct of_device_id mxs_auart_dt_ids[] = {
    {
        .compatible = "fsl,imx28-auart",
        .data = &mxs_auart_devtype[IMX28_AUART]
    }, {
        .compatible = "fsl,imx23-auart",
        .data = &mxs_auart_devtype[IMX23_AUART]
    }, { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mxs_auart_dt_ids);

static struct platform_driver mxs_auart_driver = {
    .probe = mxs_auart_probe,
    .remove = mxs_auart_remove,
    .driver = {
        .name = "mxs-auart",
        .of_match_table = mxs_auart_dt_ids,
    },
};

The compatible property takes the value “fsl, imx28-auart”, i.e. the first of the values ​​in the list present in the device tree. This matching allows association of the device with the driver.

Inclusion and overlay mechanisms

As mentioned in the previous paragraphs, the .dtsi files contain hardware descriptions at SoC level and, as such, are common to several boards. In .dts files we can include .dtsi files with the syntax:

#include "common.dtsi"

exactly as is the case for the C language preprocessor. The inclusion occurs by overlapping, i.e.:

The overlays are therefore used to enable hardware described but normally disabled (as seen in the previous example, in the paragraph concerning the syntax). As an example of inclusion, consider the following snippets:

arch/arm/boot/dts/am33xx.dtsi

    uart0: serial@44e09000 {
        compatible = "ti,omap3-uart";
        ti,hwmods = "uart1";
        clock-frequency = <48000000>;
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "disabled";
        dmas = <&edma 26>, <&edma 27>;
        dma-names = "tx", "rx";
    };


arch/arm/boot/dts/am335x-bone-common.dtsi

    &uart0 {
        pinctrl-names = "default";
        pinctrl-0 = <&uart0_pins>;
        status = "okay";
    };

Both of these files are included in:

arch/arm/boot/dts/am335x-bone.dts

#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"

...

Including am335x-bone-common.dtsi after am33xx.dtsi, the “status” value, initially set to “disabled”, is superimposed with the “okay” value, thereby activating the device.

Alternatives to use of the Device Tree

Before the introduction of Device Tree, the classic approach to supporting ARM-based hardware consisted, as mentioned above, of writing specific code to be included in the kernel. For a board based on ARM, it was a matter of writing a so-called “board-file”: a set of structures and functions to recognise the hardware as a “platform device” connected to the “platform bus” (https://lwn.net/Articles/448499/), terminated by a “MACHINE description” such as the following:

MACHINE_START(GTA04, "GTA04")
/* Maintainer: Nikolaus Schaller - http://www.gta04.org */
.atag_offset  = 0x100,
.reserve      = omap_reserve,
.map_io       = omap3_map_io,
.init_irq     = omap3_init_irq,
.handle_irq   = omap3_intc_handle_irq,
.init_early   = omap3_init_early,
.init_machine = gta04_init,
.init_late    = omap3630_init_late,
.timer        = &omap3_secure_timer,
.restart      = omap_prcm_restart,
MACHINE_END

This code snippet was used for the GTA04 board based on OMAP3. The same board is now supported by the kernel through a hardware description that uses the Device Tree. To compare the 2 solutions, reference can be made to the following links:

Pros and cons of the Device Tree

The advantages of using the Device Tree are:

Countering these (numerous) advantages, however, is a documentation that is still incomplete or lacking for certain parts of the device tree syntax. To date, the best approach to follow to write a new .dts file is to start from a pre-existing and undoubtedly functioning one and to introduce changes following a trial and error approach.