第3章:U-Boot与设备树的关系

说实话,我刚接触U-Boot和设备树那会儿,最困惑的就是——这两者到底怎么配合的?U-Boot怎么知道该用哪个设备树?设备树又是怎么传到内核手里的?

这一章,我就把这些年积累的经验掰开揉碎了讲给你听。你搞懂了这些,后面调试设备树问题的时候,心里就有底了。

3.1 U-Boot加载设备树的完整流程

U-Boot启动后,设备树不是凭空出现的。它得经过几个关键步骤,才能从存储介质跑到内存里,最后交到内核手上。

我习惯把这个过程分成三个阶段:

  1. 定位阶段:U-Boot根据环境变量或默认配置,找到设备树文件的位置
  2. 加载阶段:从存储介质(MMC、NAND、网络等)读到内存
  3. 传递阶段:把内存中的设备树地址告诉内核

来看一个典型的启动日志,你就明白了:

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中的生命周期

为了让你更直观地理解整个过程,我画了张图:

设备树在U-Boot中的生命周期 存储介质 MMC / NAND / TFTP 加载到内存 fatload / ext4load U-Boot校验 fdt_check_header U-Boot修改 fdt set / fdt rm 传递给内核 booti / bootm 💡 常见问题点 • 设备树地址与内核解压地址重叠 → 内核启动时覆盖设备树 • fdt_check_header 失败 → 加载的文件不是有效的DTB

你看,整个过程其实不复杂。但每个环节都可能出问题。我总结了一个排查清单:

  • 加载阶段:文件路径对不对?存储介质初始化成功了吗?
  • 校验阶段: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

嗯,这套流程我用了好几年,帮我在十几个项目里快速定位了设备树问题。你把它记下来,调试的时候直接套用就行。