3. OTA系统总体架构设计

OTA 系统不是“把新固件写进 Flash”这么简单,它是一套由多个存储区域、多个执行阶段和一套状态机共同约束的协同系统。任何一个区域划分不合理、元数据字段缺失或状态迁移条件不严谨,都可能导致升级中断后设备无法启动。本节从 Flash 分区入手,说明 Bootloader、App、备份区、参数区、版本元数据与升级状态机之间的职责边界和协作方式。

一、Flash 分区:先定边界,再谈逻辑

以 STM32F407(1 MB 内部 Flash,扇区 0~11,扇区大小 16 KB/64 KB/128 KB 不等)为例,一种常见的工程分区如下(示例参数,需按实际芯片和固件体积调整):

区域起始地址大小职责
Bootloader0x0800000064 KB上电入口、校验、搬运、跳转
参数区0x0801000032 KB升级标志、重试计数、版本元数据
App 运行区0x08018000384 KB当前运行的应用固件
备份/下载区0x08078000384 KB接收新固件、暂存待校验镜像
预留区0x080D8000160 KB日志、双备份或后续扩展

分区必须与芯片扇区边界对齐,否则擦除时会误伤相邻区域。Bootloader 放在最低地址,是因为 Cortex-M 复位后从 0x08000000 取向量表;App 运行区起始地址必须写入 App 的链接脚本,并在跳转前重定位向量表(设置 SCB->VTOR)。

STM32 OTA 总体架构与协同关系 Flash 物理分区(1 MB) Bootloader 64 KB 参数区 32 KB App 运行区 384 KB 备份/下载区 384 KB 预留区 160 KB 参数区字段(版本元数据 + 升级控制) magic/校验 0x4F544131 fw_version v1.2.3 fw_size 字节数 fw_crc32 镜像校验 upgrade_flag 0/1/2 retry_cnt / state 重试与状态 升级状态机(参数区持久化) IDLE 空闲 DOWNLOADING VERIFYING READY_TO_SWAP FAIL 开始 收完 CRC OK CRC 错 校验失败重试(retry_cnt++) 协同关系 Bootloader 读参数区 按状态决定搬运/跳转 备份区→App 运行区 更新元数据并跳转

二、Bootloader:只做“判断与搬运”,不做业务

Bootloader 的职责边界要刻意收窄,它只负责四件事:读取参数区、校验镜像、搬运固件、跳转 App。它不应包含通信协议栈、文件系统或业务逻辑,否则一旦 Bootloader 自身需要升级,风险会成倍放大。

上电后的典型判断顺序是:先读参数区 magic 字段,若 magic 无效则视为首次上电,直接跳转 App;若 magic 有效且 upgrade_flag == 1,说明存在待安装镜像,进入校验流程;校验通过后把备份区内容按页搬运到 App 运行区,搬运完成后回写 upgrade_flag = 0、更新 fw_version,再跳转。若校验失败,retry_cnt 加一,超过阈值(例如 3 次)则清除升级标志并回退到旧 App,避免设备反复卡在升级循环中。

Bootloader 跳转前必须关闭已开启的中断、复位 SysTick,并设置 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 表示超过重试阈值。每次状态迁移都先写参数区再执行动作,保证“动作未完成时状态已记录”,重启后可据此续做或回退。

状态迁移必须遵循“先记录、后执行”的顺序。若先搬运再写状态,搬运中途断电会导致状态仍为 DOWNLOADING,但 App 运行区已被部分覆盖,旧版本也无法启动,设备将变砖。

六、一次完整升级的协同时序

以 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 错误或版本回滚场景中暴露为不可恢复的启动故障。