3. 设备树(Device Tree)基础:设备树起源、语法结构、节点与属性、dts/dtsi/dtb文件格式

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

在早期的嵌入式 Linux 系统中,板级硬件信息(如内存地址、中断号、外设寄存器基址)被硬编码在 arch/arm/mach-xxx 下的 C 语言板级文件中。每更换一款硬件平台,就必须重新编译内核,导致内核镜像与硬件高度耦合,无法实现“一次编译,多处运行”。

设备树(Device Tree)的引入解决了这一痛点。它将硬件描述从内核源码中剥离,以结构化的树形数据格式独立存在,由 Bootloader(如 U-Boot)传递给内核。内核在启动时解析设备树,动态创建平台设备并完成驱动匹配,从而实现内核与硬件描述的彻底解耦。

核心动机: 将硬件配置信息从内核源码中分离,实现“同一内核镜像支持不同硬件平台”的通用启动模型。
历史背景: 设备树规范源于 Open Firmware(IEEE 1275),PowerPC 架构最早采用。ARM Linux 社区在 2011 年左右(Linux 3.x 时代)开始大规模迁移至设备树,至 2014 年后新平台已强制要求使用设备树。

3.2 设备树语法结构

设备树源文件采用树形层级结构,以 节点(Node) 和 属性(Property) 为核心元素。每个节点代表一个硬件设备或总线,属性描述该设备的配置参数。

3.2.1 基本语法规则

  • 根节点: 使用 / 表示,是整个设备树的起点。
  • 节点定义: node-name@unit-address,其中 unit-address 通常为寄存器基址或设备地址。
  • 属性赋值: property-name = <value>; 或 property-name = "string";。
  • 标签(Label): 使用 label: node-name 格式,便于在其他位置引用。
  • 包含文件: 使用 #include 预处理指令引入 .dtsi 文件。
// 设备树源文件示例:简单 SoC 设备树
/dts-v1/;

#include "soc-common.dtsi"

/ {
    model = "Example Gateway Board";
    compatible = "vendor,gateway-v1", "vendor,gateway";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
    };

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

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        uart0: serial@10000000 {
            compatible = "vendor,uart";
            reg = <0x10000000 0x1000>;
            interrupts = <0 33 4>;
            clock-frequency = <24000000>;
            status = "okay";
        };

        can0: can@20000000 {
            compatible = "vendor,can-controller";
            reg = <0x20000000 0x1000>;
            interrupts = <0 45 4>;
            clocks = <&clk_can>;
            status = "disabled";
        };
    };
};

3.2.2 节点与属性详解

元素 说明 示例
节点 表示一个硬件设备、总线或功能单元 uart0: serial@10000000
compatible 驱动匹配的关键字符串,格式为 "厂商,设备型号" "vendor,can-controller"
reg 描述设备地址空间,格式为 <基址 长度> <0x20000000 0x1000>
interrupts 中断号、触发类型等中断描述信息 <0 45 4>
clocks 引用时钟控制器节点 <&clk_can>
status 设备启用状态:"okay" 或 "disabled" "disabled"
#address-cells 子节点 reg 属性中地址字段占用的 32 位单元数 <1>
#size-cells 子节点 reg 属性中长度字段占用的 32 位单元数 <1>
常见错误: compatible 字符串必须与驱动中 of_match_table 定义的字符串完全一致(包括大小写和厂商前缀),否则驱动无法匹配到设备。

3.3 设备树节点层级关系 SVG 图

下图展示了一个典型网关 SoC 设备树的节点层级结构,从根节点到 CAN 外设节点的完整路径:

设备树节点层级结构(网关 SoC 示例) / (根节点) chosen memory@80000000 soc (simple-bus) uart0: serial@10000000 can0: can@20000000 can0 节点关键属性 compatible = "vendor,can-controller" reg = <0x20000000 0x1000> interrupts = <0 45 4> clocks = <&clk_can> status = "disabled" 根节点关键属性 model = "Example Gateway Board" compatible = "vendor,gateway-v1" #address-cells = <1>; #size-cells = <1>

3.4 dts / dtsi / dtb 文件格式

设备树在开发、编译和运行阶段使用三种不同的文件格式:

文件后缀 全称 格式 用途
.dts Device Tree Source 文本(ASCII) 设备树源文件,开发者直接编写和修改
.dtsi Device Tree Source Include 文本(ASCII) 公共设备树头文件,被 .dts 通过 #include 包含
.dtb Device Tree Blob 二进制 编译后的设备树二进制文件,由 Bootloader 加载并传递给内核

3.4.1 dtsi 文件的作用

.dtsi 文件用于存放 SoC 级公共硬件描述,例如 CPU 核心、中断控制器、时钟控制器、通用外设等。不同板级 .dts 文件通过 #include 引入同一个 .dtsi,然后覆盖或添加板级特有的属性(如内存大小、外设状态)。

// soc-common.dtsi —— SoC 级公共描述
/ {
    compatible = "vendor,soc-v1";

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

    intc: interrupt-controller@10400000 {
        compatible = "arm,gic-400";
        reg = <0x10400000 0x10000>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };

    clk_can: clock-controller@10500000 {
        compatible = "vendor,can-clock";
        reg = <0x10500000 0x1000>;
        #clock-cells = <0>;
        clock-frequency = <40000000>;
    };
};

3.4.2 编译流程:从 dts 到 dtb

设备树编译器(Device Tree Compiler,DTC)将 .dts 源文件编译为 .dtb 二进制文件。编译命令如下:

# 编译单个 dts 文件
dtc -I dts -O dtb -o gateway.dtb gateway.dts

# 反编译 dtb 为 dts(调试用)
dtc -I dtb -O dts -o gateway_decompiled.dts gateway.dtb

# 在内核构建系统中自动编译
make dtbs
编译流程: .dts + .dtsi → 预处理(展开 #include) → DTC 编译 → .dtb 二进制文件。

3.5 设备树编译与加载流程 SVG 图

下图展示了从设备树源文件到内核解析的完整数据流:

设备树编译与加载流程 gateway.dts 板级设备树源文件 soc-common.dtsi SoC 公共描述头文件 C 预处理器 (CPP) 展开 #include / #define DTC 编译器 dts → dtb 二进制转换 .dtb 二进制 U-Boot 加载 dtb 从存储/网络读入内存 Linux 内核解析 dtb 创建 platform_device 并匹配驱动 关键路径说明 ① 开发者编写 .dts + .dtsi ② make dtbs 生成 .dtb ③ U-Boot 将 dtb 地址传给内核 ④ 内核 of_* 接口解析并创建设备

3.6 设备树与驱动匹配机制

当内核启动时,设备树解析子系统(drivers/of/)完成以下关键步骤:

  1. dtb 解析: 内核从 Bootloader 传递的寄存器地址(r2 或设备树地址)读取 dtb 数据,调用 unflatten_device_tree() 将其展开为内存中的 device_node 树结构。
  2. 平台设备创建: 对于 compatible = "simple-bus" 的总线节点,内核遍历其子节点,调用 of_platform_populate() 为每个外设节点创建 platform_device。
  3. 驱动匹配: 当驱动注册时,内核将驱动的 of_match_table 中的 compatible 字符串与设备节点的 compatible 属性进行匹配。匹配成功则调用驱动的 probe() 函数。
// 驱动中典型的 of_match_table 定义
static const struct of_device_id can_of_match[] = {
    { .compatible = "vendor,can-controller", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, can_of_match);

static struct platform_driver can_driver = {
    .probe  = can_probe,
    .remove = can_remove,
    .driver = {
        .name = "gateway-can",
        .of_match_table = can_of_match,
    },
};
module_platform_driver(can_driver);
匹配优先级: 设备树匹配优先于传统的 platform_device.name 匹配。如果驱动同时提供了 of_match_table 和 id_table,内核会优先使用设备树匹配路径。

3.7 本章小结

知识点 核心要点
设备树起源 解决板级文件硬编码问题,实现内核与硬件描述解耦
语法结构 树形层级:根节点 → 总线节点 → 外设节点;属性描述配置
节点与属性 compatible 驱动匹配、reg 地址空间、interrupts 中断、status 启用状态
文件格式 .dts 源文件 → .dtsi 头文件 → DTC 编译 → .dtb 二进制
加载流程 Bootloader 加载 dtb → 内核解析 → 创建 platform_device → 驱动匹配 probe