第3章:U-Boot与设备树的关系
说实话,我刚接触U-Boot和设备树那会儿,最困惑的就是——这两者到底怎么配合的?U-Boot怎么知道该用哪个设备树?设备树又是怎么传到内核手里的?
这一章,我就把这些年积累的经验掰开揉碎了讲给你听。你搞懂了这些,后面调试设备树问题的时候,心里就有底了。
3.1 U-Boot加载设备树的完整流程
U-Boot启动后,设备树不是凭空出现的。它得经过几个关键步骤,才能从存储介质跑到内存里,最后交到内核手上。
我习惯把这个过程分成三个阶段:
- 定位阶段:U-Boot根据环境变量或默认配置,找到设备树文件的位置
- 加载阶段:从存储介质(MMC、NAND、网络等)读到内存
- 传递阶段:把内存中的设备树地址告诉内核
来看一个典型的启动日志,你就明白了:
U-Boot 2023.04 (Apr 12 2023 - 15:30:00 +0800)
DRAM: 2 GiB
MMC: mmc@fe320000: 0, mmc@fe330000: 1
Loading Environment from MMC... OK
# 关键在这里——加载设备树
mmc dev 0
fatload mmc 0:1 0x48000000 /imx8mp-evk.dtb
# 输出:reading /imx8mp-evk.dtb
# 29221 bytes read in 5 ms (5.6 MiB/s)
# 加载内核
fatload mmc 0:1 0x42000000 /Image
# 启动内核,传递设备树地址
booti 0x42000000 - 0x48000000
关键点:booti 命令的第三个参数就是设备树在内存中的地址。内核启动时会从这个地址解析设备树。
3.2 设备树在U-Boot中的传递机制
设备树怎么从U-Boot传到内核?这里有个重要的数据结构——tagged list(对于老内核)或者device tree blob(对于新内核)。
我当年调试一个i.MX6平台的时候,就遇到过设备树传递失败的问题。内核启动到一半就挂了,打印信息显示"unflatten_device_tree() failed"。查了两天才发现,是U-Boot传给内核的设备树地址不对,内核读到的是一堆乱码。
现在主流的方式是这样的:
| 传递方式 | 适用场景 | 特点 |
|---|---|---|
| ATAG(老方式) | ARM 32位,Linux 3.x之前 | 传递参数有限,不支持设备树 |
| FDT(扁平设备树) | ARM/ARM64/RISC-V,主流方式 | 二进制blob,内核直接解析 |
| UEFI设备树协议 | ARM64服务器,UEFI启动 | 通过UEFI runtime service传递 |
目前99%的嵌入式设备都用FDT方式。U-Boot把设备树加载到内存后,通过寄存器r2(ARM32)或x2(ARM64)把地址传给内核。就这么简单,但也是最容易出问题的地方。
我曾经踩过的坑:U-Boot加载设备树时,如果地址选在了内核解压会覆盖的区域,内核启动时就会把设备树冲掉。我建议你给设备树分配地址时,离内核地址远一点,至少隔个16MB。
3.3 FDT(扁平设备树)API详解
U-Boot提供了一套完整的FDT操作API。你在U-Boot命令行或者代码里,都可以直接操作设备树。这功能太实用了——调试的时候不用重新编译整个固件。
3.3.1 命令行操作
U-Boot的fdt命令是调试设备树的利器。我常用的几个子命令:
# 设置设备树工作地址
fdt addr 0x48000000
# 列出所有节点
fdt list
# 查看某个节点的属性
fdt list /soc/bus@30800000/i2c@30a20000
# 修改属性值(调试时太有用了)
fdt set /soc/bus@30800000/i2c@30a20000 clock-frequency <0x61a80>
# 添加新节点
fdt mknode /soc my-debug-node
# 添加属性
fdt set /soc/my-debug-node status "okay"
# 打印整个设备树
fdt print
我的调试习惯:在U-Boot启动脚本里加一行 fdt print 并保存log。这样内核启动失败时,我能确认设备树内容是不是对的。省得反复烧写固件。
3.3.2 代码级API
如果你需要在U-Boot代码里操作设备树,这些API你得记住:
| API函数 | 功能 | 我在项目中用过吗? |
|---|---|---|
fdt_check_header() |
验证设备树头是否合法 | 每次加载后必调,防止坏数据 |
fdt_path_offset() |
根据路径查找节点偏移 | 常用,比如找chosen节点 |
fdt_getprop() |
读取属性值 | 读reg、status等属性 |
fdt_setprop() |
设置属性值 | 动态修改设备配置 |
fdt_delprop() |
删除属性 | 禁用某些外设时用 |
fdt_create() |
创建空设备树 | 很少用,除非你从头造轮子 |
举个例子,我在调试一个网卡驱动时,需要在U-Boot里动态修改设备树的MAC地址:
#include <fdt_support.h>
int fixup_mac_address(void *fdt_blob, const char *mac_str)
{
int offset;
int ret;
// 先检查设备树是否合法
ret = fdt_check_header(fdt_blob);
if (ret < 0) {
printf("FDT header check failed: %s\n", fdt_strerror(ret));
return ret;
}
// 找到以太网节点
offset = fdt_path_offset(fdt_blob, "/soc/ethernet@30be0000");
if (offset < 0) {
printf("Ethernet node not found\n");
return offset;
}
// 设置MAC地址属性
ret = fdt_setprop_string(fdt_blob, offset, "local-mac-address", mac_str);
if (ret < 0) {
printf("Failed to set MAC address: %s\n", fdt_strerror(ret));
return ret;
}
return 0;
}
注意:fdt_setprop_* 系列函数会修改内存中的设备树。如果你要保存修改,得用 saveenv 或者写回存储介质。不然下次重启就没了。
3.4 设备树在U-Boot中的生命周期
为了让你更直观地理解整个过程,我画了张图:
你看,整个过程其实不复杂。但每个环节都可能出问题。我总结了一个排查清单:
- 加载阶段:文件路径对不对?存储介质初始化成功了吗?
- 校验阶段:DTB文件是不是损坏了?版本匹配吗?
- 修改阶段:修改的属性名对不对?值格式正确吗?
- 传递阶段:地址对齐了吗?内核能访问到这个地址吗?
我的经验:调试设备树问题,最有效的方法就是在U-Boot命令行里手动操作。先 fdt addr 设置地址,然后 fdt print 看看内容对不对。确认没问题了再启动内核。这样能快速定位是设备树的问题还是内核的问题。
3.5 实战:手动验证设备树传递
最后,我给你一个我在项目中实际用过的调试流程。下次遇到设备树相关的问题,照着做就行:
# 1. 确认设备树文件存在
mmc dev 0
fatls mmc 0:1 / *.dtb
# 2. 加载到内存(选个安全地址)
fatload mmc 0:1 0x48000000 /board.dtb
# 3. 验证设备树合法性
fdt addr 0x48000000
fdt list / # 如果能列出根节点,说明DTB格式正确
# 4. 检查关键节点
fdt list /chosen
fdt list /memory
# 5. 启动内核(传递设备树)
booti 0x42000000 - 0x48000000
嗯,这套流程我用了好几年,帮我在十几个项目里快速定位了设备树问题。你把它记下来,调试的时候直接套用就行。