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)在编译设备树时,会执行以下核心步骤:

  1. 预处理:C 预处理器(cpp)处理 #include、#define 等指令,将 .dtsi 头文件展开到 .dts 中。
  2. 编译:DTC 读取预处理后的 .dts,解析节点、属性、标签,生成扁平化的二进制 DTB。
  3. 打包: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 获取设备信息,中间经历了一条清晰的解析流水线。下图展示了这一流程中的关键阶段与函数调用关系。

内核 DTB 解析流程概览 Bootloader 加载 DTB 到内存 早期启动汇编 保存 DTB 地址到 r2/x0 setup_arch() early_init_dt_scan() unflatten_device_tree() DTB → 树状结构 (struct device_node) of_platform_populate() 创建 platform_device 驱动匹配与 probe of_match_table 匹配 🔍 调试切入点(printk 埋点位置) early_init_dt_scan() → unflatten_device_tree() → of_platform_populate() → 驱动 probe 在 of_find_node_by_path()、of_property_read_*() 等 API 入口/出口添加 printk

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 是验证实际运行态设备树内容的最快方法。