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树。
二、DTS、DTSI、DTB、DTC 概念辨析
初学者最容易混淆这四个缩写,它们分别处于"源码—编译—二进制"的不同阶段:
| 名称 | 全称 | 形态 | 作用 |
|---|---|---|---|
| DTS | Device Tree Source | 文本 | 单块板卡的设备树源文件,如 board.dts |
| DTSI | Device Tree Source Include | 文本 | 可被包含的公共片段,如 SoC 级 soc.dtsi |
| DTC | Device Tree Compiler | 工具 | 把 DTS 编译为 DTB 的编译器 |
| DTB | Device 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 目录核对节点是否被正确解析。