OSDev Ramblings: Part 2

a working example is TBD

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, like Linux, 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. The first series of bytes comprise a header, containing high-level information about the device tree. A C style struct representing a device tree header should 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 structure block contains the binary representation of the nodes of the devicetree and their properties, and is structured as a series of 'tags'. A 32-bit value, aligned on a 32-bit boundary denoting what the next portion of the device tree denotes. 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 */

Notably, FDT_BEGIN_NODE tags are immediately followed by the name of the node as a null-terminated string.

References

Up next...