添加普冉 PY32F040 OTA 双工程代码生成模板
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 布局(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 中 `<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` 对接 |
|
||||
Reference in New Issue
Block a user