📺 B站 嵌入式孙老师:博主个人介绍
📘 博主书籍-京东购买链接:Yocto项目实战教程
最近在分析 RK3588 U-Boot 启动代码时,我注意到 board_init() 中有这样一行:
#ifdef CONFIG_USING_KERNEL_DTB
init_kernel_dtb();
#endif
继续跟踪后,我发现它并不是简单地“初始化一下 Kernel DTB”,而是完成了一次重要的设备树切换。

这也解释了几个容易困惑的问题:
- U-Boot 和 Kernel 明明各有一份 DTB,为什么 U-Boot 还要加载 Kernel DTB?
init_kernel_dtb()执行后,两份 DTB 是否变成了一份?- U-Boot 自己的 DTB 还有没有作用?
- Kernel DTB 中有一个节点,U-Boot 就一定会配置这个设备吗?
- 为什么设备树中已经有 GPIO 的 pinctrl,实际引脚功能却可能还没有生效?
本文以 RK3588 厂商版 U-Boot 为例,围绕 init_kernel_dtb() 把这条执行链整理清楚。
一、为什么U-Boot还要使用Kernel DTB
1. U-Boot和Kernel原本各有一份DTB
U-Boot 和 Linux Kernel 是两个独立的软件工程,各自编译自己的设备树。
| 对比项 | U-Boot DTB | Kernel DTB |
|---|---|---|
| 源码位置 | u-boot/arch/arm/dts/ | kernel/arch/arm64/boot/dts/rockchip/ |
| 主要作用 | 完成启动和早期自举 | 描述完整板级硬件 |
| 典型设备 | UART、时钟、eMMC、SD、启动按键 | PMIC、显示、音频、RTC、触摸等 |
| 使用驱动 | U-Boot驱动 | Linux驱动 |
| 内容规模 | 通常较精简 | 通常较完整 |
传统做法是两边分别维护完整的板级 DTS。
问题也很直接:同一块硬件被描述了两遍。
例如背光改用了 PWM6,如果只修改 Kernel DTS,没有同步修改 U-Boot DTS,就可能出现:
U-Boot使用PWM1
Kernel使用PWM6
PMIC、GPIO 极性、regulator 关系和显示路由也可能出现类似问题。
Rockchip 厂商版 U-Boot 因此提供了:
CONFIG_USING_KERNEL_DTB
启用后,U-Boot 前期使用自己的精简 DTB,等存储可以访问后,再加载 Kernel DTB,用于后续板级设备初始化。
2. 为什么不从上电开始就使用Kernel DTB
Kernel DTB 通常存放在:
resource.img
boot分区
FIT镜像
想读取 Kernel DTB,U-Boot 必须先初始化 eMMC、SD 或 SPI Flash。
这形成了一个自举关系:
读取Kernel DTB
↑
需要访问存储
↑
初始化存储又需要设备树
所以,U-Boot 必须先使用自己的 DTB 初始化:
- DDR;
- UART;
- 时钟和复位;
- eMMC、SD、SPI Flash;
- 启动按键。
等这些基础功能工作后,才能读取 Kernel DTB。
两份 DTB 的分工可以概括为:
U-Boot DTB
负责让系统运行到init_kernel_dtb()
↓
Kernel DTB
负责后续完整板级硬件描述
3. 两份DTB实际差异有多大
可以通过构建目录中的 .dts.tmp 文件直接检查。
例如查找 U-Boot 预处理后的设备树:
find . -iname '.rk3588-custom-board.dtb.dts.tmp'
.dts.tmp 是 DTS 展开 include、宏和条件编译后的完整文本:
板级.dts
↓
展开#include和宏
↓
.<board>.dtb.dts.tmp
↓
dtc编译
↓
<board>.dtb
我对比了一块 RK3588 定制板的两份 .dts.tmp:
| 项目 | U-Boot设备树 | Kernel设备树 |
|---|---|---|
| 展开后行数 | 约7000行 | 约15600行 |
u-boot,dm-spl/pre-reloc | 约50处 | 基本没有 |
status = "okay" | 约28处 | 约164处 |
| 主要内容 | 启动骨架 | 完整板级硬件 |
U-Boot DTB 中有大量:
u-boot,dm-spl;
u-boot,dm-pre-reloc;
例如:
chosen {
u-boot,spl-boot-order =
&sdmmc, &sdhci, &spi_nand, &spi_nor;
};
&sdhci {
u-boot,dm-spl;
};
&sdmmc {
u-boot,dm-spl;
};
而 Kernel DTB 中包含更多实际板级设备:
pmic@0 {
compatible = "rockchip,rk806";
};
charger@6b {
compatible = "ti,bq25703";
reg = <0x6b>;
};
backlight: backlight {
compatible = "pwm-backlight";
};
rtc@51 {
compatible = "haoyu,hym8563";
};
codec@11 {
compatible = "everest,es8388";
};
这说明两份 DTB 并不相同:
U-Boot DTB主要描述“怎么启动”,Kernel DTB主要描述“这块板子有什么完整硬件”。
二、init_kernel_dtb()是怎么完成切换的
1. 它在什么阶段执行
Rockchip U-Boot 的 board_init() 中包含:
int board_init(void)
{
board_debug_init();
#ifdef CONFIG_OPTEE_CLIENT
optee_client_init();
#endif
#ifdef CONFIG_USING_KERNEL_DTB
init_kernel_dtb();
#endif
early_download();
clks_probe();
#ifdef CONFIG_DM_REGULATOR
regulators_enable_boot_on(
is_hotkey(HK_REGULATOR));
#endif
return rk_board_init();
}
调用顺序非常关键:
U-Boot完成早期自举
↓
init_kernel_dtb()
↓
clks_probe()
↓
regulators_enable_boot_on()
↓
后续板级设备初始化
init_kernel_dtb() 发生在 U-Boot proper 的 board_init() 阶段。
此时存储已经可以访问,但大量板级设备还没有完成初始化,因此正好可以切换到更完整的 Kernel DTB。
2. 第一步:确定Kernel DTB加载地址
函数首先获取 DTB 的内存地址:
ulong fdt_addr = 0;
if (gd->ram_size <= SZ_128M)
fdt_addr = env_get_ulong(
"fdt_addr1_r", 16, 0);
if (!fdt_addr)
fdt_addr = env_get_ulong(
"fdt_addr_r", 16, 0);
if (!fdt_addr) {
printf("No Found FDT Load Address.\n");
return -ENODEV;
}
正常情况下使用:
fdt_addr_r
如果内存不超过 128MB,则优先使用:
fdt_addr1_r
这一步只是为 Kernel DTB 找到一块加载内存。
3. 第二步:从启动镜像读取Kernel DTB
接下来执行:
ret = rockchip_read_dtb_file(
(void *)fdt_addr);
这个函数会从实际启动镜像中查找 Kernel DTB,可能的来源包括:
enum {
LOCATE_DISTRO,
LOCATE_RESOURCE,
LOCATE_FIT,
LOCATE_END,
};
对应:
| 来源 | 说明 |
|---|---|
LOCATE_DISTRO | 从可启动文件系统读取DTB |
LOCATE_RESOURCE | 从resource.img读取DTB |
LOCATE_FIT | 从FIT镜像读取FDT |
读取成功后,Kernel DTB 的完整内容已经被放到:
fdt_addr
对应的内存中。
启动日志通常会出现:
DM: v2
DTB: rk3588-custom-board-linux.dtb
其中 DTB: 后面的文件名,才是 U-Boot 实际加载的 Kernel DTB。
4. 第三步:检查DTB是否匹配
读取成功后会调用:
if (!dtb_check_ok((void *)fdt_addr,
(void *)gd->fdt_blob)) {
ret = -EINVAL;
printf("Kernel dtb mismatch this platform!\n");
}
这里的两个参数分别是:
fdt_addr → 刚加载的Kernel DTB
gd->fdt_blob → 当前U-Boot DTB
设计目的是比较两份 DTB 的根节点 compatible,避免加载错误的板级 DTB。
但在部分 SDK 版本中,这个函数仍然是:
static int dtb_check_ok(void *kfdt, void *ufdt)
{
/* TODO */
return 1;
}
也就是说,当前可能并没有真正完成平台匹配检查。
因此,实际调试时仍要检查:
启动日志中的DTB文件名
Kernel DTS的include关系
最终打包的DTB
PMIC和板级硬件是否匹配
5. 读取失败时使用内嵌DTB
如果从存储读取失败,并且启用了:
CONFIG_EMBED_KERNEL_DTB
U-Boot 会尝试使用内嵌的 Kernel DTB:
if (gd->fdt_blob_kern) {
fdt_addr = (ulong)memalign(
ARCH_DMA_MINALIGN,
fdt_totalsize(
gd->fdt_blob_kern));
memcpy((void *)fdt_addr,
gd->fdt_blob_kern,
fdt_totalsize(
gd->fdt_blob_kern));
}
不论 DTB 来自存储还是内嵌资源,最终都会整理到 fdt_addr 指向的内存区域。
如果两种方式都失败,则退出:
printf("Failed to get kernel dtb, ret=%d\n",
ret);
return -ENOENT;
6. 第四步:真正完成设备树切换
读取成功后执行:
dtb_okay:
gd->fdt_blob = (void *)fdt_addr;
这是整个函数中最关键的一行。
执行前:
gd->fdt_blob → U-Boot自身DTB
执行后:
gd->fdt_blob → Kernel DTB
这里没有把两份 DTB 合并,也没有修改 U-Boot 镜像中的 DTB,只是改变了当前设备树指针。
所以准确的说法是:
init_kernel_dtb()没有让两份DTB变得一样,而是让U-Boot从此主要读取Kernel DTB。
7. 第五步:重建Live Tree和Driver Model
只切换 gd->fdt_blob 还不够。
U-Boot 在前期已经根据自己的 DTB 创建了一部分设备对象,所以还要执行:
gd->of_root_f = gd->of_root;
of_live_build(
(void *)gd->fdt_blob,
(struct device_node **)&gd->of_root);
dm_scan_fdt(
(void *)gd->fdt_blob, false);
其中:
of_live_build();
负责根据新的 Kernel DTB 重建 Live Device Tree。
dm_scan_fdt();
负责扫描新设备树,并根据 compatible 绑定对应的 U-Boot 驱动。
整体关系是:
加载Kernel DTB
↓
切换gd->fdt_blob
↓
重建Live Device Tree
↓
扫描Kernel DTB中的节点
↓
绑定对应的U-Boot驱动
8. V2如何处理重复设备
如果启用了:
CONFIG_USING_KERNEL_DTB_V2
还会执行:
dm_rm_kernel_dev();
dm_rm_u_boot_dev();
因为此时可能同时存在:
根据U-Boot DTB创建的早期设备
根据Kernel DTB创建的后期设备
UART、eMMC、GPIO、时钟等设备可能重复,因此需要清理无效或者冲突的实例。
V2 的思路可以简化为:
保留必要的早期自举设备
加入Kernel DTB中的板级设备
清理重复设备
9. 为什么MMC需要重新初始化
随后执行:
mmc_dm_reinit();
Kernel DTB 本身就是通过 eMMC 或 SD 读取的,所以 MMC 在切换前已经工作。
但切换后,MMC 引用的时钟、PHY 或其他依赖设备可能变成了 Kernel DTB 中的新实例,因此需要重新整理绑定关系。
MMC先根据U-Boot DTB启动
↓
读取Kernel DTB
↓
时钟或PHY关系变化
↓
mmc_dm_reinit()
10. 最后处理保留内存
函数最后执行:
ret = boot_fdt_add_sysmem_rsv_regions(
(void *)gd->fdt_blob);
它会读取 Kernel DTB 中的:
reserved-memory {
...
};
将 Logo framebuffer、OP-TEE 或其他特殊内存区域加入 U-Boot 的保留内存管理,避免后续被普通内存分配覆盖。
至此,Kernel DTB 才算完成加载和接管。
三、节点存在,为什么硬件配置还可能没有生效
1. dm_scan_fdt()不等于所有设备已经probe
这是理解设备树切换时非常关键的一点。
dm_scan_fdt();
主要完成设备扫描和绑定,但通常不会立即 probe 所有设备。
整个过程需要区分:
DTB中存在节点
↓
Driver Model完成bind
↓
设备真正被使用
↓
执行probe
↓
应用pinctrl、时钟和GPIO
因此:
DTB中有这个节点
≠
对应硬件已经配置完成
2. 一个实际例子:背光pinctrl何时生效
Kernel DTB 中可能有:
&backlight {
pwms = <&pwm6 0 1000000 0>;
enable-gpios = <&gpio4 0 0>;
pinctrl-names = "default";
pinctrl-0 = <&backlight_en>;
brightness-levels =
<0 1 2 3 4 5 6 7 8 9 10>;
default-brightness-level = <8>;
status = "okay";
};
&pwm1 {
status = "disabled";
};
&pwm6 {
status = "okay";
pinctrl-names = "active";
pinctrl-0 = <&pwm6m1_pins>;
};
对应的 GPIO pinctrl 是:
backlight_en: backlight-en {
rockchip,pins =
<4 0 0 &pcfg_pull_none>;
};
其含义是:
GPIO Bank:4
Pin:0,即GPIO4_A0
复用功能:0,即普通GPIO
上下拉:不配置
执行 init_kernel_dtb() 后,这些节点已经存在于 gd->fdt_blob 指向的 Kernel DTB 中。
但是,GPIO4_A0 的 IOMUX 不一定已经立即写入硬件寄存器。
只有 Backlight 设备真正 probe,并执行类似:
pinctrl_select_state();
后,backlight_en 状态才会实际应用。
因此,如果在 Backlight probe 之前直接操作 GPIO4_A0,可能出现:
Kernel DTB中已经有backlight_en
但GPIO4_A0仍未切换成普通GPIO功能
这不是 DTB 缺少配置,而是执行时序问题。
3. Charge IC为什么必须在切换之后初始化
同样,Kernel DTB 中可能存在:
charger@6b {
compatible = "ti,bq25703";
reg = <0x6b>;
status = "okay";
};
但 U-Boot 自身 DTB 中没有这个节点。
如果 U-Boot 驱动通过下面的代码寻找设备:
const void *blob = gd->fdt_blob;
node = fdt_node_offset_by_compatible(
blob, 0, "ti,bq25703");
if (node < 0)
return -ENODEV;
那么初始化必须放在:
init_kernel_dtb();
之后:
#ifdef CONFIG_USING_KERNEL_DTB
init_kernel_dtb();
#endif
#ifdef CONFIG_CHARGER_BQ25700
bq25700_charger_preinit();
#endif
否则执行过程会变成:
gd->fdt_blob仍指向U-Boot DTB
↓
找不到ti,bq25703节点
↓
驱动probe失败
↓
不会写入芯片寄存器
正确过程则是:
init_kernel_dtb()
↓
gd->fdt_blob切换到Kernel DTB
↓
找到ti,bq25703节点
↓
U-Boot驱动完成probe
↓
配置芯片寄存器
4. 使用Kernel DTB不等于使用Linux驱动
设备树只是硬件描述,真正操作硬件的仍然是 U-Boot 驱动。
U-Boot阶段:
Kernel DTB
→ U-Boot Driver Model
→ U-Boot驱动
→ 操作硬件
进入 Linux 后:
Kernel DTB
→ Linux Device Model
→ Linux驱动
→ 重新管理硬件
所以:
使用同一份Kernel DTB
≠
使用同一份驱动
一个属性要在 U-Boot 中生效,必须同时满足:
Kernel DTB中存在节点
+
U-Boot中存在对应驱动
+
驱动认识相关属性
+
设备在正确时间完成probe
总结
重新看 board_init() 中的代码:
#ifdef CONFIG_USING_KERNEL_DTB
init_kernel_dtb();
#endif
它做的并不是简单初始化,而是把 U-Boot 从早期自举阶段带入完整板级设备初始化阶段。
执行之前:
U-Boot使用自己的精简DTB
初始化DDR、UART、时钟和存储
执行过程中:
获取fdt_addr_r
↓
读取Kernel DTB
↓
gd->fdt_blob切换
↓
重建Live Tree
↓
重新扫描Driver Model
↓
清理重复设备
↓
修复MMC依赖
↓
处理reserved-memory
执行之后:
U-Boot后续驱动主要读取Kernel DTB
按需probe PMIC、regulator、显示和其他板级设备
两份 DTB 不会因此变得一样:
U-Boot DTB
负责走到init_kernel_dtb()
Kernel DTB
负责init_kernel_dtb()之后的完整板级描述
而理解整个机制最关键的三行代码是:
gd->fdt_blob = (void *)fdt_addr;
of_live_build((void *)gd->fdt_blob,
(struct device_node **)&gd->of_root);
dm_scan_fdt((void *)gd->fdt_blob, false);
它们分别完成:
切换设备树数据源
重建Live Device Tree
让Driver Model认识新设备
最后还要记住:
DTB中存在节点,只代表硬件已经被描述;设备完成probe之后,pinctrl、时钟和GPIO等配置才会真正应用到硬件。
869

被折叠的 条评论
为什么被折叠?



