系统架构概览:Bootloader + 双App分区布局
好,咱们正式开始聊双备份升级方案的具体实现。这一章我带你从高空俯瞰整个系统的骨架——说白了,就是搞清楚 Flash 里到底怎么划分地盘,Bootloader 和两个 App 各自住在哪,以及上电后它们是怎么配合工作的。
我记得刚入行那会儿,做第一个带 OTA 的项目,根本没想那么多。Flash 分区随便一划,App 就一个区。结果升级到一半断电了,板子直接变砖。那次之后我才真正意识到——没有双备份的升级方案,就是在裸奔。
Flash 分区规划
先看一张我常用的分区布局图。这张图我几乎在每个项目里都会贴到设计文档的第一页。
典型 2MB Flash 分区规划(以 STM32H743 为例)
这张图你看懂了吗?从上到下依次是:
- Bootloader 区:128KB,存放启动代码。它负责校验、跳转和升级流程控制。
- App A 区:768KB,当前主版本固件。正常运行时,代码就在这里执行。
- App B 区:768KB,备份区。平时闲置,升级时写入新固件。
- 参数/标志位区:64KB,存放启动次数、升级状态、CRC 校验值等关键信息。
我的经验:App 区大小怎么定?我一般按实际固件体积的 1.5 倍来预留。比如你的固件编译出来 400KB,那 App 区至少给 600KB。别卡太死,后期加功能你会感谢我的。
为什么需要两个 App 区?
你可能会问:一个 Bootloader 加一个 App 不行吗?行,但风险很高。
我举个例子。有一次做智能网关项目,产品已经量产了。远程升级时网络突然中断,App 区只写了一半。重启后 Bootloader 发现 App 校验失败,直接卡死在等待恢复模式。用户设备离线了整整两天,直到我们派人现场用 J-Link 重新烧录。
双备份的核心思想就是:升级过程中永远保留一个可工作的版本。
| 方案 | 升级中断后能否恢复 | Flash 占用 | 实现复杂度 |
|---|---|---|---|
| 单 App 区 | ❌ 变砖风险高 | 低 | 低 |
| 双 App 区(A/B 切换) | ✅ 自动回滚 | 中(多一倍) | 中 |
| 外部存储 + 单 App | ✅ 可恢复 | 中 | 高 |
双备份方案虽然多占了一倍 Flash 空间,但换来的可靠性是值得的。尤其对于无人值守的物联网设备,这个投入非常划算。
启动流程总览
好,分区搞清楚了,接下来看看系统上电后到底怎么跑。我把启动流程总结成下面这张图,你跟着箭头走一遍就明白了。
启动流程其实就这几步:
- 上电复位,CPU 从复位向量直接跳转到 Bootloader 入口。
- Bootloader 读取标志位,检查参数区里记录的「上次运行状态」和「升级请求」。
- 校验 App A,如果 CRC 和签名都通过,直接跳转过去。
- 如果 App A 挂了,就去检查 App B。B 有效就跳 B,B 也无效就进恢复模式。
注意:这里有个关键点——Bootloader 本身不能太复杂。我见过有人把 TCP/IP 协议栈塞进 Bootloader,结果 Bootloader 自己就占了 300KB。这完全违背了「轻量可靠」的原则。Bootloader 只做三件事:校验、跳转、触发升级。别的功能一概不要。
标志位设计
参数区里存的标志位,是整个双备份方案的「大脑」。我常用的标志位定义如下:
| 偏移地址 | 名称 | 长度 | 说明 |
|---|---|---|---|
| 0x0000 | boot_count | 4 字节 | 启动次数计数器,用于看门狗恢复检测 |
| 0x0004 | update_flag | 4 字节 | 0x5A5A5A5A 表示请求升级,0x00000000 表示正常启动 |
| 0x0008 | active_slot | 4 字节 | 0 表示 App A 为主,1 表示 App B 为主 |
| 0x000C | crc_app_a | 4 字节 | App A 区的 CRC32 校验值 |
| 0x0010 | crc_app_b | 4 字节 | App B 区的 CRC32 校验值 |
避坑指南:我曾经踩过一个坑——标志位只用了一个字节,结果 Flash 擦写次数多了之后,那个字节意外翻转了。Bootloader 误判成升级请求,直接把正常运行的 App 覆盖了。后来我改成 4 字节的魔数校验,再也没出过问题。记住:关键标志位一定要用冗余校验,比如存两遍副本,或者用 0x5A5A5A5A 这种魔数。
Bootloader 与 App 的交互约定
Bootloader 和 App 之间不是各干各的,它们需要一些约定才能配合好。
- 中断向量表偏移:App 编译时必须指定偏移地址。比如 App A 从 0x08020000 开始,那它的 VTOR 寄存器就要设成这个值。
- 栈指针传递:Bootloader 跳转前,要把 MSP 设置成 App 的栈顶地址。这个地址通常就在 App 镜像的前 4 个字节。
- 全局状态共享:我习惯在 RAM 里保留一块固定区域(比如 0x20000000 ~ 0x200000FF),Bootloader 把升级结果、复位原因写在这里,App 启动后直接读取。
说白了,Bootloader 和 App 就像接力赛的队友。交接棒的时候,位置要对、时机要准、信息要清。任何一个环节出问题,整个系统就趴窝了。
嗯,这一章的内容差不多就这些。分区布局、启动流程、标志位设计,这三块是双备份方案的基石。你先把这些概念吃透,后面讲具体代码实现的时候,你就能理解每一行代码为什么这么写了。