Bootloaders

Device Tree

The same Linux kernel binary runs on wildly different ARM/RISC-V boards without being recompiled for each one — the device tree is why. It's a data structure describing a board's hardware (what's on the I2C bus, which pins are GPIO, where the interrupt controller lives) that the kernel reads at boot instead of having that layout hardcoded into it.

Quick reference
What it is
The compiled data structure describing a board's hardware, loaded alongside the kernel and parsed at boot instead of being hardcoded.
Mental model
A device tree is a hardware datasheet the kernel reads, not a program it runs — pure description, zero logic.
Key numbers
.dts source → dtc compiler → .dtb binary blob; an add-on board's extra nodes ship as a separate .dtbo overlay merged in at boot.
Reach for it when
Bringing up new hardware, wiring in a new sensor board, or a correctly-wired device that Linux still refuses to see.

What a device tree actually is

Before device trees, ARM Linux support meant hardcoding every board's hardware layout directly into kernel C code — a new board revision could mean a new kernel build. A device tree replaces that with a data-only description: a tree of nodes (representing buses, controllers, and devices), each with properties — the most important being compatible, a string the kernel matches against a driver's own of_device_id table to decide which driver binds to which node. This is exactly the "job posting and résumé" match described in Linux drivers — the device tree posts the jobs, drivers hold the résumés, and the kernel bus code does the matching.

Source, compiler, blob: .dts → .dtb

Device trees are written by hand as human-readable .dts (device tree source) files, then compiled by dtc (the device tree compiler) into a compact binary .dtb (device tree blob) that the bootloader actually loads into RAM.

/dts-v1/;

/ {
    compatible = "raspberrypi,4-model-b";

    soc {
        i2c1: i2c@7e804000 {
            compatible = "brcm,bcm2835-i2c";
            reg = <0x7e804000 0x1000>;
            interrupts = <2 21>;
            status = "okay";

            imu@68 {
                compatible = "invensense,mpu6050";
                reg = <0x68>;
            };
        };
    };
};

Reading this: i2c@7e804000 is a node named for its register base address; reg gives the actual address/size the kernel maps; interrupts ties it to a specific interrupt line; the nested imu@68 node describes a sensor at I2C address 0x68 sitting on that bus — the exact device tree entry that has to exist before any code on the Motion/IMU page can actually talk to that chip.

# compile a .dts source file into a .dtb blob
dtc -I dts -O dtb -o board.dtb board.dts

Overlays: .dtbo

Recompiling and replacing a board's entire base device tree just to add one sensor is both wasteful and risky. A device tree overlay is a small, separate tree — compiled to a .dtbo — that patches specific nodes into (or modifies nodes in) an already-loaded base tree, without touching the rest of it. This is exactly how add-on boards (Raspberry Pi HATs, BeagleBone capes) add their hardware to the tree without the base board's .dtb ever needing to know about every possible add-on in advance.

/dts-v1/;
/plugin/;

/ {
    compatible = "raspberrypi,4-model-b";

    fragment@0 {
        target = <&i2c1>;
        __overlay__ {
            status = "okay";

            imu@68 {
                compatible = "invensense,mpu6050";
                reg = <0x68>;
            };
        };
    };
};

/* compile as an overlay with -@ */
/* dtc -@ -I dts -O dtb -o imu-overlay.dtbo imu-overlay.dts */

target = <&i2c1> points the overlay at the i2c1 node already defined in the base tree (by its label, the i2c1: from the base .dts above) — the overlay only makes sense layered on top of a base tree that actually has a node with that label.

A base .dts and an overlay .dts are each compiled by dtc into a .dtb and .dtbo, which are then merged into the final device tree the kernel receives board.dts base tree, hand-written overlay.dts add-on board nodes dtc compiler board.dtb binary blob imu.dtbo binary overlay Final device tree merged, in RAM The overlay is merged into the base tree before the kernel ever sees it — it never exists as a standalone tree of its own.
The merge can happen in U-Boot (fdt apply) before the kernel boots, or at runtime via the kernel's configfs overlay interface.

Where the DTB fits in the boot sequence

The bootloader is responsible for getting a valid device tree blob into memory before it ever jumps to the kernel — the kernel has no way to discover its own hardware without one.

U-Boot loads the kernel and device tree blob into RAM, then jumps to the kernel with a pointer to the DTB; the kernel uses it to configure early hardware U-Boot loads kernel + DTB into RAM Linux kernel receives a pointer to the DTB Early hardware init MMU, interrupt controller, console, memory map
This is the same kernel image regardless of board — everything board-specific in that early-init step comes from parsing the DTB, not from board-specific kernel code.

Inspecting a live system

The kernel exposes the device tree it actually booted with, which is invaluable for confirming an overlay applied or a node looks the way you expect — independent of what the source files on disk say.

# the live tree the running kernel actually parsed
ls /sys/firmware/devicetree/base/
cat /proc/device-tree/compatible

# dump the live tree back into readable .dts form
dtc -I fs -O dts /sys/firmware/devicetree/base | less

# inspect a .dtb file directly, without a running kernel at all
fdtdump board.dtb

This is the same "what's actually bound" instinct as Linux drivers' i2cdetect/ls /sys/bus/platform/devices commands — trust what the running kernel reports, not what the source file on disk says it should be.

Real-world applications

Gotchas

Field notes