第四章 双区备份架构设计:A区与B区概念、备份策略选择、存储空间分配
好,咱们今天聊点实在的。双区备份,说白了就是给ECU准备两个“家”。一个住着,另一个空着或者住着旧版本。万一升级出问题,还能退回去。这个思路其实不复杂,但落地的时候,坑不少。
我个人习惯把这两个区叫做A区和B区。A区是主运行区,B区是备份区。当然,你叫Bank0和Bank1也行,叫Primary和Secondary也行。名字不重要,重要的是它们怎么配合。
4.1 A区与B区的概念
A区,就是ECU上电后默认启动的区域。这里存放的是当前正在使用的应用程序。正常情况下,Bootloader会先检查A区的有效性,没问题就直接跳过去执行。
B区呢,是备用区。它可能存着旧版本,也可能存着新下载的固件。具体看你的策略。我见过一些设计,B区干脆就是空的,只在升级时临时写入。嗯,这种做法风险有点大,我不太推荐。
为什么?你想想看,如果升级到一半断电了,A区被擦了一半,B区还没写完。那ECU就彻底变砖了。所以,我建议B区始终保留一个可用的版本。哪怕是个出厂固件,也比空着强。
核心原则:任何时候,至少有一个区是可用的、可启动的。这是双区备份的底线。
4.2 备份策略选择:全量 vs 差分
备份策略,说白了就是你怎么把新固件弄到B区去。主要有两种:全量备份和差分备份。
4.2.1 全量备份
全量备份,就是把整个固件完整地写一遍。A区多大,B区就多大。升级时,把新固件完整地写入B区。写完后,校验通过,再切换启动区。
优点很明显:简单、可靠。你不需要关心A区里有什么,直接覆盖B区就行。我在项目中遇到过,有些客户非要搞差分,结果版本管理混乱,最后反而全量最省心。
缺点呢?慢。尤其是固件体积大的时候,比如几百KB甚至几MB,擦除和写入的时间会很长。另外,需要两倍的存储空间。
4.2.2 差分备份
差分备份,只记录A区和B区之间的差异。说白了,就是只更新变化的部分。比如A区是v1.0,B区要升级到v1.1,那只需要把v1.0到v1.1的补丁包下载下来,应用到B区就行。
优点:快,省空间。补丁包通常只有全量固件的10%~30%。
缺点:复杂。你得有一套差分算法,比如bsdiff、xdelta之类的。而且,如果A区被破坏了,差分就没法用了。我曾经吃过这个亏——A区有个bit翻转,结果差分应用后,B区也坏了。
我的建议:如果存储空间够用,优先选全量。如果对升级速度有硬性要求,比如OTA升级必须30秒内完成,那可以考虑差分。但一定要做好A区的完整性校验。
4.3 存储空间分配
存储空间怎么分?这得看你的Flash有多大。我拿一个常见的256KB Flash来举例。
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 16KB | 启动加载,升级管理 |
| A区(App) | 0x08004000 | 112KB | 主运行区 |
| B区(Backup) | 0x08020000 | 112KB | 备份区 |
| 配置区 | 0x0803C000 | 16KB | 存储启动标志、版本号等 |
你看,Bootloader占了16KB,A区和B区各112KB,最后留了16KB给配置信息。这个配置区很重要,它记录了当前应该从哪个区启动、升级状态是什么、版本号是多少。
嗯,这里要注意一点:A区和B区的大小必须完全一致。否则,全量备份时没法直接对拷。我见过有人把A区设成120KB,B区设成100KB,结果升级时发现B区装不下新固件。这种低级错误,千万别犯。
避坑指南:我曾经在一个项目里,把配置区和B区放在同一个扇区。结果每次擦除B区时,配置信息也被清掉了。后来我学乖了,配置区单独用一个扇区,或者放在Bootloader区域里。记住,配置区要独立,不能被擦除操作波及。
4.4 启动流程与切换逻辑
有了A区和B区,Bootloader怎么决定启动哪个?我一般用状态机来处理。
// 伪代码示例
enum BootState {
BOOT_FROM_A,
BOOT_FROM_B,
BOOT_UPDATE
};
void bootloader_main() {
BootState state = read_boot_state_from_config();
switch(state) {
case BOOT_FROM_A:
if (validate_app(APP_A_ADDR)) {
jump_to_app(APP_A_ADDR);
} else {
// A区坏了,尝试B区
if (validate_app(APP_B_ADDR)) {
jump_to_app(APP_B_ADDR);
} else {
// 两个都坏了,进入恢复模式
enter_recovery_mode();
}
}
break;
case BOOT_FROM_B:
// 类似逻辑,优先启动B区
...
break;
case BOOT_UPDATE:
// 执行升级流程
perform_update();
break;
}
}
这个逻辑其实不复杂。每次启动时,先读配置区,看看上次标记的是哪个区。然后校验那个区的完整性。如果校验通过,就跳过去。如果没通过,就尝试另一个区。
我习惯在每次成功启动后,把当前启动的区标记为“有效”。这样下次升级时,如果新固件有问题,还能自动回滚到上一个有效区。
4.5 回滚机制
回滚,就是升级失败后,自动退回到旧版本。这个机制依赖于双区备份。
具体做法是这样的:
- 升级前,把当前A区的版本标记为“待回滚”。
- 新固件下载到B区,校验通过后,标记B区为“待启动”。
- 重启,从B区启动。如果启动成功,把B区标记为“有效”,A区标记为“旧版本”。
- 如果启动失败(比如看门狗超时、应用崩溃),Bootloader检测到异常,自动切回A区。
你看,整个过程不需要人工干预。ECU自己就能决定是继续用新版本,还是退回旧版本。这就是双区备份最大的价值。
关键点:回滚的触发条件要设置好。我一般用“启动成功标志”+“看门狗”双重保障。应用启动后,必须在规定时间内清除看门狗,并写入“启动成功”标志。否则,Bootloader就认为启动失败,执行回滚。
好了,这一章的内容就这些。双区备份的架构,说白了就是“留一手”。A区不行了,B区顶上。B区也坏了?那至少还有Bootloader能救回来。下一章,咱们聊聊具体的Flash驱动怎么实现,包括擦除、写入、校验这些底层操作。到时候我会拿实际代码来演示,保证你能直接用。