Overview

The Device Tree (DT) is a data structure used to describe the hardware components of a system in a way that is independent of the operating system and software. It is particularly relevant for systems based on the ARM architecture, where the hardware varies significantly across devices.

Instead of hardcoding hardware details in the kernel, the device tree provides a flexible way to inform the kernel about the system’s hardware layout. This simplifies kernel code and enables easier reuse across multiple hardware platforms.


Key Concepts

1. Structure

  • Device Tree Source (DTS): A human-readable text file that describes hardware.
  • Device Tree Blob (DTB): A binary representation of the DTS, passed to the kernel at boot time.
  • Device Tree Compiler (DTC): Used to convert DTS to DTB.

2. Syntax

The syntax of a DTS file resembles a hierarchical tree structure with nodes and properties:

/dts-v1/;

/ {
    compatible = "rockchip,rk3399";

    memory {
        device_type = "memory";
        reg = <0x00000000 0x80000000>; // Base address and size
    };

    uart0: serial@ff180000 {
        compatible = "rockchip,rk3399-uart";
        reg = <0xff180000 0x100>;
        interrupts = <0 12 4>;
        status = "okay";
    };
};
  • Nodes: Represent physical/logical devices (e.g., CPUs, I2C buses, GPIO controllers, serial@ff180000).
  • Properties: Key-value pairs describing device attributes (e.g., registers, interrupts, clock frequencies).

3. Key Properties

  • compatible: Identifies the driver that should handle the device.
  • reg: Specifies the base address and size of the device’s registers.
  • interrupts: Describes interrupt lines and their configuration.
  • status: Indicates if the device is active (okay) or disabled (disabled).

How the Device Tree Works

  1. Bootloader Phase
    • The bootloader (e.g., U-Boot) loads the kernel and the DTB into memory.
    • It passes the address of the DTB to the kernel.
  2. Kernel Initialization
    • The kernel reads the DTB to understand the hardware layout.
    • Based on the compatible property, the kernel matches devices to their respective drivers.
  3. Driver Binding
    • Device drivers register themselves with the kernel.
    • The kernel matches the driver’s of_match_table with the compatible property in the DTB.
    • If a match is found, the driver is bound to the device.

Example: I2C Device on Bus 2, Slave Address 0x44 Scenario: Adding a temperature sensor (e.g., tmp102) at address 0x44 on I2C bus 2.

Step 1: Device Tree Source (DTS)
// File: myboard.dts  

/ {  
    compatible = "vendor,myboard";  
    model = "My Embedded Board";  

    // I2C Controller (e.g., i2c2)  
    i2c2: i2c@40005800 {  
        compatible = "vendor,i2c-controller";  
        reg = <0x40005800 0x400>;     // Base address and size  
        #address-cells = <1>;         // Number of cells for I2C slave addresses  
        #size-cells = <0>;            // No size field for I2C slaves  

        // Temperature sensor at slave address 0x44  
        temp_sensor: tmp102@44 {  
            compatible = "ti,tmp102"; // Matches driver's `of_match_table`  
            reg = <0x44>;             // I2C slave address (7-bit format)  
            interrupt-parent = <&gpioa>;  
            interrupts = <9 IRQ_TYPE_EDGE_FALLING>; // GPIO pin 9, falling edge  
        };  
    };  
};  
Step 2: Compile the Device Tree
# Compile .dts to .dtb  
dtc -O dtb -o myboard.dtb myboard.dts  
Step 3: Load the Device Tree
  • Pass the DT blob to the kernel via the bootloader (e.g., U-Boot):
load mmc 0:1 ${fdt_addr} myboard.dtb  
bootz ${kernel_addr} - ${fdt_addr}
4. Driver Implementation for the I2C Device

Key Functions:

  • Probe Function: Called when a device with a matching compatible string is found.
  • Device Tree Parsing: Extract properties (e.g., interrupts, registers).
Example Driver Code
#include <linux/module.h>  
#include <linux/i2c.h>  
#include <linux/of.h>  

// Define device-specific data structure  
struct tmp102_data {  
    struct i2c_client *client;  
    int irq;  
};  

// Platform IDs for Device Tree matching  
static const struct of_device_id tmp102_of_match[] = {  
    { .compatible = "ti,tmp102" }, // Matches DTS `compatible` string  
    { }  
};  
MODULE_DEVICE_TABLE(of, tmp102_of_match);  

// Probe function  
static int tmp102_probe(struct i2c_client *client,  
                       const struct i2c_device_id *id)  
{  
    struct tmp102_data *data;  
    struct device *dev = &client->dev;  
    int ret;  

    // Allocate memory for device data  
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);  
    if (!data)  
        return -ENOMEM;  

    data->client = client;  

    // Get IRQ from Device Tree  
    data->irq = irq_of_parse_and_map(dev->of_node, 0);  
    if (data->irq <= 0) {  
        dev_err(dev, "Failed to get IRQ\n");  
        return -EINVAL;  
    }  

    // Read a register from the I2C device (example)  
    ret = i2c_smbus_read_word_swapped(client, 0x00);  
    if (ret < 0) {  
        dev_err(dev, "Failed to read temperature\n");  
        return ret;  
    }  

    dev_info(dev, "Probed TMP102 at address 0x%02x\n", client->addr);  
    return 0;  
}  

// I2C driver structure  
static struct i2c_driver tmp102_driver = {  
    .driver = {  
        .name = "tmp102",  
        .of_match_table = tmp102_of_match,  
    },  
    .probe = tmp102_probe,  
};  
module_i2c_driver(tmp102_driver);
5. Validating the Device Tree in Userspace

Refer: I2C#I2C-Tools Package in Userspace

# List I2C devices on bus 2  
i2cdetect -y 2
    0  1  2 ... 44 ...  
    UU -- ... 44 ...  

# View DT node for the I2C device  
ls /proc/device-tree/i2c@40005800/tmp102@44

Writing a Kernel Driver with Device Tree Support

Here is an example of a UART driver that uses the device tree.

1. Device Tree Entry

Add the following node in the DTS file for a UART device:

uart0: serial@ff180000 {
    compatible = "custom,uart";
    reg = <0xff180000 0x100>;
    interrupts = <0 12 4>;
    status = "okay";
};

2. Driver Code

Below is a minimal device driver that interacts with the device tree:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/interrupt.h>

#define DRIVER_NAME "custom_uart"

struct uart_dev {
    void __iomem *base;
    int irq;
};

static int uart_probe(struct platform_device *pdev) {
    struct resource *res;
    struct uart_dev *uart;

    uart = devm_kzalloc(&pdev->dev, sizeof(*uart), GFP_KERNEL);
    if (!uart)
        return -ENOMEM;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    uart->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(uart->base))
        return PTR_ERR(uart->base);

    uart->irq = platform_get_irq(pdev, 0);
    if (uart->irq < 0)
        return uart->irq;

    dev_info(&pdev->dev, "UART probed at %p with IRQ %d\n", uart->base, uart->irq);

    return 0;
}

static int uart_remove(struct platform_device *pdev) {
    dev_info(&pdev->dev, "UART removed\n");
    return 0;
}

static const struct of_device_id uart_of_match[] = {
    { .compatible = "custom,uart" },
    {},
};
MODULE_DEVICE_TABLE(of, uart_of_match);

static struct platform_driver uart_driver = {
    .driver = {
        .name = DRIVER_NAME,
        .of_match_table = uart_of_match,
    },
    .probe = uart_probe,
    .remove = uart_remove,
};
module_platform_driver(uart_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Developer");
MODULE_DESCRIPTION("Custom UART Driver");

3. Explanation

  • of_match_table: Matches the compatible property in the DTB with the driver.
  • platform_get_resource: Retrieves memory regions (registers) from the DT.
  • platform_get_irq: Retrieves the interrupt line from the DT.
  • devm_*: Managed functions for resource allocation, simplifying cleanup.

Example

Rockchip system:

  • Replace "custom,uart" with "rockchip,rk3399-uart".
  • Ensure the reg and interrupts values match your hardware’s specifications.
  • Use this approach to add support for other peripherals (e.g., GPIO, I2C, SPI) in your system.

References