4. U-Boot中设备树的加载与传递:FDT库介绍、bootm与booti流程、设备树在内存中的位置

设备树在U-Boot里到底是怎么跑起来的?说白了,就是三个问题:谁加载它、谁解析它、它待在内存哪个位置。这一章我们就把这几个事彻底捋清楚。

我记得刚接触U-Boot移植那会儿,最头疼的就是设备树加载失败,内核死活起不来。后来才发现,不是设备树本身写错了,而是加载流程里某个环节没对齐。嗯,今天我们就从FDT库开始,一步步拆解这个流程。

4.1 FDT库:设备树的操作工具箱

FDT(Flattened Device Tree)库是U-Boot里操作设备树的核心代码。它提供了一整套API,用来解析、修改、传递设备树二进制数据。你想想看,U-Boot在启动内核之前,需要根据硬件配置动态调整设备树——比如修改内存大小、设置MAC地址、使能某个外设——这些操作全靠FDT库来完成。

FDT库的源文件主要位于 lib/libfdt/ 目录下,核心接口包括:

函数 功能 我在项目中的使用场景
fdt_open_into() 将设备树从只读区域复制到可读写缓冲区 需要修改设备树前必须调用,否则写操作会崩溃
fdt_totalsize() 获取设备树总大小 分配内存时用来确认缓冲区是否够用
fdt_getprop() 读取指定节点的属性值 读取 reg 或 status 属性,判断外设是否使能
fdt_setprop() 设置或修改属性值 动态修改 mac-address,避免每个板子都重新编译设备树
fdt_del_node() 删除指定节点 禁用某个不用的外设,减少内核启动时的资源冲突
fdt_find_node_by_path() 通过路径查找节点偏移量 定位 /soc/ethernet 这类固定路径节点
我的习惯:每次调用 fdt_setprop() 之前,我都会先用 fdt_totalsize() 检查剩余空间。如果空间不够,提前调用 fdt_open_into() 扩展缓冲区。这个习惯帮我避免了好几次运行时崩溃。

4.2 bootm与booti:两种启动方式的设备树传递

U-Boot支持两种主流的内核镜像格式:bootm 用于uImage(传统格式),booti 用于Image(纯二进制格式,通常配合设备树使用)。两者的设备树传递逻辑有细微差别,但核心流程是一致的。

4.2.1 bootm流程

bootm 是U-Boot最经典的启动命令。它的设备树传递流程大致如下:

  1. 解析镜像头:从内存地址读取uImage头部,确认镜像类型、加载地址、入口地址。
  2. 解压(如果需要):如果内核是压缩的,先解压到指定地址。
  3. 准备设备树:检查 fdt_addr_r 或 fdtcontroladdr 指向的设备树是否有效。
  4. 修改设备树:调用FDT库API,根据当前硬件配置修改设备树(如内存节点、启动参数)。
  5. 传递控制权:将设备树地址作为参数传递给内核,跳转到内核入口。

我曾经在一个项目里遇到 bootm 启动后内核报 FDT_ERR_BADMAGIC 错误。查了半天,发现是设备树地址写错了——我传的是 fdt_addr 而不是 fdt_addr_r。前者是Flash里的地址,后者才是内存里的地址。内核只能从内存里读设备树,从Flash直接读肯定报错。

4.2.2 booti流程

booti 是ARM64平台常用的启动方式。它比 bootm 更简洁,因为不需要处理uImage头部。流程如下:

  1. 直接加载内核:从指定地址读取Image格式的内核到内存。
  2. 准备设备树:同样检查 fdt_addr_r 指向的设备树。
  3. 修改设备树:与 bootm 相同,调用FDT库进行动态修改。
  4. 传递控制权:将设备树地址放在 x0 寄存器(ARM64约定),跳转到内核。
关键区别:bootm 需要处理镜像头部和解压,booti 直接加载原始二进制。但设备树的准备和传递逻辑几乎一样。我个人更推荐 booti,因为它少了一层头部解析,出错概率更低。

4.3 设备树在内存中的位置

设备树在内存里放哪儿,这是个看似简单但容易踩坑的问题。U-Boot里通常用环境变量来指定设备树的加载地址:

  • fdt_addr:设备树在Flash或存储设备中的地址(只读)。
  • fdt_addr_r:设备树在内存中的地址(可读写)。
  • fdt_high:限制设备树在内存中的最高地址(防止覆盖内核)。
  • initrd_high:限制initrd的最高地址,与设备树地址配合使用。

典型的地址分配如下(以2GB内存为例):

区域 起始地址 大小 说明
内核加载区 0x80000000 ~16MB 存放内核镜像
设备树区 0x83000000 ~1MB 存放设备树二进制
initrd区 0x84000000 ~64MB 存放初始ramdisk
其他数据 0x88000000 剩余 留给其他用途
避坑指南:我曾经在一个项目里把设备树地址设在了 0x80000000,结果内核解压时直接覆盖了设备树。内核启动到一半就崩溃了。后来我把设备树地址改到 0x83000000,留出足够空间给内核解压,问题就解决了。记住:设备树地址一定要避开内核的加载和解压区域。

4.4 设备树加载与传递的完整流程

下面这张图展示了从U-Boot启动到内核接收设备树的完整流程。我建议你仔细看看,因为很多移植问题就出在这些环节的衔接处。

U-Boot设备树加载与传递流程 设备树加载与传递完整流程 存储设备(Flash/eMMC) fdt_addr 加载到内存(fdt_addr_r) FDT库解析与修改 bootm / booti 启动 内核接收设备树 关键检查点 1. fdt_addr_r 是否在内存中 2. 设备树是否与内核解压区重叠 3. FDT magic number 是否正确 4. 设备树大小是否超过缓冲区 5. 修改后的设备树是否合法 常见问题 • 地址写错(Flash vs 内存) • 设备树被内核覆盖 • FDT库缓冲区不足 • 修改后未重新校验

4.5 实战中的设备树地址调试

调试设备树加载问题,我一般用这几步:

  1. 打印环境变量:用 printenv fdt_addr fdt_addr_r 确认地址是否正确。
  2. 检查内存内容:用 md.b <fdt_addr_r> 64 查看设备树头部,确认 D0 0D FE ED(设备树魔数)是否存在。
  3. 验证设备树大小:用 fdt addr <fdt_addr_r> 让U-Boot解析设备树,如果返回错误,说明设备树损坏或地址不对。
  4. 测试修改操作:用 fdt list / 查看根节点,确认设备树能被正常解析。
我的调试技巧:在 board_init_f() 或 board_init_r() 里加一段打印,输出 fdt_addr_r 指向的设备树魔数和总大小。这样在U-Boot启动阶段就能确认设备树是否被正确加载,不用等到内核启动才发现问题。

嗯,设备树的加载与传递,说白了就是U-Boot和内核之间的一个握手协议。U-Boot负责把设备树放到内存里,告诉内核“嘿,你的硬件信息在这儿”。内核拿到后,根据设备树里的描述来初始化驱动。这个流程看似简单,但每个环节都可能出问题——地址重叠、缓冲区不足、魔数错误——都是我在项目里踩过的坑。希望这一章能帮你少走一些弯路。