2. 设备树基础回顾:DTB、DTS、DTC工具与内核解析流程概览
在深入 printk 调试 of_ 函数之前,必须牢固掌握设备树的三个核心形态:源文件(DTS)、编译后的二进制(DTB) 以及 编译工具(DTC)。本章将系统梳理这些概念,并勾勒出内核从引导加载程序(Bootloader)传递 DTB 到驱动模型完成解析的完整流程。
2.1 设备树的三种形态
设备树在开发与运行过程中经历三个关键阶段,理解它们的区别是调试的基础。
| 形态 | 全称 | 格式 | 用途 | 可读性 |
|---|---|---|---|---|
| DTS | Device Tree Source | 文本(.dts / .dtsi) | 开发人员编写与维护 | 高 |
| DTB | Device Tree Blob | 二进制(.dtb) | Bootloader 加载并传递给内核 | 低 |
| DTC | Device Tree Compiler | 工具 | 将 DTS 编译为 DTB,或将 DTB 反编译为 DTS | — |
调试提示: 当内核启动异常或驱动无法匹配时,首先应检查实际传递给内核的 DTB 是否与预期的 DTS 一致。使用
dtc -I dtb -O dts -o dump.dts boot.dtb 反编译 DTB 是常用的逆向验证手段。
2.2 DTC 工具与编译流程
DTC 工具是连接文本与二进制的桥梁。内核构建系统(Kbuild)在编译设备树时,会执行以下核心步骤:
- 预处理:C 预处理器(cpp)处理
#include、#define等指令,将.dtsi头文件展开到.dts中。 - 编译:DTC 读取预处理后的
.dts,解析节点、属性、标签,生成扁平化的二进制 DTB。 - 打包:DTB 文件被链接到内核镜像中,或作为独立文件存放在
/boot分区。
关键命令示例:
# 编译 DTS 为 DTB
dtc -I dts -O dtb -o my_board.dtb my_board.dts
# 反编译 DTB 为 DTS(调试常用)
dtc -I dtb -O dts -o my_board_decompiled.dts my_board.dtb
# 查看 DTB 内部结构(以树状形式输出)
dtc -I dtb -O dts -f -o /dev/stdout my_board.dtb | head -100
2.3 内核解析 DTB 的完整流程
从 Bootloader 跳转到内核入口点,到驱动能够调用 of_ 系列 API 获取设备信息,中间经历了一条清晰的解析流水线。下图展示了这一流程中的关键阶段与函数调用关系。
2.4 关键数据结构:从 DTB 到 device_node
内核解析 DTB 的核心目标是将扁平的二进制数据转换为可供 C 代码遍历的树状结构。这一转换由 unflatten_device_tree() 函数完成,其输出是 struct device_node 的链表与树。
| 数据结构 | 定义位置 | 说明 |
|---|---|---|
struct device_node |
include/linux/of.h |
表示一个设备节点,包含 name、type、phandle、parent、child、sibling 指针 |
struct property |
include/linux/of.h |
表示节点的一个属性,包含 name、value、length、next 指针 |
struct of_changeset |
include/linux/of.h |
用于动态修改设备树(overlay),调试中较少直接接触 |
调试视角: 在
unflatten_device_tree() 执行完毕后,可以通过以下方式验证解析结果:
// 在任意驱动初始化代码中插入
struct device_node *np;
for_each_node_of_type(np) {
printk("node: %s, full_name: %s\n", np->name, np->full_name);
}
如果此循环输出为空或缺少关键节点,说明 DTB 解析阶段已出现问题,应优先检查 unflatten_device_tree() 的调用条件与 DTB 地址有效性。
2.5 流程中的关键函数与调用时机
下表总结了从内核启动到驱动 probe 过程中,与设备树解析直接相关的核心函数及其调用阶段。
| 函数名 | 调用阶段 | 主要职责 |
|---|---|---|
early_init_dt_scan() |
setup_arch() 早期 |
扫描 DTB 头部,验证魔数,获取内存大小、chosen 节点等基本信息 |
unflatten_device_tree() |
setup_arch() 后期 |
将 DTB 展开为 device_node 树,建立全量节点链表 |
of_platform_populate() |
arch_initcall() 或驱动初始化 |
遍历设备树中 compatible 属性匹配的节点,创建 platform_device |
of_match_node() |
驱动 probe 时 | 在 of_match_table 中查找与当前节点 compatible 匹配的条目 |
of_property_read_*() |
驱动 probe 及运行时 | 从 device_node 中读取指定属性的值(u32、string、array 等) |
常见陷阱: 许多调试者误以为
of_platform_populate() 会自动为所有设备树节点创建 platform_device。实际上,该函数默认只处理根节点下 compatible 属性为 "simple-bus" 的子节点。对于 I2C、SPI 等总线下的设备,需要由对应的总线控制器驱动自行调用 of_platform_populate() 或使用 i2c_register_board_info() 等机制。如果设备节点没有生成 platform_device,驱动 probe 永远不会被调用。
2.6 本章小结
- DTS 是开发人员编写的源文件,DTC 将其编译为 DTB,内核解析 DTB 得到
device_node树。 - 解析流程分为:Bootloader 传递 → 早期扫描 → 展开树 → 创建设备 → 驱动匹配。
- 调试的黄金切入点是
unflatten_device_tree()之后、of_platform_populate()之前,此时可以验证节点是否被正确解析。 - 使用
dtc反编译 DTB 是验证实际运行态设备树内容的最快方法。