Device Tree: ARM Hardware Description
DTS syntax, of_ API, platform driver binding, and DT overlays
What is a Device Tree?
On x86, the kernel discovers hardware at runtime (ACPI, PCI enumeration). On ARM and other embedded platforms, hardware is not self-describing — a Device Tree (DT) is a static description passed from the bootloader to the kernel. The Device Tree format originated in Sun's OpenFirmware and was adapted for Linux/PowerPC before Grant Likely championed its adoption for ARM (LWN); ARM device tree support landed in Linux 3.0 (July 2011) (commit):
Bootloader (U-Boot)
→ loads kernel Image + DTB (device tree blob) into RAM
→ sets x0 = DTB physical address, x1 = 0 (reserved), x2 = 0, x3 = 0
→ jumps to kernel entry point
Kernel:
→ setup_machine_fdt(dtb) maps and parses the blob
→ creates struct device nodes for each hardware block
→ platform drivers bind to nodes via compatible string
DTS syntax
Device tree source (.dts) is compiled to binary blob (.dtb) with dtc:
/* arch/arm64/boot/dts/example.dts */
/dts-v1/;
/ {
model = "My Board v1.0";
compatible = "myvendor,myboard";
#address-cells = <1>; /* 1 u32 for addresses */
#size-cells = <1>; /* 1 u32 for sizes */
/* CPUs */
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a53";
device_type = "cpu";
reg = <0>;
enable-method = "psci";
};
};
/* Memory */
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; /* 1GB at 0x80000000 */
};
/* SoC peripherals */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges; /* identity mapping */
uart0: serial@11000000 {
compatible = "ns16550a";
reg = <0x11000000 0x1000>; /* MMIO base + size */
interrupts = <0 32 4>; /* type SPI, irq 32, active-low level */
clocks = <&uart_clk>;
clock-names = "baudclk";
current-speed = <115200>;
status = "okay"; /* "disabled" to exclude */
};
i2c0: i2c@12000000 {
compatible = "myvendor,i2c-v1";
reg = <0x12000000 0x100>;
interrupts = <0 40 4>;
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <400000>; /* 400kHz fast mode */
status = "okay";
/* I2C device at address 0x48 */
temp_sensor: temperature@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
gpio: gpio@13000000 {
compatible = "myvendor,gpio";
reg = <0x13000000 0x1000>;
#gpio-cells = <2>; /* pin + flags */
gpio-controller;
interrupt-controller;
#interrupt-cells = <2>;
};
};
};
# Compile DTS to DTB:
dtc -I dts -O dtb -o board.dtb board.dts
# Decompile DTB back to DTS:
dtc -I dtb -O dts -o board.dts board.dtb
# Dump live DTB from running kernel:
dtc -I dtb -O dts /proc/device-tree > running.dts
# or:
ls /proc/device-tree/
DTSI includes
/* SoC-level DTSI (shared hardware description): */
/* arch/arm64/boot/dts/myvendor/myvendor-soc.dtsi */
/ {
soc {
uart0: serial@11000000 {
compatible = "ns16550a";
reg = <0x11000000 0x1000>;
status = "disabled"; /* off by default */
};
};
};
/* Board-level DTS (board-specific configuration): */
/* Label references like &uart0 must appear at the TOP LEVEL, outside / { } */
#include "myvendor-soc.dtsi"
/ {
model = "My Board v1.0";
};
&uart0 {
status = "okay"; /* enable for this board */
current-speed = <115200>;
};
Platform driver binding
The compatible string
The kernel matches DT nodes to drivers via the compatible property:
/* drivers/tty/serial/ns16550.c */
/* Device table: matches "compatible" strings in DTS */
static const struct of_device_id ns16550_of_match[] = {
{ .compatible = "ns16550a", .data = &ns16550_config },
{ .compatible = "ns16550", .data = &ns16550_config },
{ .compatible = "snps,dw-apb-uart", .data = &dw_apb_config },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, ns16550_of_match);
static struct platform_driver ns16550_driver = {
.probe = ns16550_probe,
.remove = ns16550_remove,
.driver = {
.name = "ns16550",
.of_match_table = ns16550_of_match, /* DT binding */
.pm = &ns16550_pm_ops,
},
};
module_platform_driver(ns16550_driver);
probe() reading DT properties
static int ns16550_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node; /* DT node */
struct ns16550_port *port;
struct resource *res;
u32 clk_freq;
int irq, ret;
port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
if (!port)
return -ENOMEM;
/* Get MMIO resource from DT "reg" property */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
port->base = devm_ioremap_resource(dev, res);
if (IS_ERR(port->base))
return PTR_ERR(port->base);
/* Get IRQ from DT "interrupts" property */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* Read a u32 property */
ret = of_property_read_u32(np, "current-speed", &port->baud);
if (ret)
port->baud = 115200; /* default */
/* Read a string property */
const char *str;
ret = of_property_read_string(np, "clock-names", &str);
/* Read an array of u32s */
u32 clk_vals[2];
ret = of_property_read_u32_array(np, "clock-frequency", clk_vals, 2);
/* Check a boolean property */
if (of_property_read_bool(np, "fifo-enable"))
port->has_fifo = true;
/* Get a GPIO */
port->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
/* Get a clock */
port->clk = devm_clk_get(dev, "baudclk");
if (!IS_ERR(port->clk)) {
clk_prepare_enable(port->clk);
port->uartclk = clk_get_rate(port->clk);
}
/* Get a regulator */
port->vdd = devm_regulator_get_optional(dev, "vdd");
ret = devm_request_irq(dev, irq, ns16550_isr, 0, dev_name(dev), port);
if (ret)
return ret;
return uart_add_one_port(&ns16550_uart_driver, &port->uart);
}
of_ API reference
/* Node lookup */
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type,
const char *compatible);
struct device_node *of_find_node_by_phandle(phandle handle);
struct device_node *of_get_parent(const struct device_node *node);
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev);
/* Property reading */
int of_property_read_u32(const struct device_node *np,
const char *propname, u32 *out_value);
int of_property_read_u64(const struct device_node *np,
const char *propname, u64 *out_value);
int of_property_read_string(const struct device_node *np,
const char *propname, const char **out_string);
int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values, size_t sz);
bool of_property_read_bool(const struct device_node *np,
const char *propname);
/* Address translation */
int of_address_to_resource(struct device_node *dev, int index,
struct resource *r);
void __iomem *of_iomap(struct device_node *node, int index);
/* IRQ mapping */
int of_irq_get(struct device_node *dev, int index);
int of_irq_count(struct device_node *dev);
Phandles and cross-references
/* Phandles allow nodes to reference other nodes */
clocks {
uart_clk: uart_clk {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>; /* 24MHz */
};
spi_clk: spi_clk {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <48000000>;
};
};
uart0: serial@11000000 {
clocks = <&uart_clk>; /* phandle reference */
clock-names = "baudclk";
};
/* GPIO reference with args */
leds {
led-red {
gpios = <&gpio 5 GPIO_ACTIVE_HIGH>; /* gpio node, pin 5, active high */
};
};
/* Resolve a phandle reference in driver: */
struct device_node *clk_node = of_parse_phandle(np, "clocks", 0);
/* Or use the clock framework abstraction: */
struct clk *clk = of_clk_get_by_name(np, "baudclk");
Device Tree overlays (DTO)
Overlays allow dynamic modification of the live DT (e.g., Raspberry Pi hats):
/* overlay.dts - add a sensor on i2c0 */
/dts-v1/;
/plugin/;
/ {
compatible = "raspberrypi,3-model-b";
fragment@0 {
target = <&i2c0>;
__overlay__ {
#address-cells = <1>;
#size-cells = <0>;
bmp280@76 {
compatible = "bosch,bmp280";
reg = <0x76>;
};
};
};
};
# Compile overlay:
dtc -I dts -O dtb -o my-overlay.dtbo my-overlay.dts
# Apply overlay at runtime (Raspberry Pi / configfs):
mkdir /sys/kernel/config/device-tree/overlays/my-overlay
cat my-overlay.dtbo > /sys/kernel/config/device-tree/overlays/my-overlay/dtbo
# Remove overlay:
rmdir /sys/kernel/config/device-tree/overlays/my-overlay
# Check applied overlays:
ls /sys/kernel/config/device-tree/overlays/
Debugging Device Trees
# Live DT as filesystem:
ls /proc/device-tree/
cat /proc/device-tree/model
# My Board v1.0
# sysfs: platform devices created from DT:
ls /sys/bus/platform/devices/
# 11000000.serial 12000000.i2c ...
# Check driver binding:
ls -la /sys/bus/platform/devices/11000000.serial/driver
# → /sys/bus/platform/drivers/ns16550
# Dump all DT nodes matching a compatible:
grep -r "compatible" /proc/device-tree/ 2>/dev/null | head -20
# dtc on live kernel:
apt install device-tree-compiler
dtc -I fs /proc/device-tree 2>/dev/null | grep -A5 "serial@"
# Check for DT probe failures:
dmesg | grep -E "of_platform|platform|DT" | head -20
# Device Tree statistics:
cat /sys/kernel/debug/of_platform_bus/stats 2>/dev/null
# Find what matched a node:
cat /sys/bus/platform/devices/11000000.serial/of_node/compatible
# ns16550a
# fdt_probe_driver: check why driver didn't probe
dmesg | grep "11000000.serial"
# [ 1.234] 11000000.serial: ttyS0 at MMIO 0x11000000 (irq = 32, ...) is a NS16550A
Platform device lifecycle
DT blob → of_platform_populate() → creates struct platform_device for each node
↓
platform_device.name = "11000000.serial"
platform_device.dev.of_node = &dt_node
platform_device.resource[] = [MMIO, IRQ]
↓
platform_driver registered → of_match_table checked
↓
probe() called with struct platform_device *
/* During boot: arch/arm64/kernel/setup.c */
of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
/* Creates platform_device for every node with compatible that matches
of_default_bus_match_table (includes "simple-bus", etc.) */
/* struct platform_device handed to probe(): */
struct platform_device {
const char *name; /* "11000000.serial" */
int id; /* -1 if from DT */
struct device dev; /* contains of_node pointer */
u32 num_resources;
struct resource *resource; /* MMIO base+size, IRQ */
};
DT vs ACPI
| Feature | Device Tree | ACPI |
|---|---|---|
| Platform | ARM, RISC-V, PowerPC | x86, ARM64 servers |
| Description | Static DTS file compiled to DTB | AML bytecode in BIOS |
| Discovery | of_match_table in driver | HID/CID in ACPI tables |
| Updates | DT overlays, reboot | DSDT patches |
| Debug | /proc/device-tree, dtc | acpidump, acpiexec |
| Clocks | DT clock framework | ACPI _CRS |
| Power | DT regulators, PSCI | ACPI _PSx methods |
# Check if system uses ACPI or DT:
ls /proc/device-tree 2>/dev/null && echo "Device Tree" || echo "ACPI/other"
ls /sys/firmware/acpi 2>/dev/null && echo "ACPI present"
Further reading
- PCI Drivers — PCI device binding (vs DT platform binding)
- IRQ Affinity — DT interrupt-controller binding
- Preemption Model — RT on embedded/ARM
- Real-Time Tuning — PREEMPT_RT for ARM
Documentation/devicetree/bindings/— DT binding specificationsdrivers/of/— of_ API implementationscripts/dtc/— Device Tree Compilerarch/arm64/boot/dts/— upstream DTS files