During the Linux boot process, a "Device Tree Blob" (DTB) file is loaded into memory by U-Boot / UEFI, and a pointer to it is passed to the kernel. This DTB file describes the system's hardware layout to the Linux kernel, allowing for platform-specific code to be moved out of the kernel sources and replaced with generic code that can parse the DTB and configure the system as required. From DeviceTree.org:
[..] Device Tree is a data structure for describing hardware. Rather than hard coding every detail of a device into an operating system, many aspects of the hardware can be described in the data structure that is passed to the operating system at boot time.
For a quick introduction to Device Tree and how it relates to the kernel, see Thomas Petazzoni's "Device Tree for Dummies" slide deck. Another useful resource is the "A Symphony of Flavours: Using the device tree to describe embedded hardware" paper by Grant Likely and Josh Boyer.
We have another FAQ here which outlines how to use Device Tree to alter the number of CPUs available to Linux on Juno.
Device Tree Source (DTS) files are simple text files that can be compiled into a binary Device Tree Blob (DTB) format using the Device Tree Compiler (DTC) tool. The DTC tool is available in the Linux kernel sources under /scripts/dtc, and is also available for installation through some distribution package managers such as APT on Ubuntu:
$ sudo apt-get update $ sudo apt-get install device-tree-compiler
Decompiling a DTB using DTC is lossless, i.e. one can decompile a DTB into a DTS and then compile that DTS back into a DTB without losing any information. To decompile:
$ dtc -I dtb -O dts juno.dtb > juno.dts
And to recompile:
$ dtc -I dts -O dtb juno.dts > juno.dtb
Note: Full documentation for general Device Tree syntax can be found at DeviceTree.org, and Linux binding documentation can be found the Linux kernel sources here
Device Tree Source files are a tree structure consisting of nodes with associated properties and child nodes. The general syntax is as follows:
/ { node1 { a-string-property = "a string"; a-string-list-property = "first string", "second string"; a-byte-data-property = [0x01 0x02 0x03]; child-node1 { first-child-property; second-child-property a-string-property = "child string"; }; child-node2 { }; }; node2 { an-empty-property; a-cell-property = <1 2 3>; child-node1 { }; }; };
Where:
/
node1
node2
child-node1
child-node2
Each node has a set of properties associated with it:
Here is an example DTS entry for the Juno, specifically for the GIC-400:
interrupt-controller@2c010000 { compatible = "arm,gic-400"; reg = <0x0 0x2c010000 0x0 0x1000 0x0 0x2c02f000 0x0 0x2000 0x0 0x2c04f000 0x0 0x2000 0x0 0x2c06f000 0x0 0x2000>; #address-cells = <0x2>; #interrupt-cells = <0x3>; #size-cells = <0x2>; interrupt-controller; interrupts = <0x1 0x9 0x3f04>; ranges = <0x0 0x0 0x0 0x2c1c0000 0x0 0x40000>; linux,phandle = <0x1>; phandle = <0x1>; v2m@0 { compatible = "arm,gic-v2m-frame"; msi-controller; reg = <0x0 0x0 0x0 0x1000>; }; };
Let's break down this entry to get an idea of how a device tree node can be used to describe real hardware:
interrupt-controller@2c010000 {
The interrupt controller is at address 0x2C010000 on the parent node "bus" (more on that later). In this case, the parent node is the root node, so this means address 0x2C010000 from the point of view of the CPU.
compatible = "arm,gic-400";
This is used by the Linux kernel to decide which device driver to bind to the peripheral. In this case, the interrupt controller is compatible with the ARM GIC-400 driver.
reg = <0x0 0x2c010000 0x0 0x1000 0x0 0x2c02f000 0x0 0x2000 0x0 0x2c04f000 0x0 0x2000 0x0 0x2c06f000 0x0 0x2000>;
The reg property defines a range of addresses that the peripheral will respond to. The property consists of a number of tuples, each holding the base address of a region and the size of that region. To parse the property we need to understand three things:
reg
#address-cells
#size-cells
The interrupt-controller node is a child of the root node, which on the Juno has #address-cells = #size-cells = 2. Moving sequentially through the reg property and combining the first 2 cells for a base address, the next 2 cells for a length, the next 2 cells for a base address, and so on, gives the following regions that the interrupt controller will respond to:
#address-cells = #size-cells
64-bit address 0x0000_0000_2C01_0000 with 64-bit length 0x0000_0000_0000_1000
64-bit address 0x0000_0000_2C02_F000 with 64-bit length 0x0000_0000_0000_2000
64-bit address 0x0000_0000_2C04_F000 with 64-bit length 0x0000_0000_0000_2000
64-bit address 0x0000_0000_2C06_F000 with 64-bit length 0x0000_0000_0000_2000
interrupt-controller;
All interrupt controller nodes must include an empty interrupt-controller property.
interrupt-controller
interrupts = <0x1 0x9 0x3f04>;
The GIC's interrupt device tree binding format can be found here in the Linux kernel docs :
So here the GIC itself has PPI #9 as an Active High Level-Sensitive interrupt targeting CPUs [0..5], i.e. all CPUs on the Juno which has 6 CPUs in total. This PPI (interrupt ID 25) corresponds to the VGIC maintenance interrupt on the Juno and is required for CPUs that support the virtualization extensions.
ranges = <0x0 0x0 0x0 0x2c1c0000 0x0 0x40000>; v2m@0 { compatible = "arm,gic-v2m-frame"; msi-controller; reg = <0x0 0x0 0x0 0x1000>; };
All addresses in a node are specified as bus addresses that are local to that node. The range property provides a mechanism for mapping these bus addresses to the parent node's address space.
In the Juno's interrupt controller node is a v2m child node at address 0x0, local to the interrupt controller "bus". This node is used for the GICv2m extensions, and needs to be mapped from the interrupt controller "bus" onto the CPU's point of view of memory.
v2m
Similarly to the reg property, ranges uses #address-cells and #size-cells to parse a tuple of (Local Address, Parent Address, Length), with:
ranges
On the Juno these are all 2, so the ranges property is interpreted as "map 64-bit local address 0x0 to 64-bit parent address 0x2C1C0000 with 64-bit length 0x40000". Because the v2m node is at local address 0x0, it will be mapped to the parent node's address 0x2C1C0000, and the parent node is the root node, which will be the CPU's point of view of memory.