2. 设备树基础:DTS 的起源、语法结构与核心概念

设备树(Device Tree)是嵌入式 Linux 驱动开发中用于描述硬件拓扑结构的一种数据结构。它解决了传统板级文件(board-*.c)中硬件信息硬编码、代码冗余、难以维护的问题,是驱动架构分层重构的基石。

2.1 设备树的起源与设计动机

在 Linux 内核早期,ARM 架构的每个开发板都需要在 arch/arm/mach-* 下编写大量板级文件,用于注册平台设备、定义内存地址、中断号等硬件信息。这种模式导致内核代码膨胀、不同板卡间代码重复率高、维护成本急剧上升。

核心痛点: 硬件描述与驱动代码强耦合,每增加一个新板卡,就需要修改内核源码并重新编译。

设备树(Flattened Device Tree, FDT)最初源于 Open Firmware(IEEE 1275)规范,由 PowerPC 架构引入,后被 ARM、RISC-V、X86 等架构广泛采用。其核心思想是:将硬件描述从内核源码中剥离,以独立的数据结构(DTS)传递给内核,驱动通过解析设备树获取硬件资源,实现“一套内核镜像,支持多个板卡”。

设备树引入前后对比 传统板级文件方式 board-A.c board-B.c board-C.c board-D.c 硬件信息硬编码,每板一文件 修改硬件需重新编译内核 设备树方式 board-A.dts board-B.dts 硬件描述独立于内核源码 同一内核镜像,不同 DTB 即可适配 驱动通过 API 解析,无需硬编码

2.2 DTS 语法结构

设备树源文件(.dts)采用树形层次结构,以节点(node)和属性(property)描述硬件。每个节点代表一个硬件设备或总线,属性描述设备的配置参数。

2.2.1 基本语法规则

  • 节点定义: node-name@unit-address { ... };,其中 unit-address 通常为寄存器基地址。
  • 属性赋值: property-name = value;,支持多种数据类型。
  • 标签(Label): label: node-name@addr { ... };,用于在 DTS 中引用节点。
  • 包含文件: #include "soc.dtsi",复用通用硬件描述。
  • 注释: 支持 C 风格 /* */ 和 C++ 风格 //。
提示: DTS 文件经过 DTC(Device Tree Compiler)编译生成二进制 DTB(Device Tree Blob),由 bootloader 传递给内核。

2.2.2 完整 DTS 示例

// 根节点
/ {
    model = "Embedded Board Rev 2.0";
    compatible = "vendor,board-rev2", "vendor,board";
    #address-cells = <1>;
    #size-cells = <1>;

    // CPU 节点
    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x0>;
        device_type = "cpu";
    };

    // 内存节点
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  // 1GB RAM
    };

    // UART 控制器节点
    uart0: serial@1c090000 {
        compatible = "vendor,uart";
        reg = <0x1c090000 0x1000>;
        interrupts = <0 37 4>;
        clock-frequency = <24000000>;
        status = "okay";
    };

    // I2C 控制器及子设备
    i2c0: i2c@1c0a0000 {
        compatible = "vendor,i2c";
        reg = <0x1c0a0000 0x1000>;
        interrupts = <0 38 4>;
        #address-cells = <1>;
        #size-cells = <0>;
        status = "okay";

        eeprom@50 {
            compatible = "atmel,24c02";
            reg = <0x50>;
            pagesize = <8>;
        };
    };
};

2.3 节点与属性的核心概念

2.3.1 节点(Node)

节点是设备树的基本组成单元,每个节点对应一个硬件设备、总线或功能模块。节点通过路径(path)唯一标识,例如 /soc/uart@1c090000。

节点类型 说明 示例
根节点 设备树的顶层节点,用 / 表示 / { ... };
设备节点 描述具体外设或控制器 uart0: serial@1c090000
总线节点 描述总线控制器,可包含子节点 i2c0: i2c@1c0a0000
子节点 挂载在总线下的从设备 eeprom@50
别名节点 通过 aliases 提供快捷引用 aliases { uart0 = &uart0; };

2.3.2 属性(Property)

属性是节点的配置参数,驱动通过解析属性获取硬件资源。常用属性包括:

属性名 数据类型 说明 示例
compatible 字符串列表 设备兼容性标识,驱动匹配的关键 "vendor,uart"
reg u32/u64 数组 寄存器地址和长度 <0x1c090000 0x1000>
interrupts u32 数组 中断号、触发类型等 <0 37 4>
status 字符串 设备状态:okay / disabled "okay"
#address-cells u32 子节点 reg 中地址字段的 cell 数 <1>
#size-cells u32 子节点 reg 中长度字段的 cell 数 <1>
clock-frequency u32 时钟频率(Hz) <24000000>

2.4 设备树层次结构与寻址

设备树的树形结构反映了硬件的物理拓扑。根节点下通常包含 cpus、memory、soc(片上系统)等顶层节点。soc 节点下再挂载各类外设控制器,形成层次化描述。

设备树层次结构示例 / (根节点) cpus memory@80000000 soc serial@1c090000 i2c@1c0a0000 gpio@1c0b0000 eeprom@50 节点路径示例:/soc/i2c@1c0a0000/eeprom@50 驱动通过 of_find_node_by_path() 或 of_parse_phandle() 遍历节点

2.5 设备树在驱动架构中的角色

设备树是驱动架构分层重构的“硬件描述层”。它将硬件配置信息从驱动代码中解耦,驱动只需通过标准 API 读取设备树属性,即可获取寄存器地址、中断号、时钟频率等资源。

关键原则: 驱动代码中不应出现任何硬编码的寄存器地址或中断号。所有硬件资源必须通过设备树传递,这是分层重构的基本要求。

驱动与设备树的交互流程如下:

  1. 设备树编译: .dts 文件通过 DTC 编译为 .dtb 二进制文件。
  2. Bootloader 传递: U-Boot 等 bootloader 将 DTB 加载到内存,并将地址传递给内核。
  3. 内核解析: 内核在启动时解析 DTB,构建 device_node 树结构。
  4. 驱动匹配: 驱动通过 compatible 属性与设备节点匹配,触发 probe 函数。
  5. 资源获取: 驱动使用 of_iomap()、irq_of_parse_and_map() 等 API 获取寄存器地址和中断号。
驱动与设备树交互流程 DTS 源文件 DTC 编译 DTB 二进制 内核解析 驱动 Probe 硬件描述 dtc -I dts -O dtb Bootloader 传递 device_node 树 of_iomap() 等 驱动通过 compatible 匹配节点,通过 of_* API 获取硬件资源

2.6 本章小结

  • 设备树将硬件描述从内核源码中解耦,是驱动架构分层重构的硬件描述层。
  • DTS 采用树形结构,节点代表设备,属性描述配置参数。
  • compatible 属性是驱动匹配的关键,reg 和 interrupts 是获取硬件资源的核心属性。
  • 驱动应通过 of_* API 读取设备树,禁止硬编码寄存器地址和中断号。