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

277 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 布局(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.c``ota_param_load/store` 实现):
| 模式 | 每次升级状态区写入次数 | 10K 次擦写寿命可撑 | 策略 |
|---|---|---|---|
| 双备份 AB(swap) | 约 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。
```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 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.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` 对接 |