Device Tree Basics

Device Tree is a data structure that describes hardware. The kernel uses it to discover and configure devices at runtime.

What is Device Tree?

Device Tree provides:

  • Hardware description separate from driver code
  • Board-specific configuration without recompiling kernel
  • Standardized format for describing devices
flowchart LR
    subgraph Before["Before Device Tree"]
        BC[Board Code in C]
        BK[Kernel]
        BC -->|"Compiled into"| BK
    end

    subgraph After["With Device Tree"]
        DTS[DTS File]
        DTB[DTB Binary]
        K[Kernel]
        DTS -->|"Compile"| DTB
        DTB -->|"Pass at boot"| K
    end

    style Before fill:#8f6d72,stroke:#c62828
    style After fill:#7a8f73,stroke:#2e7d32

File Types

Extension Description
.dts Device Tree Source (main board file)
.dtsi Device Tree Source Include (shared/SoC file)
.dtb Device Tree Blob (compiled binary)
.dtbo Device Tree Blob Overlay

Basic Syntax

Nodes

/* Root node */
/ {
    /* Child nodes */
    node_name {
        /* Properties */
        property = "value";

        /* Nested child */
        child_node {
            another-property;
        };
    };
};

Node Naming

/* Format: name@unit-address */
uart0: serial@10000000 {
    /* ... */
};

/*
 * uart0     = label (for phandle references)
 * serial    = generic name
 * 10000000  = unit address (first reg value)
 */

Properties

node {
    /* String */
    compatible = "vendor,device";

    /* String list */
    compatible = "vendor,device-v2", "vendor,device";

    /* 32-bit cells */
    reg = <0x10000000 0x1000>;

    /* 64-bit value (two 32-bit cells) */
    reg = <0x0 0x10000000 0x0 0x1000>;

    /* Empty/boolean (presence means true) */
    read-only;

    /* phandle reference */
    clocks = <&clk_provider>;

    /* phandle with arguments */
    interrupts-extended = <&gic 0 15 4>;

    /* Byte array */
    mac-address = [00 11 22 33 44 55];
};

Standard Properties

compatible

Identifies the device and matches to drivers:

uart@10000000 {
    /* Most specific to least specific */
    compatible = "vendor,uart-v2", "vendor,uart", "ns16550a";
};

Driver matching:

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,uart-v2" },
    { .compatible = "vendor,uart" },
    { }
};

reg

Specifies memory-mapped addresses:

/* Parent defines cell sizes */
soc {
    #address-cells = <1>;  /* 1 cell for address */
    #size-cells = <1>;     /* 1 cell for size */

    uart@10000000 {
        reg = <0x10000000 0x1000>;
        /*      ^address   ^size */
    };
};

/* 64-bit addressing */
soc {
    #address-cells = <2>;
    #size-cells = <2>;

    device@100000000 {
        reg = <0x1 0x00000000 0x0 0x10000>;
        /*     ^--address--^  ^--size--^ */
    };
};

/* Multiple regions */
device@10000000 {
    reg = <0x10000000 0x1000>,  /* Region 0 */
          <0x10002000 0x100>;   /* Region 1 */
    reg-names = "regs", "fifo";
};

interrupts

Specifies interrupt connections:

/* Simple format */
device@10000000 {
    interrupts = <15>;  /* IRQ 15 */
};

/* GIC format: <type num flags> */
device@10000000 {
    interrupt-parent = <&gic>;
    interrupts = <0 15 4>;
    /*           ^  ^  ^
     *           |  |  |_ flags (4 = level high)
     *           |  |___ interrupt number
     *           |______ type (0=SPI, 1=PPI)
     */
};

/* Multiple interrupts */
device@10000000 {
    interrupts = <0 15 4>, <0 16 4>;
    interrupt-names = "rx", "tx";
};

status

Controls device enabling:

/* Device is enabled */
device@10000000 {
    status = "okay";
};

/* Device is disabled */
device@10000000 {
    status = "disabled";
};

/* SoC dtsi disables, board dts enables */
/* In SoC.dtsi: */
uart0: serial@10000000 {
    status = "disabled";
};

/* In board.dts: */
&uart0 {
    status = "okay";
};

Including Files

Basic Include

/dts-v1/;

/* Include SoC definitions */
#include "soc.dtsi"

/ {
    model = "My Board";
    compatible = "vendor,my-board";

    /* Board-specific additions */
};

Overriding Properties

/* In soc.dtsi */
uart0: serial@10000000 {
    compatible = "vendor,uart";
    reg = <0x10000000 0x1000>;
    status = "disabled";
};

/* In board.dts */
#include "soc.dtsi"

/* Override using label reference */
&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
};

Cell Sizes

#address-cells and #size-cells define how child reg properties are interpreted:

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    soc@0 {
        /* Uses parent's cell sizes */
        reg = <0x0 0x10000000>;

        #address-cells = <1>;
        #size-cells = <1>;

        uart@10000000 {
            /* Uses soc's cell sizes */
            reg = <0x10000000 0x1000>;
        };
    };
};

Labels and Phandles

Labels create references between nodes:

/* Define with label */
clk_uart: clock@20000000 {
    compatible = "vendor,clock";
    #clock-cells = <0>;
};

/* Reference using phandle */
uart@10000000 {
    clocks = <&clk_uart>;
};

With arguments:

/* Clock provider with multiple outputs */
clocks: clock-controller@20000000 {
    #clock-cells = <1>;
};

uart@10000000 {
    /* Reference clock output 5 */
    clocks = <&clocks 5>;
};

Special Nodes

/aliases

Provides shorthand names:

/ {
    aliases {
        serial0 = &uart0;
        serial1 = &uart1;
        ethernet0 = &eth0;
    };
};

/chosen

Kernel boot parameters:

/ {
    chosen {
        bootargs = "console=ttyS0,115200";
        stdout-path = "serial0:115200n8";
    };
};

/memory

System memory:

/ {
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  /* 1GB at 0x80000000 */
    };
};

Complete Example

/dts-v1/;

/ {
    compatible = "vendor,myboard";
    model = "Vendor MyBoard";

    #address-cells = <1>;
    #size-cells = <1>;

    aliases {
        serial0 = &uart0;
    };

    chosen {
        stdout-path = "serial0:115200n8";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;
    };

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            compatible = "arm,cortex-a53";
            device_type = "cpu";
            reg = <0>;
        };
    };

    clocks {
        osc: oscillator {
            compatible = "fixed-clock";
            #clock-cells = <0>;
            clock-frequency = <24000000>;
        };
    };

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        gic: interrupt-controller@10000000 {
            compatible = "arm,gic-400";
            #interrupt-cells = <3>;
            interrupt-controller;
            reg = <0x10000000 0x1000>,
                  <0x10001000 0x2000>;
        };

        uart0: serial@20000000 {
            compatible = "vendor,uart";
            reg = <0x20000000 0x1000>;
            interrupts = <0 10 4>;
            clocks = <&osc>;
            status = "okay";
        };
    };
};

Compiling Device Tree

# Compile DTS to DTB
dtc -I dts -O dtb -o board.dtb board.dts

# Decompile DTB to DTS
dtc -I dtb -O dts -o board_decompiled.dts board.dtb

# Kernel build system
make dtbs                    # Build all DTBs
make vendor/board.dtb        # Build specific DTB

Viewing Device Tree

# View compiled device tree on running system
ls /sys/firmware/devicetree/base/

# Read properties
cat /sys/firmware/devicetree/base/compatible
hexdump /sys/firmware/devicetree/base/soc/uart@10000000/reg

# Using dtc
dtc -I fs /sys/firmware/devicetree/base/

Exercise: Decompile and Inspect a DTB

This exercise walks through decompiling a device tree blob to understand a real board’s hardware description.

Step 1: Find a DTB

# On a running system:
ls /sys/firmware/fdt 2>/dev/null         # Raw FDT blob

# Or in the kernel build tree:
ls arch/arm64/boot/dts/*/*.dtb

# In QEMU, dump the live tree:
dtc -I fs -O dts /sys/firmware/devicetree/base/ > live.dts

Step 2: Decompile to Human-Readable DTS

# From a .dtb file
dtc -I dtb -O dts -o decompiled.dts board.dtb

# From the live system filesystem
dtc -I fs -O dts -o live.dts /sys/firmware/devicetree/base/

Step 3: Inspect Key Nodes

# Find all compatible strings (shows what drivers match)
grep "compatible" decompiled.dts

# Find all interrupt mappings
grep -A 2 "interrupts" decompiled.dts

# Find memory layout
grep -A 3 "memory@" decompiled.dts

# Find all devices with status = "okay"
grep -B 5 'status = "okay"' decompiled.dts

Step 4: Compare with an Overlay

# Compile an overlay
dtc -I dts -O dtb -@ -o overlay.dtbo overlay.dts

# Apply overlay to base DTB
fdtoverlay -i base.dtb -o merged.dtb overlay.dtbo

# Decompile merged result to verify
dtc -I dtb -O dts -o merged.dts merged.dtb
diff decompiled.dts merged.dts

Decompiled DTS loses comments and may reorder nodes. It won’t match the original source exactly, but all data is preserved.

Summary

  • Device Tree separates hardware description from driver code
  • Nodes describe devices, properties describe configuration
  • compatible matches devices to drivers
  • reg specifies memory addresses, interrupts specifies IRQs
  • Labels create phandle references between nodes
  • .dtsi files are included, .dts is the main board file

Next

Learn about device bindings and how drivers match with Device Tree nodes.


Back to top

Linux Driver Development Guide is a community resource for learning kernel driver development. Not affiliated with the Linux Foundation. Content provided for educational purposes.

This site uses Just the Docs, a documentation theme for Jekyll.