Post

Device Tree

Device Tree

Introduction

Device Tree is a data structure and language for describing hardware in embedded systems, particularly in Linux-based platforms. Instead of hardcoding hardware information directly into the kernel or drivers, Device Tree provides a standardized way to describe the hardware components, their relationships, and configuration parameters. This approach makes the kernel more portable and allows the same kernel binary to work with different hardware configurations.


What is Device Tree?

Device Tree serves as a hardware description language that tells the operating system about the hardware components that cannot be automatically discovered (non-discoverable hardware). Think of it as a “map” that describes:

  • Hardware components: CPUs, memory, peripherals, buses
  • Hardware relationships: How components are connected
  • Hardware properties: Addresses, interrupts, clocks, pins
  • Configuration parameters: Operating modes, capabilities, constraints

Key Benefits:

  • Hardware abstraction: Separates hardware description from kernel code
  • Portability: Same kernel works with different hardware configurations
  • Maintainability: Hardware changes don’t require kernel recompilation
  • Standardization: Common format across different architectures

Device Tree Structure

Device Tree uses a hierarchical tree structure similar to a filesystem, with nodes representing hardware components and properties describing their characteristics.

Basic Syntax

/dts-v1/;

/ {
    model = "Example Board";
    compatible = "vendor,example-board";
    #address-cells = <1>;
    #size-cells = <1>;

    memory@40000000 {
        device_type = "memory";
        reg = <0x40000000 0x20000000>; // 512MB at 0x40000000
    };

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a9";
            reg = <0>;
            clock-frequency = <800000000>; // 800MHz
        };
    };

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;
        
        uart0: serial@44e09000 {
            compatible = "ti,am335x-uart", "ti,omap3-uart";
            reg = <0x44e09000 0x2000>;
            interrupts = <72>;
            status = "okay";
        };

        i2c0: i2c@44e0b000 {
            compatible = "ti,omap4-i2c";
            reg = <0x44e0b000 0x1000>;
            interrupts = <70>;
            #address-cells = <1>;
            #size-cells = <0>;
            
            eeprom@50 {
                compatible = "at,24c256";
                reg = <0x50>;
            };
        };
    };
};

Key Concepts

Nodes and Properties

Nodes represent hardware components:

uart0: serial@44e09000 {
    // Properties go here
};

Properties describe node characteristics:

compatible = "ti,am335x-uart", "ti,omap3-uart";  // String list
reg = <0x44e09000 0x2000>;                       // Address/size
interrupts = <72>;                               // Interrupt number
status = "okay";                                 // Status string
clock-frequency = <800000000>;                   // Integer value

Addressing

#address-cells: Number of cells needed for addresses #size-cells: Number of cells needed for sizes reg: Register address and size

soc {
    #address-cells = <1>;  // Address needs 1 cell (32-bit)
    #size-cells = <1>;     // Size needs 1 cell (32-bit)
    
    uart0: serial@44e09000 {
        reg = <0x44e09000 0x2000>;  // Address: 0x44e09000, Size: 0x2000
    };
};

References and Phandles

Phandles allow nodes to reference other nodes:

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

    uart0: serial@44e09000 {
        compatible = "ti,am335x-uart";
        reg = <0x44e09000 0x2000>;
        clocks = <&osc>;        // Reference to oscillator
        clock-names = "fclk";
    };
};

Common Properties

Standard Properties

node-name {
    compatible = "vendor,device-model";     // Device identification
    reg = <address size>;                   // Register address/size
    interrupts = <irq-number>;             // Interrupt configuration
    status = "okay" | "disabled";          // Enable/disable status
    
    // Clock properties
    clocks = <&clock-phandle>;
    clock-names = "functional-clock";
    
    // GPIO properties  
    gpios = <&gpio-controller pin flags>;
    gpio-names = "reset-gpio";
    
    // Power management
    power-domains = <&power-controller>;
    
    // DMA properties
    dmas = <&dma-controller channel>;
    dma-names = "tx", "rx";
};

Bus-Specific Properties

I2C devices:

i2c@address {
    #address-cells = <1>;
    #size-cells = <0>;
    
    sensor@1a {
        compatible = "st,lis3dh-accel";
        reg = <0x1a>;              // I2C slave address
    };
};

SPI devices:

spi@address {
    #address-cells = <1>;
    #size-cells = <0>;
    
    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;                 // Chip select
        spi-max-frequency = <25000000>;
    };
};

Compilation and Tools

Device Tree Compiler (dtc)

Compile .dts to .dtb:

1
2
3
4
5
6
7
8
9
# Using kernel build system
make ARCH=arm dtbs                    # Build all DTBs
make ARCH=arm my-board.dtb           # Build specific DTB

# Using dtc directly
dtc -I dts -O dtb -o output.dtb input.dts

# Decompile .dtb back to .dts
dtc -I dtb -O dts -o output.dts input.dtb

Include and preprocessing:

/dts-v1/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include "am335x.dtsi"              // Include base SoC definition

/ {
    model = "Custom AM335x Board";
    
    gpio-leds {
        compatible = "gpio-leds";
        
        led0 {
            label = "status";
            gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
            default-state = "off";
        };
    };
};

Practical Examples

Example 1: GPIO LED Configuration

/ {
    gpio-leds {
        compatible = "gpio-leds";
        pinctrl-names = "default";
        pinctrl-0 = <&led_pins>;
        
        led-heartbeat {
            label = "heartbeat";
            gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
            default-state = "off";
        };
        
        led-status {
            label = "status";  
            gpios = <&gpio1 22 GPIO_ACTIVE_HIGH>;
            default-state = "on";
        };
    };
    
    gpio-keys {
        compatible = "gpio-keys";
        
        button-user {
            label = "User Button";
            gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
            linux,code = <KEY_ENTER>;
            debounce-interval = <50>;
        };
    };
};

Example 2: I2C Sensor Configuration

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;  // 400kHz
    
    /* Temperature sensor */
    tmp102@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
        interrupt-parent = <&gpio0>;
        interrupts = <7 IRQ_TYPE_LEVEL_LOW>;
    };
    
    /* Accelerometer */
    lis3dh@19 {
        compatible = "st,lis3dh-accel";
        reg = <0x19>;
        
        interrupt-parent = <&gpio1>;
        interrupts = <6 IRQ_TYPE_EDGE_RISING>;
        
        st,drdy-int-pin = <1>;
        st,click-single-x;
        st,click-single-y;  
        st,click-single-z;
    };
    
    /* EEPROM */
    eeprom@50 {
        compatible = "atmel,24c256";
        reg = <0x50>;
        pagesize = <64>;
    };
};

Example 3: SPI Device Configuration

&spi0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi0_pins>;
    
    spidev@0 {
        compatible = "rohm,dh2228fv";
        reg = <0>;
        spi-max-frequency = <1000000>;
    };
    
    flash@1 {
        compatible = "jedec,spi-nor";
        reg = <1>;
        spi-max-frequency = <25000000>;
        
        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;
            
            bootloader@0 {
                label = "bootloader";
                reg = <0x0 0x40000>;
                read-only;
            };
            
            kernel@40000 {
                label = "kernel";
                reg = <0x40000 0x300000>;
            };
            
            rootfs@340000 {
                label = "rootfs";
                reg = <0x340000 0xcc0000>;
            };
        };
    };
};

Device Tree Overlays

Overlays allow runtime modification of the device tree without rebuilding the entire DTB. Commonly used in systems like Raspberry Pi.

Overlay Syntax

/dts-v1/;
/plugin/;

/ {
    compatible = "ti,am335x-bone-black";
    
    /* Fragment to enable I2C2 */
    fragment@0 {
        target = <&i2c2>;
        __overlay__ {
            status = "okay";
            clock-frequency = <100000>;
            
            tmp102@48 {
                compatible = "ti,tmp102";
                reg = <0x48>;
            };
        };
    };
    
    /* Fragment to add GPIO LEDs */
    fragment@1 {
        target-path = "/";
        __overlay__ {
            my-leds {
                compatible = "gpio-leds";
                
                led@0 {
                    label = "overlay-led";
                    gpios = <&gpio1 16 0>;
                    default-state = "off";
                };
            };
        };
    };
};

Apply overlay at runtime:

1
2
3
4
5
# Load overlay
echo "my-overlay.dtbo" > /sys/kernel/config/device-tree/overlays/my-overlay/path

# Unload overlay  
rmdir /sys/kernel/config/device-tree/overlays/my-overlay/

Debugging Device Tree

Verification and Analysis Tools

Check compiled device tree:

1
2
3
4
5
6
7
8
9
# View current device tree in kernel
ls -la /sys/firmware/devicetree/base/
cat /sys/firmware/devicetree/base/model

# Compare device trees
scripts/dtc/dtx_diff arch/arm/boot/dts/am335x-boneblack.dts /proc/device-tree

# Validate device tree
dtc -I dts -O dtb -o /dev/null -W no-unit_address_vs_reg my-board.dts

Debug kernel boot messages:

1
2
3
4
5
# Boot with device tree debugging
dmesg | grep -i "device tree\|of:"

# Check for driver binding issues
dmesg | grep -i "bound\|match\|probe"

Common Issues and Solutions

Issue 1: Device not detected

// Problem: Missing or incorrect compatible string
sensor@48 {
    compatible = "wrong,sensor-name";  // Wrong!
    reg = <0x48>;
};

// Solution: Use correct compatible string
sensor@48 {
    compatible = "ti,tmp102";          // Correct!
    reg = <0x48>;
};

Issue 2: Address/size cell mismatch

// Problem: Inconsistent addressing
parent {
    #address-cells = <2>;    // Parent expects 2 cells
    #size-cells = <1>;
    
    child@100 {
        reg = <0x100 0x10>;  // Only 1 address cell! Wrong!
    };
};

// Solution: Match parent's addressing scheme
parent {
    #address-cells = <2>;
    #size-cells = <1>;
    
    child@100 {
        reg = <0x0 0x100 0x10>;  // 2 address cells, 1 size cell
    };
};

Best Practices

Organization and Maintainability

1. Use includes and modularity:

// base-soc.dtsi - SoC-level definitions
// board-common.dtsi - Board family common features  
// specific-board.dts - Board-specific configuration

#include "am335x.dtsi"           // SoC base
#include "am335x-bone-common.dtsi"  // Board family

2. Consistent naming:

// Good naming convention
uart0: serial@44e09000 { };      // Label: uart0, node: serial@address
i2c0: i2c@44e0b000 { };         // Label: i2c0, node: i2c@address
gpio_keys: gpio-keys { };        // Label: gpio_keys, node: gpio-keys

3. Status management:

// Disable by default in SoC file
uart1: serial@44e09100 {
    compatible = "ti,am335x-uart";
    reg = <0x44e09100 0x2000>;
    status = "disabled";    // Default disabled
};

// Enable in board file
&uart1 {
    status = "okay";        // Enable for this board
    pinctrl-names = "default";
    pinctrl-0 = <&uart1_pins>;
};

Integration with Linux

Kernel Driver Integration

Driver matching process:

  1. Kernel reads device tree at boot
  2. Creates platform devices from DT nodes
  3. Matches drivers using compatible strings
  4. Calls driver probe function with device tree data

Example driver integration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In driver code
static const struct of_device_id my_sensor_dt_ids[] = {
    { .compatible = "vendor,my-sensor-v1" },
    { .compatible = "vendor,my-sensor-v2" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_dt_ids);

static struct platform_driver my_sensor_driver = {
    .probe = my_sensor_probe,
    .remove = my_sensor_remove,
    .driver = {
        .name = "my-sensor",
        .of_match_table = my_sensor_dt_ids,
    },
};

Reading device tree properties in driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int my_sensor_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    u32 frequency;
    const char *name;
    
    // Read integer property
    if (of_property_read_u32(np, "clock-frequency", &frequency))
        frequency = 1000000;  // Default value
    
    // Read string property
    if (of_property_read_string(np, "sensor-name", &name))
        name = "default-sensor";
    
    // Check boolean property
    bool is_active = of_property_read_bool(np, "active-low");
    
    return 0;
}

Conclusion

Device Tree is a powerful and essential technology for describing hardware in modern embedded Linux systems. Key takeaways:

  • Separates hardware description from kernel code, improving portability and maintainability
  • Uses hierarchical tree structure with nodes representing hardware components and properties describing their characteristics
  • Provides standardized syntax for describing addresses, interrupts, clocks, GPIOs, and other hardware resources
  • Supports overlays for runtime hardware configuration changes
  • Integrates seamlessly with Linux kernel driver model through compatible string matching

Best practices for success:

  • Start with existing examples for similar hardware
  • Use modular organization with includes
  • Follow consistent naming conventions
  • Validate syntax with dtc compiler
  • Test thoroughly with actual hardware

Device Tree knowledge is essential for embedded Linux developers working with ARM, RISC-V, and other architectures. Understanding DT enables you to effectively describe hardware, debug hardware-related issues, and create portable, maintainable embedded systems.


This post is licensed under CC BY 4.0 by the author.