1. 设备树(DTS)基础概念

1.1 什么是设备树

设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形节点来记录系统中所有可访问的设备、总线、中断控制器、时钟、GPIO等硬件信息。设备树源文件通常以 .dts 为扩展名,经过编译后生成 .dtb 二进制文件,由Bootloader传递给Linux内核。

设备树的核心思想是将硬件描述从内核代码中剥离出来,形成独立的配置文件。每个节点代表一个硬件设备或总线,节点包含若干属性(property)来描述设备的寄存器地址、中断号、时钟频率等参数。

关键术语对照表
术语 全称 说明
DTS Device Tree Source 设备树源文件,人类可读的文本格式
DTB Device Tree Blob 设备树二进制文件,由DTC编译生成
DTC Device Tree Compiler 设备树编译器,将DTS转换为DTB
DTSI Device Tree Source Include 设备树头文件,用于公共节点复用
OF Open Firmware 设备树的驱动框架,提供API接口
实战提示:在嵌入式Linux开发中,.dts 文件通常存放在内核源码的 arch/arm/boot/dts/ 或 arch/arm64/boot/dts/ 目录下。修改设备树后,使用 make dtbs 命令即可重新编译。

1.2 为什么需要设备树

在设备树出现之前,Linux内核使用大量的板级文件(board file)来描述硬件。每个开发板都需要在 arch/arm/mach-xxx 目录下编写C语言结构体来注册平台设备、GPIO、中断等资源。这种方式存在三个严重问题:

  • 代码冗余:同一款SoC的不同开发板需要重复编写大量相似的板级代码
  • 维护困难:硬件变更时必须修改内核源码,无法做到内核与硬件解耦
  • 碎片化严重:ARM架构下大量板级文件导致主线内核难以统一支持

设备树解决了上述问题,带来了以下核心价值:

  1. 硬件描述与内核分离:修改硬件只需要修改DTS文件,无需重新编译内核
  2. 统一硬件抽象层:同一款SoC可以共用一套内核镜像,通过不同的DTB适配不同硬件
  3. 减少主线内核维护负担:ARM架构从Linux 3.x开始全面转向设备树,板级文件被逐步淘汰
  4. 支持多平台启动:一个内核镜像可以同时支持多种ARM/ARM64开发板
避坑指南:虽然设备树极大简化了硬件适配,但并非所有硬件信息都适合放在设备树中。例如:硬件版本号、校准参数、运行时动态配置等不应写入DTS,而应通过其他机制(如ATAG、U-Boot环境变量)传递。

1.3 设备树在Linux内核中的角色

设备树在Linux内核启动和驱动加载过程中扮演着"硬件信息数据库"的角色。内核启动时,Bootloader将DTB加载到内存中,内核通过 unflatten_device_tree() 函数将扁平化的DTB解析为树形结构,供驱动框架使用。

设备树与内核各子系统的交互关系如下图所示:

设备树在Linux内核中的角色架构图 设备树在Linux内核中的角色架构 Bootloader (U-Boot) DTB 二进制 内核 OF 核心层 unflatten_device_tree() / of_scan_flat_dt() Platform 驱动 of_match_table 匹配 I2C / SPI 驱动 of_i2c_register_devices() 中断/时钟/GPIO irq_domain / clk / gpio 设备树是硬件信息的中转站,驱动通过OF API读取节点属性完成资源申请 of_property_read_u32() / of_get_named_gpio() / irq_of_parse_and_map()

设备树在内核中的核心作用可以归纳为以下四个层面:

层面 作用 关键API/机制
设备枚举 内核启动时扫描DTB,为每个节点创建platform_device of_platform_populate()
驱动匹配 驱动通过compatible属性与设备节点进行匹配绑定 of_match_table / of_match_node()
资源获取 驱动从节点属性中读取寄存器地址、中断号、DMA通道等 platform_get_resource() / of_iomap()
运行时配置 通过设备树属性传递板级差异参数,如时钟频率、电压阈值 of_property_read_u32() / of_get_property()

1.4 设备树的基本语法结构

一个典型的设备树节点包含节点名称、节点路径和若干属性。以下是一个简单的设备树片段示例:

/dts-v1/;

/ {
    model = "Embedded Development Board v2.0";
    compatible = "vendor,emb-board-v2", "vendor,emb-board";

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

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

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

        gpio0: gpio@10001000 {
            compatible = "vendor,gpio-ctrl";
            reg = <0x10001000 0x100>;
            gpio-controller;
            #gpio-cells = <2>;
            interrupts = <0 34 4>;
        };
    };
};
语法要点解析:
  • /dts-v1/; — 设备树版本声明,必须位于文件开头
  • / { }; — 根节点,所有设备节点都挂在根节点下
  • uart0: serial@10000000 — uart0 是标签(label),serial@10000000 是节点名称,@后跟寄存器基地址
  • reg = <0x10000000 0x1000>; — 寄存器地址和长度,地址由 #address-cells 决定
  • status = "okay"; — 设备状态,可选 okay、disabled、fail

1.5 设备树与Platform驱动的关联

在Platform驱动模型中,设备树是设备信息的提供者。当内核启动时,OF核心层遍历设备树中的所有节点,为每个 compatible 属性匹配的节点创建一个 platform_device。驱动通过 of_match_table 声明自己支持的设备列表,内核在注册驱动时自动完成匹配。

设备树节点与Platform驱动的匹配流程如下图所示:

设备树节点与Platform驱动匹配流程 设备树节点与Platform驱动匹配流程 步骤1:内核启动 解析DTB生成设备树 步骤2:创建设备 of_platform_populate() 步骤3:驱动注册 platform_driver_register() 步骤4:匹配 of_match_table 比对 步骤5:probe执行 读取节点资源初始化 匹配条件说明 1. 驱动 of_match_table 中的 compatible 值 与设备节点 compatible 属性完全一致 2. 支持通配符匹配,如 "vendor,device-v1" 3. 匹配成功后调用驱动的 probe 回调函数
实战要点:在编写Platform驱动时,of_match_table 必须使用 of_device_id 结构体数组,并以空结构体结尾。驱动加载后,可以通过 dmesg | grep "匹配的设备名" 查看匹配是否成功。

1.6 设备树的调试手段

在实际开发中,设备树配置错误是导致驱动加载失败的常见原因。以下是几种常用的调试方法:

  • 查看设备树节点:在系统启动后,通过 /proc/device-tree/ 目录查看当前生效的设备树结构
  • 使用dtc反编译:通过 dtc -I dtb -O dts -o output.dts input.dtb 将DTB反编译为DTS,验证配置是否正确
  • 内核启动日志:关注 dmesg 中与设备树相关的输出,如 OF: fdt: ... 或 platform ... 信息
  • 驱动匹配验证:在驱动probe函数中添加 dev_info() 打印,确认匹配是否触发
常见错误:设备树中 reg 属性的地址和长度必须与 #address-cells、#size-cells 严格对应。例如,#address-cells = <1> 表示地址占1个32位单元,#size-cells = <1> 表示长度占1个32位单元。如果配置错误,驱动将无法正确获取寄存器地址,导致系统崩溃。