双备份区架构:A/B分区设计原理
双备份区,圈里人常叫它 A/B 分区。说白了,就是给固件准备两个“窝”——一个跑着,一个闲着。万一跑着的那个出事了,Bootloader 立马把闲着的拉上来顶班。
我最早接触这个设计,是在做一款工业网关的时候。OTA 升级到一半,突然断电了。重启后设备变砖,客户差点把售后电话打爆。从那以后,A/B 分区就成了我项目里的标配。
为什么需要两个分区?
你想想看,单分区架构下,升级就是“擦除→写入”。这个过程中一旦掉电、复位、或者传输数据出错,固件就残了。设备连启动都做不到,只能进厂返修。
A/B 分区解决了这个痛点:
- 原子性切换——要么完整升级,要么不动当前系统
- 即时回滚——新固件跑不起来,自动切回旧版本
- 零停机升级——后台下载到 B 区,不影响 A 区运行
核心原则:任何时候,至少有一个分区是可启动的完整固件。
分区表布局
分区表是 Bootloader 的“地图”。它告诉芯片:固件放哪、多大、哪个是当前激活的。
我习惯把 Flash 规划成下面这样:
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64 KB | 启动加载器,不可被应用固件覆盖 |
| 分区表 | 0x08010000 | 4 KB | 记录 A/B 状态、版本号、CRC |
| Slot A | 0x08011000 | 512 KB | 主固件区 A |
| Slot B | 0x08091000 | 512 KB | 备份固件区 B |
| 配置区 | 0x08111000 | 16 KB | 用户参数、校准数据 |
我的习惯:分区表里额外存一个 32 位的 CRC 校验值。每次启动时 Bootloader 先读分区表,校验通过才去加载固件。这能防止分区表本身被写坏。
Bootloader 的角色
Bootloader 是整个回滚机制的“裁判”。它不参与业务逻辑,只做三件事:
- 校验——检查 A/B 分区的完整性(CRC、签名)
- 决策——根据分区表状态,决定启动哪个分区
- 切换——更新分区表,标记下次启动的目标
我见过不少团队把 Bootloader 写得很重,塞了驱动、协议栈、甚至文件系统。我个人建议:Bootloader 越轻越好。它只负责启动,其他事交给应用固件去做。
曾经踩过的坑:有一次我在 Bootloader 里加了 USB 升级功能,结果 USB 驱动本身有 bug,导致设备连 Bootloader 都进不去。从那以后,我的 Bootloader 只支持最简单的 UART 或 SPI 升级,复杂协议一律放应用层。
A/B 切换流程
下面这张图展示了完整的切换逻辑:
分区状态管理
分区表里除了地址和大小,还要记录状态。我常用的状态机是这样的:
| 状态 | 值 | 含义 |
|---|---|---|
| INVALID | 0x00 | 分区内容无效,不可启动 |
| VALID | 0x01 | 固件完整,可正常启动 |
| UPDATING | 0x02 | 正在写入新固件,此时不可断电 |
| COMMITTED | 0x03 | 新固件已确认运行正常,锁定为当前版本 |
避坑指南:我曾经把状态值定义成 0x00~0x03,结果 Flash 擦除后默认是 0xFF,导致 Bootloader 误判为 INVALID。后来我改成:VALID=0xA5, INVALID=0x00,擦除后的 0xFF 直接视为无效,安全多了。
回滚触发条件
什么时候触发回滚?我总结了三种场景:
- 启动超时——应用固件启动后,5 秒内没有向 Bootloader 发送“心跳”确认
- 看门狗复位——连续三次看门狗复位,说明新固件不稳定
- 用户强制回滚——通过外部按键或串口命令,主动切回旧版本
嗯,这里要注意:不要一复位就回滚。有时候只是电源波动导致的偶然复位。我一般设置一个“失败计数器”,连续复位超过 3 次才触发回滚。
代码示例:分区表结构体
下面是我常用的分区表定义,C 语言实现:
typedef struct {
uint32_t magic; // 魔数,用于识别分区表有效性
uint32_t version; // 分区表版本
uint32_t slot_a_addr; // Slot A 起始地址
uint32_t slot_a_size; // Slot A 大小
uint32_t slot_a_crc; // Slot A 固件 CRC32
uint8_t slot_a_status; // Slot A 状态
uint8_t slot_b_status; // Slot B 状态
uint16_t boot_count; // 当前分区启动失败计数
uint32_t reserved[4]; // 保留字段
} partition_table_t;
关键点:magic 字段我固定为 0xABABABAB。Bootloader 启动时先检查 magic,不对就直接进紧急模式。这能防止 Flash 全空或全 0xFF 时误判。
实际项目中的考量
A/B 分区不是银弹。它需要双倍 Flash 空间,对成本敏感的 MCU 来说是个负担。我做过一个项目,Flash 只有 256 KB,硬塞 A/B 分区后,应用代码只剩 80 KB 可用空间。最后只能退而求其次,用单分区 + 外部备份 Flash 的方案。
另外,分区表的位置要固定。我习惯把它放在 Bootloader 之后、第一个分区之前。这样 Bootloader 升级时不会覆盖分区表,应用固件升级时也不会误写。
曾经踩过的坑:有一次我把分区表放在 Flash 末尾,结果应用固件升级时写越界,把分区表覆盖了。重启后 Bootloader 找不到有效分区,设备彻底变砖。后来我强制规定:分区表必须放在 Bootloader 后面,且应用固件写入时做边界检查。