4. 设备树与内核启动
设备树(Device Tree)在运行时并不是“内核自己从磁盘读出来”的,而是由 bootloader 在跳转内核之前,把一份编译好的二进制 DTB(Device Tree Blob)放到内存中,并通过寄存器把它的物理地址告诉内核。内核启动早期在内存管理、驱动模型尚未建立时,就必须先“看懂”这份 DTB,才能知道串口在哪、内存多大、有哪些 CPU。因此,理解 DTB 的传递与解析链路,是设备树调试与故障定位的第一道关口。
一、核心原理:DTB 的三段旅程
从 bootloader 到内核真正使用设备树,DTB 经历三个关键阶段:传递、扫描、展开。传递阶段解决“DTB 在哪里”;扫描阶段解决“DTB 里有什么关键信息”;展开阶段解决“如何把扁平二进制变成内核可遍历的树形结构”。
- 传递:bootloader 将 DTB 加载到内存,ARM64 通过
x0寄存器、ARM32 通过r2寄存器、部分架构通过bootargs或atags约定传递 DTB 地址。 - 扫描:内核入口
setup_arch()调用early_init_dt_scan(),在 slab、kmalloc 可用之前,用最原始的方式读取 DTB 头部和关键节点。 - 展开:
unflatten_device_tree()把扁平的struct fdt_header+ 字符串块 + 结构块,转换成由struct device_node和struct property组成的树,供后续of_*接口使用。
early_init_dt_scan() 阶段内核还没有完整的内存分配器,所以它只能“就地读取”DTB,不能创建 device_node;真正建树要等到 unflatten_device_tree()。
二、方法流程:从 bootloader 到设备树可用
下面以 ARM64 为例,梳理一条可对照日志和源码的完整链路。ARM64 的 DTB 地址由 bootloader 放在 x0,内核在 head.S 中保存,随后在 setup_arch() 中交给设备树子系统。
bootloader
├─ 加载 Image 到内存
├─ 加载 DTB 到内存(如 0x40000000)
├─ x0 = DTB 物理地址
└─ 跳转内核入口
内核启动
head.S : 保存 x0 到 boot_args
start_kernel()
setup_arch()
setup_machine_fdt() // 校验 magic、扫描根节点
early_init_dt_scan() // 扫描 memory、chosen、cpus
early_init_dt_scan_nodes()
unflatten_device_tree() // 展开为 device_node 树
of_alias_scan() // 处理 aliases
... 后续驱动 probe 使用 of_* 接口
其中 early_init_dt_scan() 内部会依次调用 early_init_dt_scan_nodes(),重点扫描三类节点:/chosen(bootargs、initrd)、/memory(内存布局)、/cpus(CPU 数量与启动 CPU)。这些信息必须在内存管理初始化之前拿到,否则内核无法建立页表、无法知道可用物理内存范围。
三、关键数据结构与转换关系
DTB 是扁平二进制,内部由三块组成:头部 fdt_header、结构块 structure block、字符串块 strings block。展开后,每个节点变成 struct device_node,每个属性变成 struct property,属性名指向字符串块中的偏移,属性值指向结构块中的数据。
| 阶段 | 核心函数 | 输入 | 输出/作用 |
|---|---|---|---|
| 传递 | bootloader 约定 | DTB 镜像 | 寄存器中保存 DTB 物理地址 |
| 校验 | early_init_dt_verify() | DTB 地址 | 检查 magic、版本、大小 |
| 扫描 | early_init_dt_scan() | 扁平 DTB | 提取 memory、chosen、cpus |
| 展开 | unflatten_device_tree() | 扁平 DTB | 生成 device_node 树 |
| 使用 | of_find_node_by_path() | 节点路径 | 驱动获取资源 |
四、案例与场景:一次“内核找不到串口”的排查
某 ARM64 板卡启动后内核无串口输出,但 bootloader 阶段串口正常。排查时先确认 DTB 是否被正确传递:在 bootloader 中打印 DTB 地址,并在内核命令行加入 earlycon。若 earlycon 能输出,说明 DTB 已到达内核且 /chosen 中的 stdout-path 被解析;若仍无输出,则问题可能出在 DTB 地址错误或 DTB 内容缺失。
# 查看内核是否识别到 DTB
dmesg | grep -i "device tree"
# 典型输出
OF: fdt: Machine model: MyBoard
OF: fdt: Memory scan node memory, reg size 2
OF: fdt: 8 available CPUs
# 查看展开后的节点
ls /proc/device-tree/
cat /proc/device-tree/chosen/bootargs
cat /proc/device-tree/soc/serial@10000000/status
如果 /proc/device-tree 为空,通常说明 unflatten_device_tree() 没有成功执行,或者 DTB 地址无效。此时应回到 bootloader 检查 DTB 加载地址是否与内核 CONFIG_PHYS_OFFSET 冲突,以及是否被覆盖。
early_init_dt_scan() 前后加 pr_debug 或使用 earlycon,可以最早看到 DTB 是否被识别。若 earlycon 无输出,优先怀疑 DTB 地址或 magic 校验失败。
五、误区与复盘
- 误区一:DTB 由内核从文件系统读取。实际上 DTB 由 bootloader 提供,内核只接收地址。文件系统中的
.dtb通常只是备份或供用户空间工具使用。 - 误区二:
early_init_dt_scan()已经建好设备树。它只做早期扫描,不创建device_node;真正建树在unflatten_device_tree()。 - 误区三:DTB 地址可以随意指定。DTB 必须放在内核不会覆盖的内存区域,且要满足对齐要求,否则 magic 校验会失败。
- 复盘要点:先确认传递(寄存器/地址),再确认校验(magic/版本),再确认扫描(memory/chosen/cpus),最后确认展开(
/proc/device-tree)。这条顺序能快速定位大多数启动期设备树问题。
把这条链路记牢,后续遇到“设备树节点不生效”“驱动 probe 失败”“内存大小不对”等问题时,就能按“传递—校验—扫描—展开—使用”的顺序逐段排查,而不是盲目修改 DTS。设备树调试的本质,是确认每一段的数据是否完整、地址是否正确、解析是否成功。