1. 设备树基础回顾:从编译到加载的完整链路
设备树这东西,说白了就是一块描述硬件信息的结构化数据。Linux 内核通过它来知道板子上接了哪些外设、内存多大、中断怎么连。我刚开始接触嵌入式 Linux 时,总觉得设备树就是个“黑盒子”,编译出来扔给内核就完事了。直到有一次调试一个 SPI 驱动死活不工作,才发现是设备树里一个 reg 属性写错了。
嗯,咱们先把基础链路捋清楚。从 .dts 源文件到内核真正用上设备树,中间经历了四个阶段:编写 → 编译 → 加载 → 解析。每一步都有坑,也都有优化空间。
核心链路速览:
.dts 源文件 → dtc 编译器 → .dtb 二进制 → bootloader 加载 → 内核解析 → device_node 树 → platform_device 创建
1.1 .dts 源文件:一切从这里开始
.dts 文件是设备树的“源代码”。它用树形结构描述硬件:根节点下面是 cpu、memory、chosen 等子节点,每个节点又包含 reg、interrupts、compatible 等属性。
我个人习惯把 .dts 拆成三部分:
- .dtsi:SoC 级头文件,描述芯片内部外设。这部分基本不动。
- .dts:板级文件,描述具体板子上的外设和引脚复用。
- dt-bindings:头文件目录,定义宏和常量,比如 GPIO_ACTIVE_HIGH。
举个例子,一个典型的 .dts 文件长这样:
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "MyBoard v1.0";
compatible = "mycompany,imx6ull-myboard";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
&uart1 {
status = "okay";
pinctrl-0 = <&pinctrl_uart1>;
};
};
我的经验:尽量把板级差异放在 .dts 里,不要改 .dtsi。这样换板子时只需要改一个文件,编译也快。
1.2 dtc 编译器:把文本变成二进制
dtc(Device Tree Compiler)负责把 .dts 编译成 .dtb。命令很简单:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
但这里有个坑——编译速度。我遇到过一个大项目,.dts 里 include 了十几个头文件,每次编译都要跑好几秒。后来发现是头文件路径没优化,dtc 反复搜索目录。
常用的 dtc 选项:
| 选项 | 作用 | 我常用的场景 |
|---|---|---|
-I dts |
输入格式为 .dts | 标准编译 |
-O dtb |
输出格式为 .dtb | 标准编译 |
-@ |
生成符号表,支持 overlay | 需要动态加载时必加 |
-H epapr |
生成 EPAPR 格式(PowerPC 用) | 老平台兼容 |
-W no-unit_address_vs_reg |
关闭某些警告 | 清理编译输出 |
注意:不加 -@ 选项编译出来的 .dtb 无法用于 overlay。如果你后续要做动态设备树,记得加上。我曾经因为这个浪费了半天调试时间。
1.3 .dtb 二进制:内核能读懂的语言
.dtb 是扁平化的二进制格式。它把树形结构拍平成一个一个的“块”(fragment),每个块包含节点路径、属性名和属性值。内核在启动时直接解析这个二进制,不需要反编译。
你可以用 dtc -I dtb -O dts 把 .dtb 反编译回 .dts,检查编译结果对不对。我经常用这招来验证 overlay 是否生效。
# 反编译 .dtb 查看内容
dtc -I dtb -O dts -o decompiled.dts myboard.dtb
1.4 Bootloader 加载:把 .dtb 交给内核
Bootloader(U-Boot 或 Barebox)负责把 .dtb 从存储介质读到内存,然后把地址传给内核。U-Boot 里常用的命令:
# 从 mmc 加载 .dtb 到内存地址 0x83000000
load mmc 0:1 0x83000000 /boot/myboard.dtb
# 启动内核时传递 dtb 地址
bootz 0x82000000 - 0x83000000
这里有个细节:dtb 地址必须 8 字节对齐。我遇到过 U-Boot 加载后地址没对齐,内核直接 panic 的情况。嗯,这种问题查起来特别头疼。
避坑指南:我曾经在 U-Boot 里用 fdt addr 命令检查 dtb 是否加载正确。如果 fdt list 能正常显示节点,说明 dtb 没问题。
1.5 内核解析:从 .dtb 到 device_node
内核在启动早期(setup_arch 阶段)调用 unflatten_device_tree(),把 .dtb 解析成 struct device_node 的树形结构。这个过程是递归的,每个节点变成一个 device_node,每个属性变成一个 property。
关键数据结构:
- device_node:代表一个设备节点,包含 name、type、compatible、parent、children 等字段。
- property:代表一个属性,包含 name、value、length。
- of_root:全局根节点指针,所有设备树操作都从它开始。
解析完成后,内核会遍历 device_node 树,为每个 compatible 匹配的节点创建 platform_device。这就是驱动和设备绑定的起点。
1.6 从 device_node 到 platform_device
这一步由 of_platform_populate() 完成。它遍历 device_node 树,对每个节点调用 of_platform_device_create()。创建出来的 platform_device 会被注册到内核的设备模型中,等待驱动 probe。
匹配规则很简单:驱动的 of_match_table 里列出 compatible 字符串,和设备节点的 compatible 属性比对。匹配上了就调用驱动的 probe 函数。
static const struct of_device_id my_driver_of_match[] = {
{ .compatible = "mycompany,mydevice" },
{ /* sentinel */ }
};
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "mydevice",
.of_match_table = my_driver_of_match,
},
};
整个链路总结:
.dts → dtc → .dtb → bootloader → 内核 unflatten → device_node 树 → of_platform_populate → platform_device → 驱动 probe
每一步都有优化空间。比如编译阶段用 -@ 加速 overlay,加载阶段用 fdt 命令检查,解析阶段减少不必要的节点。后面几章我会逐一展开。
好了,基础链路就讲到这里。记住这个流程,后面讲优化技巧时你会知道每一步对应哪个环节。