Compiling Linux Device Trees
A free embedded Linux course lecture from EmbeddedPathashala
A kernel image alone can’t boot most ARM boards — it also needs a device tree, the binary description of exactly what hardware is present and how it’s wired up. This lecture, part of our free linux kernel development course, covers how the kernel build system compiles device tree source files into the .dtb binaries your bootloader hands to the kernel, and what to do when that step goes wrong. If you’re working through this as part of a broader free embedded systems course, this is the natural follow-on to building the kernel image itself.
dtbs
dtc
.dts / .dtsi
overlays
free embedded linux course
What You Will Learn
- What a device tree is for and why the kernel can’t hardcode hardware layout instead
- How the
dtbsbuild target finds and compiles device tree source files - The relationship between
.dts,.dtsi, and the compiled.dtb - How to build device trees for your board and verify the result
- Common device tree build and boot failures, and how to debug them
Prerequisites
- A kernel source tree already configured for your target ARCH (see our kbuild lecture)
- Basic familiarity with how kbuild locates architecture-specific files under
arch/$ARCH/ - The
device-tree-compilerpackage installed, or the in-treedtcthat kbuild builds automatically
Why Device Trees Exist
Early embedded ARM support in Linux hardcoded board details directly into C files — one file per board, describing every peripheral address, IRQ number, and GPIO wiring in code. That approach didn’t scale once ARM SoC vendors started shipping dozens of board variants a year. The device tree moved that description out of code and into data: a plain-text source format (.dts) that describes the hardware as a tree of nodes and properties, compiled into a compact binary (.dtb) that the bootloader passes to the kernel at boot time. The same kernel binary can then run on many boards, picking its hardware configuration from whichever .dtb was loaded rather than from board-specific code paths.
A minimal, original example — a fictional board node describing one UART and one LED, written the way you’d find it in a real .dts file:
/dts-v1/;
/ {
model = "EP Devboard Rev A";
compatible = "ep,devboard";
soc {
uart0: serial@4a000000 {
compatible = "ep,ep-uart";
reg = <0x4a000000 0x1000>;
interrupts = <38>;
status = "okay";
};
};
leds {
compatible = "gpio-leds";
status_led {
gpios = <&gpio1 5 0>;
label = "ep:status";
};
};
};
None of this needs to change if you swap the SoC’s silicon revision — only the device tree does, and the kernel driver for ep,ep-uart is matched purely through the compatible string.
The dtbs Build Target
Kbuild treats device tree compilation as its own target, separate from the kernel image itself. Running it looks like this:
$ make ARCH=arm64 dtbs
This walks the rules defined in arch/$ARCH/boot/dts/Makefile, which lists which .dts files belong to that architecture (often grouped by vendor subdirectory — arch/arm64/boot/dts/<vendor>/). Each listed source file is run through the device tree compiler, dtc, producing one .dtb per board in the same directory as its source:
$ make ARCH=arm64 dtbs
DTC arch/arm64/boot/dts/ep/ep-devboard-a.dtb
DTC arch/arm64/boot/dts/ep/ep-devboard-b.dtb
DTC arch/arm64/boot/dts/ep/ep-devboard-c.dtb
You can also target a single board directly, which is faster during iterative development than rebuilding every .dtb in the tree:
$ make ARCH=arm64 arch/arm64/boot/dts/ep/ep-devboard-a.dtb
dts, dtsi, And dtb: What Each File Actually Is
| Extension | Role |
|---|---|
.dts |
Device Tree Source — one file per board, the top-level description you compile |
.dtsi |
Device Tree Source Include — shared fragments (an SoC’s common peripherals) pulled in with #include, so multiple boards sharing the same chip don’t duplicate every node |
.dtb |
Device Tree Blob — the compiled binary your bootloader loads and passes to the kernel |
.dtbo |
Device Tree Blob Overlay — a compiled fragment applied on top of a base dtb at boot or runtime, common for add-on boards/HATs |
A typical SoC vendor’s .dtsi describes everything common to that chip family — CPU cores, memory controller, standard peripheral blocks — and each board’s .dts includes it, then adds or overrides only what’s specific to that board, like which UART is wired to a debug header or which GPIO drives a status LED.
Verifying A Compiled dtb
Because a .dtb is binary, you can’t just read it — but dtc can decompile it back to readable source for inspection, which is useful for confirming what actually got baked into the binary versus what you expected from the source:
$ dtc -I dtb -O dts arch/arm64/boot/dts/ep/ep-devboard-a.dtb | less
This round-trip is one of the fastest ways to catch a mistake like a node accidentally disabled with status = "disabled", or an overlay that didn’t apply the way you expected.
|
v
dtc compiler (make ARCH=… dtbs)
|
v
board.dtb (binary blob)
|
v
bootloader loads dtb + kernel image
|
v
kernel parses dtb –> builds internal device tree –> matches drivers via “compatible”
Real-World Use Case: Bringing Up A New Board Revision
Say your team spins a new hardware revision that moves the debug UART to a different pin and adds a second LED. With the device tree model, that change lives entirely in one new .dts file that includes the existing SoC .dtsi and overrides just the two changed nodes — no kernel or driver source changes, no separate kernel build. You add the new file to the vendor’s Makefile list, run make ARCH=arm64 dtbs, flash the new .dtb alongside the existing kernel image, and the same kernel now boots correctly on both revisions.
Common Mistakes And Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
New board’s .dtb never appears in the build output |
Board file not added to arch/$ARCH/boot/dts/<vendor>/Makefile |
Add the .dtb target explicitly to that Makefile’s dtb-y list |
| Kernel boots but a peripheral driver never probes | Node’s status property left as "disabled", or compatible string mismatch |
Decompile the built dtb with dtc -I dtb -O dts and check the node directly |
dtc warnings about missing #address-cells/#size-cells |
A container node’s addressing properties weren’t inherited or set | Explicitly set #address-cells/#size-cells on the parent node |
| Board hangs immediately after “Starting kernel…” | Wrong or mismatched dtb passed by the bootloader for that kernel/board combination | Confirm the bootloader’s dtb load command points at the correct, freshly built file |
Best Practices
- Keep shared SoC details in a
.dtsiand board-specific details in the.dts— don’t duplicate common nodes across boards - Use overlays (
.dtbo) for optional add-on hardware instead of maintaining separate full.dtsfiles per configuration - Decompile your built
.dtbafter any node change to confirm the compiled result matches your intent - Give new nodes clear, vendor-prefixed
compatiblestrings to avoid collisions with upstream driver matches
Security Considerations
A device tree is trusted, unauthenticated input to the kernel on most embedded boot chains — if an attacker can substitute the dtb, they can potentially remap memory regions or disable protections a driver relies on. On products where firmware integrity matters, verify the dtb alongside the kernel image using your bootloader’s signature/verification mechanism rather than treating it as inert configuration data.
Summary And Key Takeaways
- Device trees separate hardware description from kernel code, letting one kernel binary support many boards
make ARCH=$ARCH dtbscompiles every listed.dtsinto a.dtbusing the in-treedtccompiler.dtsifiles hold shared SoC-level detail;.dtsfiles hold board-specific detail;.dtbis the compiled result the bootloader hands to the kernel- Decompiling a built
.dtbis the fastest way to confirm what actually got compiled
Conclusion
Between this lecture and the previous one on building kernel images with kbuild, you now have the complete picture of what a bootloader needs to start Linux on an ARM board: a boot-ready kernel image and a matching device tree blob. Understanding both halves — and how kbuild builds each one independently — is one of the more practical, immediately useful skills covered in this free linux device drivers course, especially once you start bringing up your own board revisions.
Frequently Asked Questions
Why can’t the kernel just detect hardware automatically instead of using a device tree?
Many embedded SoC peripherals aren’t self-describing the way PCI or USB devices are — there’s no bus protocol to query for what’s attached. The device tree exists specifically to supply that information for hardware that can’t announce itself.
What’s the difference between a dts and a dtsi file?
A .dts is a complete, board-level source file you compile directly. A .dtsi is an include fragment, typically holding SoC-common nodes, that one or more .dts files pull in with #include to avoid duplication.
Do I need to rebuild the kernel image after changing a device tree?
No — the dtbs target is independent of the kernel image target. You can rebuild just the .dtb and reflash it alongside your existing kernel image.
How do I check what’s actually inside a compiled dtb?
Run dtc in reverse: dtc -I dtb -O dts yourfile.dtb decompiles the binary back into readable source so you can inspect exactly what was compiled.
What are device tree overlays used for?
Overlays let you apply a small device tree fragment on top of a base dtb, typically for optional add-on hardware, without maintaining a full separate .dts for every possible hardware combination.
My new board’s dtb isn’t being built — why?
The most common cause is forgetting to list the new .dtb target in the vendor’s arch/$ARCH/boot/dts/<vendor>/Makefile. Kbuild only compiles device trees it’s explicitly told about.
Keep Building Your Embedded Linux Skills
More free lectures on kernel internals, device drivers, and Bluetooth stack development are waiting for you on EmbeddedPathashala.
