Files
mcu-codegen-template/Doc/OTA_AB_双备份升级设计.md

15 KiB
Raw Permalink Blame History

PY32F040 开发框架 — OTA 升级设计(三模式可配置)

本框架提供可配置的三种 OTA 模式:单备份 / 双备份+RAM 交换 / 双备份+Flash 暂存区交换。 协议对接 jb_protocol(见宝 IOT 协议,OTA 命令 0x30/0x32/0x34),协议层原样使用,无需修改


0. 模式配置(唯一入口:Shared/ota_config.hAPP 与 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 布局(128KBPY32F040xB,页 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 各 57Kscratch 1页放 Flash 最底部):

0x08000000 Bootloader(11K) | 0x08002C00 状态区(1K)
0x08003000 A 区(57K) | 0x08011400 B 区(57K) | 0x0801F800 scratch(1页, 最底部)
0x0801FC00 剩余空闲

2. OTA 状态区(0x08002C001KB = 4 页,策略由备份模式决定)

写入频率差异决定策略(由 Shared/flash.cota_param_load/store 实现):

模式 每次升级状态区写入次数 10K 次擦写寿命可撑 策略
双备份 ABswap 约 440 次(每页交换 2 次记 phase/progress 约 23 次升级 4 页轮换磨损均衡
单备份 SINGLE 2~3 次(无逐页交换过程) 数千次升级 固定页 0 直写

AB 模式(4 页轮换):

每页副本格式:[ota_param_t(含 seq28B][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 20BOTA_PARAM_SIZE 自动) */

active_slot / target_slot运行区固定 = OTA_RUN_ADDR_BASE0x08003000, 设备无需知道"当前在哪个区",目标区固定 = OTA_BAK_ADDR_BASE。

3. 状态机(核心)

双备份(ABswap):

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 自检确认

  1. Bootloader 交换/拷贝完成后写 BOOT_NEW使能 IWDG(约 1s,交换期间逐页喂狗;
  2. 新固件 app_Init() 完成后,app_boot_ota_report() 调用 ota_boot_check_confirm():见 BOOT_NEW → 写确认(IDLE) → 上报 0x34
  3. 新固件启动即崩溃 → IWDG 复位 → Bootloader 见 BOOT_NEW 未确认 → 双备份反向交换回滚;单备份无回滚目标,继续运行;
  4. 确认后运行区正式生效,备份区保留旧固件作为下次回滚备份。

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.cSCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET0x08003000)。

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 对接