2. 设备树基础

在传统 ARM Linux 板级移植中,内核源码树里充斥着大量以板子命名的 arch/arm/mach-xxx 目录和 board-xxx.c 文件,每块板子的内存地址、外设基址、中断号、时钟频率都硬编码在 C 代码里。这导致内核中"板级垃圾代码"膨胀,同一颗 SoC 的不同板卡要重复维护大量差异极小的文件。设备树(Device Tree)正是为解决这一问题而引入的:它把"硬件描述"从内核代码中剥离出来,交给一份独立的数据文件,内核只负责解析并据此创建设备。

一、设备树的起源与作用

设备树最早来自 OpenFirmware(IEEE 1275)规范,PowerPC 平台长期使用它描述硬件。ARM 社区在 2011 年前后大规模采纳该机制,核心动机是"一份内核镜像支持多块板卡"。其作用可归纳为三点:

  • 硬件描述与代码解耦:SoC 外设的地址、中断、时钟、引脚复用等信息写在 DTS 中,内核驱动通过匹配 compatible 字符串来绑定设备。
  • 减少板级冗余:公共部分抽成 .dtsi 被多块板卡包含,差异部分单独覆盖,符合 DRY 原则。
  • 引导阶段传递硬件信息:Bootloader(如 U-Boot)把编译好的 DTB 加载到内存并传给内核,内核在启动早期展开为 device_node 树。
设备树不是"驱动",它只描述"有什么硬件、在哪里、怎么连",不描述"怎么操作硬件"。驱动的职责仍在内核 C 代码中。

二、DTS、DTSI、DTB、DTC 概念辨析

初学者最容易混淆这四个缩写,它们分别处于"源码—编译—二进制"的不同阶段:

名称全称形态作用
DTSDevice Tree Source文本单块板卡的设备树源文件,如 board.dts
DTSIDevice Tree Source Include文本可被包含的公共片段,如 SoC 级 soc.dtsi
DTCDevice Tree Compiler工具把 DTS 编译为 DTB 的编译器
DTBDevice Tree Blob二进制内核实际解析的扁平化设备树

典型编译命令如下,-I dts 指定输入格式,-O dtb 指定输出格式,-o 指定输出文件:

dtc -I dts -O dtb -o board.dtb board.dts
# 反向查看 DTB 内容
dtc -I dtb -O dts -o board.dts board.dtb

三、设备树语法结构与节点、属性

设备树在语法上是一棵有根树,根节点为 /。每个节点用大括号包裹,节点内可包含属性(键值对)和子节点。属性值可以是字符串、32 位整数数组、字节序列或空(布尔)。下面是一段最小可读的示例:

/dts-v1/;
#include "soc.dtsi"

/ {
    model = "My RISC-V Board";
    compatible = "vendor,myboard", "riscv,rv64";
    #address-cells = <2>;
    #size-cells = <2>;

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

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

    uart0: serial@10000000 {
        compatible = "ns16550a";
        reg = <0x0 0x10000000 0x0 0x100>;
        interrupts = <10>;
        clock-frequency = <100000000>;
        status = "okay";
    };
};

其中 uart0: 是标签(label),便于其他节点用 &uart0 引用;serial@10000000 中 @ 后是单元地址(unit-address),用于区分同类型不同地址的节点。属性命名遵循 厂商,属性名 的约定,避免冲突。

四、常见数据类型与引用机制

设备树属性值只有几种基本类型,理解它们对写驱动解析至关重要:

  • 字符串:compatible = "ns16550a";,双引号包裹。
  • 字符串列表:compatible = "vendor,myboard", "riscv,rv64";,按优先级从左到右匹配。
  • u32 数组:reg = <0x0 0x10000000 0x0 0x100>;,尖括号内为 32 位大端单元。
  • 空属性(布尔):interrupt-controller;,存在即为真。
  • 字节序列:mac-address = [00 11 22 33 44 55];,方括号内为十六进制字节。
  • phandle 引用:clocks = <&clk_uart>;,用 & 引用带标签的节点。
#address-cells 和 #size-cells 决定子节点 reg 属性中地址和长度各占几个 32 位单元。RISC-V 64 位平台常用 <2> 和 <2>,写错会导致地址解析错位。

五、案例场景:从 DTS 到内核设备

以一块 RISC-V 开发板为例,移植流程大致为:编写 board.dts 并包含 SoC 的 .dtsi → 用 dtc 编译为 board.dtb → 放入 U-Boot 可加载的分区 → 启动时 U-Boot 将 DTB 地址写入 a1 寄存器 → 内核展开 DTB 生成 device_node 树 → 驱动通过 of_match_table 中的 compatible 与节点匹配 → 调用 of_iomap、irq_of_parse_and_map 获取资源。整个过程体现了"描述与驱动分离"的设计哲学。

六、常见误区与复盘

误区一:把设备树当成配置文件随意加自定义属性。内核只识别标准属性,自定义属性需驱动自行解析,且应加厂商前缀。
误区二:status 忘记改为 "okay",导致设备默认 "disabled" 不生效。
误区三:reg 的地址单元数与 #address-cells 不匹配,内核解析出错误地址。
误区四:修改 DTS 后忘记重新编译 DTB,或 U-Boot 加载了旧 DTB,现象是"改了没反应"。

复盘建议:每次修改 DTS 后,先用 dtc -I dtb -O dts 反编译确认最终生效内容,再结合内核启动日志中的 OF: fdt 打印和 /proc/device-tree 目录核对节点是否被正确解析。

设备树编译与解析链路 DTS / DTSI 文本源文件 board.dts + soc.dtsi 人类可读 · 可包含 dtc 编译 DTB 二进制 Blob 扁平化设备树 内核实际解析对象 U-Boot 加载 Linux 内核 展开 device_node 树 of_match_table 匹配 生成 platform_device DTS 节点树结构示意 / (根节点) model · compatible chosen bootargs 启动参数传递 memory@80000000 device_type · reg 内存范围描述 serial@10000000 compatible · reg interrupts · status 属性类型: "字符串" <u32 数组> [字节序列] &phandle 引用 布尔空属性 关键约束:#address-cells / #size-cells 决定 reg 中地址与长度的单元个数