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)。这条顺序能快速定位大多数启动期设备树问题。
DTB 传递与内核解析链路 Bootloader 加载 Image + DTB x0 = DTB 物理地址 跳转内核入口 传递 early_init_dt_scan 校验 magic / 版本 扫描 /memory 扫描 /chosen /cpus 展开 unflatten_device_tree 扁平 DTB → device_node 树 生成 struct device_node 生成 struct property 扁平 DTB 内部结构 fdt_header magic / totalsize structure block 节点 / 属性 / 值 strings block 属性名字符串 对齐填充 padding 解析来源 展开后的 device_node 树(/proc/device-tree) / (root) /chosen /memory /cpus /soc

把这条链路记牢,后续遇到“设备树节点不生效”“驱动 probe 失败”“内存大小不对”等问题时,就能按“传递—校验—扫描—展开—使用”的顺序逐段排查,而不是盲目修改 DTS。设备树调试的本质,是确认每一段的数据是否完整、地址是否正确、解析是否成功。