☰
RK3576启动流程详解:从BootROM到Linux根文件系统的完整链路
2026/10/1 16:43:25 网站建设 项目流程

手里拿到一块 RK3576 开发板,第一次上电,串口里一片空白,只有电源灯在闪。这个场景我遇到过太多次。如果是 x86 主机,至少还有 BIOS 自检画面可以看,但嵌入式 Linux 板卡在 BootROM、SPL、U-Boot 阶段出了问题,终端就是死寂一片。没有日志、没有提示、没有任何可以交互的界面。

RK3576 是瑞芯微面向 AIoT 和边缘计算场景推出的一颗高性能 SoC,ARM 架构,带 6 TOPS 算力的 NPU,不少工业 HMI、智能网关、边缘盒子都在用它。它的启动流程和 RK 之前的 3288/3399/3568 一脉相承,又因为引入了更灵活的 TPL/SPL 拆分和更复杂的镜像打包方式,让不少刚转过来的开发者栽了跟头。

这篇文章把 RK3576 从片上 BootROM 到 Linux 根文件系统挂载的完整启动链路掰开揉碎讲一遍,重点放在每一级 bootloader 的职责边界、镜像格式、传递参数的方式,以及我实际调试时踩过的一些坑。适合刚接触 RK 平台的嵌入式工程师、正在做 BSP 移植的同事,以及想搞明白“按下电源键之后到底发生了什么”的好奇者。

1. 先看懂整条启动链路:从复位向量到根文件系统

RK3576 的上电启动路径可以分成四个大阶段:BootROM(固化在芯片内部)、SPL(Secondary Program Loader)、U-Boot 主引导、Linux 内核与根文件系统。每一级都是前一级的“放大镜”——BootROM 只负责把一小段代码从存储介质搬到 SRAM,SPL 负责初始化 DDR 并把主 U-Boot 装入内存,U-Boot 负责加载内核和设备树,最后内核挂载 rootfs 交出控制权。

这个过程如果画成图,大概长这样:

上电复位 │ ├─ BootROM(芯片内部,只读) │ 从 EMMC/SD/NOR/NAND 扫描启动介质 │ 校验 ID Block,加载 SPL 到 SRAM │ 跳转 SPL │ ├─ SPL(位于 SRAM 中运行) │ 初始化时钟、DDR 控制器 │ 加载主 U-Boot(trust + uboot)到 DDR │ 跳转 U-Boot │ ├─ U-Boot 主引导(运行在 DDR) │ 初始化外设、驱动、环境变量 │ 根据 bootcmd 从存储/网络加载 Image 与 DTB │ 设置 bootargs,跳转内核 │ └─ Linux 内核 解压/启动、初始化设备驱动 挂载 rootfs(initramfs 或真机存储分区) 执行 /sbin/init,进入用户空间

这个拆分设计的核心逻辑是“容量”和“复杂度”的矛盾。BootROM 固化在芯片里,只有几十到几百 KB 的代码空间,不可能内置完整的 MMC、DDR 控制器驱动;而 U-Boot 主引导又需要至少几 MB 的内存空间才能跑起来,DDR 没初始化之前 CPU 只能在 SRAM 里执行代码。所以中间塞了一个 SPL 做“桥梁”,先把 DDR 点亮,再让完整版 U-Boot 在大内存里施展拳脚。

从我实际调试的感受来说,理解这条链路的最大价值不在于背流程,而在于知道“哪一级坏了该看什么现象”。BootROM 阶段出问题,串口完全没有输出;SPL 阶段出问题,通常有部分打印但卡在 DDR 初始化;U-Boot 阶段出问题,能看到 U-Boot logo 或命令行但进不了内核;内核阶段出问题,串口已经有大量内核日志但最后停在某个驱动上。定位问题的时候,先看卡在哪一级,比闷头翻代码高效得多。

2. 准备工作:交叉编译环境与 Rockchip SDK 基础

2.1 工具链选择:aarch64 交叉编译

RK3576 是 64 位 ARM 处理器,所有跑在它上面的软件——SPL、U-Boot、内核——都需要用 aarch64 交叉编译器构建。我常用的是 ARM 官方提供的aarch64-none-linux-gnu-工具链,或者 Linaro 的aarch64-linux-gnu-。在实际项目中,Rockchip 的 SDK 通常已经锁定了一个特定版本的交叉编译器,放在 SDK 的prebuilts/gcc/linux-x86/aarch64/目录下。

这里有一个非常实际的建议:优先使用 SDK 自带的工具链,而不是系统包管理器装的最新版。RK 的 BSP 代码经过厂商测试,编译器版本和内核、U-Boot 的兼容性已经验证过。我踩过用系统自带 GCC 12 编译老内核导致 LTO 链接报错的坑,最后换回 SDK 目录里的 GCC 10 才顺利通过。

环境变量建议这样设置:

export CROSS_COMPILE=aarch64-linux-gnu- export ARCH=arm64 export PATH=/path/to/prebuilts/gcc/linux-x86/aarch64/gcc-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH

2.2 SDK 目录结构与镜像输出

Rockchip 的 SDK(比如 rk3576_linux_sdk)把 U-Boot、内核、rootfs、打包工具分散在几个独立目录里。U-Boot 是独立的 git 仓库,内核也是独立的仓库,外层的 SDK 脚本只负责把它们串起来编译。

需要关注的几个关键产物:

产物来源作用
idbloader.imgU-Boot 编译产物 + DDR 微码BootROM 要加载的 SPL 打包镜像
u-boot.itbU-Boot 编译产物FIT 格式的主 U-Boot 镜像,含 ATF、U-Boot
boot.imgAndroid 结构内核 + DTB + ramdisk 的打包
rootfs.imgrootfs 构建根文件系统镜像

注意idbloader.img这个名字,它就是 BootROM 眼中的“SPL”。Rockchip 平台和很多其他 SoC 平台不一样的地方在于,SPL 不是单独编译出来的,而是由 U-Boot 的 TPL/SPL 机制生成,再配合闭源的 DDR 初始化二进制微码(ddr.bin)一起打包。这也是新人最容易困惑的地方:我在 U-Boot 目录里明明编出了u-boot-spl.bin,为什么烧录时用的是idbloader.img?

答案很简单:idbloader.img包含了u-boot-tpl.bin(DDR 初始化之前的极小引导)+ DDR 微码 +u-boot-spl.bin(DDR 初始化之后的 SPL),通过 Rockchip 的打包脚本整合成一个文件,BootROM 只需要认这一个文件就够了。

2.3 烧录工具:从 MaskROM 到板端烧写

开发调试阶段最常用的烧录方式是 RK 的 MaskROM 模式。操作方法:按住板子上的恢复按键(RECOVERY),先按复位或重新上电,此时 BootROM 检测到强制下载请求,进入 USB 下载模式。PC 端用rkdeveloptool或 Rockchip 的upgrade_tool就可以识别设备并烧录。

# 烧录 SPL sudo rkdeveloptool db idbloader.img # 烧录主 U-Boot sudo rkdeveloptool wl 64 u-boot.itb

这里db表示 download boot,先把一个最小引导跑起来;wl表示 write to LBA,按扇区偏移写入 EMMC。偏移 64 是 Rockchip 约定的主 U-Boot 存放位置,前 64 个扇区留给 idbloader 和分区表。

这些工具有个共同脾气:要求 USB 连接稳定,虚拟机环境下尤其容易出问题。我的经验是优先用物理机,或者给虚拟机直通 USB 控制器,否则经常出现擦写到一半设备掉线、EMMC 分区表损坏的“欢喜大结局”。

3. BootROM 阶段:芯片自己怎么找饭吃

3.1 BootROM 的启动介质扫描顺序

RK3576 上电后,CPU 从芯片内部固化的 BootROM 开始执行。这段代码不可修改,烧录在芯片的 ROM 里,作用非常纯粹:初始化最基础的外设接口,然后按照预设的优先级去扫描可启动介质,找到一份合法的引导代码,加载到 SRAM 里执行。

扫描顺序大致是:EMMC 的 boot 分区、SD 卡、SPI NOR Flash、SPI NAND Flash。具体顺序取决于芯片的 OTP 或上下拉电阻配置,但默认情况下 EMMC 的优先级最高。这意味着如果 EMMC 里有一个损坏的引导代码,BootROM 很可能不会自动降级到 SD 卡启动,而是卡在那里反复尝试。很多人的 SD 卡怎么也启动不了,查到最后发现是 EMMC 里残留了一份半残的镜像。

BootROM 加载的并不是“整个 SPL”,而是一个固定大小的“引导块”,Rockchip 称之为 ID Block。这个块的大小是固定的,对 RK 平台来说通常是 4KB 对齐的若干扇区,里面包含一个头部结构,记录了镜像大小、校验值、加载地址等信息。BootROM 会把 ID Block 读到 SRAM 中,校验通过后跳转执行。

这里有个关键点:BootROM 加载的是 SPL,而 SPL 的输出是经过idbloader.img打包的。BootROM 不认识u-boot-spl.bin这个裸文件,只认符合 Rockchip ID Block 格式的打包镜像。

3.2 强制下载模式:开发者的救命通道

如果 EMMC、SD 卡、Flash 里都没有合法的引导代码,BootROM 会进入一个特殊的下载模式,也就是前文提到的 MaskROM 模式。在这个模式下,BootROM 不做正常的启动扫描,而是枚举 USB/串口等接口,等待主机端下发数据。

对这个模式,我用一句话概括:它是 RK 平台的“BIOS 恢复盘”,哪怕板子上的 bootloader 已经彻底损坏,只要 SoC 本身没烧,按住恢复键重新上电就能把手伸进芯片肚子里,重写一切。

实际调试中我经常这么用:把编译好的 idbloader 先用rkdeveloptool db临时加载并执行,它初始化 DDR 之后,再用rkdeveloptool wl把完整镜像写进 EMMC。这比每次都进 MaskROM 模式烧全量镜像快很多。

3.3 安全启动对 BootROM 行为的影响

RK3576 支持安全启动链。如果芯片烧写了安全 BootROM 配置和公钥,那么 BootROM 在加载 ID Block 时会校验其签名,签名不合法直接拒绝执行。这个机制会改变整个启动链路的调试方式——不能再随便用自己编译的 SPL,必须保证镜像经过签名。

这在量产产品里是个常规要求,防止固件被篡改或替换。但在开发板上,默认是不开启安全启动的,所以我们可以随意烧自己编译的 U-Boot。如果你的板子在量产阶段开了安全启动,一定要把签名流程集成到 CI 或编译脚本里,否则每次改完代码都要手动跑一次签名工具,很容易出错。

4. SPL:点亮 DDR 的“临时代理”

4.1 SPL 的职责边界:不止是 DDR 初始化

BootROM 跳转到 SPL 之后,SPL 要完成的第一个任务就是初始化 DDR 控制器。在 DDR 起来之前,整个芯片只有 SRAM 可用,容量非常小。RK3576 的内部 SRAM 只有几百 KB,跑一个完整的 U-Boot 是痴心妄想,所以 SPL 必须精打细算地在 SRAM 里完成 DDR 初始化、读取主 U-Boot、跳转这三件事。

SPL 跑的代码是 U-Boot 项目用CONFIG_SPL_BUILD配置裁剪出来的,它本质上还是 U-Boot 的一部分,只是去掉了很多不需要的驱动和命令行功能。RK3576 的 SPL 代码在arch/arm/mach-rockchip/目录下,DDR 初始化的核心逻辑则来自 Rockchip 提供的 DDR 驱动和闭源二进制微码。

DDR 初始化本身是个精细活。需要设置控制器的工作频率、时序参数、驱动强度、ZQ 校准等。RK 的做法是把不同频率、不同 DDR 颗粒型号的参数编译进一个二进制微码(ddr.bin)里,SPL 启动时加载这个微码并调用其中的函数来完成具体的初始化流程。这也是为什么 RK 的平台移植 DDR 颗粒型号时,往往需要更换或重新生成ddr.bin,而不是去改几行源码就能搞定。

4.2 为什么需要 TPL:拆开看 Rockchip 的三段式引导

RK3576 的引导链其实是三段式的:BootROM 加载 TPL,TPL 初始化 DDR 并加载 SPL,SPL 再加载主 U-Boot。为什么要把“DDR 初始化”和“加载主 U-Boot”拆成两个独立阶段?

原因是 BootROM 能加载的镜像大小限制太苛刻。TPL 是一个比 SPL 更小的引导程序,足够完成 DDR 点亮这么一件事,占用空间极小。TPL 运行在 SRAM 中,自身的代码量控制在几十 KB 级别,DDR 起来之后它再从存储介质读取更大的 SPL 到 DDR 中运行。SPL 有了 DDR 的空间支撑,就能加载更复杂的 FIT 镜像,包括 ATF(ARM Trusted Firmware)和主 U-Boot。

当你在 U-Boot 的编译配置里同时看到 TPL 和 SPL 两个选项时,不要觉得多余,这是 Rockchip 在极限空间条件下的工程妥协。STM32MP1 等平台就不搞 TPL,BootROM 直接加载 SPL,因为它们的 SRAM 更大、BootROM 支持的加载大小更宽裕。RK3576 选择三段式,背后是成本和功耗的综合考量。

4.3 SPL 阶段的镜像打包与 DDR 微码

SDK 里的打包脚本通常叫tools/mkimage配合 Rockchip 专有的打包逻辑生成idbloader.img。如果你只想单独编译 U-Boot 而不想动整个 SDK,可以单独进 U-Boot 目录执行:

make rk3576_defconfig ARCH=arm CROSS_COMPILE=aarch64-linux-gnu- make ARCH=arm CROSS_COMPILE=aarch64-linux-gnu-

编译完成后,在 U-Boot 目录里能找到u-boot-tpl.bin、u-boot-spl.bin等产物。再通过 SDK 的打包脚本或者手动用loaderimage工具将 TPL、DDR 微码、SPL 合并成idbloader.img。

这里我特别提醒一点:ddr.bin和 TPL 的匹配关系。不同的 DDR 类型(DDR4、DDR5、LPDDR4、LPDDR5)、不同的频率等级,对应的微码是不同的。你在 SDK 里看到多个ddr相关的 bin 文件,如果选错或者打包顺序不对,现象通常是 SPL 串口上打印了U-Boot SPL字样,但在 DDR 初始化阶段就卡住,没有任何后续输出。排查这类问题,第一步就是确认 DDR 微码选型对没对。

5. U-Boot 主引导:真正的操作系统加载器

5.1 主 U-Boot 的启动配置与设备树

SPL 把主 U-Boot 加载到 DDR 后,跳转到主 U-Boot 的入口。此时 U-Boot 有了充足的内存空间,可以初始化完整的驱动栈:显示控制器、网络、USB、存储、文件系统等。

主板相关的配置主要在 U-Boot 的板级目录下,RK3576 的 EVB 配置对应configs/rk3576-evb.config。设备树源文件在arch/arm/dts/rk3576-evb.dts,它描述了板子的硬件拓扑,包括 DDR 类型、SD 卡/ EMMC / USB 控制器、SDIO WiFi、以太网 PHY 等。

U-Boot 启动到命令行之后,最核心的两个环境变量是bootcmd和bootargs。bootcmd定义了“从哪读内核、怎么读、读完后执行什么命令”;bootargs定义了“内核启动后怎么挂文件系统、用什么参数初始化控制台”。

我调试时习惯在 U-Boot 命令行里手动拆解bootcmd的每一步,而不是直接执行整个宏。比如默认的bootcmd可能是:

mmc dev 0; mmc read ${kernel_addr_r} 0x4000 0x2000; bootm ${kernel_addr_r}

意思很清楚:切换到 EMMC(设备 0),从扇区偏移 0x4000 开始读 0x2000 个扇区到内存kernel_addr_r,然后启动内核。手动执行时,我会把这三条命令拆开,分步确认设备是否识别、读取是否成功、读出的数据是不是内核镜像。

5.2 FIT 镜像:从传统 bootm 到 booti 的演进

RK3576 平台的 U-Boot 默认采用 FIT(Flattened Image Tree)格式打包固件,主 U-Boot 本身就作为一个 FIT 镜像的一部分存在。FIT 格式用设备树语法描述一组镜像之间的关系,可以同时包含 ATF、U-Boot、DTB,并且支持签名校验。

传统的内核启动方式是用bootm加载 uImage 格式的内核镜像,它把内核、设备树、ramdisk 打包在一起。而现代 ARM64 平台更常用booti直接启动原始的内核镜像Image,配合独立的 DTB 文件:

setenv loadaddr 0x10000000 mmc read ${loadaddr} 0x8000 0x10000 # 读内核 Image mmc read ${fdt_addr_r} 0x1000 0x1000 # 读 DTB setenv bootargs root=/dev/mmcblk0p5 rootwait console=ttyS0,1500000 booti ${loadaddr} - ${fdt_addr_r}

我看到很多新手在 U-Boot 里怎么都启动不了内核,最后发现是地址用错了。kernel_addr_r、fdt_addr_r、ramdisk_addr_r这三个环境变量在启动脚本里经常被修改,但内核镜像和 DTB 的加载地址必须合理:不能重叠,而且要留足内核解压的空间。RK3576 的默认配置里,这些地址通常已经选择好了,不要随意改,除非你明确知道内存布局的约束。

5.3 bootargs:一行参数里的系统工程

bootargs是 U-Boot 传给内核的“启动参数包”,里面包含的内容直接影响内核的启动行为。最核心的几个参数:

参数含义典型值
root=根文件系统的挂载源/dev/mmcblk0p5、/dev/nfs
rootwait等待根设备就绪无参数
console=控制台输出设备ttyS0,1500000
earlycon早期串口输出earlycon=uart8250,mmio32,0xfeb00000
rw/ro根文件系统读写/只读挂载rw

我调试时最常用的组合是console=ttyS0,1500000+earlycon,这样可以在内核早期初始化阶段就从串口看到输出,对定位内核 panic 帮助巨大。另外一个常用参数是init=,它可以直接指定内核启动后的第一个用户进程。默认情况下内核会依次尝试/sbin/init、/etc/init、/bin/init等路径,用init=/bin/sh可以在 rootfs 起不来时直接进入 shell 排查,这是救命的调试手段。

设置 bootargs 有两种方式:一种是在 U-Boot 命令行里用setenv临时设置,适合调试;另一种是把默认 bootargs 编译进 U-Boot 的配置中,适合量产固件。量产的 bootargs 还会加入systemd相关的参数或 Android 的androidboot.*参数,取决于 rootfs 的类型。

5.4 网络启动:没有存储介质时的调试利器

开发阶段最让我省心的启动方式其实是 TFTP 网络启动。只要板子和开发机在同一网段,U-Boot 可以从 TFTP 服务器直接拉内核镜像和设备树,完全不需要反复烧写存储介质。

基本配置如下:

setenv ipaddr 192.168.1.100 # 板子 IP setenv serverip 192.168.1.10 # 开发机 IP setenv netmask 255.255.255.0 setenv bootcmd 'tftp ${kernel_addr_r} Image; tftp ${fdt_addr_r} rk3576-evb.dtb; booti ${kernel_addr_r} - ${fdt_addr_r}' saveenv

网络启动调试内核时,每次修改内核后只需要重启开发机重新拉取镜像,省掉了烧写流程。这对频繁改内核、调驱动的场景帮助巨大。有个小技巧:可以在 TFTP 服务器上放多个不同版本的内核,用软链切换默认版本,实测比反复改 bootcmd 参数更直观。

不过要注意的是,RK3576 U-Boot 的以太网驱动依赖 PHY 的初始化,某些板子的 PHY 需要配置 reset GPIO 才能被正确识别。如果tftp命令一直报Retry count exceeded,先查 PHY 的复位引脚有没有被正确的设备树节点引用,这比怀疑网络线更常见。

6. Linux 内核与根文件系统:从 bootm 到 /sbin/init

6.1 内核镜像的加载与解压

U-Bootbooti命令跳转内核后,ARM64 架构的内核会先在入口处做一段解压和重定位的逻辑。这里有个常见误解:从 U-Boot 加载的Image文件不是直接就能在加载地址执行的,它内部包含了自解压代码。内核会把自己重定位到合适的内存地址,完成 BSS 清零、页表初始化等操作。

如果 U-Boot 传给内核的 DTB 地址不对,或者 DTB 在内存中被后续操作覆盖,内核会在早期阶段就报出FATAL: kernel too old、OF: fdt #size-cells等莫名其妙的错误。这些错误往往不是内核太老,而是设备树地址无效或者内容被破坏。我的排查习惯是:在 U-Boot 里执行fdt addr ${fdt_addr_r}和fdt print /确认 DTB 在内存里能被正确解析,再跳转内核。

6.2 rootfs 的挂载机制选择

Linux 内核启动到驱动初始化完成后,会根据 bootargs 中的root=参数挂载根文件系统。RK3576 平台常见两种方案:

第一种是 initramfs(内嵌 rootfs)。内核镜像里打包一个 cpio 格式的根文件系统,内核启动时自动解压到内存里作为临时的根文件系统。这种方案适合启动早期需要大量驱动的场景,比如需要用复杂驱动去读取真正的根文件系统所在的存储设备。缺点是对内存占用较大,而且改动 rootfs 需要重新打包内核。

第二种是外部存储上的根文件系统。bootargs 中指定root=/dev/mmcblk0p5这类设备节点,内核在 drivers 初始化完成后,通过块设备驱动挂载 ext4、squashfs 等真实文件系统。这是量产产品最常用的方式:rootfs 独立存放在 EMMC 或 SD 卡分区中,内核只需要一个最小的驱动集合就能完成挂载。

实际开发中,我习惯先用 initramfs 验证系统能不能跑起来,等驱动全部稳定后再切到外部 rootfs。这样可以避免为 rootfs 挂载失败的问题反复烧写、反复重启。

6.3 用户空间启动:PID 1 与 systemd

内核完成 rootfs 挂载后,执行用户空间的第一个进程。传统方案是/sbin/init,现在大多数现代发行版直接运行/lib/systemd/systemd作为 PID 1。systemd 会读取单元文件,按依赖关系启动服务,最终拉起业务进程。

这里有一个 RK3576 平台的常见开发场景:根文件系统挂载成功但串口没有终端登录提示。常见原因是getty服务配置的串口设备名不对,没有在/etc/systemd/system/getty.target.wants/下正确创建getty@ttyS0.service的软链。内核和 U-Boot 的 console 用的是ttyS0,但 systemd 不一定默认在ttyS0拉起登录 shell。解决方式是手动创建服务软链,或者在 rootfs 构建阶段就把 getty 服务指向正确的串口。

rootfs 构建这块,RK 的 SDK 通常用 buildroot 或 Yocto 生成。如果是自己手动构建,至少要包含/sbin/init、/bin/busybox(或 systemd 全套)、/etc/inittab(或 systemd 的 system 目录)、/lib下的动态链接器和必要库,以及/dev、/proc、/sys、/tmp这几个关键目录。

7. 常见问题与排查技巧实录

7.1 串口无输出:BootROM 还是电源问题

  • 先查电源轨和复位信号:RK3576 有多路电源,如果某一路电压没起来,BootROM 不会执行。
  • 再用示波器量 24MHz 晶振是否起振:晶振没起振,一切都白搭。
  • 确认串口工具连接的是调试串口,RK 平台默认调试串口是 UART2,波特率是 1500000,这个波特率不是常见的 115200,很容易被忽略。
  • 按住 RECOVERY 键上电,看 PC 端能否识别到 MaskROM 设备。如果能识别到,说明 SoC 活着,只是启动介质里的代码有问题。

7.2 SPL 阶段卡死:DDR 微码不匹配的典型表现

如果串口能打印U-Boot SPL但没有任何后续日志,大概率是 DDR 初始化没通过。排查顺序:

  1. 确认idbloader.img里打包的ddr.bin是否匹配板子的 DDR 颗粒。
  2. 检查板子硬件上 DDR 的布线是否正常,尤其是地址线的焊接。开发板一般没问题,如果是自己画的核心板,先查这步。
  3. 换用 U-Boot 提供的串口调试版ddr_uart.bin,它可以在 DDR 初始化过程中输出详细的错误码。

DDR 初始化失败还有一种非常恶心的现象:时好时坏。冷机启动没问题,热机重启偶尔起不来,大概率是 DDR 时序裕量不足,或者供电电压偏低。这已经不是软件能解决的,需要回到硬件设计层面去查。

7.3 U-Boot 能找到内核但 booti 没反应

U-Boot 打印了加载地址,也能看到读取成功,但booti之后串口没有任何内核日志。这种情况先确认:

  • 内核镜像是否真的是 ARM64 格式,用file Image查看。
  • DTB 是否是 RK3576 对应的设备树编译产物,别把 rk3399 的 DTB 拿来用。
  • 在 bootargs 里加上earlycon=uart8250,mmio32,0xfeb00000,让内核在最早期就通过串口打印,定位具体卡住的位置。

我见过一次很隐蔽的问题:内核镜像本身没有损坏,但是 U-Boot 环境变量里kernel_addr_r和fdt_addr_r指向了重叠的地址,加载 DTB 时把内核的前 1MB 数据覆盖了。这类问题用上面的fdt print方法比较容易发现。

7.4 内核起来了但 rootfs 挂载失败

内核日志停在类似VFS: Unable to mount root fs on unknown-block(179,5)的位置,说明 bootargs 里的root=设备节点不存在,或者内核里对应的文件系统驱动和块设备驱动没有编进去。

处理思路:

  1. 检查 bootargs 中 root 分区号是否与 EMMC 实际分区一致,mmcblk0p5对应第 5 个分区。
  2. 确认内核打开了 EXT4 支持,很多精简内核只编了 initramfs 相关的代码,没编通用的块设备文件系统。
  3. 如果 rootfs 在 SD 卡上,确认内核有 MMC/SD 驱动且设备树节点使能。

还有一个容易忽略的点:rootwait这个参数。如果 EMMC 的驱动初始化比内核尝试挂载根文件系统慢,内核会在设备还没有 ready 时就尝试挂载,结果失败。加上rootwait可以让内核等待块设备备好再挂载,绝大多数场景下能解决“看起来根文件系统路径明明没错但还是失败”的问题。

8. 提升调试效率的几个小习惯

最后分享几个我自己的实操习惯。第一,始终把串口日志保存到文件,不只是盯着终端看。嵌入式启动问题经常需要反复对比前后两次启动的差异,有日志文件能快速用 diff 找出异常点。

第二,给 U-Boot 环境变量的默认 bootcmd 做“备份”。调试过程中我会频繁修改 bootcmd 和 bootargs,改到一半发现忘了原版内容的情况经常发生。所以拿到新板子我做的第一件事就是:

printenv bootcmd printenv bootargs saveenv

把原始配置打印并保存一次,后面乱改也有后悔药。

第三,把固件版本信息写进环境变量。量产阶段最怕现场反馈“启动不了”,但拿回来一查发现 U-Boot 是三个月前的版本、内核是另一套 SDK 编的。在 U-Boot 的板级初始化代码里加几行打印,把编译时间和 git 版本号输出到串口,能省掉大量扯皮。

RK3576 的启动链路看着环节多,但只要把每一级的职责边界和交接协议搞明白,遇到问题时按链路逐级排查,大多数问题都能在半小时内定位到具体的环节。这也是为什么我强烈建议每个做 RK 平台的人也手工编译一次 U-Boot、手动打包一次 idbloader 和 FIT 镜像,而不是每次都依赖 SDK 的一键脚本。跑通一次完整的低级流程,你对整个系统的理解会比读十遍文档都深。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询