2. 设备树基础
设备树(Device Tree)是 Linux 内核用于描述硬件平台信息的一种数据结构。它把“板级硬件长什么样”从内核源码中剥离出来,交给独立的二进制数据文件表达。内核启动时读取这份数据,动态识别 CPU、内存、总线、外设、中断控制器、时钟、引脚等资源,从而做到“同一份内核镜像,适配多种硬件板卡”。理解 DTS、DTSI、DTB、DTC 四个基本概念,是后续跨平台迁移工作的起点。
一、四个基本概念与它们的关系
设备树在开发与运行阶段以不同形态存在,四者构成一条清晰的“源码 → 编译 → 二进制 → 内核解析”链路:
- DTS(Device Tree Source):设备树源文件,通常以
.dts结尾,描述某一块具体板卡的硬件。它是人类可读的文本,包含根节点、CPU、内存、外设节点及其属性。 - DTSI(Device Tree Source Include):设备树包含文件,通常以
.dtsi结尾,用于存放多个板卡共用的部分,例如 SoC 内部外设定义。DTS 通过#include引入 DTSI,实现“公共部分复用 + 板级差异覆盖”。 - DTC(Device Tree Compiler):设备树编译器,把 DTS/DTSI 编译成 DTB,也可反向把 DTB 反编译为 DTS,是迁移调试中最常用的工具。
- DTB(Device Tree Blob):编译产物,扁平化二进制设备树,由 Bootloader 加载并传递给内核,内核在启动早期解析它。
它们的关系可以概括为:DTS + DTSI 经 DTC 编译生成 DTB,DTB 由 Bootloader 传给内核,内核据此完成平台设备初始化。下图展示了这一完整链路及关键工具位置。
二、设备树在 Linux 启动流程中的作用
在引入设备树之前,ARM Linux 内核中充斥着大量 board-xxx.c 板级文件,每支持一块新板就要改内核、重新编译。设备树改变了这一模式,其作用集中在启动早期:
- Bootloader 阶段:U-Boot 等引导程序把 DTB 加载到内存,并在跳转内核时通过寄存器(如 ARM64 的
x0)或ATAGS传递 DTB 地址。 - 内核解压与早期初始化:内核解析 DTB,读取
/memory节点确定物理内存布局,读取/cpus确定 CPU 数量与特性。 - 平台设备创建:内核遍历 DTB 中
compatible属性,把每个节点转换为platform_device,并与已注册的platform_driver匹配。 - 驱动 probe:匹配成功后调用驱动的 probe 函数,完成寄存器映射、中断申请、时钟与引脚配置,外设正式可用。
设备树的核心价值:内核不再“认识”具体板卡,只认识设备树描述的硬件拓扑。硬件差异被下沉到 DTB,内核镜像得以通用。
三、DTS/DTSI 的组织方式与常用语法
实际项目中,DTS 通常只写板级差异,公共部分放在 DTSI。下面是一个典型结构:
// soc.dtsi —— SoC 公共定义
/ {
compatible = "vendor,soc";
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <10>;
status = "disabled";
};
};
// board-a.dts —— 板级差异
#include "soc.dtsi"
/ {
model = "Board A";
compatible = "vendor,board-a", "vendor,soc";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
};
&uart0 {
status = "okay";
clock-frequency = <100000000>;
};
其中 &uart0 是“引用并覆盖”语法,用于在板级文件中修改 DTSI 里已定义的节点。这种“公共 + 覆盖”的写法,正是跨平台迁移时最需要掌握的组织方式。
四、DTC 常用命令与调试手段
DTC 是迁移过程中最常用的工具,掌握以下命令即可完成大部分编译与排障:
| 命令 | 作用 | 典型场景 |
|---|---|---|
dtc -I dts -O dtb -o board.dtb board.dts |
DTS 编译为 DTB | 构建板级设备树 |
dtc -I dtb -O dts -o board.dts board.dtb |
DTB 反编译为 DTS | 分析厂商提供的二进制设备树 |
fdtdump board.dtb |
查看 DTB 头部与结构 | 确认 magic、版本、大小 |
ls /proc/device-tree/ |
查看内核实际解析结果 | 验证节点是否生效 |
cat /proc/device-tree/model |
读取板卡型号 | 确认加载了正确 DTB |
五、跨平台迁移的意义与典型场景
跨平台迁移的本质,是把一套已经跑通的设备树工程,从 A 平台搬到 B 平台。设备树机制让这件事变得可控:
- 内核复用:同一内核镜像可同时支持多块板卡,只需更换 DTB,无需重新编译内核。
- 差异隔离:SoC 公共部分放 DTSI,板级差异放 DTS,迁移时主要修改 DTS 和少量 DTSI 覆盖。
- 快速验证:通过 DTC 反编译对比两块板卡的 DTB,可快速定位节点、属性、地址差异。
- 降低维护成本:硬件变更不再牵动内核代码,驱动与板级描述解耦。
迁移时优先对比三样东西:
compatible 字符串、reg 地址范围、interrupts 中断号。这三者不一致,往往是驱动 probe 失败的首要原因。
六、常见误区与复盘
初学者在设备树迁移中容易踩以下坑:
- 把 DTS 当 C 代码写:DTS 没有变量、循环和函数,只有节点、属性与引用,语法错误会直接导致 DTC 编译失败。
- 忽略
status属性:DTSI 中外设默认disabled,板级 DTS 必须显式改为okay,否则驱动不会 probe。 - 地址与中断号照搬:不同 SoC 的寄存器基地址和中断号不同,直接复制会导致驱动读写错误地址。
- 只改 DTS 不改 DTSI:若公共 DTSI 中已固定某外设参数,板级覆盖必须使用
&label引用方式,否则不生效。 - 不验证实际加载结果:编译通过不等于内核解析正确,务必查看
/proc/device-tree/确认节点存在。
迁移完成后,建议用
dtc 把新旧两块板卡的 DTB 都反编译成 DTS,做一次结构化 diff。差异清单就是迁移改动清单,也是后续回归测试的重点。