In-Depth Analysis of Linux Device Trees

In-Depth Analysis of Linux Device Trees

1. Basic Concepts and Historical Background of Device Trees

1.1 What is a Device Tree?

A Device Tree is a data structure that describes hardware configuration, using a tree structure to represent system hardware resources in text form, including CPU, memory, buses, peripherals, etc. The core idea of the Device Tree is to separate hardware description from the operating system kernel, allowing the same kernel image to support different hardware platforms.

// Simple example of a Device Tree
/dts-v1/;

/ {
    model = "My Board";
    compatible = "my-company,my-board";
    
    cpus {
        cpu@0 {
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
    };
    
    memory@0 {
        device_type = "memory";
        reg = <0x00000000 0x40000000>;
    };
    
    uart@101f1000 {
        compatible = "ns16550";
        reg = <0x101f1000 0x1000>;
        interrupts = <0 0 4>;
    };
};

1.2 Development History of Device Trees

Period Technical Solution Advantages and Disadvantages
Before 2000 Hard-coded hardware information Difficult kernel porting, code redundancy
2000-2005 Open Firmware Used in PowerPC architecture, limited cross-platform capability
2005-2011 Platform Device (platform_device) Partially decoupled, but still requires kernel recompilation
2011 to present Flattened Device Tree (FDT) Truly separates hardware description from the kernel

Key Turning Point: In 2011, Linus Torvalds strongly advocated for the adoption of Device Trees in ARM architecture discussions, marking the widespread adoption of Device Trees in embedded Linux.

2. Working Principles and Core Mechanisms of Device Trees

2.1 Device Tree Data Flow Architecture

Boot Phase
Compile Phase






DTS Source File
DTC Compiler
DTB Binary File
Bootloader Loads
Kernel Parses
Device Node Creation
Driver Matching
Device Initialization

2.2 Core Data Structures of Device Trees

// Core data structure of Device Tree in the kernel
struct device_node {
    const char *name;           // Node name
    const char *type;           // Device type
    phandle phandle;            // Node handle
    const char *full_name;      // Full path name of the node
    struct property *properties; // Property list
    struct device_node *parent; // Parent node
    struct device_node *child;  // Child node
    struct device_node *sibling;// Sibling node
    struct kobject kobj;        // kobject for sysfs
    unsigned long _flags;       // Internal flags
    void *data;                 // Device private data
};

struct property {
    char *name;                 // Property name
    int length;                 // Property value length
    void *value;                // Property value pointer
    struct property *next;      // Next property
    unsigned long _flags;       // Internal flags
    unsigned int unique_id;     // Unique ID
};

2.3 Device Tree and Driver Matching Mechanism

Device Trees achieve driver matching through the <span>compatible</span> property:

// Definition in Device Tree
mydevice@0x10000000 {
    compatible = "vendor,device-model", "generic-driver";
    reg = <0x10000000 0x1000>;
    interrupts = <0 25 4>;
};

// Matching table in the driver
static const struct of_device_id my_driver_match[] = {
    { .compatible = "vendor,device-model" },
    { .compatible = "generic-driver" },
    {}
};

MODULE_DEVICE_TABLE(of, my_driver_match);

// Platform driver structure
static struct platform_driver my_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-device",
        .of_match_table = my_driver_match,
    },
};

3. Detailed Explanation of Device Tree Syntax and Semantics

3.1 Device Tree Node Structure

// Basic syntax of a node
[label:] node-name[@unit-address] {
    [properties]
    [child-nodes]
};

// Actual example
i2c@1c22000 {
    compatible = "allwinner,sun6i-a31-i2c";
    reg = <0x01c22000 0x400>;
    interrupts = <0 7 4>;
    clocks = <&bus_gates 110>;
    resets = <&apb1_rst 0>;
    #address-cells = <1>;
    #size-cells = <0>;
    
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <16>;
    };
};

3.2 Detailed Explanation of Core Properties

Property Name Data Type Function Example
compatible String List Key for driver matching <span>"vendor,model"</span>
reg Integer Array Register address range <span><0x1000 0x100></span>
interrupts Integer Array Interrupt number, trigger type <span><0 25 4></span>
#address-cells Integer Number of address cells for child nodes <span><1></span>
#size-cells Integer Number of size cells for child nodes <span><1></span>
status String Device status <span>"okay"</span>, <span>"disabled"</span>

Please open in WeChat client

3.3 Device Tree Inclusion and Overlay Mechanism

// base.dtsi - Basic definitions
/ {
    model = "My Board";
    compatible = "my-company,my-board";
    
    memory@0 {
        device_type = "memory";
        reg = <0x00000000 0x40000000>;
    };
};

// board.dts - Board-specific configuration
#include "base.dtsi"

/ {
    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
    };
    
    leds {
        compatible = "gpio-leds";
        led0 {
            label = "heartbeat";
            gpios = <&pio 0 10 0>;
            linux,default-trigger = "heartbeat";
        };
    };
};

4. Compilation and Loading Process of Device Trees

4.1 Detailed Compilation Process

Kernel DTB Binary DTC Compiler DTS Preprocessor DTS Source File Kernel DTB Binary DTC Compiler DTS Preprocessor DTS Source File Preprocessing Phase Compilation Phase Kernel Usage Phase Preprocessed DTS Generates Flattened Device Tree Binary Bootloader Passes DTB Parses Device Tree Creates Device Nodes Driver Matching Initialization

4.2 Kernel Parsing Code Flow

// Core function call chain for Device Tree parsing
start_kernel()
    --> setup_arch(&command_line)
        --> unflatten_device_tree()
            --> __unflatten_device_tree()
                --> unflatten_dt_nodes()
    
// Driver matching process
driver_register()
    --> bus_add_driver()
        --> driver_attach()
            --> __driver_attach()
                --> driver_match_device()
                    --> of_driver_match_device()

// Device creation process
of_platform_populate()
    --> of_platform_bus_create()
        --> of_platform_device_create_pdata()

5. Device Tree and Driver Interaction Mechanism

5.1 Accessing Device Tree APIs in Drivers

#include <linux/of.h>
#include <linux/of_device.h>

// 1. Get device node
struct device_node *np = pdev->dev.of_node;

// 2. Read string property
const char *name = NULL;
of_property_read_string(np, "device-name", &name);

// 3. Read integer property
u32 reg_value;
of_property_read_u32(np, "reg", &reg_value);

// 4. Read array property
int array[10];
int count = of_property_read_variable_u32_array(np, "values", array, 0, 10);

// 5. Get GPIO descriptor
struct gpio_desc *gpio = gpiod_get(&pdev->dev, "enable", GPIOD_OUT_LOW);

// 6. Get interrupt number
int irq = platform_get_irq(pdev, 0);

// 7. Get register resource
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
void __iomem *base = devm_ioremap_resource(&pdev->dev, res);

5.2 Example of Accessing Complex Devices

// Device Tree usage in a complex device driver
static int my_driver_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct my_private_data *priv;
    int ret;
    
    // Allocate private data structure
    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    
    // Get register base address
    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);
    
    // Get clock
    priv->clk = devm_clk_get(dev, "core");
    if (IS_ERR(priv->clk)) {
        dev_err(dev, "failed to get core clock\n");
        return PTR_ERR(priv->clk);
    }
    
    // Get reset control
    priv->reset = devm_reset_control_get_optional(dev, NULL);
    if (IS_ERR(priv->reset)) {
        dev_err(dev, "failed to get reset control\n");
        return PTR_ERR(priv->reset);
    }
    
    // Get interrupt
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0) {
        dev_err(dev, "failed to get IRQ\n");
        return priv->irq;
    }
    
    // Parse custom properties
    if (of_property_read_bool(np, "big-endian"))
        priv->flags |= BIG_ENDIAN_FLAG;
    
    u32 dma_mask;
    if (!of_property_read_u32(np, "dma-mask", &dma_mask))
        dma_set_mask(dev, dma_mask);
    
    // Register interrupt handler
    ret = devm_request_irq(dev, priv->irq, my_irq_handler, 
                           IRQF_SHARED, dev_name(dev), priv);
    if (ret) {
        dev_err(dev, "failed to request IRQ %d: %d\n", priv->irq, ret);
        return ret;
    }
    
    platform_set_drvdata(pdev, priv);
    return 0;
}

6. Advanced Features of Device Trees

6.1 Device Tree Overlay Mechanism

Device Tree overlays allow dynamic modification of the device tree at runtime, commonly used for modular hardware:

// Base Device Tree
/ {
    fragment@0 {
        target = <&i2c1>;
        
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";
            
            touchscreen@38 {
                compatible = "edt,edt-ft5406";
                reg = <0x38>;
                interrupt-parent = <&gpio>;
                interrupts = <5 2>;
            };
        };
    };
};

6.2 Conditional Compilation and Configuration

// Using preprocessing for conditional configuration
/dts-v1/;

#include "base.dtsi"

/ {
    model = "My Board Rev ";
    
    leds {
        compatible = "gpio-leds";
        
#ifdef BOARD_REV_A
        led0 {
            gpios = <&pio 0 10 0>;
        };
#else
        led0 {
            gpios = <&pio 1 5 0>;
        };
#endif
    };
};

7. Practical Example: Simple GPIO Device Implementation

7.1 Device Tree Definition

// my-gpio-device.dts
/dts-v1/;
/plugin/;

/ {
    compatible = "my-company,my-board";
    
    fragment@0 {
        target = <&gpio>;
        __overlay__ {
            my_gpio_pins: my_gpio_pins {
                pins = "PA10", "PA11";
                function = "gpio_out";
            };
        };
    };
    
    fragment@1 {
        target-path = "/";
        __overlay__ {
            my_gpio_device {
                compatible = "my-company,my-gpio-device";
                status = "okay";
                pinctrl-names = "default";
                pinctrl-0 = <&my_gpio_pins>;
                
                led-gpios = <&pio 0 10 GPIO_ACTIVE_HIGH>,  /* PA10 */
                           <&pio 0 11 GPIO_ACTIVE_HIGH>;  /* PA11 */
                
                button-gpios = <&pio 0 12 GPIO_ACTIVE_LOW>;
                
                device-name = "my-custom-device";
                clock-frequency = <100000>;
            };
        };
    };
};

7.2 Corresponding Driver Program

// my-gpio-driver.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/of.h>

struct my_gpio_data {
    struct gpio_desc *leds[2];
    struct gpio_desc *button;
    int irq;
    struct device *dev;
};

static irqreturn_t button_irq_handler(int irq, void *dev_id)
{
    struct my_gpio_data *priv = dev_id;
    
    // Read button state and control LED
    int state = gpiod_get_value(priv->button);
    gpiod_set_value(priv->leds[0], state);
    gpiod_set_value(priv->leds[1], !state);
    
    dev_info(priv->dev, "Button state: %d\n", state);
    return IRQ_HANDLED;
}

static int my_gpio_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct my_gpio_data *priv;
    int ret, i;
    u32 clock_freq;
    const char *device_name;
    
    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    
    priv->dev = dev;
    
    // Get LED GPIOs
    for (i = 0; i < 2; i++) {
        priv->leds[i] = devm_gpiod_get_index(dev, "led", i, GPIOD_OUT_LOW);
        if (IS_ERR(priv->leds[i])) {
            dev_err(dev, "failed to get LED GPIO %d\n", i);
            return PTR_ERR(priv->leds[i]);
        }
    }
    
    // Get button GPIO
    priv->button = devm_gpiod_get(dev, "button", GPIOD_IN);
    if (IS_ERR(priv->button)) {
        dev_err(dev, "failed to get button GPIO\n");
        return PTR_ERR(priv->button);
    }
    
    // Get device tree properties
    ret = of_property_read_string(np, "device-name", &device_name);
    if (ret)
        device_name = "default";
    
    ret = of_property_read_u32(np, "clock-frequency", &clock_freq);
    if (ret)
        clock_freq = 100000; // Default value
    
    dev_info(dev, "Device %s probed, clock frequency: %d Hz\n", 
             device_name, clock_freq);
    
    // Set up interrupt
    priv->irq = gpiod_to_irq(priv->button);
    ret = devm_request_irq(dev, priv->irq, button_irq_handler,
                          IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                          "my-gpio-button", priv);
    if (ret) {
        dev_err(dev, "failed to request IRQ: %d\n", ret);
        return ret;
    }
    
    platform_set_drvdata(pdev, priv);
    return 0;
}

static int my_gpio_remove(struct platform_device *pdev)
{
    struct my_gpio_data *priv = platform_get_drvdata(pdev);
    
    // Turn off LEDs
    gpiod_set_value(priv->leds[0], 0);
    gpiod_set_value(priv->leds[1], 0);
    
    dev_info(&pdev->dev, "Device removed\n");
    return 0;
}

static const struct of_device_id my_gpio_of_match[] = {
    { .compatible = "my-company,my-gpio-device" },
    {}
};
MODULE_DEVICE_TABLE(of, my_gpio_of_match);

static struct platform_driver my_gpio_driver = {
    .probe = my_gpio_probe,
    .remove = my_gpio_remove,
    .driver = {
        .name = "my-gpio-device",
        .of_match_table = my_gpio_of_match,
    },
};

module_platform_driver(my_gpio_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple GPIO device driver with Device Tree support");

8. Debugging and Diagnostics of Device Trees

8.1 Common Tool Commands

Tool Command Function Description Usage Example
<span>dtc</span> Device Tree Compiler <span>dtc -I dtb -O dts -o output.dts input.dtb</span>
<span>fdtdump</span> Display DTB content <span>fdtdump input.dtb</span>
<span>dtc -O dtb</span> Compile DTS to DTB <span>dtc -O dtb -o output.dtb input.dts</span>
<span>of_find_node_by_name</span> Kernel debugging function Find node in driver
<span>cat /proc/device-tree</span> View loaded device tree <span>find /proc/device-tree -type f</span>

8.2 Debugging Tips and Best Practices

# 1. Check Device Tree syntax
dtc -I dts -O dtb -o /dev/null my-device.dts

# 2. Decompile existing DTB
dtc -I dtb -O dts -o extracted.dts /boot/device_tree.dtb

# 3. View the device tree parsed by the kernel
ls /proc/device-tree/
cat /proc/device-tree/compatible

# 4. Check if the device matched successfully
cat /sys/firmware/devicetree/base/device@1000/compatible
dmesg | grep -i "device tree"

# 5. Debug driver matching
echo -n "my-company,my-device" > /sys/bus/platform/drivers/my-driver/new_id

8.3 Kernel Debugging Configuration

// Add debugging output in the driver
#define DEBUG

static int my_driver_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    
    // Print device tree information
    dev_dbg(&pdev->dev, "Device tree node: %s\n", np->full_name);
    
    // Iterate properties
    struct property *prop;
    for_each_property_of_node(np, prop) {
        dev_dbg(&pdev->dev, "Property: %s\n", prop->name);
    }
    
    return 0;
}

9. Design Patterns and Best Practices for Device Trees

9.1 Summary of Design Principles

Principle Description Example
Integrity of Hardware Description Complete description of hardware connections Correct addresses, interrupts, clocks
Compatibility First Use standard compatible strings <span>"vendor,model"</span> format
Hierarchical Design Reasonable use of node hierarchy SoC level → Board level → Peripheral level
Reusability Use includes and references <span>.dtsi</span> base file
Maintainability Clear comments and structure Group related properties

9.2 Common Pitfalls and Solutions

// Error example - Missing necessary properties
mydevice@10000000 {
    compatible = "my-company,my-device";
    // Missing reg property!
};

// Correct example
mydevice@10000000 {
    compatible = "my-company,my-device";
    reg = <0x10000000 0x1000>;
    interrupts = <0 25 4>;
    clocks = <&myclock>;
    clock-names = "core";
};

10. Conclusion

The Linux Device Tree is one of the core technologies of modern embedded Linux systems, greatly enhancing system portability and maintainability by separating hardware descriptions from kernel code.

Key Points Review:

  • • Device Trees achieve a complete separation of hardware description from the kernel
  • • The <span>compatible</span> property is the core mechanism for driver matching
  • • Proper use of Device Trees can significantly improve code reusability
  • • Device Tree debugging requires a combination of compilation tools and kernel debugging features

By mastering the principles and practical techniques of Device Trees, developers can more efficiently engage in embedded Linux system development, building more flexible and maintainable embedded solutions.

Leave a Comment