4、U-Boot设备树加载流程:SPL阶段加载、U-Boot proper阶段加载、从存储介质读取DTB

设备树(DTB)到底是怎么从存储介质跑到内存里去的?这个问题,我当年刚接触U-Boot时也困惑了很久。说白了,整个加载过程分三个阶段:SPL、U-Boot proper,以及最终的OS启动。每个阶段对设备树的态度都不一样。

嗯,咱们今天就把这条链路彻底捋清楚。

4.1 整体加载链路概览

先看一张总图,心里有个框架。

U-Boot 设备树加载全流程 SPL 阶段 加载最小DTB U-Boot proper 加载完整DTB OS 启动 传递DTB给内核 存储介质:eMMC / NAND Flash / SD卡 / SPI NOR Flash 关键说明: • SPL阶段:只加载裁剪后的最小DTB(通常只包含DDR、时钟、串口等基础外设) • U-Boot proper阶段:加载完整DTB,包含所有外设节点 • 存储介质读取:通过驱动层(MMC/NAND/SPI等)读取原始DTB二进制数据

你看,整个流程其实不复杂。但每个阶段的细节,坑不少。

4.2 SPL阶段的DTB加载

SPL(Secondary Program Loader)是什么?说白了就是U-Boot的"迷你版"。它负责初始化最基本的外设,然后把U-Boot proper从存储介质搬到DDR里。

SPL阶段加载DTB,有两种常见做法:

做法 描述 适用场景
内置DTB DTB直接链接到SPL镜像中,编译时固定 小容量BootROM、SPI NOR Flash
外置DTB SPL从存储介质读取DTB到临时缓冲区 eMMC、NAND、SD卡等大容量介质

我个人习惯在项目初期用内置DTB,调试方便。但量产时,我建议用外置DTB——灵活性高,换板卡不用重新编译SPL。

小技巧:SPL阶段的DTB可以非常精简。我在一个项目里,只保留了DDR控制器、UART和GPIO三个节点,DTB大小从30KB砍到4KB。SPL加载速度明显提升。

SPL加载DTB的代码路径,通常在 spl_fit.c 或 spl_load.c 里。核心逻辑是这样的:

// SPL阶段加载DTB的简化流程
int spl_load_dtb(struct spl_image_info *spl_image)
{
    // 1. 确定DTB在存储介质中的位置
    ulong dtb_offset = CONFIG_SYS_SPL_ARGS_ADDR;
    
    // 2. 从存储介质读取DTB
    //    这里调用底层驱动(MMC/NAND/SPI等)
    ret = spl_load_image_ext(spl_image, "dtb");
    if (ret) {
        // 如果外置DTB加载失败,回退到内置DTB
        debug("Fallback to built-in DTB\n");
        memcpy(spl_image->dtb, __dtb_start, __dtb_end - __dtb_start);
    }
    
    // 3. 校验DTB魔数
    if (fdt_check_header(spl_image->dtb)) {
        printf("Invalid DTB magic!\n");
        return -EINVAL;
    }
    
    return 0;
}

这里有个细节:SPL阶段的内存非常有限。我见过一些平台,SPL可用的SRAM只有64KB。所以DTB不能太大,否则连U-Boot proper都放不下。

注意:SPL阶段不要做完整的DTB校验。我曾经在某个项目里加了完整的fdt_check_full()校验,结果SPL镜像直接超了8KB。后来只校验魔数,问题解决。

4.3 U-Boot proper阶段的DTB加载

U-Boot proper启动后,它需要加载完整的设备树。这个阶段,DTB的来源就多了:

  • SPL传递过来的DTB:SPL已经加载好了,U-Boot proper直接拿过来用
  • 从存储介质重新读取:U-Boot proper自己从eMMC/NAND/SD卡读取DTB
  • 从FIT镜像中提取:如果使用FIT(Flattened Image Tree)格式,DTB是FIT的一部分
  • 网络加载:通过TFTP从服务器下载DTB(调试阶段常用)

嗯,这里我要重点说一下FIT镜像。FIT把内核、DTB、ramdisk打包成一个文件。U-Boot proper从FIT里提取DTB,代码在 common/image-fit.c 里:

// 从FIT镜像中提取DTB
int fit_image_load_dtb(const void *fit, int images_noffset)
{
    const char *uname = NULL;
    int dtb_noffset;
    
    // 1. 查找DTB子节点
    dtb_noffset = fdt_subnode_offset(fit, images_noffset, "fdt-1");
    if (dtb_noffset < 0) {
        printf("FDT node not found in FIT\n");
        return -ENOENT;
    }
    
    // 2. 获取DTB数据
    const void *dtb_data;
    size_t dtb_size;
    ret = fit_image_get_data(fit, dtb_noffset, &dtb_data, &dtb_size);
    
    // 3. 解压(如果DTB被压缩了)
    // 有些场景会压缩DTB以节省存储空间
    if (fit_image_get_comp(fit, dtb_noffset) == IH_COMP_GZIP) {
        ret = gunzip(dtb_buf, CONFIG_SYS_FDT_LOAD_ADDR,
                     dtb_data, dtb_size);
    }
    
    return 0;
}

你想想看,为什么U-Boot proper要重新加载DTB,而不是直接用SPL传过来的?原因很简单:SPL加载的DTB可能不完整。SPL只需要知道DDR大小和串口地址,但U-Boot proper需要知道所有外设的信息。

4.4 从存储介质读取DTB

不管哪个阶段,最终都要从存储介质里把DTB的二进制数据读出来。不同的存储介质,读取方式完全不同。

存储介质 驱动接口 典型分区 读取速度
eMMC mmc_read() /dev/mmcblk0p1 ~50MB/s
NAND Flash nand_read() mtdblock0 ~10MB/s
SPI NOR Flash spi_flash_read() mtdblock0 ~5MB/s
SD卡 mmc_read() /dev/mmcblk0p1 ~20MB/s

我在一个NAND Flash的项目里踩过坑。NAND有坏块,DTB刚好存到了坏块上。结果U-Boot proper启动时,DTB读出来全是0xFF,系统直接挂掉。

避坑指南:如果使用NAND Flash存储DTB,一定要做坏块管理。我曾经在某个产品里,DTB被写到坏块上,导致2000台设备无法启动。后来改用UBIFS文件系统存储DTB,利用文件系统的坏块管理机制,问题解决。

从存储介质读取DTB的通用流程:

  1. 确定DTB的存储位置:分区号、偏移地址、文件名
  2. 调用底层驱动读取数据:mmc_read() / nand_read() / spi_flash_read()
  3. 数据校验:检查DTB魔数(0xD00DFEED),可选CRC校验
  4. 重定位到目标地址:通常拷贝到 CONFIG_SYS_FDT_LOAD_ADDR

代码示例(从eMMC读取DTB):

// 从eMMC的FAT分区读取dtb文件
int load_dtb_from_mmc(void)
{
    struct mmc *mmc;
    int dev = 0;  // eMMC设备号
    
    // 1. 初始化MMC控制器
    mmc = find_mmc_device(dev);
    if (!mmc) {
        printf("MMC device %d not found\n", dev);
        return -ENODEV;
    }
    
    // 2. 挂载FAT分区
    if (fat_register_device(&mmc->block_dev, 1)) {
        printf("FAT partition not found\n");
        return -EIO;
    }
    
    // 3. 读取dtb文件到内存
    ulong addr = CONFIG_SYS_FDT_LOAD_ADDR;
    int ret = file_fat_read("board.dtb", (void *)addr, 0);
    if (ret <= 0) {
        printf("Failed to read board.dtb\n");
        return -EIO;
    }
    
    // 4. 校验DTB
    if (fdt_check_header((void *)addr)) {
        printf("Invalid DTB at 0x%lx\n", addr);
        return -EINVAL;
    }
    
    printf("DTB loaded from eMMC, size=%d bytes\n", ret);
    return 0;
}

4.5 实战中的加载策略选择

不同的项目,DTB加载策略差别很大。我整理了几种常见方案:

方案 SPL加载 U-Boot proper加载 适用场景
方案A 内置最小DTB 从存储介质加载完整DTB 大多数嵌入式Linux产品
方案B 从存储介质加载完整DTB 直接使用SPL传递的DTB 内存极度受限的平台
方案C 内置最小DTB 从FIT镜像提取DTB 使用FIT打包的系统
方案D 不加载DTB 从网络TFTP加载DTB 开发调试阶段

我个人最推荐方案A。SPL阶段用内置的最小DTB,只初始化DDR和串口。U-Boot proper阶段再从eMMC或NAND读取完整的DTB。这样既保证了SPL的启动速度,又保留了设备树的灵活性。

经验之谈:方案D虽然看起来"不专业",但调试时特别好用。我每次开发新板卡,都是先用TFTP加载DTB,确认设备树没问题后,再烧写到存储介质里。省去了反复烧写的麻烦。

最后说一句:DTB加载看似简单,但每个环节都可能出问题。SPL阶段内存不够、存储介质坏块、FIT镜像格式错误……这些坑我都踩过。嗯,理解了整个流程,遇到问题就知道从哪里下手排查了。