2. 设备端升级架构设计

OTA 能否安全落地,关键不在云端推送,而在设备端是否有一套“可切换、可验证、可回退”的升级架构。这一节我们把设备端拆成四个相互咬合的部件:Bootloader、分区表、固件镜像格式、启动切换与回滚策略。它们共同回答一个问题——新固件写进去之后,设备凭什么敢启动它,又凭什么在它失败时退回旧版本。

Bootloader:升级架构的信任起点

Bootloader 是上电后最先运行的代码,职责是初始化最小硬件、读取启动标志、校验并跳转到应用固件。它必须足够小、足够稳,且自身通常不参与 OTA 更新,否则一旦刷坏就彻底变砖。工程上常把它分为两级:一级 Bootloader(BL1)固化在 ROM 或受保护扇区,只负责加载二级 Bootloader;二级 Bootloader(BL2)实现分区管理、镜像校验、启动计数与回滚决策。这样即使 BL2 升级失败,BL1 仍能恢复它。

Bootloader 需要具备的最小能力包括:读取分区表、计算镜像哈希、验证签名、维护启动标志、支持串口或 USB 恢复通道。以常见的 MCU 方案为例,BL2 可占用 64 KB Flash,应用分区各 512 KB,升级缓存分区 512 KB,参数区 8 KB 用于存放启动标志和版本信息。

分区表:把 Flash 划成有明确职责的区域

分区表定义了每块 Flash 区域的偏移、大小和用途。OTA 场景下最典型的是 A/B 双分区布局:A 分区运行当前固件,B 分区接收新固件;升级成功后切换启动目标,失败则保留原分区继续运行。相比单分区“原地覆盖”,双分区牺牲一半空间,换来的是断电安全和可回滚。

一个可参考的分区表示例如下(以 2 MB Flash 为例):

分区名起始偏移大小用途
bootloader0x00000064 KBBL2,含校验与切换逻辑
slot_a0x010000768 KB应用固件 A
slot_b0x0D0000768 KB应用固件 B
params0x1900008 KB启动标志、版本、重试计数
factory0x192000剩余空间出厂恢复固件

分区表通常以固定结构体或 JSON 描述,编译时生成并烧录到固定偏移。Bootloader 与应用程序必须使用同一份分区定义,否则会出现“写对了地址、读错了分区”的隐蔽故障。

固件镜像格式:让 Bootloader 能读懂、能验证

裸 bin 文件没有元数据,Bootloader 无法判断它属于哪个版本、是否完整、是否被篡改。因此 OTA 镜像通常采用“头部 + 负载 + 尾部”的封装格式。头部包含魔数、格式版本、目标型号、固件版本、负载长度、负载哈希、签名算法与签名值;负载是实际可执行代码;尾部可放整镜像 CRC。

typedef struct {
    uint32_t magic;        // 0x4F544131 "OTA1"
    uint16_t format_ver;   // 镜像格式版本
    uint16_t target_id;    // 硬件型号标识
    uint32_t fw_version;   // 固件版本号
    uint32_t payload_len;  // 负载长度
    uint8_t  payload_hash[32]; // SHA-256
    uint8_t  signature[64];    // ECDSA P-256
} ota_header_t;

Bootloader 的校验顺序应为:魔数与型号匹配 → 头部哈希比对 → 签名验证 → 负载哈希比对。任何一步失败都不得切换启动目标。签名验证保证来源可信,哈希比对保证传输完整,两者缺一不可。

启动切换机制与回滚策略

启动切换依赖参数区中的一组标志:active_slot、pending_slot、boot_attempts、confirmed。典型流程是:新固件写入 B 分区并校验通过后,Bootloader 将 pending_slot 设为 B、boot_attempts 清零;下次启动时尝试引导 B,并递增尝试计数。若应用在限定次数内调用“确认接口”将 confirmed 置位,则 B 转为正式分区;若连续尝试超过阈值仍未确认,Bootloader 判定启动失败,回退到 A 分区并清除 pending 标志。

建议将 boot_attempts 阈值设为 3。应用启动后应在完成网络、外设自检后尽快确认,避免“能启动但功能异常”的固件被误判为成功。

回滚策略还需考虑“确认后仍出问题”的场景。可在参数区保留上一版本槽位信息,并提供手动回滚命令;同时记录每次启动的复位原因与版本号,便于云端判断设备是否处于反复回滚状态。若 A、B 均不可用,则进入 factory 恢复分区,通过串口或本地接口重新烧录。

设备端 OTA 启动切换与回滚架构 Flash 分区布局 Bootloader Slot A 当前运行 v1.2 Slot B 待验证 v1.3 Params Factory 恢复区 Params 关键字段 active_slot = A pending_slot = B boot_attempts = 0 confirmed = 0 fw_version = 1.2.0 prev_slot = A max_attempts = 3 reset_reason = OTA 启动决策与回滚路径 上电 / 复位 读取 pending_slot 校验镜像签名 尝试引导 Slot B boot_attempts++ 应用确认成功 confirmed = 1 B 转正 attempts ≥ 3 回退 Slot A 清除 pending 上报回滚事件 回滚至 Slot A 继续运行 成功路径 回滚路径 待验证状态

把上述四部分串起来看:分区表决定“写到哪里”,镜像格式决定“写进去的东西是否可信”,Bootloader 决定“启动谁”,启动标志与回滚策略决定“启动失败怎么办”。四者缺一,OTA 就只是“把文件传下去”,而不是“把设备安全地升级上去”。实际项目中,建议先用串口手动触发一次 A/B 切换与回滚,确认参数区读写、签名校验和计数逻辑都正确,再接入云端 OTA 流程。