15 KiB
PY32F040 开发框架 — OTA 升级设计(三模式可配置)
本框架提供可配置的三种 OTA 模式:单备份 / 双备份+RAM 交换 / 双备份+Flash 暂存区交换。 协议对接 jb_protocol(见宝 IOT 协议,OTA 命令 0x30/0x32/0x34),协议层原样使用,无需修改。
0. 模式配置(唯一入口:Shared/ota_config.h,APP 与 Bootloader 共用)
/* ① 备份模式 —— 唯一必选开关 */
#define OTA_MODE_SINGLE 0 /* 单备份:BAK 下载→校验→拷贝覆盖主区 */
#define OTA_MODE_AB 1 /* 双备份:A/B 区交换(新固件崩溃可回滚旧固件)*/
#define OTA_BACKUP_MODE OTA_MODE_AB
/* ═══ 以下 ②③ 仅 OTA_BACKUP_MODE == OTA_MODE_AB 时编译(#if 保护)═══ */
#if (OTA_BACKUP_MODE == OTA_MODE_AB)
/* ② 双备份交换策略 */
#define OTA_SWAP_SCRATCH 0 /* Flash 暂存区交换:升级快、掉电恢复最稳 */
#define OTA_SWAP_RAM 1 /* RAM 缓冲交换:不预留暂存区,A/B 保持 58K */
#define OTA_SWAP_STRATEGY OTA_SWAP_SCRATCH
/* ③ 暂存区参数(仅 SCRATCH 生效,需页对齐) */
#if (OTA_SWAP_STRATEGY == OTA_SWAP_SCRATCH)
#define OTA_SCRATCH_ADDR 0x0801F800UL
#define OTA_SCRATCH_SIZE OTA_PAGE_SIZE /* 暂存区只需 1 页:交换时每次仅暂存 1 页(256B) */
#endif
#endif
布局宏、状态结构体(seq/progress/phase 字段)、OTA_STATE_SWAPPING 状态
均由上述 #if 自动联动:单备份模式下不存在的配置/字段/状态根本不会编译,
只需改 OTA_BACKUP_MODE 一个开关即可,无需关心哪些配置生效。
三种模式均只编译/上传一份固件(链接运行区 0x08003000),BLE 按原协议流程发同一份 bin, 不需要 active_slot、不需要打包路由。
| 模式 | 固件份数 | 回滚 | 升级耗时 | 掉电安全 | 布局 |
|---|---|---|---|---|---|
| 单备份 | 1 | ❌ | ~1s | 拷贝中掉电=主区需重烧 | 主区 58K + BAK 58K |
| 双备份+RAM | 1 | ✅ | 7~12s | 位图恢复,最坏=升级失败回旧版 | A/B 各 58K,无暂存区 |
| 双备份+暂存区 | 1 | ✅ | 1~3s | 最稳(scratch 持久) | A/B 各 57K + scratch 1页 |
1. Flash 布局(128KB,PY32F040xB,页 256B)
模式 ① 单备份 / 模式 ② 双备份+RAM(布局相同):
0x08000000 ┌─────────────────────┐
│ Bootloader │ 11KB (0x00000 ~ 0x02C00) 固定,芯片复位入口
0x08002C00 ├─────────────────────┤
│ OTA 状态区 │ 1KB (0x02C00 ~ 0x03000) 状态机 + 固件信息
0x08003000 ├─────────────────────┤
│ 运行区 (主区/A区) │ 58KB (0x03000 ~ 0x11800) 固件链接地址,出厂固件
0x08011800 ├─────────────────────┤
│ 备份区 (BAK/B区) │ 58KB (0x11800 ~ 0x20000) 下载目标 / 回滚备份
0x08020000 └─────────────────────┘ (128KB 边界,A+B 正好填满)
模式 ③ 双备份+暂存区(A/B 各 57K,scratch 1页放 Flash 最底部):
0x08000000 Bootloader(11K) | 0x08002C00 状态区(1K)
0x08003000 A 区(57K) | 0x08011400 B 区(57K) | 0x0801F800 scratch(1页, 最底部)
0x0801FC00 剩余空闲
2. OTA 状态区(0x08002C00,1KB = 4 页,策略由备份模式决定)
写入频率差异决定策略(由 Shared/flash.c 的 ota_param_load/store 实现):
| 模式 | 每次升级状态区写入次数 | 10K 次擦写寿命可撑 | 策略 |
|---|---|---|---|
| 双备份 AB(swap) | 约 440 次(每页交换 2 次记 phase/progress) | 约 23 次升级 | 4 页轮换磨损均衡 |
| 单备份 SINGLE | 2~3 次(无逐页交换过程) | 数千次升级 | 固定页 0 直写 |
AB 模式(4 页轮换):
每页副本格式:[ota_param_t(含 seq,28B)][0xFF 填充]
写:顺序写下一页(seq 单调递增,uint32 实际不会溢出)
读:扫描 4 页,取"seq 最大且 magic+CRC16 校验通过"的副本
- 磨损均摊到 4 页(÷4):升级可承受次数 23 → 约 92 次(按 10K 保守寿命;普冉标称值可能更高);
- 擦除任一页时其他 3 页仍有有效数据,擦除窗口掉电安全;最新页损坏自动回退次新状态;
- 若量产需要更大余量,可把
OTA_PARAM_ADDR_BASE前移扩大状态区页数(Bootloader 相应缩小链接)。
SINGLE 模式(固定页 0): 每次写入直接擦写页 0(结构体无 seq 字段,无轮换),简单且寿命足够。
注:
seq存放在ota_param_t结构体内部(AB 模式首字段,参与 CRC16 保护), 坏 seq 会被 CRC16 拒绝并回退次新副本,比单独放在页头更稳。 结构体由#if条件编译:AB 28B / SINGLE 20B。
#define OTA_MAGIC 0xA55A
#define OTA_STATE_IDLE 0x00 /* 正常运行 */
#define OTA_STATE_PENDING 0x01 /* 新固件已下载,待 Bootloader 校验/拷贝/交换 */
#if (OTA_BACKUP_MODE == OTA_MODE_AB)
#define OTA_STATE_SWAPPING 0x02 /* 双备份:交换进行中(掉电恢复入口)*/
#endif
#define OTA_STATE_BOOT_NEW 0x03 /* 已切到新固件,等待 APP 确认(未确认=回滚)*/
typedef struct {
#if (OTA_BACKUP_MODE == OTA_MODE_AB)
uint32_t seq; /* 写入序号:4 页轮换判新(参与 CRC16) */
#endif
uint16_t magic; /* 0xA55A,有效标志 */
uint8_t state; /* 状态机(见上) */
uint8_t rsv;
uint32_t fw_size; /* 新固件长度(字节) */
uint32_t fw_crc32; /* 新固件 CRC32 */
uint32_t fw_version; /* 新固件版本(透传) */
#if (OTA_BACKUP_MODE == OTA_MODE_AB)
uint16_t progress; /* 双备份:交换已完成页数(掉电恢复) */
uint8_t phase; /* 双备份:交换阶段标志(掉电恢复) */
#endif
uint8_t rsv2;
uint16_t crc16; /* 结构体自身 CRC16 保护(含 seq,读取无效视为 IDLE) */
} ota_param_t; /* AB 28B / SINGLE 20B(OTA_PARAM_SIZE 自动) */
无 active_slot / target_slot:运行区固定 = OTA_RUN_ADDR_BASE(0x08003000),
设备无需知道"当前在哪个区",目标区固定 = OTA_BAK_ADDR_BASE。
3. 状态机(核心)
双备份(AB,swap):
IDLE(运行区跑旧固件)
│ 0x30 通知 → 校验版本/空间 → 应答 JB_OK
│ 0x32 分片 → 写入备份区(固定) + 累计 CRC32
│ 收完 CRC32==fwCrc32 → 写 {PENDING} → 复位
▼
Bootloader:
PENDING → 校验备份区(向量+CRC32)
├ 通过 → 写 {SWAPPING} → 交换 A/B(逐页)→ 写 {BOOT_NEW} → 使能 IWDG → 跳运行区
└ 失败 → 写 {IDLE} → 跳运行区(旧固件)
SWAPPING → 交换掉电中断 → 续做交换(progress/phase 恢复)→ {BOOT_NEW} → 跳转
BOOT_NEW → 新固件未确认(崩溃) → 反向交换回滚 → 写 {IDLE} → 跳运行区
IDLE/无效 → 跳运行区
▼
新固件(运行区):
启动 → 外设自检 → app_boot_ota_report() 确认并上报 0x34「OTA升级完成」
崩溃 → IWDG 复位 → Bootloader 见 BOOT_NEW 未确认 → 回滚
单备份:
IDLE → PENDING → Bootloader 校验 BAK → 拷贝 BAK→主区 → 写 {BOOT_NEW} → 跳主区
新固件启动 → 确认 → 回「OTA 升级完成」(无回滚目标,BOOT_NEW 未确认也继续运行)
拷贝中途掉电 → PENDING 未清,下次重新校验+拷贝(BAK 完好,天然安全)
4. 关键机制
4.1 “切换成功”判定
采用 BOOT_NEW 状态 + 独立看门狗(IWDG) + APP 自检确认:
- Bootloader 交换/拷贝完成后写
BOOT_NEW并使能 IWDG(约 1s),交换期间逐页喂狗; - 新固件
app_Init()完成后,app_boot_ota_report()调用ota_boot_check_confirm():见 BOOT_NEW → 写确认(IDLE) → 上报 0x34; - 新固件启动即崩溃 → IWDG 复位 → Bootloader 见 BOOT_NEW 未确认 → 双备份反向交换回滚;单备份无回滚目标,继续运行;
- 确认后运行区正式生效,备份区保留旧固件作为下次回滚备份。
4.2 单份固件 + swap 交换(双备份核心)
固件为绝对地址链接(运行区 0x08003000),只编译一份。双备份不再"双区跳转", 而是 Bootloader 把 A/B 内容交换:
- 正向(部署新):A=旧,B=新 → A=新(能跑),B=旧(备份)
- 反向(回滚):A=新(崩溃),B=旧 → A=旧(能跑),B=新
RAM 缓冲交换(A/B 各 58K 布局):每页用 RAM 双缓冲(512B)交换,逐页写
progress/phase 到状态区。掉电恢复:phase==1 时 B 页仍完好(新固件页),从 B 重建 A,
放弃本页旧备份——保证新固件可收敛;最坏情况=升级成功但该页回滚失效。
Flash 暂存区交换(scratch,A/B 各 57K):交换前先把被覆盖页存入 scratch(Flash 持久, 位于 Flash 最底部 0x0801F800,仅占 1 页),掉电后可完整恢复两页,回滚永远可用。
- 构建方式(重要):Keil 命令行(UV4)构建的链接地址由 Target 对话框 ROM1
(uvprojx 中
<OCR_RVCT4>标签) 决定,散列文件(ScatterFile)在命令行模式下不生效。tools/build_all.py将 APP 链接固定为 0x08003000。 - Bootloader 校验复位向量必须落在运行区地址范围,从硬件层面兜底"固件地址不匹配"。
4.3 中断向量表(PY32F040 带 VTOR)
- Bootloader 跳转 APP 前设置
SCB->VTOR并关中断;APP 启动时__enable_irq()恢复; - APP
system_py32f040.c中SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET(0x08003000)。
4.4 固件校验(CRC32)
- 协议 0x30 携带
fwCrc32,APP 接收分片时累计 CRC32,收完与fwCrc32比对; - Bootloader 启动前重新计算备份区 CRC32 验证;
- CRC32 采用标准 CRC-32/IEEE(多项式 0x04C11DB7,初值 0xFFFFFFFF,输出异或 0xFFFFFFFF),APP 与 Bootloader 共用
Shared/crc32.c。
4.5 协议层(原样使用)
- 0x31 应答保持协议原格式(3 字节:结果码 + MaxDataLen 2B),无 active_slot;
- 0x30 应答支持用户层拒绝:
jbOnOtaNotify返回结果码(0=接受,非 0=拒绝码如JB_OTA_NO_NEED/JB_OTA_VER_LOW/JB_OTA_SPACE_ERR),按返回值构造 ACK; jbOnOtaData已带shardIdx/totalShards,用户层据此计算写入偏移。
5. 升级完整时序(双备份+RAM 为例)
BLE/APP MCU APP(运行区) Bootloader Flash
│ 0x30 通知 │ │ │
│ ────────────────────► │ 校验版本/空间 │ │
│ ◄── 0x31 应答(3B) │ │ │
│ 0x32 分片×N ────────► │ 写备份区 + 累计 CRC32 │ 写B区
│ ◄── 0x32 ACK │ │ │
│ 数据收完 │ CRC32 校验 → 写状态区 │ 写状态区
│ │ {PENDING} → 复位 ────────────► │ │
│ │ │ 校验 B 区(向量+CRC) │
│ │ │ 交换 A/B(逐页) │
│ │ │ 写 {BOOT_NEW} + IWDG │
│ │ ◄── 跳转运行区 ────────────────│ │
│ │ 自检 → 确认 → 回 0x34 完成 │ 写状态区
│ ◄── 0x34 完成(JB_OK) ─│ │ │
└───────────────────────┴──────────────────────────────┴──────────────────────┘
若新固件崩溃:IWDG 复位 → Bootloader 见 BOOT_NEW 未确认 → 反向交换回滚旧固件
6. 文件清单
共享层 Shared/(APP 与 Bootloader 共用,单一真源)
| 文件 | 说明 |
|---|---|
Shared/ota_config.h |
三模式唯一配置入口:模式开关 + 布局宏 + 状态结构/状态机常量 |
Shared/flash.c + 各工程 flash.h |
OTA 状态区读写、Flash 擦写 |
Shared/crc32.c/.h |
CRC32 算法 |
Shared/iwdg_config.h |
IWDG 超时参数(BL/APP 一致) |
Shared/log.h |
日志宏 |
APP 工程 App/
| 文件 | 说明 |
|---|---|
Core/ota_ab.c/.h |
OTA 下载:分片写入备份区 / 校验 / 写 PENDING |
Core/iwdg.c/.h |
IWDG HAL 封装(启动接管 + 主循环喂狗) |
Core/app_boot.c/.h |
启动流程:中断交接 / 时钟 / IWDG / OTA 确认与 0x34 上报 |
Src/main.c |
入口:app_startup() → app_lication() |
Src/application.c |
应用初始化 + 主循环 |
Protocol/jb_product.c |
实现 jbOnOtaNotify / jbOnOtaData / jbOnOtaStop |
MDK-ARM/project.uvprojx |
Keil 工程,ROM1 = 0x08003000 |
Bootloader 工程 Bootloader/
| 文件 | 说明 |
|---|---|
Src/boot_main.c |
三模式状态机:单备份拷贝 / 双备份交换(RAM 与 scratch,按宏裁剪) |
Code/flash.h |
Flash 头文件(实现引用 Shared/flash.c) |
MDK-ARM/Bootloader.uvprojx |
Keil 工程,ROM1 = 0x08000000 |
构建/工具
| 文件 | 说明 |
|---|---|
build.bat |
一键编译 Bootloader + APP |
tools/build_all.py |
构建脚本,产出 hex/bin 并打印 fwCrc32 |
tools/merge_hex.py |
合并 bootloader + app hex(出厂烧录) |
tools/pack.py |
打包可分发 zip(工程 + Drivers + Shared) |
7. 升级失败/异常场景汇总
| 场景 | 单备份 | 双备份+RAM | 双备份+暂存区 |
|---|---|---|---|
| 下载中掉电 | 备份区半写,主区不受影响 | 同左 | 同左 |
| 下载完 CRC32 失败 | 不写状态区,继续旧固件 | 同左 | 同左 |
| Bootloader 校验备份区失败 | 继续主区旧固件 | 同左 | 同左 |
| 拷贝/交换中掉电 | PENDING 未清,下次重新拷贝(安全) | SWAPPING 续做(新固件可收敛) | scratch 持久,完整恢复 |
| 新固件启动崩溃(IWDG) | 无回滚(主区已覆盖) | 反向交换回滚 | 反向交换回滚 |
| 新固件确认成功 | 运行新固件 | 运行新固件,旧固件留备份 | 同左 |
8. 框架特性摘要
| 项 | 说明 |
|---|---|
| 固件份数 | 一份 app.bin,链接 0x08003000,三模式通用 |
| 升级写入 | 固定备份区 OTA_BAK_ADDR_BASE |
| 切换方式 | 单备份=拷贝覆盖;双备份=A/B 页交换 |
| 校验 | CRC32(协议 0x30 携带 fwCrc32) |
| 回滚 | 双备份:BOOT_NEW 未确认 → 反向交换;单备份:无自动回滚 |
| 确认机制 | APP 自检 + Flash 写 IDLE + IWDG 兜底 |
| 协议 | jb_protocol 0x30/0x32/0x34,用户层在 jb_product.c 对接 |