Linux Device Tree (DTS / DTSI)
Practices for writing correct Device Tree source files for embedded automotive IVI, HUD, and RSE targets.
Source of truth:
- Linux Device Tree Usage
- Device Tree Specification
- Binding documentation:
Documentation/devicetree/bindings/in the kernel source
When to Use This Skill
- Describing a new peripheral in the board DTS file.
- Enabling or disabling an existing node for your board.
- Writing a DT overlay for hardware add-ons.
- Verifying a binding against kernel documentation.
DTS File Types
| Extension | Role |
|---|---|
.dts |
Top-level board file — included by the build system |
.dtsi |
Include file — shared between multiple boards (/include/) |
.dtbo |
Compiled overlay (binary form of a .dts overlay) |
Basic Syntax
/dts-v1/;
#include "myvendor-soc.dtsi" // include shared SoC DTSI
/ {
model = "My Board Rev 1.0";
compatible = "myvendor,myboard", "myvendor,mysoc";
/* Memory node — required */
memory@40000000 {
device_type = "memory";
reg = <0x0 0x40000000 0x0 0x80000000>; // base 0x40000000, size 2 GiB (64-bit)
};
/* Chosen — kernel command line and stdout */
chosen {
bootargs = "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait";
stdout-path = &uart0;
};
/* Aliases */
aliases {
serial0 = &uart0;
mmc0 = &sdhci0;
};
};
Node Structure
node-name@unit-address {
compatible = "vendor,model"; // string list — first is most specific
reg = <base size>; // physical address and size
status = "okay"; // "okay" or "disabled"
// ... properties
};
#address-cells and #size-cells — tell how many 32-bit cells encode address and size in child reg properties:
soc {
#address-cells = <1>; // 1 cell for address (32-bit)
#size-cells = <1>; // 1 cell for size (32-bit)
uart0: uart@40010000 {
compatible = "arm,pl011";
reg = <0x40010000 0x1000>; // 1 address cell + 1 size cell
// ...
};
};
For 64-bit addresses:
#address-cells = <2>;
#size-cells = <2>;
uart0: uart@40010000 {
reg = <0x0 0x40010000 0x0 0x1000>;
};
Phandles and References
clocks: clock-controller@50000000 {
compatible = "myvendor,clkctrl";
reg = <0x50000000 0x1000>;
#clock-cells = <1>; // required for clock providers
};
&uart0 {
clocks = <&clocks 3>; // phandle to clocks node, clock ID = 3
clock-names = "uartclk";
};
&uart0is a reference to the node labeleduart0:.- Referencing with
&labelmerges properties into the target node.
Common Bindings
Interrupt Controller and Interrupts
intc: interrupt-controller@20000000 {
compatible = "arm,cortex-a15-gic";
reg = <0x20000000 0x1000 0x20010000 0x2000>;
interrupt-controller;
#interrupt-cells = <3>;
};
&uart0 {
interrupt-parent = <&intc>;
interrupts = <0 32 IRQ_TYPE_LEVEL_HIGH>; // type, hwirq, flags
};
GPIO
gpio0: gpio@40020000 {
compatible = "myvendor,gpio";
reg = <0x40020000 0x1000>;
gpio-controller;
#gpio-cells = <2>; // pin number + flags
};
led {
compatible = "gpio-leds";
status-led {
gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
};
Pinctrl
pinctrl0: pinctrl@40030000 {
compatible = "myvendor,pinctrl";
reg = <0x40030000 0x1000>;
uart0_pins: uart0-pins {
pins = "GPIO_A0", "GPIO_A1";
function = "uart0";
bias-pull-up;
};
};
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
};
I2C Device
&i2c0 {
clock-frequency = <400000>; // 400 kHz Fast Mode
temp_sensor: temperature@48 {
compatible = "ti,lm75";
reg = <0x48>; // 7-bit I2C address
};
};
SPI Device
&spi0 {
#address-cells = <1>;
#size-cells = <0>;
flash: flash@0 {
compatible = "jedec,spi-nor";
reg = <0>; // chip select 0
spi-max-frequency = <50000000>;
};
};
status Property
// Enable a disabled node from a DTSI
&uart1 {
status = "okay";
};
// Disable a node that was enabled in the SoC DTSI
&usb0 {
status = "disabled";
};
DT Overlays
Overlays patch the live device tree at runtime (used for add-on boards, Raspberry Pi HATs, etc.):
/dts-v1/;
/plugin/;
&i2c0 {
#address-cells = <1>;
#size-cells = <0>;
my_sensor: sensor@40 {
compatible = "myvendor,my-sensor";
reg = <0x40>;
status = "okay";
};
};
Apply at runtime:
dtoverlay my-sensor.dtbo
# or specify in /boot/config.txt (Raspberry Pi)
Specify in U-Boot FIT:
# Load base dtb, then apply overlay
fdt addr ${fdtaddr}
fdt resize 0x1000
fdt apply ${overlayaddr}
dtc — Device Tree Compiler
# Compile DTS → DTB
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# Decompile DTB → DTS (inspect a binary dtb)
dtc -I dtb -O dts -o myboard_decoded.dts myboard.dtb
# Check for errors only (no output file)
dtc -I dts -O dtb myboard.dts -o /dev/null
In the kernel build system, DTBs are compiled automatically:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
# Output: arch/arm64/boot/dts/<vendor>/myboard.dtb
Verifying Bindings
Kernel binding documentation is in Documentation/devicetree/bindings/ in RST or YAML format.
# Validate a DTS against YAML bindings (requires python3-jsonschema)
make ARCH=arm64 dt_binding_check DT_SCHEMA_FILES=Documentation/devicetree/bindings/serial/arm,pl011.yaml
make ARCH=arm64 dtbs_check
Prerequisites
- Linux host machine (Ubuntu 20.04 LTS or Debian 11 recommended).
- Cross-compilation toolchain:
arm-linux-gnueabihf-gccoraarch64-linux-gnu-gcc. - Target board with serial console access (USB-to-UART adapter).
sshaccess to target (optional but recommended).
Step-by-Step Workflows
Step 1: Identify the hardware node
Find the parent bus node in the board DTS/DTSI; prepare to add a child device node.
Step 2: Set required properties
Add compatible, reg, interrupts, and subsystem-specific properties per binding docs.
Step 3: Add phandle references
Add clocks, resets, gpio, and pinctrl-* references as needed by the driver.
Step 4: Compile and check for errors
Run dtc -I dts -O dtb -o output.dtb input.dts; fix all compiler warnings.
Step 5: Validate binding compliance
Run dt-validate -p <schemas-path> output.dtb; fix any schema constraint violations.
Troubleshooting
dtcwarning:unit_address_vs_reg— node has a unit address but noregproperty (or vice versa); either addregor remove the unit address.- Driver not binding — the
compatiblestring in DTS must exactly match the driver'sof_match_tableentry (case-sensitive). dt-validateschema not found — pass the kernelDocumentation/devicetree/bindings/path with-p; ensure the schema YAML is present.- DT overlay not applied — check U-Boot
fdtoverlay applyoutput; ensurefdtfileand overlay path are correct.
Pre-Commit Checklist
-
compatiblestring follows"vendor,model"format — no generic strings alone. -
regcells match the parent node's#address-cellsand#size-cells. -
#gpio-cells,#clock-cells,#interrupt-cellspresent on provider nodes. -
status = "okay"present on all nodes that must be active on this board. -
dtc -I dts -O dtbcompiles without errors or warnings. -
make dtbs_checkpasses (if YAML bindings are available for the peripheral). - Node labels (
uart0:) are unique across all included DTSI files.