Linux Kernel – The Device Tree
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:
- processor configuration
- virtual memory setup
- console setup
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:
- loads the kernel image and the DTB into memory
- loads the address of the DTB in registry R2
- jumps to the kernel entry point
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:
- my-tree.dtb is the name Device Tree Blob that is generated
- my-tree.dts is the description of the hardware
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:
- .dts file for board-level hardware definitions
- .dtsi files that are included (and shared) by multiple .dts files and that generally contain SoC level definitions
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:
auart0: serial@8006a000
: name_node@addresscompatible
a string that allows the kernel to identify the device driver capable of managing the devicereg
Base address and size of the area containing the device registriesinterrupts
Interrupt numberdmas
edma-names
Description of the DMA with DMA channels and namesclocks
reference (phandle) to the clock used by the devicestatus
the status of the device (not enabled)
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 key-value pairs present in the .dts are added to those present in the .dtsi
- if the key-value pair to be added is already present, the value from the include file (the .dts, in our example) is superimposed on the one already present.
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.dts
i, 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:
- Greater simplicity in changing the system configuration, without the need to recompile the kernel.
- Greater simplicity in adding support for hardware that has minor changes compared to an already supported version (e.g.: new board revision).
- Reuse of pre-existing code, at device driver level (which can be written in a more generic manner, relegating to the device tree file the specific differences between similar hardware) and at the device tree file level (thanks to the inclusion and overlay mechanisms).
- Greater possibility of obtaining support from the kernel community for the inclusion in mainline of support for new hardware.
- Possibility of providing a more easily legible description of the hardware and with more descriptive names (useful for anyone who needs to develop applications on a board and must be familiar with the hardware details).
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.