4. 设备树(Device Tree)基础

4.1 设备树概述与设计动机

在ARM架构的车载嵌入式系统中,硬件配置千差万别。传统方式将硬件信息硬编码在Linux内核中,导致每更换一个板卡就需要重新编译内核,维护成本极高。设备树(Device Tree)的出现解决了这一问题:它将硬件描述从内核代码中剥离,以结构化数据的形式传递给内核,实现“一次编译,多处运行”。

核心思想: 设备树是一种描述硬件资源的数据结构,由引导加载程序(如U-Boot)传递给内核。内核根据设备树动态识别外设、分配资源、加载驱动,无需重新编译。

4.2 设备树文件体系:dts / dtsi / dtb

设备树涉及三种文件类型,它们之间的关系类似于C语言中的源文件、头文件和编译后的二进制文件。

文件类型 扩展名 说明 类比
设备树源文件 .dts 描述具体板卡的硬件配置,是顶层源文件 .c 源文件
设备树包含文件 .dtsi 描述SoC公共部分、IP核、通用外设,可被多个.dts包含 .h 头文件
设备树二进制文件 .dtb 由.dts编译生成的二进制文件,供内核解析 .o / .bin 目标文件
编译命令: dtc -I dts -O dtb -o myboard.dtb myboard.dts
反编译命令: dtc -I dtb -O dts -o myboard.dts myboard.dtb

典型的文件组织方式如下:

#include "soc.dtsi"          // 包含SoC公共定义
#include "pinmux.dtsi"       // 包含引脚复用配置

/ {
    model = "MyCar Board v1.0";
    compatible = "mycompany,mycar-board", "arm,vexpress";

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

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

    &uart1 {
        status = "okay";
        clock-frequency = <24000000>;
    };
};

4.3 设备树语法与结构

设备树采用树形结构,以根节点 / 为起点,每个节点代表一个硬件设备或总线,节点内包含属性(键值对)来描述设备特征。

4.3.1 基本语法规则

  • 节点定义: node-name@unit-address { ... };,其中 unit-address 通常为设备基地址。
  • 属性格式: property-name = <value>; 或 property-name = "string";。
  • 标签(Label): 用于引用节点,如 uart1: serial@101f1000 { ... };,可通过 &uart1 引用。
  • 包含文件: 使用 #include 指令引入 .dtsi 文件。
  • 路径引用: &{/soc/i2c@1000} 可直接访问指定路径的节点。

4.3.2 常用标准属性

属性名 含义 示例值
compatible 设备兼容性字符串,驱动匹配依据 "mycompany,my-i2c", "snps,designware-i2c"
reg 设备地址空间(基地址 + 长度) <0x101f1000 0x1000>
interrupts 中断号与触发类型 <0 33 4>(SPI 33, 高电平触发)
clocks 时钟句柄引用 <&clk_uart 0>
status 设备状态:okay / disabled "okay"
#address-cells 地址字段占用的32位字数量 1 或 2
#size-cells 长度字段占用的32位字数量 1 或 2

4.3.3 设备树结构示例

/dts-v1/;

/ {
    compatible = "arm,vexpress";
    #address-cells = <2>;
    #size-cells = <2>;

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

    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x40000000>;
    };

    soc {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        uart0: serial@1c090000 {
            compatible = "snps,dw-apb-uart";
            reg = <0x0 0x1c090000 0x0 0x1000>;
            interrupts = <0 33 4>;
            clocks = <&uart_clk>;
            status = "disabled";
        };
    };
};

4.4 设备树在启动过程中的解析流程

从引导加载程序到内核完成驱动绑定,设备树的解析贯穿整个启动过程。下图展示了核心流程:

设备树启动解析流程 设备树启动解析流程 U-Boot 加载 DTB 从存储/网络读取 .dtb 传递 DTB 地址 通过 r2 寄存器传递 内核早期解析 unflatten_device_tree() 构建 device_node 树 内存中建立树形结构 驱动匹配与绑定 of_match_table 匹配 驱动初始化 probe() 读取资源并注册 关键函数调用链: start_kernel() → setup_arch() → unflatten_device_tree() → of_platform_populate()

4.4.1 详细解析步骤

  1. U-Boot 加载 DTB: 引导加载程序从存储介质(eMMC、NAND Flash)或网络(TFTP)读取 .dtb 文件到内存中。
  2. 传递 DTB 地址: U-Boot 通过 ARM 的 r2 寄存器将 DTB 在内存中的物理地址传递给内核。内核启动时首先读取该寄存器获取 DTB 位置。
  3. 早期解析(unflatten_device_tree): 内核调用 unflatten_device_tree() 将扁平的 DTB 二进制数据解析为内存中的 device_node 树结构,每个节点对应一个设备。
  4. 构建 device_node 树: 内核遍历 DTB 的节点和属性,为每个节点分配 struct device_node 结构体,建立父子关系,形成完整的设备树。
  5. 驱动匹配与绑定: 内核遍历 device_node 树,根据节点的 compatible 属性与驱动注册的 of_match_table 进行匹配。匹配成功后,调用驱动的 probe() 函数。
  6. 驱动初始化: 在 probe() 函数中,驱动通过 of_* API(如 of_iomap()、of_get_property()、of_irq_get())读取设备树中的资源信息(寄存器地址、中断号、时钟等),完成设备初始化。

4.4.2 驱动中常用的设备树 API

API 函数 功能 使用场景
of_match_device() 匹配设备与驱动兼容性表 probe 入口判断
of_iomap() 将设备寄存器地址映射到虚拟地址 访问硬件寄存器
of_get_property() 读取设备树属性值 获取自定义配置
of_irq_get() 获取设备中断号 注册中断处理函数
of_clk_get() 获取设备时钟句柄 时钟使能/频率配置
of_platform_populate() 遍历子节点并创建 platform_device 总线设备注册

4.5 设备树在车载系统中的特殊考量

车载安全关键点: 在车载系统中,设备树配置错误可能导致外设无法工作、中断冲突甚至系统崩溃。务必遵循以下原则:
  • 所有外设的 reg 地址必须与硬件手册严格一致。
  • 中断号需确认是 SPI(共享外设中断)还是 PPI(私有外设中断),并正确填写触发类型。
  • 时钟依赖关系必须完整描述,避免驱动 probe 时因时钟未就绪而超时。
  • 使用 status = "disabled" 禁用未使用的设备,减少内核资源占用。

4.6 设备树调试技巧

  • 查看当前设备树: 在 Linux 终端执行 dtc -I fs -O dts /sys/firmware/devicetree/base 可导出内核当前使用的设备树。
  • 检查设备树节点: ls /sys/firmware/devicetree/base/ 可浏览设备树节点目录结构。
  • 验证设备匹配: 查看 /sys/bus/platform/devices/ 下是否有对应设备目录,确认驱动是否成功绑定。
  • 内核启动日志: 使用 dmesg | grep -i "dtb\|of\|device tree" 过滤设备树相关日志,定位解析错误。
实战建议: 在开发阶段,可在 U-Boot 中通过 fdt set 命令动态修改设备树属性,快速验证硬件配置,无需重新编译内核。例如:
fdt set /soc/uart@1c090000 status "okay"