双备份方案(A/B分区):基本原理、分区布局、启动流程
双备份方案,圈里人常叫它 A/B 分区,或者无缝升级。说白了,就是给固件准备两套完整的运行环境。一套在跑,另一套闲着。升级的时候,直接往闲的那套里写新固件,写完了重启切换过去。
我最早接触这个方案是在做车机项目的时候。客户要求升级不能黑屏,不能中断音乐播放。你想想看,传统的单分区方案根本做不到——擦写 Flash 那几秒,系统就是死的。A/B 分区就是为这种场景设计的。
基本原理
核心思想其实很简单:永远保证有一个可启动的固件副本。
系统里有两套完整的固件分区,分别叫 slot A 和 slot B。当前启动的是哪个 slot,另一个 slot 就是待更新的目标。升级时,Bootloader 把新固件写到非活跃 slot,写完后通过一个标志位告诉 Bootloader「下次启动用新的」。如果新固件启动失败,Bootloader 能自动回退到旧的 slot。
关键点:任何时候,至少有一个 slot 是已知可用的。这就是双备份方案的底气。
我见过不少团队在初期觉得双分区浪费空间,想用单分区加压缩的方式省 Flash。结果呢?升级过程中断电,设备变砖,售后成本远高于那几 MB 的 Flash 差价。嗯,这里要注意,空间换可靠性,在固件升级这个领域是铁律。
分区布局
一个典型的 A/B 分区布局长这样:
| 分区名称 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x0000_0000 | 64 KB | 引导加载程序,不可升级 |
| Slot A | 0x0001_0000 | 2 MB | 固件副本 A |
| Slot B | 0x0021_0000 | 2 MB | 固件副本 B |
| Metadata | 0x0041_0000 | 4 KB | 启动状态、版本号、CRC |
| User Data | 0x0041_1000 | 剩余空间 | 用户配置、日志等 |
这里我特别想强调 Metadata 分区。别看它只有 4KB,它才是整个方案的灵魂。里面存了什么?
- 当前活跃 slot 编号:0 表示 slot A,1 表示 slot B
- 尝试计数:新固件启动失败后,Bootloader 靠这个决定是否回退
- 固件 CRC 校验值:启动前验证固件完整性
- 升级状态标志:标记升级是否完成、是否正在升级中
我曾经踩过的坑:Metadata 分区一定要用独立的擦写策略。别跟固件分区共用同一个擦除块。否则升级过程中 Metadata 被意外擦掉,Bootloader 就不知道启动哪个 slot 了。我建议 Metadata 单独放在一个 4KB 的 sector 里,并且每次写入前先读回验证。
启动流程
整个启动流程,我习惯把它拆成四个阶段。每个阶段 Bootloader 都要做决策。
- 读取 Metadata
Bootloader 上电后第一件事,就是读 Metadata 分区。拿到当前活跃 slot 编号、尝试计数、CRC 值。 - 校验固件完整性
计算目标 slot 的固件 CRC,跟 Metadata 里存的值比对。不一致?直接切到另一个 slot。 - 尝试启动
跳转到固件入口。同时把尝试计数加 1,写回 Metadata。这一步很关键——如果新固件启动后崩溃重启,计数已经增加了。 - 固件确认
新固件正常运行后,主动调用一个确认接口,告诉 Bootloader「我跑起来了」。Bootloader 收到确认后,把尝试计数清零。
为什么会设计尝试计数?我解释一下。假设新固件有 bug,启动到一半就挂了。系统复位后,Bootloader 发现尝试计数已经变成 1,超过阈值(通常是 3),就判定这个 slot 不可用,自动切回旧 slot。这就是回滚的触发机制。
个人经验:尝试计数的阈值别设太大。3 次足够了。设成 10 次,意味着设备要反复重启 10 次才回滚,用户体验极差。我一般设 3 次,配合 5 秒的看门狗超时,15 秒内就能完成回滚决策。
下面这张图把整个流程串起来了:
流程里有个细节容易被忽略:固件确认的时机。我见过有人把确认放在 main 函数的第一行。这不对。万一 main 函数之前就崩了呢?正确的做法是:等系统所有关键初始化完成,外设自检通过,再调用确认。我一般放在任务调度器启动之后,确保 RTOS 已经跑起来了。
总结一下双备份方案的核心优势:
- 升级过程中设备功能不中断(无缝升级)
- 升级失败自动回滚,不会变砖
- 可以在后台下载固件,用户无感知
当然,代价也很明显——Flash 空间翻倍。对于 Flash 紧张的项目,这是个不小的负担。但话说回来,现在 Flash 越来越便宜,用一倍的空间换一个「永不砖机」的承诺,我个人觉得值。