1. 设备树基础:概念、动机与文件格式
设备树(Device Tree)是一种描述硬件资源的数据结构,它把嵌入式系统中原本硬编码在板级文件(board-*.c)里的硬件信息抽离出来,变成一份独立于内核的、可编译的文本描述。内核在启动时解析这份描述,动态构建平台设备、中断控制器、时钟树、GPIO 控制器等关键资源,从而让同一份内核镜像能够适配不同硬件配置。
一句话理解:设备树就是硬件的“配置文件”——它告诉内核“这块板子上有哪些外设、它们挂在哪个总线、中断号是多少、寄存器地址范围是什么”,内核不再需要为每块板子单独写代码。
1.1 为什么需要设备树?
在设备树出现之前,ARM Linux 社区长期依赖 arch/arm/mach-* 下的板级文件。每增加一块新板子,就要新增或修改一个 board-xxx.c,里面充斥着 platform_device、resource、platform_data 的手动注册。这种模式带来了三个严重问题:
| 问题 | 具体表现 | 后果 |
|---|---|---|
| 代码膨胀 | 每个板子都要写数百行重复的注册代码 | 内核源码体积失控,维护成本激增 |
| 硬件变更困难 | 修改一个 GPIO 或中断号需要重新编译内核 | 调试周期长,厂商不愿升级内核 |
| 碎片化严重 | 不同厂商的板级代码风格各异 | 社区难以统一维护,主线化阻力大 |
设备树解决了上述痛点:硬件描述与内核代码分离,修改硬件配置只需修改 .dts 文件并重新编译 .dtb,无需重新编译内核;同一份内核镜像通过更换 .dtb 即可支持不同板子;社区统一了硬件描述语法,降低了碎片化。
关键转折:Linux 主线在 2011 年(v3.0 左右)开始强制 ARM 平台使用设备树,到 v3.10 之后,新平台基本不再接受纯板级文件。如今,x86、RISC-V、MIPS 等架构也广泛采用设备树。
1.2 设备树源文件(.dts)与编译(.dtb)
设备树的完整工作流包含三个层次:
- .dts(Device Tree Source):人类可读的文本文件,使用类似 C 语言的语法描述硬件节点、属性、寄存器、中断等。
- .dtsi(Device Tree Source Include):公共头文件,通常存放 SoC 级通用描述,被多个
.dts包含。 - .dtb(Device Tree Blob):
.dts经过dtc(Device Tree Compiler)编译后生成的二进制文件,是内核启动时直接解析的格式。
编译命令示例:
# 将 .dts 编译为 .dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译 .dtb 为 .dts(调试时常用)
dtc -I dtb -O dts -o myboard_decompiled.dts myboard.dtb
常见陷阱:
.dts 中引用的 .dtsi 路径依赖 -i 参数或环境变量 DTC_INCLUDE。编译时若找不到头文件,dtc 会报错退出。内核源码树中通常使用 make dtbs 自动处理包含路径。
1.3 设备树的核心结构:节点与属性
设备树的基本单元是节点(node),每个节点代表一个硬件设备或总线。节点内包含属性(property),描述设备的地址、中断、时钟、复位等资源。一个典型的 .dts 片段如下:
/dts-v1/;
/ {
model = "MyBoard v1.0";
compatible = "vendor,myboard", "vendor,common";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clocks = <&clk_uart0>;
};
};
};
关键语法说明:
| 元素 | 示例 | 含义 |
|---|---|---|
/dts-v1/; |
文件头 | 声明 DTS 版本,必须位于文件第一行 |
/ { }; |
根节点 | 设备树的顶层容器,所有设备都是它的子节点 |
model |
"MyBoard v1.0" |
板子名称,用于用户空间识别 |
compatible |
"vendor,myboard" |
匹配内核驱动的重要标识,按优先级排列 |
#address-cells |
<1> |
地址字段占用的 32 位单元数 |
#size-cells |
<1> |
长度字段占用的 32 位单元数 |
reg |
<0x80000000 0x10000000> |
地址与长度对,描述物理地址范围 |
interrupts |
<0 33 4> |
中断描述:类型、中断号、触发方式 |
clocks |
<&clk_uart0> |
引用时钟节点,& 表示 phandle |
1.4 设备树解析流程概览
内核启动时,设备树的解析分为三个阶段:
- Bootloader 加载 DTB:U-Boot 等引导程序从存储介质(eMMC、NAND、网络)读取
.dtb文件,放入内存,并将物理地址通过寄存器r2(ARM)或a1(RISC-V)传递给内核。 - 内核早期解析:内核入口函数
start_kernel调用setup_arch→unflatten_device_tree,将扁平二进制 DTB 展开为树状结构device_node,并初始化of_root全局根节点。 - 驱动匹配与资源获取:总线驱动(如 platform bus)遍历设备树,对每个节点调用
of_match_node匹配compatible字符串,匹配成功后调用驱动的probe函数,驱动内部通过of_*API 读取reg、interrupts、clocks等属性。
ftrace 切入点:本课程后续章节将使用 ftrace 追踪
unflatten_device_tree、of_match_node、platform_probe 等关键函数的调用栈与耗时,精准定位设备树解析瓶颈。
1.5 设备树结构示意图
下图展示了一个简化的设备树层次结构,包含根节点、SoC 总线、内存、串口和 GPIO 控制器,以及它们之间的 phandle 引用关系: