RK3588 U-Boot 为什么要执行 `init_kernel_dtb()`?一次设备树切换机制的实战总结


📺 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 DTBKernel 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_RESOURCEresource.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等配置才会真正应用到硬件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值