添加普冉 PY32F040 OTA 双工程代码生成模板

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 18:59:56 +08:00
co-authored by Cursor
commit 331864dc64
496 changed files with 329040 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
# 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` 对接 |