3. OTA系统总体架构设计
OTA 系统不是“把新固件写进 Flash”这么简单,它是一套由多个存储区域、多个执行阶段和一套状态机共同约束的协同系统。任何一个区域划分不合理、元数据字段缺失或状态迁移条件不严谨,都可能导致升级中断后设备无法启动。本节从 Flash 分区入手,说明 Bootloader、App、备份区、参数区、版本元数据与升级状态机之间的职责边界和协作方式。
一、Flash 分区:先定边界,再谈逻辑
以 STM32F407(1 MB 内部 Flash,扇区 0~11,扇区大小 16 KB/64 KB/128 KB 不等)为例,一种常见的工程分区如下(示例参数,需按实际芯片和固件体积调整):
| 区域 | 起始地址 | 大小 | 职责 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64 KB | 上电入口、校验、搬运、跳转 |
| 参数区 | 0x08010000 | 32 KB | 升级标志、重试计数、版本元数据 |
| App 运行区 | 0x08018000 | 384 KB | 当前运行的应用固件 |
| 备份/下载区 | 0x08078000 | 384 KB | 接收新固件、暂存待校验镜像 |
| 预留区 | 0x080D8000 | 160 KB | 日志、双备份或后续扩展 |
分区必须与芯片扇区边界对齐,否则擦除时会误伤相邻区域。Bootloader 放在最低地址,是因为 Cortex-M 复位后从 0x08000000 取向量表;App 运行区起始地址必须写入 App 的链接脚本,并在跳转前重定位向量表(设置 SCB->VTOR)。
二、Bootloader:只做“判断与搬运”,不做业务
Bootloader 的职责边界要刻意收窄,它只负责四件事:读取参数区、校验镜像、搬运固件、跳转 App。它不应包含通信协议栈、文件系统或业务逻辑,否则一旦 Bootloader 自身需要升级,风险会成倍放大。
上电后的典型判断顺序是:先读参数区 magic 字段,若 magic 无效则视为首次上电,直接跳转 App;若 magic 有效且 upgrade_flag == 1,说明存在待安装镜像,进入校验流程;校验通过后把备份区内容按页搬运到 App 运行区,搬运完成后回写 upgrade_flag = 0、更新 fw_version,再跳转。若校验失败,retry_cnt 加一,超过阈值(例如 3 次)则清除升级标志并回退到旧 App,避免设备反复卡在升级循环中。
SCB->VTOR = APP_BASE,否则 App 的中断向量会指向 Bootloader 的向量表,出现“能跳转但一进中断就死”的典型故障。三、App:升级的发起者与状态上报者
App 运行期间承担升级的“客户端”角色:接收新固件分片、写入备份区、计算 CRC、更新参数区状态。它不直接擦写自己所在的运行区,这是保证“升级失败仍可运行旧版本”的关键。App 在写入每个分片后应更新参数区的 fw_size 与已接收长度,使断电后 Bootloader 或 App 能判断下载是否完整。
App 还需要在启动后读取参数区,若发现 upgrade_flag == 1 且自身版本与元数据不一致,说明上次升级未完成,应主动上报或重新触发下载。版本元数据中的 fw_version、fw_crc32、fw_size 三者必须同时匹配,缺一不可:只比版本号会被“同版本不同构建”欺骗,只比 CRC 又无法做版本回滚决策。
四、备份区与参数区的分工
备份区是“数据面”,只存固件字节;参数区是“控制面”,只存状态与元数据。两者分离的好处是:擦写备份区不会影响升级标志,更新标志也不需要动大块 Flash。参数区建议采用“双副本 + 序号”结构,每次写入交替使用 A/B 两份并递增序号,读取时取序号较大且 CRC 正确的一份,这样即使写入过程中断电,也至少有一份完整记录。
typedef struct {
uint32_t magic; // 0x4F544131
uint32_t seq; // 双副本序号,递增
uint8_t fw_version[16];
uint32_t fw_size;
uint32_t fw_crc32;
uint8_t upgrade_flag; // 0=无 1=待安装 2=已安装待确认
uint8_t state; // 状态机当前状态
uint8_t retry_cnt;
uint8_t reserved;
uint32_t crc; // 本结构体校验
} ota_param_t;
五、状态机:把“可能出错的每一步”显式化
状态机的价值在于把隐式的流程变成可持久化、可恢复的显式状态。IDLE 表示无升级任务;DOWNLOADING 表示正在向备份区写入;VERIFYING 表示整包 CRC 校验;READY_TO_SWAP 表示镜像已就绪、等待重启搬运;FAIL 表示超过重试阈值。每次状态迁移都先写参数区再执行动作,保证“动作未完成时状态已记录”,重启后可据此续做或回退。
六、一次完整升级的协同时序
以 v1.2.3 升级到 v1.3.0 为例:App 收到升级指令后置 upgrade_flag=1、state=DOWNLOADING,分片写入备份区并累计长度;收完后计算整包 CRC32,与服务器下发的 fw_crc32 比对,一致则置 state=READY_TO_SWAP 并重启。Bootloader 上电读到 READY_TO_SWAP,校验备份区 CRC,通过后搬运到 App 运行区,更新 fw_version=v1.3.0、upgrade_flag=0、state=IDLE,跳转新 App。新 App 首次启动可置 upgrade_flag=2 表示“待确认”,运行稳定一段时间后再清零,形成可回滚的确认机制。
这套架构的核心不是某个单独模块,而是“分区边界清晰、元数据完整、状态可持久化、失败可回退”四者同时成立。任何一处缺失,都会在断电、CRC 错误或版本回滚场景中暴露为不可恢复的启动故障。