系统架构概览:Bootloader + 双App分区布局

好,咱们正式开始聊双备份升级方案的具体实现。这一章我带你从高空俯瞰整个系统的骨架——说白了,就是搞清楚 Flash 里到底怎么划分地盘,Bootloader 和两个 App 各自住在哪,以及上电后它们是怎么配合工作的。

我记得刚入行那会儿,做第一个带 OTA 的项目,根本没想那么多。Flash 分区随便一划,App 就一个区。结果升级到一半断电了,板子直接变砖。那次之后我才真正意识到——没有双备份的升级方案,就是在裸奔。

Flash 分区规划

先看一张我常用的分区布局图。这张图我几乎在每个项目里都会贴到设计文档的第一页。

典型 2MB Flash 分区规划(以 STM32H743 为例)

Flash 分区布局图(2MB 总容量) Bootloader 区 (128KB) App A 区 (768KB) 当前运行版本 / 主工作区 App B 区 (768KB) 备份区 / 升级目标区 参数 / 标志位区 (64KB) 总容量:128KB + 768KB + 768KB + 64KB = 1728KB(预留 320KB 给文件系统或日志)

这张图你看懂了吗?从上到下依次是:

  • 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 空间,但换来的可靠性是值得的。尤其对于无人值守的物联网设备,这个投入非常划算。

启动流程总览

好,分区搞清楚了,接下来看看系统上电后到底怎么跑。我把启动流程总结成下面这张图,你跟着箭头走一遍就明白了。

系统启动流程总览 上电 / 复位 Bootloader 启动:读取标志位 App A 有效? 是 跳转到 App A 否 检查 App B App B 有效? 是 跳转到 App B 否 进入恢复模式 / 等待升级

启动流程其实就这几步:

  1. 上电复位,CPU 从复位向量直接跳转到 Bootloader 入口。
  2. Bootloader 读取标志位,检查参数区里记录的「上次运行状态」和「升级请求」。
  3. 校验 App A,如果 CRC 和签名都通过,直接跳转过去。
  4. 如果 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 就像接力赛的队友。交接棒的时候,位置要对、时机要准、信息要清。任何一个环节出问题,整个系统就趴窝了。

嗯,这一章的内容差不多就这些。分区布局、启动流程、标志位设计,这三块是双备份方案的基石。你先把这些概念吃透,后面讲具体代码实现的时候,你就能理解每一行代码为什么这么写了。