4、双备份启动策略(A/B分区)
说到固件升级,我最怕什么?
不是升级失败,而是升级失败后设备变砖。
你想想看,内窥镜正在手术室里用着,突然OTA升级断了电。重启后系统起不来,这责任谁担得起?
所以,双备份启动策略(A/B分区)就派上用场了。说白了,就是给系统准备两套固件,一套运行,一套备用。出问题了,立马切回来。
4.1 A/B分区原理
A/B分区的思路其实很简单。把Flash存储空间分成两个区:A区和B区。每个区都存放一份完整的固件镜像。
系统启动时,Bootloader会检查当前哪个分区是“健康”的。如果A区正常,就从A区启动。如果A区挂了,就切到B区。
我参与过一个项目,客户要求固件升级不能中断业务。当时我就用了A/B分区方案。升级时,先把新固件下载到B区,下载完成后标记B区为“待验证”。下次重启时,Bootloader自动从B区启动。如果启动成功,就把B区标记为“已生效”。整个过程,用户完全无感。
来看一个典型的分区布局:
Flash地址空间:
+------------------+
| Bootloader | 0x0000_0000 - 0x0000_3FFF (16KB)
+------------------+
| A区 (Active) | 0x0000_4000 - 0x000F_FFFF (约1MB)
+------------------+
| B区 (Standby) | 0x0010_0000 - 0x001F_FFFF (约1MB)
+------------------+
| 配置区 | 0x0020_0000 - 0x0020_0FFF (4KB)
+------------------+
配置区里存的是啥?就是当前启动分区、版本号、启动次数这些关键信息。Bootloader每次启动都会读它。
核心要点:A/B分区不是简单的“两份固件”,而是一套完整的启动管理机制。Bootloader才是真正的决策者。
4.2 版本回滚机制
版本回滚,说白了就是“后悔药”。
新固件上线后,如果发现有问题,能快速切回旧版本。这在A/B分区方案里,实现起来非常自然。
具体怎么做?
- 系统当前从A区启动,版本号v1.0
- 新固件v2.0下载到B区
- 重启后,Bootloader从B区启动v2.0
- 如果v2.0运行正常,用户确认后,标记B区为“已生效”
- 如果v2.0运行异常(比如频繁死机),系统自动切回A区v1.0
我曾经遇到过一个坑:新固件在实验室测试了三个月,什么问题都没有。结果一上线,发现和某款老型号的摄像头不兼容。用户反馈说图像花屏。幸好有A/B分区,一键回滚到旧版本,问题立刻解决。
我的建议:回滚操作一定要保留用户数据。我习惯在升级前,把关键配置参数备份到配置区。这样回滚后,用户设置不会丢失。
版本回滚的触发条件,我一般设三个:
- 用户手动触发:用户觉得新版本不好用,主动回滚
- 系统自动检测:新版本启动后,连续崩溃3次以上,自动回滚
- 看门狗超时:系统在规定时间内没有完成初始化,看门狗复位后触发回滚
4.3 故障检测与自动恢复
故障检测,是A/B分区方案里最考验功力的部分。
你想想看,系统怎么知道当前固件是“健康”的还是“有病”的?
我常用的方法有四种:
| 检测方法 | 说明 | 我的经验 |
|---|---|---|
| CRC校验 | 启动时对固件镜像做CRC32校验 | 必须做,但不够。CRC只能检测数据损坏,不能检测逻辑错误 |
| 启动计数 | 记录系统启动次数,超过阈值则标记为故障 | 我一般设3次。连续3次启动失败,就切分区 |
| 心跳检测 | 应用程序定期向Bootloader报告“我还活着” | 这个很实用。如果5秒内没收到心跳,Bootloader就认为系统挂了 |
| 用户确认 | 用户手动确认新固件运行正常 | 最可靠的方法。但用户体验稍差,需要用户点一下“确认” |
来看一段Bootloader的故障检测伪代码:
void bootloader_main(void)
{
// 1. 读取配置区
boot_config_t config = read_config();
// 2. 检查当前分区是否有效
if (is_partition_valid(config.current_partition) == FALSE)
{
// 当前分区挂了,切到另一个分区
config.current_partition = switch_partition(config.current_partition);
config.boot_count = 0;
save_config(&config);
}
// 3. 检查启动次数
if (config.boot_count >= MAX_BOOT_FAILURES)
{
// 连续启动失败,标记为故障
mark_partition_failed(config.current_partition);
config.current_partition = switch_partition(config.current_partition);
config.boot_count = 0;
save_config(&config);
}
// 4. 启动应用程序
config.boot_count++;
save_config(&config);
jump_to_app(config.current_partition);
}
// 应用程序启动后,如果正常,会调用这个函数
void app_report_healthy(void)
{
boot_config_t config = read_config();
config.boot_count = 0; // 清零启动计数
config.partition_status = STATUS_HEALTHY;
save_config(&config);
}
注意:启动计数器的清零时机很关键。我见过有人把清零放在应用程序初始化函数里。结果应用程序初始化到一半就崩了,计数器没清零,下次启动就误判为故障。正确的做法是:应用程序确认自己完全正常运行后,再调用report_healthy()。
自动恢复的流程,我总结为三步:
- 检测:Bootloader发现当前分区有问题(CRC错误、启动超限、心跳丢失)
- 切换:自动切换到另一个分区,并更新配置区
- 通知:启动后,应用程序通过某种方式(比如LED闪烁、日志记录)告知用户发生了切换
嗯,这里要注意一点:切换分区后,原来的故障分区不要立刻擦除。我习惯保留它,方便后续分析问题。等下次升级成功,再覆盖掉。
最后说一句:A/B分区不是万能的。它解决的是“固件坏了怎么办”的问题,但解决不了“两个分区都坏了”的问题。所以,Bootloader本身一定要足够稳定,最好放在独立的、写保护的存储区域。
我个人习惯,Bootloader用ROM或者OTP区域存储,确保它永远不会被破坏。这样,就算A区和B区都挂了,Bootloader至少还能进入恢复模式,通过USB或串口重新烧录固件。
好了,双备份启动策略就讲到这里。下一章,我们聊聊升级过程中的数据安全,怎么防止固件被篡改。