# PY32F040 开发框架 — OTA 升级设计(三模式可配置) > 本框架提供**可配置的三种 OTA 模式**:单备份 / 双备份+RAM 交换 / 双备份+Flash 暂存区交换。 > 协议对接 jb_protocol(见宝 IOT 协议,OTA 命令 0x30/0x32/0x34),**协议层原样使用,无需修改**。 --- ## 0. 模式配置(唯一入口:`Shared/ota_config.h`,APP 与 Bootloader 共用) ```c /* ① 备份模式 —— 唯一必选开关 */ #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。 ```c #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 自检确认**: 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 中 `` 标签)** 决定,散列文件(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` 对接 |