4. 备份机制设计:双备份区(A/B分区)原理、备份区切换逻辑
说到固件升级,我最怕什么?不是升级失败,而是升级失败后设备变砖。你想想看,一个智能电表或者工业控制器,OTA 到一半断电了,重启后系统起不来——这种事故要是发生在现场,那可真叫一个头疼。
双备份区,说白了就是给系统准备了两套完整的固件镜像。一套跑着,一套等着。我习惯叫它 A/B 分区方案,也叫无缝升级。这玩意儿在手机行业早就普及了,但在嵌入式领域,很多人还是舍不得那点 Flash 空间。我个人觉得,这钱不能省。
4.1 双备份区的基本原理
先看一张图,把整体架构搞清楚。
这张图里,Bootloader 是决策者。它不看心情,只看一个叫「启动标志位」的东西。标志位指向 A,就从 A 启动;指向 B,就从 B 启动。升级管理器负责把新固件写到当前不用的那个分区里。
核心要点:任何时候,至少有一个分区是完整可用的。这就是双备份区最大的价值——永不掉线。
4.2 分区布局与元数据
分区怎么划分?我一般这样安排 Flash 布局:
| 区域 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64 KB | 启动加载程序 + 分区表 |
| 分区 A | 0x08010000 | 512 KB | 固件镜像 A + 元数据 |
| 分区 B | 0x08090000 | 512 KB | 固件镜像 B + 元数据 |
| 共享存储区 | 0x08110000 | 64 KB | 配置参数、日志、标志位 |
每个分区头部都有一小块元数据区域,记录着:
- 固件版本号——用来判断哪个分区更新
- CRC32 校验值——启动时验证固件完整性
- 升级状态标志——标记该分区是否完成升级
- 启动计数——记录该分区已成功启动的次数
嗯,这里要注意。元数据一定要放在分区的最前面,并且用两个备份。我在项目中遇到过,Flash 写入到一半断电,元数据损坏了,结果 Bootloader 读到一个乱码,直接不知道该启动谁。后来我改成双备份元数据——写两份,启动时对比,不一致就用备份的。
4.3 备份区切换逻辑
切换逻辑其实不复杂,但细节决定成败。我把它拆成几个步骤:
- 升级触发:系统收到新固件包,校验签名和完整性。
- 确定目标分区:查看当前运行的是 A 还是 B,目标就是另一个。
- 写入固件:按块写入目标分区,每写完一块做一次 CRC 校验。
- 标记待验证:写入完成后,将目标分区的状态设为「待验证」。
- 切换标志位:修改启动标志位,指向新分区。
- 复位重启:系统复位,Bootloader 读取标志位,从新分区启动。
- 健康检查:新分区启动后,运行自检程序。如果通过,将状态改为「已验证」。
- 回滚(如果失败):如果自检失败或超时,Bootloader 自动切回旧分区。
我的习惯:在健康检查阶段,我会让应用层上报一个「启动成功」的心跳。如果 30 秒内没收到心跳,Bootloader 就认为新固件有问题,自动回滚。这个机制救过我两次。
4.4 回滚机制与防砖保护
回滚是双备份区的灵魂。没有回滚,双备份就只是个双倍浪费。回滚逻辑通常放在 Bootloader 里:
/* 伪代码:Bootloader 启动决策逻辑 */
uint8_t select_boot_partition(void) {
uint8_t active = read_boot_flag(); // 读取启动标志位
// 检查目标分区的元数据
if (is_partition_valid(active)) {
// 检查该分区是否标记为"待验证"
if (get_partition_status(active) == STATUS_PENDING) {
// 首次启动,启动计数 +1
increment_boot_count(active);
return active;
}
return active;
}
// 目标分区无效,尝试另一个
uint8_t backup = (active == PARTITION_A) ? PARTITION_B : PARTITION_A;
if (is_partition_valid(backup)) {
// 记录回滚事件
log_rollback_event(active, backup);
set_boot_flag(backup);
return backup;
}
// 两个分区都坏了... 进入恢复模式
enter_recovery_mode();
return PARTITION_NONE;
}
这段代码看着简单,但有个坑。我曾经遇到过,分区 A 的元数据被写坏了,但固件本身是好的。Bootloader 判断分区 A 无效,切到了 B。结果 B 是个旧版本,功能不全。用户反馈说设备异常,我排查了半天才发现是元数据损坏导致的误判。
避坑指南:我曾经在元数据校验上吃过亏。不要只校验元数据,还要对固件主体做快速 CRC 扫描。如果元数据坏了但固件完好,应该允许修复元数据后继续使用该分区。
4.5 升级过程中的异常处理
升级过程中可能出现的异常,我列了个清单:
| 异常场景 | 影响 | 处理策略 |
|---|---|---|
| 写入中途断电 | 目标分区固件不完整 | 重启后 Bootloader 发现 CRC 不匹配,自动切回旧分区 |
| 写入时 Flash 坏块 | 部分数据无法写入 | 跳过坏块,记录坏块表;如果坏块过多,放弃升级 |
| 新固件启动崩溃 | 系统反复重启 | Bootloader 检测到启动计数异常,回滚到旧分区 |
| 标志位写入失败 | 无法切换分区 | 保持当前分区,下次升级时重试 |
| 两个分区都损坏 | 设备变砖 | 进入 DFU 模式,等待外部恢复 |
你想想看,如果没有双备份,上面任何一种异常都可能导致设备变砖。有了双备份,最坏的情况也就是回滚到旧版本,设备还能用。
4.6 实际项目中的经验总结
做了这么多年固件,我总结了几条关于双备份区的铁律:
- 永远不要覆盖正在运行的分区。这是底线。升级管理器必须确认当前运行的是 A,才能写 B;反之亦然。
- Bootloader 要足够简单。Bootloader 越复杂,出 bug 的概率越大。我只让它做三件事:读标志位、校验分区、跳转。别的什么都不干。
- 标志位要有冗余。我习惯在 Flash 的三个不同位置各存一份标志位,启动时三取二投票。Flash 的位翻转不是闹着玩的。
- 升级过程中不要依赖网络。如果升级到一半网络断了,设备应该能正常回滚,而不是卡在半死不活的状态。
一句话总结:双备份区不是用来「升级」的,是用来「兜底」的。它让你在升级失败时,还有一条退路。