3、双备份机制(A/B 系统):A/B 系统原理、分区布局、切换逻辑
双备份机制,圈里人常叫它 A/B 系统,或者「无缝升级」。说白了,就是给固件准备两套房子——一套住着当前系统,另一套空着等新固件搬进来。升级时新固件写进空房子,写完了再让系统「搬家」过去。
我最早接触这个方案是在做智能网关的项目。那会儿客户要求设备升级不能断网超过 2 秒,单分区方案根本做不到。后来我硬着头皮上了 A/B 系统,效果确实好。嗯,今天就把这套东西掰开揉碎了讲清楚。
3.1 A/B 系统原理
核心思想其实很简单:永远保留一个可启动的固件副本。系统里同时存在两个分区——A 分区和 B 分区。当前运行的那个叫「活动分区」,另一个叫「非活动分区」。
升级流程是这样的:
- 系统在 A 分区正常运行
- 新固件下载到 B 分区(非活动分区)
- 校验 B 分区的完整性(哈希、签名)
- 设置启动标志,指向 B 分区
- 重启设备,从 B 分区启动
- 如果 B 分区启动失败,自动回滚到 A 分区
你想想看,这个过程中最妙的是什么?升级失败不会导致设备变砖。因为旧固件一直完好无损地待在另一个分区里。
核心优势:升级过程中任何一步出问题,设备都能自动回滚到上一个正常版本。用户几乎感知不到升级过程。
3.2 分区布局
分区布局是 A/B 系统的骨架。我见过不少新手在这个环节栽跟头——分区大小没算好,结果新固件塞不进去。
一个典型的 A/B 系统分区布局长这样:
| 分区名称 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x00000000 | 64 KB | 引导加载程序,负责选择启动分区 |
| 分区表 | 0x00010000 | 4 KB | 存储分区布局信息 |
| Slot A | 0x00020000 | 1 MB | 固件 A 分区(含应用和内核) |
| Slot B | 0x00120000 | 1 MB | 固件 B 分区(含应用和内核) |
| 数据分区 | 0x00220000 | 2 MB | 用户数据、配置参数 |
| 元数据分区 | 0x00420000 | 8 KB | 存储启动计数、回滚状态 |
这里有个关键点——元数据分区。它虽然小,但作用巨大。里面存着当前活动分区是 A 还是 B、启动尝试次数、回滚标志等信息。Bootloader 启动时第一件事就是读这个分区,决定从哪边启动。
我的经验:元数据分区最好用独立的 Flash 扇区,并且做冗余存储。我曾经遇到过元数据被写坏导致系统反复重启的故障,后来改成双备份元数据才解决。
分区大小怎么定?我个人习惯这样算:
- 每个 Slot 的大小 = 当前固件最大体积 × 1.5(留 50% 余量)
- 如果支持增量升级,可以适当缩小余量到 20%
- 数据分区至少留够存 3 次配置备份的空间
3.3 切换逻辑
切换逻辑是 A/B 系统的「大脑」。它决定了什么时候切、怎么切、切失败了怎么办。
我画了一张流程图,把整个切换过程串起来:
切换逻辑里有个细节容易被忽略——启动计数。Bootloader 每次从新分区启动时,会在元数据里记一笔。如果启动后系统正常运行了一段时间(比如 60 秒),应用层会发一个「确认成功」的信号给 Bootloader,这时候才真正把新固件标记为「可靠」。
避坑指南:我曾经遇到过一个问题——新固件能启动,但运行 30 秒后必崩溃。如果确认时间设得太短(比如 10 秒),系统就会误判升级成功。后来我把确认窗口设成了 120 秒,并且加入了心跳检测,才算彻底解决。
3.4 切换策略对比
不同的应用场景适合不同的切换策略。我整理了一张表,方便你对照选择:
| 策略名称 | 触发条件 | 回滚方式 | 适用场景 | 风险等级 |
|---|---|---|---|---|
| 立即切换 | 固件校验通过后立即重启 | 启动失败自动回滚 | 对停机时间不敏感的设备 | 低 |
| 延迟切换 | 用户确认或定时触发 | 手动回滚或自动回滚 | 医疗设备、工业控制器 | 极低 |
| 灰度切换 | 先切一部分设备验证 | 批量回滚 | 大规模 IoT 部署 | 中 |
| 强制切换 | 安全补丁、紧急修复 | 禁止回滚 | 安全漏洞修复 | 高 |
我个人最常用的是「延迟切换」。原因很简单——给运维留个缓冲期。比如设备升级后先跑 24 小时,确认没问题了再切过去。万一新固件有隐藏 bug,还能及时回滚。
3.5 实现要点
最后聊几个实现时的关键点,都是我在项目里踩过的坑:
- Bootloader 必须支持 A/B 选择——这是基础中的基础。Bootloader 要能读元数据,判断从哪个 Slot 启动。
- 元数据要防写坏——我建议用双备份,或者用 Flash 的原子写入特性。否则写到一半断电,元数据就废了。
- 升级过程中不要清空旧分区——新固件写进非活动分区,旧固件保留。等新固件确认运行正常后,再考虑是否覆盖旧分区。
- 回滚次数要有限制——防止系统陷入「升级→回滚→升级→回滚」的死循环。一般设 3 次上限,超过后锁定当前版本。
一个小技巧:在元数据里加一个「升级进度」字段。这样即使升级过程中断电重启,Bootloader 也能知道上次升级进行到哪一步,可以断点续传或者直接回滚。
嗯,A/B 系统就讲这么多。这套机制虽然多占了一倍 Flash 空间,但换来的可靠性提升是值得的。尤其是那些部署后很难人工维护的设备——你想想看,一个在楼顶的 5G 基站,升级失败变砖了,爬上去换芯片的成本有多高?