1. 设备树基础概念

1.1 什么是设备树 (Device Tree)

设备树(Device Tree,简称 DT)是一种描述硬件资源的数据结构,它使用树形节点来记录系统中所有可发现的硬件设备及其连接关系、寄存器地址、中断号、时钟、DMA 通道等资源信息。设备树源文件(.dts)经过设备树编译器(dtc)编译后生成设备树二进制文件(.dtb),由 Bootloader 传递给内核。

核心定义:设备树 = 硬件拓扑 + 资源清单 + 配置参数,是嵌入式 Linux 中“硬件描述”与“驱动代码”解耦的关键机制。

一个典型的设备树节点结构如下:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            reg = <0x0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "okay";
    };
};

1.2 为什么需要设备树

在设备树出现之前(Linux 2.6 ~ 3.0 时代),ARM 架构的嵌入式 Linux 内核中充斥着大量 board-xxx.c 文件,每个开发板都需要在 C 代码中硬编码硬件资源信息。这种方式带来了三个严重问题:

问题 具体表现 后果
代码膨胀 每个板子一个 board 文件,动辄上千行结构体赋值 内核源码树臃肿,维护成本极高
耦合过紧 硬件资源描述与驱动代码深度绑定 更换外设或修改地址必须重新编译内核
碎片化严重 厂商各自维护私有 board 文件,社区难以统一 主线内核支持滞后,厂商 BSP 与主线分裂

设备树解决了上述问题,其核心优势包括:

  • 硬件描述与驱动分离:驱动代码只关心 compatible 匹配,不关心具体地址和中断号。
  • 单内核镜像支持多板:同一份 zImage 搭配不同的 .dtb 即可启动不同硬件。
  • 减少主线内核碎片:社区统一使用设备树,厂商只需提交 .dts 文件即可。
  • Bootloader 可动态修改:U-Boot 支持 fdt 命令,可在启动前调整设备树内容。
关键转折:自 Linux 3.x 起,ARM 架构强制要求使用设备树,board 文件被彻底淘汰。PowerPC、RISC-V、MIPS 等架构也全面采用设备树。

1.3 设备树与平台设备 (Platform Device) 的关系

平台设备(Platform Device)是 Linux 内核中用于管理“非可枚举总线设备”的抽象层,典型代表包括:片上外设(UART、I2C、SPI、GPIO 控制器)、内存映射设备、中断控制器等。设备树与平台设备的关系可以概括为:设备树是“硬件清单”,平台设备是“运行时对象”。

内核在启动过程中,通过 of_platform_populate() 函数遍历设备树中的所有节点,为每个匹配的节点创建一个 platform_device 实例,并注册到内核的设备模型中。驱动则通过 of_match_table 与设备树节点进行匹配。

/* 驱动中的匹配表 */
static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "ti,am3352-uart" },
    { .compatible = "ti,omap3-uart" },
    { /* end of list */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);

static struct platform_driver my_uart_driver = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name = "my_uart",
        .of_match_table = my_uart_of_match,
    },
};
module_platform_driver(my_uart_driver);

下表总结了设备树节点与平台设备之间的对应关系:

维度 设备树节点 (DT Node) 平台设备 (Platform Device)
存在形式 编译期文本(.dts / .dtb) 运行时内核对象(struct platform_device)
生命周期 Bootloader 加载后常驻内存 驱动 probe 时创建,remove 时销毁
资源描述 reg、interrupts、clocks 等属性 struct resource 数组
匹配方式 compatible 字符串 of_match_table 匹配
创建时机 由内核 of 子系统解析 of_platform_populate() 调用时
常见误区:并非所有设备树节点都会生成平台设备。例如 i2c@xxx 节点下的子节点(如 eeprom@50)属于 I2C 客户端设备,由 I2C 核心创建 i2c_client,而非平台设备。只有直接挂载在“平台总线”上的节点才会被转换为平台设备。

下图展示了设备树从源文件到平台设备实例的完整转换流程:

设备树到平台设备转换流程 设备树 → 平台设备 转换流程 .dts 源文件 硬件描述文本 .dtsi 包含文件 SoC 公共定义 .dts 板级覆盖 板级定制修改 dtc 设备树编译器 编译、合并、检查语法 .dtb 二进制文件 扁平化设备树 (FDT) Bootloader (U-Boot) 加载 dtb 到内存,传递地址给内核 内核 of 子系统 → platform_device of_platform_populate() 创建平台设备实例

从图中可以看出,整个流程是一个从“文本描述”到“运行时对象”的线性转换:.dts 源文件经过 dtc 编译生成 .dtb,Bootloader 将其加载到内存并传递给内核,内核的 of 子系统解析 .dtb 并调用 of_platform_populate() 为每个匹配的节点创建 platform_device,最终触发驱动的 probe 函数。

一句话总结:设备树是“硬件蓝图”,平台设备是“施工结果”。驱动开发者只需要在 of_match_table 中声明自己支持的 compatible 字符串,内核会自动完成从设备树节点到平台设备的创建与匹配。