What Is a Device Tree?

August 7, 2026 · visitors

If you have ever tried to bring Linux up on an embedded board, sooner or later you meet a file ending in .dts. Usually at the worst possible moment: the board won't boot, nothing comes out on the screen, and someone tells you to "check your device tree". This is about what that file is, why it exists, and what you are actually looking for inside it.

The Problem It Solves

On a desktop, the kernel can largely find the hardware by itself. PCI and USB are formally specified, enumerable buses (1). Devices announce themselves with a vendor and product ID; a driver lists the IDs it supports in a table, and the kernel calls that driver's probe() for every device matching an entry (2). USB works the same way through its own ID table (3). Nobody has to tell anybody anything in advance.

Embedded boards do not get that luxury. The kernel's own documentation draws the line plainly: peripherals integrated into a system-on-chip hang off buses with minimal infrastructure, "as opposed to large formally specified ones like PCI or USB", and what those devices have in common is direct addressing from a CPU bus (1). An I²C sensor or a display controller hanging off SPI does not announce itself; it just sits there, at that address, wired to that interrupt line. The only way the kernel knows about it is if somebody says so.

For a long time that somebody was the code itself. Every board had a file inside the kernel tree describing in C which device was wired where — the kernel still describes this path, where board-specific setup code registers the devices one by one (1). On ARM these files multiplied so badly that in 2011 Linus Torvalds took the situation apart in an email, saying the ARM code was heading somewhere unsustainable. The way out that got adopted was the device tree.

The Idea

A device tree is a data file describing how the hardware is wired. It is not code — it does not run, and it is not compiled into the kernel. It is a description: there is a controller at this address, it uses this interrupt line, it is fed by this clock, and these devices hang beneath it.

The format itself rests on a formal specification (4). It is called a tree because it genuinely is one. It starts at a root node, buses branch off it, and the devices attached to those buses branch off them — mirroring how the hardware is physically connected:

Rather than an invented example, here is a real one. The node below comes from k3-am62p-main.dtsi in the U-Boot tree for TI's J722S/AM67A family — the actual description an AM67A board reads when it boots:

main_i2c0: i2c@20000000 {
    compatible = "ti,am64-i2c", "ti,omap4-i2c";
    reg = <0x00 0x20000000 0x00 0x100>;
    interrupts = <GIC_SPI 161 IRQ_TYPE_LEVEL_HIGH>;
    #address-cells = <1>;
    #size-cells = <0>;
    power-domains = <&k3_pds 102 TI_SCI_PD_EXCLUSIVE>;
    clocks = <&k3_clks 102 2>;
    clock-names = "fck";
    status = "disabled";
};

Every line here means something. reg gives the controller's address in the memory map — 0x20000000, length 0x100. The addresses are written as pairs of numbers because the address space is 64-bit; the first number carries the upper 32 bits. interrupts says which interrupt line is used; names like GIC_SPI and IRQ_TYPE_LEVEL_HIGH come from included header files, so although device tree source is plain data, it still goes through the C preprocessor.

It is worth noticing that compatible carries two values: "ti,am64-i2c" and "ti,omap4-i2c". The list is read most-specific first. The kernel looks for a driver specific to am64, and failing that falls back to the older, more general omap4 driver (5). That way a new chip can work without anyone writing a new driver for it.

clocks and clock-names say where the device gets its clock: output 2 of device 102 on &k3_clks, which the driver will find under the name "fck". power-domains wires up the power domain the same way. These are not frequency settings — they describe the wiring, which clock goes where.

The last line matters: status = "disabled". The chip vendor describes the peripheral but leaves it off, because it cannot know whether that I²C bus is used on your board. Turning it on is the job of the board's own .dts. In the J722S EVM's board file it looks like this:

&main_i2c0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&main_i2c0_pins_default>;
    clock-frequency = <400000>;

    exp1: gpio@23 {
        compatible = "ti,tca6424";
        reg = <0x23>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

The leading &main_i2c0 is a reference to the node above — it does not create a new node, it writes over the existing one. status is switched on, pins are assigned, the speed is set to 400 kHz, and a real device is added underneath: a TCA6424 GPIO expander sitting at address 0x23 on the I²C bus. Because the controller declared #address-cells = <1>, that address is written as a single number, unlike the controller's own reg.

This is how a device tree works in practice: the vendor describes the SoC once, and you say what is wired to your board.

The property that really matters is compatible. This is where the kernel looks to pair a driver with a piece of hardware. A driver says "I am compatible with ti,tca6424", a node in the device tree carries the same string, and the kernel puts the two together. Which is why typos fail silently: if the string doesn't match, there is no error — the device simply never appears.

Worth keeping in mind: a device tree describes hardware, not driver configuration. The kernel's guide to writing bindings stresses this boundary in particular (6). The line blurs constantly in practice, and it is the single most argued-about subject on the kernel lists — "is this genuinely a property of the hardware, or is it your preference?"

The Files

You will see three extensions, and they are easy to confuse (11).

.dts is the source you write, the human-readable form. .dtsi is a fragment other files can include — the "i" is for include. A chip vendor typically describes the whole SoC in a .dtsi; you write a short .dts for your own board, include it, and add only what is specific to your board.

.dtb is the compiled form — the device tree blob. This is what the kernel actually reads. The compiler is dtc (7):

dtc -I dts -O dtb -o my-board.dtb my-board.dts

It also runs the other way, which is invaluable when hunting a problem. If you have a working .dtb, you can open it up and look inside:

dtc -I dtb -O dts -o decoded.dts my-board.dtb

On a running system you can also read the device tree straight from the filesystem. The kernel exposes the tree it is actually using here:

ls /proc/device-tree/

This is the fastest answer to "is the file I edited the file that got loaded?" A surprising share of the time, the answer is no.

What Happens at Boot

The order goes like this: the bootloader — usually U-Boot (8) — loads the .dtb into memory, loads the kernel, and then hands the kernel the blob's memory address in a register as it starts it. On ARM that register and the conditions the bootloader must meet are set out in the kernel's booting document (9). Before the kernel brings up any of its drivers, it reads that tree, extracts the devices from it, and matches drivers against the compatible strings.

The practical consequence is that the kernel and the device tree can be updated separately. Adding a new sensor rarely means rebuilding the kernel; updating the .dtb is usually enough. But that cuts both ways — because they are independent, they can drift apart. Boot a new kernel with an old .dtb and the board quietly comes up incomplete, and it takes a while to work out why.

There is also the idea of overlays: fragments applied on top of an existing tree at runtime (10). This is what happens when you plug a HAT into a Raspberry Pi — the main tree stays as it is and a small fragment gets laid over it. Useful when the base board is always the same and only the module plugged into it changes.

When Something Doesn't Work

Device tree faults have a particular kind of unpleasantness, because they usually don't produce an error. The device is simply absent. Working through it in this order saves time.

Start by checking whether the device was created at all. Is the node there under /proc/device-tree? If not, the problem is not in the file you edited — most likely the wrong .dtb is being loaded, or the node is switched off a level up with status = "disabled". That second one happens constantly: peripherals usually arrive disabled in the vendor's .dtsi files, and you are expected to turn them on one by one with status = "okay" in your board's .dts.

If the node is there but no driver attached, put your eye on the compatible string. Find the of_device_id table in the driver's source and compare the strings character by character. One comma, one hyphen is enough.

There is also this: dtc warnings arrive switched off in most projects, and they genuinely tell you useful things. Address and reg mismatches, a missing #address-cells — exactly the sort of fault that turns into a silent failure at boot. It is worth keeping warnings on when you build.

Closing Thought

What makes a device tree hard to grasp is that nobody says up front what it actually is. It looks like a configuration file, but it isn't; it looks like a driver, but it isn't that either. The most useful framing is to think of it as the contract between hardware and software: how the board is wired, written down in a language the kernel understands.

Once it sits in that frame, the rest follows. Why compatible is so critical, why the .dtb lives separately from the kernel, why /proc/device-tree is the first place to look when something is missing — all of it falls out of the same idea.

References

  1. Platform Devices and Drivers — why SoC peripherals are not enumerable the way PCI and USB are, and how board-specific code registers them.
  2. How To Write Linux PCI Drivers — the pci_device_id table and the probe() call for matching devices.
  3. USB Device Drivers — ID-based driver matching on the USB side.
  4. Devicetree Specification — the formal definition of the format.
  5. Linux and the Devicetree — how the kernel reads the tree and matches drivers.
  6. Writing Devicetree Bindings — the boundary between describing hardware and configuring a driver.
  7. dtc — Device Tree Compiler — the compiler's own repository and usage.
  8. U-Boot: Devicetree Control — how U-Boot handles the device tree.
  9. Booting ARM Linux — which register the bootloader uses to hand the blob to the kernel.
  10. Devicetree Overlay Notes — how overlays are applied at runtime.
  11. Device Tree Reference (eLinux) — community reference with worked examples.

Comments