OSDev Ramblings: Part 2
a working example of interacting with devicetrees can be found on my GitHub repository.
Synopsis
From the previous articles in this series, we have a reasonably generic ARM kernel stub, that would at least boot on a variety of ARM devices that implement the Linux Kernel's boot protocol. However, ARM systems are diverse in that for a particular board, the hardware present and their locations vary from model to model.
With that, then how do operating system kernels figure out what devices are present on a particular board? and where/how to access them? Historically (circa 2012), this was done through various mechanisms, such as compiling board specific configuration into the kernel. This worked, although would typically restrict the usage of the kernel to a specific revision of a board, and otherwise make it difficult to configure specific devices without recompiling parts of the kernel.
This was suboptimal at best. Recognizing this, various ARM stakeholders got together to design a mechanism for describing hardware components, namely the Devicetree Specification. The specification of devicetrees is broadly split into two parts. A textual representation of device trees, which is easy to edit or otherwise configure, and a binary representation which is easier for programmatic usage.
DT[B,S] Structure
A devicetree, is represented as a tree data structure (funnily enough) with nodes indicating the presence of some device. Each node in the device tree may have properties, key-value pairs that give some more information about the associated node. Additionally a node may have children, expressing ownership semantics of a particular device. For example a very simple devicetree source file might look like1
/dts-v1/;
/ {
model = "simple,dummy-board";
#size-cells = <0x02>;
#address-cells = <0x02>;
compatible = "simple,dummy-board";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a7";
reg = <0>;
};
};
memory@00000000 {
reg = <0x00 0x00 0x00 0x8000000>;
device_type = "memory";
};
pl011@9000000 {
reg = <0x00 0x9000000 0x00 0x1000>;
compatible = "arm,pl011";
};
chosen {
bootargs = "some-arguments-to-a-kernel";
};
};
This devicetree source file describes a board layout with a single ARM Cortex A7 CPU core, 128MB of RAM rooted at address 0x00, a single PL011 UART with MMIO registers at address 0x9000000. Lastly the chosen node specifies boot time configuration, typically
this includes arguments to passed to the kernel, through the bootargs property. For example, in Linux this would contain configuration specifying where the root filesystem can be found. Additionally, other properties may be specified such as linux,initrd-start
and linux,initrd-end
which specify where an initial ramdisk (loaded by a bootloader) may be found.
How might this structure be represented in a devicetree binary (commonly called a flattened device tree)?
flattened devicetrees are composed of several parts, that are concatenated together to give the overall binary. Namely, there is a header, containing high level infromation about the binary. A memory reservation block containing regions of memory that are not to be used by the kernel. A strings block containing null-terminated strings that are used for property names, and the structure block, that encodes the tree structure of the device tree. Visually, the structure is presented in the table below.
+-----------------------------+
| FDT Header |
| (magic, totalsize, ... ) |
+-----------------------------+
| Memory Reservation |
| Block |
+-----------------------------+
| Structure Block |
| (FDT_BEGIN_NODE, props, |
| FDT_END_NODE, etc.) |
+-----------------------------+
| Strings Block |
| (property name strings used |
| by structure block) |
+-----------------------------+
The first series of bytes, The header. Contains high-level information about the devicetree, such as offsets to the other components of the blob and version information. Integer fields of the header are stored using a big-endian byte ordering (largest byte first) although typically2 ARM devices are operated using a little-endian ordering.
A C style struct representing the header of a devicetree may look like...
struct fdt_header {
u32 magic; /* Magic number, identifying the blob as a FDT e.g. 0xD00DFEED */
u32 total_size; /* Size of the device tree in bytes */
u32 struct_offset; /* Byte offset to the 'structure block' */
u32 string_offset; /* Byte offset to the 'strings block' */
u32 memory_offset; /* Byte offset to the 'memory reservation map' */
u32 version; /* Version information */
u32 version_compat; /* Minimum devicetree version, this devicetree is compatible with */
u32 boot_cpuid; /* Physical CPU ID the system boots on */
u32 string_size; /* Size in bytes of the strings block */
u32 struct_size; /* Size in bytes of the structure block */
};
The 'main' portion of interest in the devicetree is the structure block. Where information about particular devices and their relationships are stored, and is structured as a series of 'tags'. A tag is a 32-bit value, that is conditionally followed by additional data (dependant on the tag value). Tags are aligned on a 32-bit boundary denoting what the next portion of the device is. The potential values for a tag are as follows.
#define FDT_BEGIN_NODE 0x00000001 /* Denotes the start of a new node */
#define FDT_END_NODE 0x00000002 /* Denotes the end of a node */
#define FDT_NODE_PROP 0x00000003 /* Denotes the a node property */
#define FDT_NODE_NOP 0x00000004 /* a no-op node, to be ignored by software reading the FDT */
#define FDT_END 0x00000009 /* Denotes the end of the devicetree's structure block */
For example, all devicetrees begin with a FDT_BEGIN_NODE
tag, denoting the root-node of the devicetree. Which is then immediately followed by the nodes name, in the case
of the root node of the device tree, this is a null-terminated empty string, with 2 padding bytes up to the next 32-bit boundary.
- 1 This example omits various properties such as clock sources and interrupt configurations. Although it covers the core concepts/structure of a devicetree.
- 2 Endianness on ARM is configurable, although little-endian is most commonly used.
References
- The Devicetree Specification(s), an exhaustive text on the structure of devicetree source files and flattened devicetrees.
- libfdt, a freestanding devicetree parser library.
Up next...
- Virtual memory / MMU