先说一个容易踩的坑:网上和文档里常说的"SPL 阶段就决定加载 boot 还是 recovery",在这份 SDK 里是不成立的——那段代码被一个默认关闭的编译开关挡住了。真正的分流全在 U-Boot proper 里。中间还夹着一层环境变量,可以越过整个判断链。
问题场景
上一篇讲清楚了"进不进 recovery"取决于 misc 分区 16K 处那 32 个字节的 command 字段。但那是数据,还得有人去读它、并且据此改变启动路径。
官方文档第 3 章贴了一段 SD 卡启动的串口日志,里面有一行 enter Recovery mode!,后面跟着 ANDROID: reboot reason: "recovery"。(这行和代码里实际打的不完全一样——代码里是小写的 entering recovery mode!,对着串口找的时候别被大小写卡住。)这两行分别是谁打的?除了 misc,还有没有别的办法让设备进 recovery?按住某个键上电是怎么生效的?
一、先纠正一个常见的误解
先明确一个前提,否则后面容易看错代码。
RK 的 U-Boot 里确实写了一套「SPL 直接决定加载 boot 还是 recovery」的逻辑,函数叫 spl_kernel_partition()。但它被关在 CONFIG_SPL_KERNEL_BOOT 后面,而这份 SDK 里这个开关是关的:
# u-boot/.config# CONFIG_SPL_KERNEL_BOOT is not set
所以在这份 SDK 上:
| 阶段 | 谁做决定 |
|---|---|
| SPL | 只管把U-Boot proper(loader)拉起来,不碰 boot/recovery |
| U-Boot proper | rockchip_get_boot_mode() ——真正的分流在这里 |
CONFIG_SPL_KERNEL_BOOT 是给Thunder Boot(rk3576-tb 那类配置)用的——那种方案要让 SPL 直接把内核拉起来以缩短启动时间,所以才需要在 SPL 阶段就决定加载哪个分区。普通配置下 SPL 只负责引导 U-Boot proper,分区选择留到后面做。
这一篇主要讲 U-Boot proper 的逻辑;第二节顺带介绍 SPL 那套(只在 Thunder Boot 配置下生效),看的时候留意别把它当成当前配置的行为。
二、SPL 阶段:读 16K 处的那个字符串
SPL 的判断代码短到可以整段看完:
// u-boot/arch/arm/mach-rockchip/spl.cconst char *spl_kernel_partition(struct spl_image_info *spl,struct spl_load_info *info){...sector = 0;ret = part_get_info_by_name(info->dev, PART_MISC, &part_info);if (ret >= 0)sector = part_info.start;if (sector) {ret = info->read(info, sector + BCB_MESSAGE_BLK_OFFSET, cnt, bmsg);if (ret == cnt && !strcmp(bmsg->command, "boot-recovery")) {free(bmsg);return PART_RECOVERY;}...}boot_mode = readl((void *)CONFIG_ROCKCHIP_BOOT_MODE_REG); // 无条件读return (boot_mode == BOOT_RECOVERY) ? PART_RECOVERY : PART_BOOT;}
几个细节和直觉不太一样:
- 返回的是字符串,不是整数枚举。 PART_RECOVERY / PART_BOOT 就是字面量 "recovery" / "boot"(定义在 boot_rkimg.h)——SPL 拿这个字符串去当分区名用
- boot_mode 寄存器是无条件读的 ,不是"读 misc 失败才读"。只要 command 不等于 boot-recovery,就会走到最后那行三元表达式
- 所以逻辑是两句:misc 里写着 boot-recovery 就加载 recovery 分区;否则看 boot_mode 寄存器,是 BOOT_RECOVERY 加载 recovery,其余都加载 boot。
BCB_MESSAGE_BLK_OFFSET 这个偏移的定义在 U-Boot 的头文件里:
// u-boot/include/boot_rkimg.h#defineBCB_MESSAGE_BLK_OFFSET(16 * 1024 >> 9)
>> 9 就是除以 512 换成扇区数:32 扇区 = 16384 字节 = 16 KiB。和 recovery 那边 BOOTLOADER_MESSAGE_OFFSET_IN_MISC = 16 * 1024 完全对得上,也和 mk-misc.sh 里写死的 seek=$((16*1024)) 对得上。三处独立写的数字,指的是同一个位置。
boot_mode 那一路对应的是"reboot recovery 命令"——那是另一条独立入口,第三节会讲全。
三、U-Boot 阶段:三路来源,优先级写得很死
到了 U-Boot proper,判断逻辑集中在 boot_mode.c 的 rockchip_get_boot_mode()。注释里把三路来源列得很清楚:
/** There are three ways to get reboot-mode:** No1. Android BCB which is defined in misc.img (0KB or 16KB offset)* No2. CONFIG_ROCKCHIP_BOOT_MODE_REG that supports "reboot xxx" commands* No3. Env variable "reboot_mode" which is added by U-Boot** Recovery mode from:*- Android BCB in misc.img*- "reboot recovery" command*- recovery key pressed without usb attach*/
函数内部用了三个优先级槽位来缓存结果:
enum {PH = 0,/* P: Priority, H: high, M: middle, L: low*/PM,PL,};
对应的判断顺序是:
这个优先级设计有个很实际的用意,注释里也写了:reboot loader / reboot bootloader / reboot fastboot 这类"下载模式"命令必须优先级最高,否则设备一旦 misc 里残留了 boot-recovery,你就再也进不了烧写模式——只能拆机短接。
所以这里的顺序是:下载模式 > misc 里的 recovery > 其它 reboot 参数(这是 U-Boot proper 内部三路来源之间的顺序,加上环境变量之后的完整优先级见第四节末尾)。
串口上那句 boot mode: recovery (misc) 就是 PM 那一路打出来的,boot mode: recovery (cmd) 则是 PL 那一路(来自寄存器)。
四、热键那条路为什么能"抢先"
reboot_mode 环境变量是在更早的下载模式检测里被设上的,位置在 boot_rkimg.c:
// u-boot/arch/arm/mach-rockchip/boot_rkimg.cvoid setup_download_mode(void){int vbus = 1;if (rockchip_dnl_key_pressed() || is_hotkey(HK_ROCKUSB_DNL)) {vbus = rockchip_u2phy_vbus_detect();if (vbus > 0) {printf("%sentering download mode...n", ...);run_command("download", 0);} else {printf("entering recovery mode!n");env_set("reboot_mode", "recovery-key");}} else if (is_hotkey(HK_FASTBOOT)) {env_set("reboot_mode", "fastboot");}}
官方文档那段日志里的entering recovery mode! 就是这里打的。
关键在 vbus 那个分支——同一个按键,插不插 USB 走向完全不同:
| 上电时按键 + USB | 结果 |
|---|---|
| 按键 + 插着 USB(vbus > 0) | 进 download 模式(能烧写整片) |
| 按键 + 没插 USB(vbus == 0) | reboot_mode = recovery-key,进 recovery |
这个设计避免了误触发:做量产烧写的人习惯按住键插线,而现场维护的人只是按键上电,两者不会互相干扰。
而 rockchip_get_boot_mode() 里,环境变量是第一个查的,查到就直接 return——不碰寄存器,也不读 misc。这就是为什么热键能"抢先":它绕过了整个 misc 判断链路。
按键本身在 boot_rkimg.c 里是弱定义,板级可以覆盖:
// 默认实现:有 DM_KEY 就用按键驱动,否则退回 adc-keys__weak int rockchip_dnl_key_pressed(void){#ifdefined(CONFIG_DM_KEY)return key_is_pressed(key_read(KEY_VOLUMEUP));#elifdefined(CONFIG_ADC)// 走 adc-keys,判断电压是否落在 KEY_DOWN_MIN_VAL ~ KEY_DOWN_MAX_VAL...return 0;#endif}
默认认的是 KEY_VOLUMEUP(音量加)。自己的板子按键映射不一样的话,在板级文件里覆盖这个弱函数就行——这也是官方文档 9.1 节提到的 RECOVERY + VOLUMEUP 触发恢复出厂设置的硬件基础。
五、三种进入 recovery 的方式
把上面两条路合起来,进 recovery 一共有三个入口:
| 方式 | 触发条件 | 代码位置 | 串口特征 |
|---|---|---|---|
| misc 命令 | command 字段是 boot-recovery | misc_require_recovery() | boot mode: recovery (misc) |
| 热键 | 按住 VOLUMEUP 上电且未插 USB | setup_download_mode() | entering recovery mode! |
| 命令 | reboot recovery | boot_mode 寄存器 | boot mode: recovery (cmd) |
对应到实际使用场景:
- 升级 走第一种——Normal 系统写 misc,重启
- 救砖/裸片升级 走第二种——不依赖任何已有系统
- 调试 走第三种——reboot recovery 一条命令
三条路最后都汇到同一个 BOOT_MODE_RECOVERY,但只有第一条依赖 misc 分区完好。这也是为什么误刷 misc 不会让设备变砖:按住键上电照样能进 recovery。
六、BCB 偏移量上有个隐患
回到第二节那个偏移。SPL 和 U-Boot proper 读 misc 用的偏移算法不一样:SPL 那边是写死的 BCB_MESSAGE_BLK_OFFSET(32 扇区 = 16 KiB),U-Boot proper 这边走下面这个函数:
// u-boot/common/image-android.cu32 android_bcb_msg_sector_offset(void){static int android_version = -1;/** There are two types of misc.img:*Rockchip platforms defines BCB message at the 16KB offset of*misc.img except for the Android version >= 10. Because Google*defines it at 0x00 offset, and from Android-10 it becoms mandary*on Google VTS.*/if (android_version < 0)android_version = android_version_init();return (android_version >= 10) ? 0x00 : 0x20;}
android_version 是从 boot 分区里的 Android boot image header 的 os_version 读出来的。两份代码的偏移量来源不同:SPL 定死 16 KiB,U-Boot proper 会随 boot image 的版本在 0x00 和 0x20(=16 KiB)之间切换。
在这份 Linux SDK 上两者是一致的——因为 boot.img 是 FIT 镜像而不是 Android boot image,android_version_init() 走不到有效值,android_version 保持 -1,-1 >= 10 为假,返回 0x20,正好是 16 KiB。
但这个一致性是有条件的:
| 场景 | U-Boot proper 读 | SPL 读 | 一致? |
|---|---|---|---|
| 当前 Linux SDK(FIT boot.img) | 16 KiB | 16 KiB | |
| 换成 Android 10+ 风格的 boot.img | 0 KiB | 16 KiB |
如果不一致会怎样:Normal 系统把 boot-recovery 写在 16 KiB,U-Boot proper 去 0 字节处找,找不到 → 认为不该进 recovery → 但 SPL 已经根据 16 KiB 处的内容加载了 recovery 分区。结果就是"内核是 recovery 的,但 U-Boot 按正常系统配参数",行为会很怪。
如果你的板子是从 Android 方案改过来的,或者自定义过 boot image,务必确认这两个偏移量目前落在同一格。排查方法很简单:updateEngine --misc=display 看 16 KiB 处有没有意料之外的内容。
七、还有一条隐藏路径
boot_rkimg.c 里还有个函数,一般在升级流程里见不到:
// u-boot/arch/arm/mach-rockchip/boot_rkimg.cvoid board_run_recovery_wipe_data(void){...memset((char *)&bmsg, 0, sizeof(struct bootloader_message));strcpy(bmsg.command, "boot-recovery");strcpy(bmsg.recovery, "recoveryn--wipe_data");bmsg.status[0] = 0;...blk_dwrite(dev_desc, part_info.start + bcb_offset, cnt, &bmsg);...env_set("reboot_mode", "recovery");run_command("run bootcmd", 0);}
它在 U-Boot 里直接构造 BCB 并重启,等价于让设备自己把命令写进 misc 然后进 recovery。
它的唯一调用点在 fastboot 的 oem unlock_accept 流程里(f_fastboot.c 的解锁确认分支)。但这条路径在本 SDK 上默认是关闭的:
# u-boot/.config# CONFIG_FASTBOOT_OEM_UNLOCK is not set
这个开关没开,整个 oem unlock 分支(包括后面的确认步骤)都会落到#else返回 FAILnot implemented,所以实际走不到。留在这里是为了完整性——如果你以后要开 oem unlock,就会碰到它。
顺带澄清一个容易混淆的地方:fastboot 里还有个 reboot-recovery 子命令,它会调 android_bcb_write("boot-recovery") 写 BCB。那个函数不是这里的调用者——它只写个 BCB 然后重启,不带 --wipe_data,和 board_run_recovery_wipe_data() 是两码事。
注意它写的是命令是 --wipe_data,不是 --wipe_all。两者在代码里效果实际是一样的(04 篇核对过:都只擦 /userdata),但写在 misc 里的字符串不同——对照 --misc=display 的输出判断"这次到底是谁触发的",靠的就是这个差别。
小结
| 阶段 | 代码 | 判断依据 | 串口输出 |
|---|---|---|---|
| SPL(本 SDK 未启用) | spl_kernel_partition() | misc 16K 处 command == "boot-recovery" | — |
| 热键 | setup_download_mode() | VOLUMEUP + 无 USB | entering recovery mode! |
| U-Boot | rockchip_get_boot_mode() | env → 寄存器 → misc | boot mode: recovery (misc) |
| fastboot | board_run_recovery_wipe_data() | oem unlock | Rebooting into recovery to do wipe_data |
上表第一行在本 SDK 里不生效:spl_kernel_partition() 被 CONFIG_SPL_KERNEL_BOOT 挡着(第一节核实过),只有 Thunder Boot 配置下才会编进去。当前配置下真正生效的是中间两行——热键和 U-Boot proper。
优先级从高到低是:热键/环境变量 > 下载模式(寄存器)> misc 命令 > 其它 reboot 参数。
注意环境变量排在最前面——rockchip_get_boot_mode() 一进来就查 reboot_mode,匹配上就直接 return,连寄存器都不读。所以按住键上电这条路能盖过一切,包括下载模式。
