尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

RK3588 U-Boot 为什么要执行 `init_kernel_dtb()`?一次设备树切换机制的实战总结 B站 嵌入式孙老师博主个人介绍博主书籍-京东购买链接Yocto项目实战教程最近在分析 RK3588 U-Boot 启动代码时我注意到board_init()中有这样一行#ifdefCONFIG_USING_KERNEL_DTBinit_kernel_dtb();#endif继续跟踪后我发现它并不是简单地“初始化一下 Kernel DTB”而是完成了一次重要的设备树切换。这也解释了几个容易困惑的问题U-Boot 和 Kernel 明明各有一份 DTB为什么 U-Boot 还要加载 Kernel DTBinit_kernel_dtb()执行后两份 DTB 是否变成了一份U-Boot 自己的 DTB 还有没有作用Kernel DTB 中有一个节点U-Boot 就一定会配置这个设备吗为什么设备树中已经有 GPIO 的 pinctrl实际引脚功能却可能还没有生效本文以 RK3588 厂商版 U-Boot 为例围绕init_kernel_dtb()把这条执行链整理清楚。一、为什么U-Boot还要使用Kernel DTB1. U-Boot和Kernel原本各有一份DTBU-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使用PWM6PMIC、GPIO 极性、regulator 关系和显示路由也可能出现类似问题。Rockchip 厂商版 U-Boot 因此提供了CONFIG_USING_KERNEL_DTB启用后U-Boot 前期使用自己的精简 DTB等存储可以访问后再加载 Kernel DTB用于后续板级设备初始化。2. 为什么不从上电开始就使用Kernel DTBKernel DTB 通常存放在resource.img boot分区 FIT镜像想读取 Kernel DTBU-Boot 必须先初始化 eMMC、SD 或 SPI Flash。这形成了一个自举关系读取Kernel DTB ↑ 需要访问存储 ↑ 初始化存储又需要设备树所以U-Boot 必须先使用自己的 DTB 初始化DDRUART时钟和复位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 中包含更多实际板级设备pmic0 { compatible rockchip,rk806; }; charger6b { compatible ti,bq25703; reg 0x6b; }; backlight: backlight { compatible pwm-backlight; }; rtc51 { compatible haoyu,hym8563; }; codec11 { compatible everest,es8388; };这说明两份 DTB 并不相同U-Boot DTB主要描述“怎么启动”Kernel DTB主要描述“这块板子有什么完整硬件”。二、init_kernel_dtb()是怎么完成切换的1. 它在什么阶段执行Rockchip U-Boot 的board_init()中包含intboard_init(void){board_debug_init();#ifdefCONFIG_OPTEE_CLIENToptee_client_init();#endif#ifdefCONFIG_USING_KERNEL_DTBinit_kernel_dtb();#endifearly_download();clks_probe();#ifdefCONFIG_DM_REGULATORregulators_enable_boot_on(is_hotkey(HK_REGULATOR));#endifreturnrk_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_addr0;if(gd-ram_sizeSZ_128M)fdt_addrenv_get_ulong(fdt_addr1_r,16,0);if(!fdt_addr)fdt_addrenv_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接下来执行retrockchip_read_dtb_file((void*)fdt_addr);这个函数会从实际启动镜像中查找 Kernel DTB可能的来源包括enum{LOCATE_DISTRO,LOCATE_RESOURCE,LOCATE_FIT,LOCATE_END,};对应来源说明LOCATE_DISTRO从可启动文件系统读取DTBLOCATE_RESOURCE从resource.img读取DTBLOCATE_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 版本中这个函数仍然是staticintdtb_check_ok(void*kfdt,void*ufdt){/* TODO */return1;}也就是说当前可能并没有真正完成平台匹配检查。因此实际调试时仍要检查启动日志中的DTB文件名 Kernel DTS的include关系 最终打包的DTB PMIC和板级硬件是否匹配5. 读取失败时使用内嵌DTB如果从存储读取失败并且启用了CONFIG_EMBED_KERNEL_DTBU-Boot 会尝试使用内嵌的 Kernel DTBif(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_fgd-of_root;of_live_build((void*)gd-fdt_blob,(structdevice_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. 最后处理保留内存函数最后执行retboot_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 Bank4 Pin0即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 中可能存在charger6b { compatible ti,bq25703; reg 0x6b; status okay; };但 U-Boot 自身 DTB 中没有这个节点。如果 U-Boot 驱动通过下面的代码寻找设备constvoid*blobgd-fdt_blob;nodefdt_node_offset_by_compatible(blob,0,ti,bq25703);if(node0)return-ENODEV;那么初始化必须放在init_kernel_dtb();之后#ifdefCONFIG_USING_KERNEL_DTBinit_kernel_dtb();#endif#ifdefCONFIG_CHARGER_BQ25700bq25700_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()中的代码#ifdefCONFIG_USING_KERNEL_DTBinit_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,(structdevice_node**)gd-of_root);dm_scan_fdt((void*)gd-fdt_blob,false);它们分别完成切换设备树数据源 重建Live Device Tree 让Driver Model认识新设备最后还要记住DTB中存在节点只代表硬件已经被描述设备完成probe之后pinctrl、时钟和GPIO等配置才会真正应用到硬件。
返回列表