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最经典的启动命令。它的设备树传递流程大致如下:
- 解析镜像头:从内存地址读取uImage头部,确认镜像类型、加载地址、入口地址。
- 解压(如果需要):如果内核是压缩的,先解压到指定地址。
- 准备设备树:检查
fdt_addr_r或fdtcontroladdr指向的设备树是否有效。 - 修改设备树:调用FDT库API,根据当前硬件配置修改设备树(如内存节点、启动参数)。
- 传递控制权:将设备树地址作为参数传递给内核,跳转到内核入口。
我曾经在一个项目里遇到 bootm 启动后内核报 FDT_ERR_BADMAGIC 错误。查了半天,发现是设备树地址写错了——我传的是 fdt_addr 而不是 fdt_addr_r。前者是Flash里的地址,后者才是内存里的地址。内核只能从内存里读设备树,从Flash直接读肯定报错。
4.2.2 booti流程
booti 是ARM64平台常用的启动方式。它比 bootm 更简洁,因为不需要处理uImage头部。流程如下:
- 直接加载内核:从指定地址读取Image格式的内核到内存。
- 准备设备树:同样检查
fdt_addr_r指向的设备树。 - 修改设备树:与
bootm相同,调用FDT库进行动态修改。 - 传递控制权:将设备树地址放在
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启动到内核接收设备树的完整流程。我建议你仔细看看,因为很多移植问题就出在这些环节的衔接处。
4.5 实战中的设备树地址调试
调试设备树加载问题,我一般用这几步:
- 打印环境变量:用
printenv fdt_addr fdt_addr_r确认地址是否正确。 - 检查内存内容:用
md.b <fdt_addr_r> 64查看设备树头部,确认D0 0D FE ED(设备树魔数)是否存在。 - 验证设备树大小:用
fdt addr <fdt_addr_r>让U-Boot解析设备树,如果返回错误,说明设备树损坏或地址不对。 - 测试修改操作:用
fdt list /查看根节点,确认设备树能被正常解析。
board_init_f() 或 board_init_r() 里加一段打印,输出 fdt_addr_r 指向的设备树魔数和总大小。这样在U-Boot启动阶段就能确认设备树是否被正确加载,不用等到内核启动才发现问题。
嗯,设备树的加载与传递,说白了就是U-Boot和内核之间的一个握手协议。U-Boot负责把设备树放到内存里,告诉内核“嘿,你的硬件信息在这儿”。内核拿到后,根据设备树里的描述来初始化驱动。这个流程看似简单,但每个环节都可能出问题——地址重叠、缓冲区不足、魔数错误——都是我在项目里踩过的坑。希望这一章能帮你少走一些弯路。