4、U-Boot设备树加载流程:SPL阶段加载、U-Boot proper阶段加载、从存储介质读取DTB
设备树(DTB)到底是怎么从存储介质跑到内存里去的?这个问题,我当年刚接触U-Boot时也困惑了很久。说白了,整个加载过程分三个阶段:SPL、U-Boot proper,以及最终的OS启动。每个阶段对设备树的态度都不一样。
嗯,咱们今天就把这条链路彻底捋清楚。
4.1 整体加载链路概览
先看一张总图,心里有个框架。
你看,整个流程其实不复杂。但每个阶段的细节,坑不少。
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的代码路径,通常在 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都放不下。
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,系统直接挂掉。
从存储介质读取DTB的通用流程:
- 确定DTB的存储位置:分区号、偏移地址、文件名
- 调用底层驱动读取数据:mmc_read() / nand_read() / spi_flash_read()
- 数据校验:检查DTB魔数(0xD00DFEED),可选CRC校验
- 重定位到目标地址:通常拷贝到
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的启动速度,又保留了设备树的灵活性。
最后说一句:DTB加载看似简单,但每个环节都可能出问题。SPL阶段内存不够、存储介质坏块、FIT镜像格式错误……这些坑我都踩过。嗯,理解了整个流程,遇到问题就知道从哪里下手排查了。