mcm-ddc-ble V1.00.16~17:实测板载硅片为256KB(0x01040000起CPU读回绕/SWD fault),Flash布局回退256KB(Boot16K/bs8K/APP_DATA8K不变,APP1 0x01008000/112K,APP2 0x01024000/112K);新增appsw命令切换运行bank(记录校验+目标镜像CRC32复算防呆);uvprojx新增APP2 target(链接0x01024000,VTOR基址宏化,版本号可被target覆盖);新增make_dual_package.bat双APP整包脚本(merge_image.py支持--app2/--with-appdata);新增信息项0x07 CUR_BANK识别当前运行bank;OTA双bank配套:出包同时产bank1/bank2两份载荷(清单带target_bank),OTA_END校验镜像向量表Reset地址落在目标bank(新状态6 bank_mismatch);flash_package.bat改用DFP pack的n32wb031keq6_2 target+smart_flash=false;appsw双bank切换与CUR_BANK已实测通过;文档同步(开发日志37~39/ble_protocol.md/AGENTS.md)

This commit is contained in:
evan.liu 2026-09-04 15:22:53 +08:00
parent 4b4f20774d
commit a546dc5f46
38 changed files with 19655 additions and 4372 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ tools/__pycache__/
Boot/MDK-ARM/Objects
mcm-ddc-ble/MDK-ARM/Objects
mcm-ddc-ble/MDK-ARM/build.log
mcm-ddc-ble/MDK-ARM/build-app2.log
mcm-ddc-ble/MDK-ARM/Objects-app2

View File

@ -4,15 +4,15 @@
本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分:
1. **设备固件** — 基于国民技术(Nations)**N32WB031 BLE SoC**(实际型号 **N32WB031KEQ6-2**:Cortex-M0,64MHz HSI,**Flash 512KB**,RAM 48KB+16KB):
- **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/224KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令。当前版本 **V1.00.14**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。
1. **设备固件** — 基于国民技术(Nations)**N32WB031 BLE SoC**(Cortex-M0,64MHz HSI,**板载硅片实测 256KB Flash**(0x01000000~0x0103FFFF,开发日志 §37),RAM 48KB+16KB):
- **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/112KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令。当前版本 **V1.00.17**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。
- **`Boot/`** — 自写精简 bootloader(链接在 `0x01000000`/16KB):校验 bootsetting 记录自身完整性(magic + 结构体 CRC32)后按 active bank 的 `start_address` 直接跳转;**不校验镜像 CRC(CRC 校验在 OTA 升级过程中完成)**;记录无效或地址越界回退 APP1。
2. **手机 App `SmartAssiter/`** — uni-app **Vue3 + TypeScript** 工程(HBuilderX 项目管理,无 package.json),通过 BLE 连接设备:查看运行参数(温度/电压/转速等)、CLI 终端、OTA 升级页(当前停用,见下)。另有登录/注册/用户信息等云端业务页面。
3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。
关键文档:
- `docs/开发日志.md` — 固件全程开发日志(34 节,含所有踩坑根因与设计决策,排查问题先查这里)
- `docs/开发日志.md` — 固件全程开发日志(39 节,含所有踩坑根因与设计决策,排查问题先查这里)
- `docs/ble_protocol.md` — CAIIC BLE 通信协议 V1.1(帧格式/GATT UUID/三类业务流程/维护命令/手机端开发指南)
- `SmartAssiter/docs/修改记录_*.md` — App 侧每次改造的详细记录
@ -33,15 +33,17 @@ SDK 源码(固件库、CMSIS、FreeRTOS V9.0.0、BLE 协议栈/profile/ns_libr
## Flash / RAM 布局(铁律)
Flash 布局(512KB,`Boot/src/dfu_layout.h` 为唯一权威定义,官方 256KB DFU 布局每区加倍):
> 板载硅片实测 **256KB** Flash(0x01000000~0x0103FFFF;曾按 512KB 规划,实测 0x01040000 起 CPU 读回绕、SWD fault,见开发日志 §37)。以下布局已按 256KB 重排(§38),双 bank 切换 `appsw` 实测通过。
Flash 布局(256KB,`Boot/src/dfu_layout.h` 为唯一权威定义;Boot/bootsetting/APP_DATA 保持 16/8/8KB,余下 224KB 均分双 bank):
| 区域 | 地址 | 大小 |
|---|---|---|
| Bootloader | 0x01000000 | 16KB |
| bootsetting | 0x01004000 | 8KB(用 1 个 4KB 扇区,结构体见 dfu_layout.h) |
| APP_DATA | 0x01006000 | 8KB(区头为 `caiic_params_t` 参数记录,见 app_params.h) |
| APP1 | 0x01008000 | 224KB(0x38000) |
| APP2 | 0x01040000 | 224KB(0x38000) |
| APP1 | 0x01008000 | 112KB(0x1C000) |
| APP2 | 0x01024000 | 112KB(0x1C000) |
- **RAM 铁律**:SRAM 为 48KB+16KB,**低 16KB(0x20000000~0x20003FFF)由芯片 ROM/BLE 子系统占用**(rwip 堆描述符、patch 数组、中断中继表,由 ROM 启动代码预备)。**任何工程的 IRAM 执行区必须从 0x20004000 起**(含 Boot 和任何小工具),否则 .data/.bss 初始化冲掉 ROM 数据,BLE init 必然 HardFault(实测根因,开发日志 §21)。注意:mcm-ddc-ble.uvprojx 里 `<IRAM>` 镜像元素显示 0x20000000 是过期显示值,实际生效的是 OCR_RVCT9=0x20004000/0xC000——以 `Listings/mcm-ddc-ble.map` 的 `RW_IRAM1 (Exec base: 0x20004000)` 为准。
- **VTOR**:Cortex-M0 无 SCB->VTOR,用 `PWR->VTOR_REG`(bit31=EN|[30:0]=基址)。规则:**BLE 初始化之前** VTOR 指向 APP 自身向量表(main 开头设 `0x80000000|0x01008000`,USART1 RX 中断需要);**`ns_ble_stack_init()` 之后 VTOR 必须为 0**,此后所有中断经芯片 ROM 跳板 + RAM 中继。**绝对不要在 BLE init 后重设 VTOR**(实测崩溃根因,开发日志 §18)。
@ -58,14 +60,15 @@ mcm-ddc-ble/
│ ├── app_cli.c # CLI UART 前端:CliTask 行接收/编辑/历史/Tab/Ctrl+Z,提示符 caiic->
│ ├── cli_core.c # CLI 核心(命令表 s_cmds[]/handler/分词执行),输出经可切换 cli_out_fn
│ │ # 命令:help / version / sysinfo / devinfo / blelog / led / appget / appset /
│ │ # bsdump / bsset / reset / factory / uartrst / uartinfo
│ │ # bsdump / bsset / appsw / reset / factory / uartrst / uartinfo
│ ├── app_ble.c # BLE 栈初始化/广播(名 CAIIC-MCM-20260902)/连接事件 + BLE 调度任务
│ ├── app_ble_proto.c # 0xCA 帧协议:字节流重组、CLI_REQ/RSP、INFO_QUERY/RSP、OTA 帧分发、
│ │ # CLI 读写特征(...e0003) 与只读参数特征(...e0004) 的执行/应答
│ ├── app_info.c # 设备信息:ADC 芯片温度(CH7)/电压(CH6)、运行时长、堆剩余;风扇 weak 数据源
│ ├── app_ota.c # BLE OTA:双 bank 直写(惰性擦扇区,仅 4B 对齐暂存)+ CRC32 校验 + bootsetting 更新 + 复位
│ ├── app_params.c # APP_DATA 参数记录(caiic_params_t,magic+CRC)+ appget/appset 命令(led/pwm)
│ ├── app_bootset.c # bootsetting 结构化读写 bsdump/bsset(自动重算 CRC,防裸写变砖)
│ ├── app_bootset.c # bootsetting 结构化读写 bsdump/bsset + appsw 切 bank(自动重算 CRC,防裸写变砖;
│ │ # appsw 切前复算目标镜像 CRC32)
│ ├── ble_up.c # CLI 应答的 BLE notify 上行(ke_msg 上下文约束:环形缓冲 + BLE 任务内发送)
│ ├── n32wb03x_it.c # 异常处理(HardFault 栈帧打印)+ USART1_IRQHandler 转发
│ ├── app_usart.c # 遗留文件,已不在工程中编译(rdtss 原透传 FIFO,勿使用)
@ -73,11 +76,13 @@ mcm-ddc-ble/
│ │ # 惰性初始化);供参数接口 JSON 化等使用,MicroLIB 下整数走 %d 快路径
│ └── app_profile/ # app_rdtss.c(GATT 服务实现,读请求按 att_idx 分发 CLI_VAL/INFO_VAL)等
├── inc/ # 对应头文件 + FreeRTOSConfig.h(heap 20KB)+ app_user_config.h(广播名/连接参数)
└── MDK-ARM/ # mcm-ddc-ble.uvprojx(target "N32WB03x" 为活跃 target;
# OTA_IMG_1/2 是 rdtss 遗产,保留不用)、Objects/、bin/、Listings/
└── MDK-ARM/ # mcm-ddc-ble.uvprojx(target "N32WB03x" 为活跃 target,链接 APP1;
# target "APP2" 为同源码链接 0x01024000 的 APP2 bank 构建(同版本),
# 供 make_dual_package.bat 出双 APP 整包;OTA_IMG_1/2 是 rdtss 遗产,保留不用)、
# Objects/、Objects-app2/、bin/、Listings/、Listings-app2/
```
工程配置:器件 `N32WB031KEQ6-2`,IROM 0x01008000/0x38000(APP1),IRAM **0x20004000/0xC000**,MicroLIB,全局宏 `N32WB03X, USE_STDPERIPH_DRIVER`;链接器 Misc 附加 BLE ROM 符号表 `symbol_g15.obj`(SDK `middlewares/Nationstech/ble_library/ns_ble_stack/symdef/`)。
工程配置:器件 `N32WB031KEQ6-2`(实际硅片 256KB),IROM 0x01008000/0x1C000(APP1),IRAM **0x20004000/0xC000**,MicroLIB,全局宏 `N32WB03X, USE_STDPERIPH_DRIVER`;链接器 Misc 附加 BLE ROM 符号表 `symbol_g15.obj`(SDK `middlewares/Nationstech/ble_library/ns_ble_stack/symdef/`)。
## 构建与烧录
@ -90,7 +95,7 @@ mcm-ddc-ble/
"D:\Keil_v5\UV4\UV4.exe" -b caiic_boot.uvprojx -j0 -o build.log # Boot 增量构建
```
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.14):`Code=46912 RO-data=4604 RW-data=2060 ZI-data=28604`。
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.17):`Code=47896 RO-data=4752 RW-data=2060 ZI-data=28612`。
**固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
@ -100,9 +105,11 @@ mcm-ddc-ble/
1. `tools\make_package.bat` — 一键:`UV4 -r` **全量重建** Boot + APP(出包必须 `-r`,增量构建曾产生栈顶/ZI 不一致的砖包,见开发日志 §24)→ `merge_image.py` 合并。产物在 `tools/out/`:
- `caiic_ble_full.hex/.bin` — 整片包(Boot + 缺省 bootsetting + APP1)
- `caiic_ble_full_ota.bin` + `_ota.json` — OTA 载荷与清单(size/crc32/version;version 自动取 `APP_FW_VERSION_NUM`,手机端 OTA_BEGIN 直接取用)
- `caiic_ble_full_ota.bin` + `_ota.json` — APP1 链接的 OTA 载荷与清单(size/crc32/version/target_bank=1;version 自动取 `APP_FW_VERSION_NUM`);`make_package.bat` 同时构建 APP2 target 并出 `caiic_ble_full_ota_app2.bin`/`_ota_app2.json`(target_bank=2)。**客户端按信息项 0x07 CUR_BANK 选对侧 bank 的载荷**;固件 OTA_END 有向量表防呆(status=6 bank_mismatch)
- 注意:`merge_image.py` 不带参数运行时默认 app 路径仍指向已删除的 `mcm-ddc-04/`,**手动调用必须显式传参**:`python tools/merge_image.py mcm-ddc-ble/MDK-ARM/bin/mcm-ddc-ble.bin caiic_ble_full`
2. 首次/变砖恢复:`tools\flash_package.bat tools\out\caiic_ble_full.hex`(调 SDK 自带 NSpyocd,整片擦除 + 烧录;需 NS-LINK 接 SWD PA4/PA5 + 复位脚,先关闭 Keil)
2. 首次/变砖恢复:`tools\flash_package.bat tools\out\caiic_ble_full.hex`(调 SDK 自带 NSpyocd,整片擦除 + 烧录;需 NS-LINK 接 SWD PA4/PA5 + 复位脚,先关闭 Keil;target 用 DFP pack 的 `n32wb031keq6_2` + `-O smart_flash=false`,内置 `n32wb031` target 只有 256KB 映射)
- **双 APP 整包(appsw 测试)**:`tools\make_dual_package.bat` 出 `caiic_ble_dual.{hex,bin}`(Boot+bootsetting 双 bank 记录+APP_DATA 缺省记录+APP1/APP2 **同版本**双镜像,APP2 由 uvprojx target "APP2" 链接 0x01024000 构建;运行 bank 用信息项 0x07 CUR_BANK 识别);`flash_package.bat tools\out\caiic_ble_dual.hex` 烧录后可用 `appsw` 在两 bank 间切换(已实测双向切换)。
- **OTA 双 bank 选包**:OTA 写对侧 bank,载荷必须链接在目标 bank 地址;两份载荷见上,客户端按 CUR_BANK 选包(详见开发日志 §39)。
3. 日常开发调试:Keil F7 编译、F8 下载即可(Boot 不校验镜像 CRC,可直接下载 APP 调试);**Keil 下载选项必须是 "Erase Sectors",整片擦除会杀掉 Boot/bootsetting**(杀掉后需重烧整片包恢复)。板子上烧过其他 0x01000000 起步的程序同样会覆盖 Boot,恢复也是重烧整片包。
4. 日常固件升级设计路径是 BLE OTA(手机 App 或 PC bleak 客户端)。
@ -122,9 +129,9 @@ mcm-ddc-ble/
- **已知未决问题**:固件 notify 上行链路不通(帧协议应答收不到,下行写入正常),见开发日志 §28 末尾。因此 **App(SmartAssiter)已整体切换为只使用 `...e0003`/`...e0004` 两个读写特征**,删除了 notify/帧协议代码;**App 的 OTA 页当前停用**(OTA 应答依赖 notify)。固件侧帧协议与 OTA 代码仍完整保留,PC 端 `tools/ble_cli_test.py` 两种方式都支持。修复 notify 是本仓库最重要的待办。
- 帧格式:`0xCA | TYPE | SEQ | LEN(LE16) | PAYLOAD | CRC16-CCITT(0x1021/0xFFFF, LE)`;OTA 为双 bank 直写、扇区级 ack 流控、END 整镜像 zlib CRC32 校验后更新 bootsetting 并复位。详见 ble_protocol.md。
- 连接后设备主动发起 MTU=247 交换;失败退化为默认 20B/包,协议按字节流重组,两种情形都正确。
- 信息项 id:0x01 固件版本 u32 / 0x02 芯片温度 i16(0.1°C,ADC CH7) / 0x03 风扇转速 u16(0xFFFF=无硬件,`app_fan_get_rpm()` 弱符号,产品板重写即可) / 0x04 电压 u16(mV,ADC CH6) / 0x05 运行时长 u32(s) / 0x06 剩余堆 u32(B)。
- 信息项 id:0x01 固件版本 u32 / 0x02 芯片温度 i16(0.1°C,ADC CH7) / 0x03 风扇转速 u16(0xFFFF=无硬件,`app_fan_get_rpm()` 弱符号,产品板重写即可) / 0x04 电压 u16(mV,ADC CH6) / 0x05 运行时长 u32(s) / 0x06 剩余堆 u32(B) / 0x07 当前运行 bank u8(1=APP1,2=APP2,OTA 选包依据)。
- 调试手段:CLI 命令 `blelog on` 后,串口打印每个收/发协议帧 hex dump、连接/断开、CCCD 订阅事件(走 printf,不经 BLE 通道,避免自激)。
- **维护命令(V1.00.10 起)**:bootsetting 与 APP_DATA 参数区为**结构化**读写(`appget`/`appset`/`bsdump`/`bsset`,不暴露裸读写),UART 与 BLE CLI 读写特征 `...e0003` 均可调用;V1.00.11 起新增 `reset`(复位单板)/`factory`(恢复出厂设置)/`uartrst`(复位串口)/`uartinfo`(查询串口参数)。详见 `docs/ble_protocol.md` §6.5。
- **维护命令(V1.00.10 起)**:bootsetting 与 APP_DATA 参数区为**结构化**读写(`appget`/`appset`/`bsdump`/`bsset`,不暴露裸读写),UART 与 BLE CLI 读写特征 `...e0003` 均可调用;V1.00.11 起新增 `reset`(复位单板)/`factory`(恢复出厂设置)/`uartrst`(复位串口)/`uartinfo`(查询串口参数);V1.00.15 起新增 `appsw`(切换运行 bank,切前复算目标镜像 CRC32)。详见 `docs/ble_protocol.md` §6.5。
## 手机 App(SmartAssiter)
@ -140,8 +147,9 @@ mcm-ddc-ble/
## PC 工具(tools/)
- `merge_image.py` — 纯 Python 无三方依赖;合并 boot+bootsetting(缺省记录,脚本生成)+APP 为整片 hex/bin,同时产出 `_ota.bin`/`_ota.json`;自动从 `app_version.h` 取 `APP_FW_VERSION_NUM` 写入清单
- `merge_image.py` — 纯 Python 无三方依赖;合并 boot+bootsetting(缺省记录,脚本生成)+APP 为整片 hex/bin,同时产出 `_ota.bin`/`_ota.json`(bank1 载荷);`--ota-app2` 追加 bank2 载荷 `_ota_app2.bin/.json`;`--app2 <bin> --with-appdata` 生成双 bank 整包(make_dual_package.bat 使用);自动从 `app_version.h` 取 `APP_FW_VERSION_NUM` 写入清单(含 `target_bank` 字段)
- `make_package.bat` / `flash_package.bat` — 一键出包 / NSpyocd 整片烧录(脚本会自动找 Python312 全路径,本机 `python` 可能是商店占位 stub)
- `make_dual_package.bat` — 双 APP 整包(Boot+bootsetting 双 bank+APP_DATA+APP1+APP2,APP2 升一版链接 0x01024000),用于 appsw 切换测试
- `ble_cli_test.py` + `ble_cli.bat` — PC 端 bleak 蓝牙 CLI 测试客户端(帧协议模式 + `-rw` 读写特征模式,交互模式 `-i`,venv 在 `tools/.venv-ble`,首次运行 bat 自动创建)
- `ble_temp_watch.py` — 温度监测小工具

Binary file not shown.

View File

@ -1,10 +1,13 @@
/**
* @file dfu_layout.h
* @brief Flash memory map and bootsetting record for the CAIIC dual-bank
* bootloader + OTA design (N32WB031KEQ6-2, 512KB flash).
* bootloader + OTA design (256KB flash - the boards actually carry
* the 256KB silicon; reads above 0x0103FFFF alias/fault, see dev
* log section 37).
*
* Layout: official N32WB03x 256KB DFU map with every region doubled.
* Shared by the bootloader (Boot/) and the application (mcm-ddc-04/).
* Layout: Boot / bootsetting / APP_DATA unchanged (16KB/8KB/8KB); the
* remaining 224KB is split into two 112KB APP banks.
* Shared by the bootloader (Boot/) and the application (mcm-ddc-ble/).
*/
#ifndef __DFU_LAYOUT_H__
#define __DFU_LAYOUT_H__
@ -18,7 +21,7 @@ extern "C" {
/* Flash base of this chip family */
#define CAIIC_FLASH_BASE 0x01000000u
/* Region map (512KB total, ends at 0x01080000) */
/* Region map (256KB total, ends at 0x01040000) */
#define CAIIC_BOOT_BASE 0x01000000u
#define CAIIC_BOOT_SIZE 0x00004000u /* 16KB bootloader */
#define CAIIC_BOOTSETTING_ADDR 0x01004000u
@ -26,10 +29,10 @@ extern "C" {
#define CAIIC_APP_DATA_ADDR 0x01006000u
#define CAIIC_APP_DATA_SIZE 0x00002000u /* 8KB reserved (user data) */
#define CAIIC_APP1_BASE 0x01008000u
#define CAIIC_APP_BANK_SIZE 0x00038000u /* 224KB per bank */
#define CAIIC_APP2_BASE 0x01040000u
#define CAIIC_IMAGE_UPDATE_BASE 0x01078000u /* 32KB reserved, unused */
#define CAIIC_IMAGE_UPDATE_SIZE 0x00008000u
#define CAIIC_APP_BANK_SIZE 0x0001C000u /* 112KB per bank */
#define CAIIC_APP2_BASE 0x01024000u
#define CAIIC_IMAGE_UPDATE_BASE 0x01040000u /* end of flash; no update region */
#define CAIIC_IMAGE_UPDATE_SIZE 0x00000000u
/* Flash erase unit */
#define CAIIC_FLASH_SECTOR_SIZE 0x1000u /* 4KB */

View File

@ -1,6 +1,6 @@
# CAIIC BLE 通信协议 V1.1
适用于 mcm-ddc-ble 固件(N32WB031KEQ6-2,Cortex-M0 64MHz,512KB Flash)。
适用于 mcm-ddc-ble 固件(N32WB031,Cortex-M0 64MHz,板载 256KB Flash 硅片)。
手机 APP(自研)或通用 BLE 调试工具(如 nRF Connect)通过本文协议与设备通信,
支持三类业务:**CLI 命令透传**、**设备信息查询**(温度/风扇转速等)、**BLE OTA 固件升级**。
@ -54,7 +54,7 @@ MTU:**设备在连接建立后主动发起 MTU 交换(请求 247)**;手
| 0x21 | 设备→手机 | INFO_RSP | TLV 序列 {id u8, len u8, value…}×n |
OTA_RSP status:0=ok,1=bad_frame,2=bad_state/offset 乱序(offset 字段=设备期望的下一字节偏移),
3=size_too_big,4=crc_fail,5=flash_fail。
3=size_too_big,4=crc_fail,5=flash_fail,6=bank_mismatch(镜像链接地址与目标 bank 不符,见 §6 选包规则)。
## 4. CLI 透传流程
@ -80,6 +80,7 @@ UART CLI 特有的交互功能(行编辑、Tab 补全、历史、Ctrl+Z)不
| 0x04 | VDD_MV | u16 | 电源电压,单位 mV(ADC CH6) |
| 0x05 | UPTIME_S | u32 | 系统运行时间,单位 s |
| 0x06 | FREE_HEAP | u32 | FreeRTOS 堆剩余,单位 B |
| 0x07 | CUR_BANK | u8 | 当前运行 bank:1=APP1,2=APP2(按链接基址判定;OTA 选包依据,见 §6) |
示例:查询温度+风扇 = INFO_QUERY payload `02 03`;
应答 INFO_RSP payload 形如 `02 02 0A 01 03 02 FF FF`(温度 26.6°C,风扇无硬件)。
@ -90,17 +91,17 @@ UART CLI 特有的交互功能(行编辑、Tab 补全、历史、Ctrl+Z)不
## 6. OTA 升级流程(双 bank 直写,无中转区)
Flash 布局(512KB,官方 256KB DFU 布局每区加倍):
Flash 布局(256KB 实装硅片;Boot/bootsetting/APP_DATA 保持 16/8/8KB,余下 224KB 均分双 bank):
| 区域 | 地址 | 大小 |
|---|---|---|
| Bootloader | 0x01000000 | 16KB |
| bootsetting | 0x01004000 | 8KB(用 1 个 4KB 扇区) |
| APP_DATA(预留) | 0x01006000 | 8KB |
| APP1 | 0x01008000 | 224KB |
| APP2 | 0x01040000 | 224KB |
| APP1 | 0x01008000 | 112KB |
| APP2 | 0x01024000 | 112KB |
设备当前运行 APP1 则新固件写入 APP2,反之亦然。bank 上限 224KB(0x38000)。
设备当前运行 APP1 则新固件写入 APP2,反之亦然。bank 上限 112KB(0x1C000)。
镜像 = Keil 产物 bin(裸二进制,从 bank 基址开始的镜像)。
flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM 缓存**:
数据首次落入某个未擦除的扇区时先擦除该扇区(惰性擦除),收到的数据立即编程写入,
@ -129,6 +130,14 @@ flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM
注意:flash 擦写期间设备关中断数十 ms/扇区(Qflash 算法在 RAM 执行),BLE 链路靠
5s supervision timeout 维持,属正常现象;但请避免在 OTA 期间主动断开。
**OTA 载荷必须链接在目标 bank 地址**(Cortex-M0 代码位置相关)。出包脚本同时产出
两份 bank 匹配的载荷:`<包名>_ota.bin`(target_bank=1,链接 0x01008000)与
`<包名>_ota_app2.bin`(target_bank=2,链接 0x01024000),清单 json 内含
`target_bank` 字段。**客户端选包规则**:先读信息项 0x07 CUR_BANK(当前运行 bank),
OTA 目标 = 对侧 bank,选用对应的载荷。固件侧兜底:OTA_END 整镜像 CRC 通过后,
还会校验镜像向量表 Reset 地址落在目标 bank 范围内,不符则回
OTA_RSP(status=6 bank_mismatch) 并中止、不切换(V1.00.17 起)。
## 6.5 维护命令:bootsetting 与 APP_DATA 参数区(V1.00.09 起,结构化访问;V1.00.10 起参数命令为 appget/appset;V1.00.11 起新增 reset/factory/uartrst/uartinfo)
bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形式提供,
@ -141,6 +150,7 @@ bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形
| `appset <led\|pwm> <val>` | 修改参数并立即落盘(led 50~10000 即时生效于 LED1 闪烁;pwm 0~100 仅存储备用);`appset json {"led":500,"pwm":50}` JSON 入参(V1.00.14 起,未知键忽略、任一越界整体不生效) |
| `bsdump` | 打印 bootsetting 记录全字段 + magic/CRC 校验结果 |
| `bsset <field> <val>` | 写 bootsetting 单字段(`active 1\|2`、`b1addr/b2addr/b1size/b2size/b1crc/b2crc/b1ver/b2ver`),自动重算 CRC、擦除+编程+校验。**写错 active/addr 会导致 Boot 跳错,恢复靠 SWD 重烧整片包** |
| `appsw [1\|2]` | 切换运行 bank 并复位(V1.00.15 起):无参切对侧 bank;切前校验 bootsetting 记录、目标 bank 记录一致性,并复算目标镜像 CRC32 与记录比对,不匹配拒绝(坏 bank 需先跑 OTA 写入有效镜像) |
| `reset` | 复位单板:应答后延时 200ms 执行 `NVIC_SystemReset()`,Boot 按 active bank 跳转 |
| `factory` | 恢复出厂设置:APP_DATA 参数区恢复缺省值并落盘后复位(**不动 bootsetting**) |
| `uartrst` | 复位串口:USART1 按 115200 8N1 重新初始化并清空 RX 队列 |

View File

@ -857,3 +857,142 @@ V1.00.12 的 help 紧凑列表是绕过 512B 上限的权宜之计;根因是 A
构建 0 Error/0 Warning,V1.00.14 `Code=46912 RO=4604 RW=2060 ZI=28604`,
整包/OTA 镜像已生成(_ota.bin 53072B,crc32=0x603CBC54)。
## 35. appsw 命令:切换运行 bank(2026-09-04,V1.00.15)
`appsw [1|2]`(app_bootset.c):无参切到对侧 bank,带参切到指定 bank。
安全校验层层兜底,防止跳进坏 bank:
- bootsetting 记录必须 valid(无效直接拒绝,不像 bsset 那样从零重建);
- 目标 bank 记录一致性:`start_address` 必须等于该 bank 固定基址
(APP1=0x01008000 / APP2=0x01040000),`size` 在 1B~224KB 内;
- **复算目标 bank 镜像 CRC32 与记录比对**(软件 CRC 逐位实现,224KB 约数秒,
期间不关中断、BLE 不断连),不匹配则拒绝并提示先跑 OTA;
- 通过后 `bootset_commit` 改 `active_bank`(自动重算 CRC + 擦除编程 + 读回校验),
延时 200ms 让应答出去,`NVIC_SystemReset()`。Boot 端不校验镜像 CRC,
切换安全性全部由本命令的前置校验承担。
构建 0 Error/0 Warning,V1.00.15 `Code=47680 RO=4752 RW=2060 ZI=28604`,
整包/OTA 镜像已生成(_ota.bin 53988B,crc32=0xC0500D4E)。
## 36. 双 APP 整包脚本(appsw 测试包,2026-09-04,V1.00.15/APP2=V1.00.16)
`tools\make_dual_package.bat` 一键出**完整整片包** `tools/out/caiic_ble_dual.{hex,bin}`,
包含全部 5 个区域:Boot + bootsetting(双 bank 记录,size/crc32/version 均填实)
+ APP_DATA(缺省 `caiic_params_t` 52B 记录,led=500/pwm=0)+ APP1(V1.00.15,
链接 0x01008000)+ APP2(V1.00.16,链接 0x01040000)。烧录后 `appsw` 可在两个
真实镜像间来回切换,`version`/`devinfo` 读到的版本号即当前 bank。
实现要点:
- **APP2 必须链接在 0x01040000**:Cortex-M0 代码位置相关,直接把 APP1 的 bin
烧进 APP2 会因绝对地址/字库指回 0x01008xxx 而"偷跑"APP1 内容。uvprojx 新增
target **APP2**(克隆 N32WB03x target):IROM/OCR_RVCT4 改 0x01040000,
独立 `Objects-app2/`、`Listings-app2/`、`bin/mcm-ddc-ble-app2.bin`,
Define 追加 `APP_BASE_ADDR=0x01040000u, APP_FW_VERSION=\"V1.00.16\",
APP_FW_VERSION_NUM=0x00010010u`(字符串宏在 Define 里用 `\"` 转义,
`&quot;` 不会被还原,实测编译报 #20 后改)。**APP2 版本号同时写在 uvprojx
Define 与 make_dual_package.bat 的 APP2_VERSION,两处必须同步**。
- 固件配套改动(对 APP1 行为中性,故 APP1 版本不递增):`main.c` VTOR 基址由
宏 `APP_BASE_ADDR`(缺省 0x01008000)替代硬编码;`app_version.h` 两个宏加
`#ifndef` 允许 target 级覆盖。`app_ota.c` 本来就用 `Image$$ER_IROM1$$Base`
判当前 bank,APP2 target 天然正确(OTA 目标自动为 APP1)。
- `merge_image.py` 扩展:`--app2 <bin> --app2-version N --with-appdata`,
bootsetting 生成双 bank 记录,APP_DATA 生成缺省参数记录;位置参数兼容
原 `make_package.bat` 流程(单 bank 包行为不变)。
- **OTA 跨 bank 链接地址注意**:OTA 载荷必须链接在**目标 bank** 地址。当前
`_ota.bin` 是 APP1 链接的镜像——只有运行在 APP2 时 OTA(写 APP1)才是严格
正确的;运行在 APP1 时 OTA 写 APP2 的 APP1 链接镜像,boot 后名义 active=APP2
实际执行会滑回 APP1 flash(VTOR 也指向 APP1)。要完整支持双 bank OTA,需出
两份 bank 匹配的 OTA 包(本工程的 APP1/APP2 双 target 已具备这个能力)。
## 37. 实测:板载芯片 Flash 只有 256KB 可用(2026-09-04,重要)
双 APP 整包(§36)烧录后 `appsw 2` 被自身 CRC 校验拦下(记录 0xCCDBC8B0,
实算 0xD30FB2BD)。逐层定位:
1. 本地复算:`mcm-ddc-ble-app2.bin` 的 zlib CRC32 = 0xCCDBC8B0(与 bootsetting
记录一致),而 0xD30FB2BD 恰好等于 **dual 整包 [0x01000000, +54288) 区段**的
CRC——即固件(CPU)读 0x01040000 拿到的是 0x01000000 处的内容(256KB 回绕别名)。
2. NSpyocd(AHB-AP/SWD)读 0x01040000 直接 TransferFault;读 0x0103FF00 正常。
总线在 0x01040000 以上没有存储器。
3. NSpyocd 烧写 0x01040000 以上"成功"(FLM 未报错),但 CPU 读回证明数据实际
不可见;Boot/APP1 未被破坏说明写也未回绕到低区——上方写入被静默丢弃。
**结论:当前开发板上的芯片实际表现为 256KB Flash(0x01000000~0x0103FFFF),
与"N32WB031KEQ6-2 = 512KB"的预期不符。** 需核对板子实际丝印/物料(可能实装了
256KB 的 KCQ6-1)。受此影响:
- §36 的双 APP 整包/`appsw` 功能在这块板上不可用(APP2 地址无效);
脚本与 APP2 target 保留,待 512KB 芯片到位后可直接复用。
- **现有 512KB 双 bank 布局(dfu_layout.h)在此板上整体不成立**:BLE OTA 向
APP2(0x01040000)的直写会被静默丢弃,OTA"成功"后会切到无效 bank。
若确认板上就是 256KB 料,应回退到官方 256KB DFU 布局(APP1 0x01008000/112KB、
APP2 0x01020000/112KB,bootsetting/APP_DATA 相应下移),改动涉及
dfu_layout.h、Boot、app_ota.c、merge_image.py 与全部文档。
- flash_package.bat 已改用 DFP pack 的 n32wb031keq6_2 target(512KB 映射)
+ `-O smart_flash=false`(pack target 的预编程 diff 读 0x01000000 会 fault,
编程+校验正常);对 256KB 内的常规整包烧录同样适用(已实测)。
## 附:本次调试的正面结果
- `appsw` 的三层安全校验按设计拦下了无效 bank(记录 CRC ≠ 实算 CRC),
没有跳进坏镜像——校验逻辑本身验证通过。
- 分包读 CLI 应答(§33)实测正常:BLE 读 `version`/`appsw` 输出完整。
## 38. 回退 256KB 布局 + appsw 双 bank 实测通过(2026-09-04,V1.00.16/APP2=V1.00.17)
确认板载硅片为 256KB(§37)后的重排:Boot 16KB / bootsetting 8KB / APP_DATA 8KB
不动,余下 224KB 均分:**APP1 0x01008000/112KB(0x1C000),APP2 0x01024000/112KB**,
末尾 0x0103FFFF 对齐 256KB 终点;IMAGE_UPDATE 区取消(BASE 改为 0x01040000 作
flash 结束标记,SIZE=0,Boot 的 start_address 上界检查沿用该区间)。
改动点:
- `Boot/src/dfu_layout.h`:`CAIIC_APP_BANK_SIZE` 0x38000→0x1C000,
`CAIIC_APP2_BASE` 0x01040000→0x01024000,IMAGE_UPDATE 归零。Boot/APP 全量重建。
- uvprojx:target N32WB03x 的 IROM/OCR_RVCT4 size 0x38000→0x1c000;
target APP2 的 start 0x1040000→0x1024000、size 0x1c000,
Define `APP_BASE_ADDR=0x01024000u`、版本覆盖升至 **V1.00.17/0x00010011u**
(APP1 默认版本升 V1.00.16/0x00010010u;bat 的 APP2_VERSION 同步)。
- `merge_image.py`:`APP2_ADDR` 0x01024000。
- app_bootset.c 提示文案去掉硬编码地址(改指宏名)。
实测(NSpyocd 烧 `caiic_ble_dual.hex`,bleak 客户端操作):
- 烧录后跑 APP1:BLE `version` → **V1.00.16**;`appsw 2` → bank2 CRC 校验通过、
更新 active=2、复位;
- 重连后 `version` → **V1.00.17**(APP2,链接 0x01024000,BLE/CLI 全功能正常,
VTOR 宏化生效);`bsdump` 记录 valid,双 bank size/crc/ver 正确;
- 无参 `appsw` 切回 APP1,`version` 再次 → **V1.00.16**。双向切换全通。
- 已知小现象:设备在写应答 ACK 前后复位时,bleak 写特征偶报
WinError -2147023673(操作已取消),实际命令已送达执行——属 Windows BLE
栈对端复位的时序表现,非固件问题。
构建 0 Error/0 Warning:APP1 `Code=47704 RO=4752 RW=2060 ZI=28604`,
APP2 `Code=48004`(同源码、不同链接地址/版本号,体积差 300B 属正常链接差异)。
整包/双包/OTA 镜像均已重新生成。
## 39. OTA 双 bank 配套:CUR_BANK 信息项 + 双载荷出包 + 向量表防呆(2026-09-04,V1.00.17)
回答"不知道当前跑 APP1 还是 APP2、OTA bin 链接地址不匹配怎么办":
- **信息项 0x07 CUR_BANK**(u8,1=APP1/2=APP2,按链接基址 `Image$$ER_IROM1$$Base`
判定):`devinfo` 首行新增 `cur bank: APPn`,帧协议 INFO_QUERY 与只读特征
`...e0004` 的 TLV 同步包含。任何时刻客户端都能确定运行 bank,不依赖版本号。
- **双 OTA 载荷**:`make_package.bat`/`make_dual_package.bat` 现在都构建 APP2
target,`merge_image.py` 新增 `--ota-app2`:出 `<包名>_ota.bin`(target_bank=1)
与 `<包名>_ota_app2.bin`(target_bank=2),清单 json 增加 `target_bank` 字段。
客户端规则:读 CUR_BANK → OTA 目标 = 对侧 bank → 选对应载荷。
- **固件防呆**:OTA_END 整镜像 CRC 通过后,校验镜像向量表 Reset 地址落在目标
bank 范围内,否则回 OTA_RSP(status=6 bank_mismatch)、中止不切换。
(通知上行未修复,手机端 OTA 页仍停用;该校验 PC 帧协议客户端可达。)
- **APP2 target 取消版本号覆盖**:两个 bank 必须出同一 release(否则 OTA 清单
version 与镜像自报版本不一致)。bank 区分改由 CUR_BANK 承担——比"+1 版本号"
更通用(任意 OTA 之后仍有效)。`--app2-version` 缺省=主版本号。
- ble_cli_test.py 的 `param` 命令同步解码 cur_bank。
实测(双包烧录):APP1 devinfo `cur bank: APP1` → `appsw` → APP2 devinfo
`cur bank: APP2`(两 bank 同为 V1.00.17)→ e0004 TLV 含 `cur_bank` →
`appsw 1` 切回。构建 0 Error/0 Warning(双 target),整包/双包/双 OTA 载荷
均已重新生成。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,7 @@ extern "C" {
#define BLE_OTA_ST_SIZE_TOO_BIG 3
#define BLE_OTA_ST_CRC_FAIL 4
#define BLE_OTA_ST_FLASH_FAIL 5
#define BLE_OTA_ST_BANK_MISMATCH 6 /* image linked for the wrong bank */
/**
* @brief Reset the protocol state machine.

View File

@ -18,6 +18,9 @@ void AppBootset_CmdBsdump(int argc, char* argv[]);
/** "bsset <field> <val>": modify one bootsetting field (see app_bootset.c). */
void AppBootset_CmdBsset(int argc, char* argv[]);
/** "appsw [1|2]": switch active boot bank (image CRC verified) and reboot. */
void AppBootset_CmdAppswitch(int argc, char* argv[]);
#ifdef __cplusplus
}
#endif

View File

@ -22,6 +22,7 @@ extern "C" {
#define BLE_INFO_VDD_MV 0x04 /* u16, mV (ADC CH6) */
#define BLE_INFO_UPTIME_S 0x05 /* u32, seconds */
#define BLE_INFO_FREE_HEAP 0x06 /* u32, bytes */
#define BLE_INFO_CUR_BANK 0x07 /* u8, 1 = running APP1, 2 = APP2 (link base) */
#define APP_FAN_RPM_INVALID 0xFFFFu
@ -50,6 +51,9 @@ uint16_t app_info_last_temp_raw(void);
/** VDD voltage in mV; 0 when the ADC read timed out. */
uint16_t app_info_vdd_mv(void);
/** Current running bank: 1 = APP1, 2 = APP2 (from the link base address). */
uint8_t app_info_cur_bank(void);
/**
* @brief Fan speed in rpm. Weak default returns APP_FAN_RPM_INVALID;
* product code overrides this once the fan tachometer is hooked up.

View File

@ -1,12 +1,20 @@
/**
* @file app_version.h
* @brief Firmware version string.
* Both macros may be overridden per Keil target (-DAPP_FW_VERSION=...):
* the "APP2" target builds the same sources one version up, linked at
* the APP2 bank base (see tools/make_dual_package.bat).
*/
#ifndef __APP_VERSION_H__
#define __APP_VERSION_H__
#define APP_FW_VERSION "V1.00.14"
#ifndef APP_FW_VERSION
#define APP_FW_VERSION "V1.00.17"
#endif
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
#define APP_FW_VERSION_NUM 0x0001000Eu
#ifndef APP_FW_VERSION_NUM
#define APP_FW_VERSION_NUM 0x00010011u
#endif
#endif /* __APP_VERSION_H__ */

View File

@ -23,6 +23,9 @@
#include <stddef.h>
#include <stdlib.h>
#include "FreeRTOS.h"
#include "task.h"
static uint8_t s_qflash_ready;
/* ------------------------------------------------------------------ */
@ -163,7 +166,7 @@ void AppBootset_CmdBsset(int argc, char* argv[])
{
if (v != CAIIC_APP1_BASE && v != CAIIC_APP2_BASE)
{
cli_write("bsset: addr must be 0x01008000 or 0x01040000\r\n");
cli_write("bsset: addr must be CAIIC_APP1_BASE or CAIIC_APP2_BASE\r\n");
return;
}
bank->start_address = v;
@ -172,7 +175,7 @@ void AppBootset_CmdBsset(int argc, char* argv[])
{
if (v > CAIIC_APP_BANK_SIZE)
{
cli_write("bsset: size exceeds 224KB bank\r\n");
cli_write("bsset: size exceeds the bank size\r\n");
return;
}
bank->size = v;
@ -206,3 +209,88 @@ void AppBootset_CmdBsset(int argc, char* argv[])
cli_write("bsset: ok (verified)\r\n");
bootset_print(&bs);
}
/**
* @brief "appsw [1|2]": switch the active boot bank and reboot into it.
* Without an argument, switches to the other bank. Refuses to switch
* when the bootsetting record is invalid, the target bank record is
* inconsistent, or the target image fails a CRC32 re-read (prevents
* jumping into a half-written/blank bank).
*/
void AppBootset_CmdAppswitch(int argc, char* argv[])
{
caiic_bootsetting_t bs;
const caiic_bank_t* bank;
uint32_t target;
uint32_t expect_addr;
uint32_t crc;
memcpy(&bs, (const void*)CAIIC_BOOTSETTING_ADDR, sizeof(bs));
if (!bootset_valid(&bs))
{
cli_write("appsw: bootsetting record invalid, refuse to switch\r\n");
return;
}
if (argc >= 2)
{
if (parse_u32(argv[1], &target) != 0 ||
(target != CAIIC_ACTIVE_BANK1 && target != CAIIC_ACTIVE_BANK2))
{
cli_write("usage: appsw [1|2] (no arg = switch to the other bank)\r\n");
return;
}
}
else
{
target = (bs.active_bank == CAIIC_ACTIVE_BANK1) ? CAIIC_ACTIVE_BANK2
: CAIIC_ACTIVE_BANK1;
}
if (target == bs.active_bank)
{
cli_printf("appsw: already on APP%lu\r\n", (unsigned long)target);
return;
}
bank = (target == CAIIC_ACTIVE_BANK1) ? &bs.bank1 : &bs.bank2;
expect_addr = (target == CAIIC_ACTIVE_BANK1) ? CAIIC_APP1_BASE : CAIIC_APP2_BASE;
if (bank->start_address != expect_addr ||
bank->size == 0 || bank->size > CAIIC_APP_BANK_SIZE)
{
cli_printf("appsw: bank%lu record invalid (addr=0x%08lX size=%lu)\r\n",
(unsigned long)target,
(unsigned long)bank->start_address,
(unsigned long)bank->size);
return;
}
cli_write("appsw: verifying target image crc (few seconds)...\r\n");
crc = caiic_crc32((const uint8_t*)bank->start_address, bank->size);
if (crc != bank->crc32)
{
cli_printf("appsw: bank%lu image crc mismatch (record 0x%08lX, actual 0x%08lX)\r\n",
(unsigned long)target,
(unsigned long)bank->crc32,
(unsigned long)crc);
cli_write(" run BLE OTA to write a valid image first\r\n");
return;
}
cli_printf("appsw: bank%lu image ok (ver 0x%08lX, %lu B), switching...\r\n",
(unsigned long)target,
(unsigned long)bank->version,
(unsigned long)bank->size);
bs.active_bank = target;
if (bootset_commit(&bs) != 0)
{
cli_write("appsw: flash fail\r\n");
return;
}
cli_write("appsw: done, resetting...\r\n");
vTaskDelay(pdMS_TO_TICKS(200));
NVIC_SystemReset();
}

View File

@ -12,6 +12,8 @@
#include "app_info.h"
#include "app_ble_proto.h"
#include "app_version.h"
#include "app_ota.h" /* app_ota_current_bank_base */
#include "dfu_layout.h" /* CAIIC_APP1/2_BASE */
#include "n32wb03x.h"
#include "n32wb03x_adc.h"
@ -128,6 +130,12 @@ __weak uint16_t app_fan_get_rpm(void)
return APP_FAN_RPM_INVALID; /* no fan tachometer on this board yet */
}
uint8_t app_info_cur_bank(void)
{
return (app_ota_current_bank_base() == CAIIC_APP2_BASE) ? CAIIC_ACTIVE_BANK2
: CAIIC_ACTIVE_BANK1;
}
/* ------------------------------------------------------------------ */
/* INFO_QUERY handler */
/* ------------------------------------------------------------------ */
@ -186,6 +194,13 @@ static uint16_t info_fill_item(uint8_t* buf, uint16_t fill, uint8_t id)
v32 = (uint32_t)xPortGetFreeHeapSize();
return info_put_tlv(buf, fill, id, &v32, 4);
case BLE_INFO_CUR_BANK:
{
uint8_t bank = (app_ota_current_bank_base() == CAIIC_APP2_BASE)
? CAIIC_ACTIVE_BANK2 : CAIIC_ACTIVE_BANK1;
return info_put_tlv(buf, fill, id, &bank, 1);
}
default:
return fill; /* unknown id: skip */
}
@ -195,6 +210,7 @@ static uint16_t info_fill_item(uint8_t* buf, uint16_t fill, uint8_t id)
static const uint8_t s_all_items[] = {
BLE_INFO_FW_VERSION, BLE_INFO_CHIP_TEMP, BLE_INFO_FAN_RPM,
BLE_INFO_VDD_MV, BLE_INFO_UPTIME_S, BLE_INFO_FREE_HEAP,
BLE_INFO_CUR_BANK,
};
/**
@ -204,8 +220,8 @@ static const uint8_t s_all_items[] = {
*/
const uint8_t* app_info_tlv_snapshot(uint16_t* out_len)
{
/* worst case: 6 items x (2B header + 4B value) = 36B */
static uint8_t s_tlv[40];
/* worst case: 7 items x (2B header + 4B value) = 42B */
static uint8_t s_tlv[48];
uint16_t fill = 0;
uint16_t i;
@ -219,8 +235,8 @@ const uint8_t* app_info_tlv_snapshot(uint16_t* out_len)
void app_info_handle_query(uint8_t seq, const uint8_t* ids, uint16_t n)
{
/* worst case: 6 items x (2B header + 4B value) = 36B */
uint8_t rsp[40];
/* worst case: 7 items x (2B header + 4B value) = 42B */
uint8_t rsp[48];
uint16_t fill = 0;
uint16_t i;

View File

@ -380,6 +380,20 @@ static void ota_on_end(uint8_t seq, const uint8_t* p, uint16_t len)
return;
}
/* Link-address sanity: the image vector table's reset handler must land
* inside the target bank, otherwise the payload was linked for the wrong
* bank (code is position-dependent on Cortex-M0) - refuse to switch. */
{
uint32_t reset_pc = ota_rd32((const uint8_t*)s_target_base + 4u);
if (reset_pc < s_target_base + 8u ||
reset_pc >= s_target_base + s_total_size)
{
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BANK_MISMATCH, s_offset);
app_ota_abort();
return;
}
}
if (ota_update_bootsetting() != 0)
{
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_FLASH_FAIL, s_offset);

View File

@ -189,6 +189,7 @@ static void CmdDevInfo(int argc, char* argv[])
cli_printf("firmware: %s (0x%08lX)\r\n", APP_FW_VERSION,
(unsigned long)APP_FW_VERSION_NUM);
cli_printf("cur bank: APP%u\r\n", (unsigned int)app_info_cur_bank());
if (temp == 0x7FFF)
{
cli_write("chip temp: ADC read timeout\r\n");
@ -315,6 +316,7 @@ static const CliCmd_t s_cmds[] = {
{"appset", "appset <led|pwm> <val> | appset json {\"led\":500}", AppParams_CmdAppset},
{"bsdump", "bsdump: show bootsetting record + crc check", AppBootset_CmdBsdump},
{"bsset", "bsset <field> <val>: write bootsetting field (careful!)", AppBootset_CmdBsset},
{"appsw", "appsw [1|2]: switch boot bank (crc verified) + reboot", AppBootset_CmdAppswitch},
{"reset", "reset: system reset (reboot)", CmdReset},
{"factory", "factory: restore params to defaults and reboot", CmdFactory},
{"uartrst", "uartrst: re-init USART1 (115200 8N1), flush rx queue", CmdUartRst},

View File

@ -87,10 +87,17 @@
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Base address this image is linked at: APP1 by default; the "APP2" Keil
* target overrides it via -DAPP_BASE_ADDR=<CAIIC_APP2_BASE>. app_ota.c
* derives the same value from Image$$ER_IROM1$$Base. */
#ifndef APP_BASE_ADDR
#define APP_BASE_ADDR 0x01008000u
#endif
/* Vector remap before any APP interrupt is enabled (USART1 RX IRQ needs our
* own table). ns_ble_stack_init() clears VTOR to 0 afterwards ON PURPOSE
* (ROM trampoline + RAM relay) - do not restore it. */
#define APP_VTOR_VALUE (0x80000000u | 0x01008000u)
#define APP_VTOR_VALUE (0x80000000u | APP_BASE_ADDR)
#define BLE_SCHED_TASK_STACK (512) /* stack in words */
#define BLE_SCHED_TASK_PRIORITY (2)

View File

@ -176,7 +176,8 @@ async def run_cli_rw(client, cmd):
_INFO_ITEM_NAMES = {0x01: "fw_version", 0x02: "chip_temp", 0x03: "fan_rpm",
0x04: "vdd_mv", 0x05: "uptime_s", 0x06: "free_heap"}
0x04: "vdd_mv", 0x05: "uptime_s", 0x06: "free_heap",
0x07: "cur_bank"}
async def run_param_read(client):

View File

@ -9,6 +9,11 @@ rem ============================================================
setlocal
set ROOT=%~dp0..
set PYOCD="%ROOT%\nations-tec\N32WB03x_SDK_V2.0.0\utilities\dfu\NSpyocd\NSpyocd.exe"
rem The builtin "n32wb031" target only maps 256KB of flash (fails at
rem 0x01040000 on the dual-bank package); the DFP pack target
rem "n32wb031keq6_2" maps the full 512KB of the KEQ6-2 we actually use.
set PACK="%ROOT%\nations-tec\N32WB03x_DFP.1.4.0.pack"
set TARGET=n32wb031keq6_2
set IMAGE=%~1
if "%IMAGE%"=="" set IMAGE=out\caiic_full.hex
if not "%IMAGE:~0,1%"=="\" if not "%IMAGE:~1,1%"==":" set IMAGE=%~dp0%IMAGE%
@ -21,9 +26,11 @@ if not exist "%IMAGE%" (
rem -M under-reset: connect while holding the chip in reset
rem -f 1000000: 1 MHz SWD clock (slow but tolerant of wiring)
echo Erasing chip...
%PYOCD% erase --chip -M under-reset -f 1000000 -t n32wb031 || goto :fail
%PYOCD% erase --chip --pack %PACK% -M under-reset -f 1000000 -t %TARGET% || goto :fail
echo Programming %IMAGE% ...
%PYOCD% load -M under-reset -f 1000000 -t n32wb031 "%IMAGE%" || goto :fail
rem -O smart_flash=false: with the pack target the pre-program diff pass
rem faults reading 0x01000000; programming+verify itself is fine.
%PYOCD% load --pack %PACK% -M under-reset -f 1000000 -t %TARGET% -O smart_flash=false "%IMAGE%" || goto :fail
echo Program Finish!
goto :eof

View File

@ -0,0 +1,57 @@
@echo off
rem ============================================================
rem make_dual_package.bat - build Boot + APP1 + APP2 and merge
rem one COMPLETE flash image for the appsw bank-switch test:
rem Boot 0x01000000
rem bootsetting 0x01004000 (both bank records, crc filled)
rem APP_DATA 0x01006000 (default params record)
rem APP1 0x01008000 target "N32WB03x" (current version, 112KB bank)
rem APP2 0x01024000 target "APP2" (same release, linked at the
rem APP2 base; banks are told apart via info item 0x07
rem CUR_BANK, see docs/ble_protocol.md section 5)
rem Output: tools\out\caiic_ble_dual.{hex,bin} (+ _ota.bin/_ota.json)
rem Flash: tools\flash_package.bat tools\out\caiic_ble_dual.hex
rem ============================================================
setlocal
set ROOT=%~dp0..
set UV4="D:\Keil_v5\UV4\UV4.exe"
rem Both banks build the SAME release (same version); the running bank is
rem reported via info item 0x07 CUR_BANK (devinfo / read-only characteristic).
echo [1/4] Building bootloader...
%UV4% -r "%ROOT%\Boot\MDK-ARM\caiic_boot.uvprojx" -j0 -o "%ROOT%\Boot\MDK-ARM\build.log"
if errorlevel 1 goto :fail
findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\Boot\MDK-ARM\build.log" >nul || goto :fail
echo [2/4] Building APP1 (target N32WB03x)...
%UV4% -r "%ROOT%\mcm-ddc-ble\MDK-ARM\mcm-ddc-ble.uvprojx" -t "N32WB03x" -j0 -o "%ROOT%\mcm-ddc-ble\MDK-ARM\build.log"
if errorlevel 1 goto :fail
findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\mcm-ddc-ble\MDK-ARM\build.log" >nul || goto :fail
echo [3/4] Building APP2 (target APP2)...
%UV4% -r "%ROOT%\mcm-ddc-ble\MDK-ARM\mcm-ddc-ble.uvprojx" -t "APP2" -j0 -o "%ROOT%\mcm-ddc-ble\MDK-ARM\build-app2.log"
if errorlevel 1 goto :fail
findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\mcm-ddc-ble\MDK-ARM\build-app2.log" >nul || goto :fail
echo [4/4] Merging images...
set PYEXE=
python --version >nul 2>&1 && set PYEXE=python
if not defined PYEXE (
if exist "%LOCALAPPDATA%\Programs\Python\Python312\python.exe" (
set PYEXE="%LOCALAPPDATA%\Programs\Python\Python312\python.exe"
)
)
if not defined PYEXE (
echo python not found - install Python 3 or fix PATH
goto :fail
)
%PYEXE% "%~dp0merge_image.py" "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble.bin" caiic_ble_dual --app2 "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble-app2.bin" --with-appdata || goto :fail
echo.
echo Package ready: tools\out\caiic_ble_dual.hex / caiic_ble_dual.bin
goto :eof
:fail
echo.
echo *** FAILED - check the build logs ***
exit /b 1

View File

@ -13,12 +13,17 @@ echo [1/3] Building bootloader...
if errorlevel 1 goto :fail
findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\Boot\MDK-ARM\build.log" >nul || goto :fail
echo [2/3] Building APP...
%UV4% -r "%ROOT%\mcm-ddc-ble\MDK-ARM\mcm-ddc-ble.uvprojx" -j0 -o "%ROOT%\mcm-ddc-ble\MDK-ARM\build.log"
echo [2/4] Building APP1 (target N32WB03x)...
%UV4% -r "%ROOT%\mcm-ddc-ble\MDK-ARM\mcm-ddc-ble.uvprojx" -t "N32WB03x" -j0 -o "%ROOT%\mcm-ddc-ble\MDK-ARM\build.log"
if errorlevel 1 goto :fail
findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\mcm-ddc-ble\MDK-ARM\build.log" >nul || goto :fail
echo [3/3] Merging images...
echo [3/4] Building APP2-linked OTA payload (target APP2)...
%UV4% -r "%ROOT%\mcm-ddc-ble\MDK-ARM\mcm-ddc-ble.uvprojx" -t "APP2" -j0 -o "%ROOT%\mcm-ddc-ble\MDK-ARM\build-app2.log"
if errorlevel 1 goto :fail
findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\mcm-ddc-ble\MDK-ARM\build-app2.log" >nul || goto :fail
echo [4/4] Merging images...
set PYEXE=
python --version >nul 2>&1 && set PYEXE=python
if not defined PYEXE (
@ -30,10 +35,14 @@ if not defined PYEXE (
echo python not found - install Python 3 or fix PATH
goto :fail
)
%PYEXE% "%~dp0merge_image.py" "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble.bin" caiic_ble_full || goto :fail
rem --ota-app2: emit a second OTA payload linked for the APP2 bank
rem (OTA writes the bank opposite to the running one, so both link
rem variants must ship; the client picks by info item 0x07 CUR_BANK).
%PYEXE% "%~dp0merge_image.py" "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble.bin" caiic_ble_full --ota-app2 "%ROOT%\mcm-ddc-ble\MDK-ARM\bin\mcm-ddc-ble-app2.bin" || goto :fail
echo.
echo Package ready: tools\out\caiic_ble_full.hex / caiic_ble_full.bin
echo OTA payloads: caiic_ble_full_ota.bin (bank1) / caiic_ble_full_ota_app2.bin (bank2)
goto :eof
:fail

View File

@ -4,31 +4,45 @@
merge_image.py - Merge the CAIIC bootloader and application binaries into a
single flash package for N32WB031KEQ6-2 (512KB).
Segments produced:
Segments produced (single-bank mode):
Boot bin -> 0x01000000 (16KB bootloader region)
bootsetting -> 0x01004000 (default record, generated here)
APP bin -> 0x01008000 (APP1 bank)
Dual-bank mode (--app2, optionally --with-appdata), used by
make_dual_package.bat for the appsw bank-switch test:
Boot bin -> 0x01000000
bootsetting -> 0x01004000 (both bank records filled)
APP_DATA -> 0x01006000 (default caiic_params_t record, 52B)
APP1 bin -> 0x01008000 (linked at APP1 base, 112KB bank)
APP2 bin -> 0x01024000 (linked at APP2 base, Keil target "APP2")
Default bootsetting record (44 bytes, little-endian, matches
Boot/src/dfu_layout.h caiic_bootsetting_t):
magic = 0xCA11C011
active_bank = 1 (APP1)
bank1 = {start_address=0x01008000, size=<app bin size>,
crc32=<IEEE CRC32 of app bin>, version=0}
bank2 = all zero
bank1 = {start_address=0x01008000, size=<app1 size>,
crc32=<IEEE CRC32 of app1 bin>, version=<version>}
bank2 = same layout for the APP2 image (zeros in single-bank mode)
crc32 = IEEE CRC32 over the 40 bytes above
Outputs (written to tools/out/):
<name>.hex Intel HEX, sparse (gaps omitted) - for pyocd/NSpyocd/J-Flash
<name>.bin raw binary from 0x01000000, gaps padded with 0xFF
<name>_ota.bin the APP image alone - this is the BLE OTA payload
<name>_ota.bin the APP1 image alone - this is the BLE OTA payload
<name>_ota.json OTA manifest for the phone app: size / crc32 / version
(crc32 = IEEE/zlib over the whole _ota.bin, version is
the value to send in OTA_BEGIN; default 0)
Usage: python tools/merge_image.py [app_bin] [out_name] [version]
[--app2 app2_bin [--app2-version N]] [--with-appdata]
[--ota-app2 app2_linked_bin]
Defaults: app_bin = mcm-ddc-04/MDK-ARM/bin/embeddedSrc.bin, out_name = caiic_full,
version = 0
--app2 merges the APP2-linked image into the package; --ota-app2 only emits
an extra OTA payload <name>_ota_app2.bin/.json (target_bank=2) without
merging. OTA manifests carry "target_bank" - the client must pick the payload
matching the bank the device is NOT running (info item 0x07 CUR_BANK).
Run from anywhere; paths are resolved relative to this script.
"""
import json
@ -45,11 +59,18 @@ DEFAULT_APP_BIN = os.path.join("mcm-ddc-04", "MDK-ARM", "bin", "embeddedSrc.bin"
BOOT_ADDR = 0x01000000
BOOTSETTING_ADDR = 0x01004000
APPDATA_ADDR = 0x01006000
APP1_ADDR = 0x01008000
APP2_ADDR = 0x01024000
BOOT_MAGIC = 0xCA11C011
ACTIVE_BANK1 = 1
# APP_DATA default record (mcm-ddc-ble/inc/app_params.h caiic_params_t)
PARAMS_MAGIC = 0xCA12DA7A
PARAMS_LAYOUT_VER = 1
PARAMS_LED1_BLINK_MS_DEF = 500
OUT_DIR = "out"
HEX_REC_LEN = 16
@ -60,13 +81,29 @@ def crc32(data):
return zlib.crc32(data) & 0xFFFFFFFF
def default_bootsetting(app_blob):
"""Build the default 44-byte bootsetting record for APP1 = app_blob."""
def default_bootsetting(app_blob, app2_blob=None, version=0, app2_version=0):
"""Build the default 44-byte bootsetting record; bank2 is zero-filled
unless an APP2 image is given."""
bank2 = (APP2_ADDR, len(app2_blob), crc32(app2_blob), app2_version) \
if app2_blob else (0, 0, 0, 0)
body = struct.pack(
"<IIIIIIIIII",
BOOT_MAGIC, ACTIVE_BANK1,
APP1_ADDR, len(app_blob), crc32(app_blob), 0, # bank1
0, 0, 0, 0, # bank2 (empty)
APP1_ADDR, len(app_blob), crc32(app_blob), version, # bank1
bank2[0], bank2[1], bank2[2], bank2[3], # bank2
)
return body + struct.pack("<I", crc32(body))
def default_appdata():
"""Build the 52-byte default APP_DATA parameter record
(caiic_params_t: magic/layout_ver/led/flags/pwm/reserved[8]/crc32)."""
body = struct.pack(
"<IIHHI8I",
PARAMS_MAGIC, PARAMS_LAYOUT_VER,
PARAMS_LED1_BLINK_MS_DEF, 0, # led1_blink_ms, flags
0, # pwm_duty_pct
0, 0, 0, 0, 0, 0, 0, 0, # reserved[8]
)
return body + struct.pack("<I", crc32(body))
@ -115,29 +152,85 @@ def read_app_fw_version(root):
def main():
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
app_rel = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_APP_BIN
out_name = sys.argv[2] if len(sys.argv) > 2 else "caiic_full"
opts = sys.argv[1:]
app2_rel = None
ota_app2_rel = None
app2_version = None
with_appdata = "--with-appdata" in opts
if "--app2" in opts:
i = opts.index("--app2")
app2_rel = opts[i + 1]
if "--ota-app2" in opts:
i = opts.index("--ota-app2")
ota_app2_rel = opts[i + 1]
if "--app2-version" in opts:
i = opts.index("--app2-version")
app2_version = int(opts[i + 1], 0)
# positional args minus the values consumed by the --flags
pos = []
skip_next = False
for a in sys.argv[1:]:
if skip_next:
skip_next = False
continue
if a in ("--app2", "--app2-version", "--ota-app2"):
skip_next = True
continue
if a == "--with-appdata":
continue
pos.append(a)
app_rel = pos[0] if len(pos) > 0 else DEFAULT_APP_BIN
out_name = pos[1] if len(pos) > 1 else "caiic_full"
# version: explicit argv wins, otherwise take APP_FW_VERSION_NUM from the
# firmware source so the OTA manifest always matches the flashed image
if len(sys.argv) > 3:
version = int(sys.argv[3], 0)
if len(pos) > 2:
version = int(pos[2], 0)
else:
version = read_app_fw_version(root) or 0
boot_blob = load_bin(os.path.join(root, BOOT_BIN), "bootloader")
app_path = app_rel if os.path.isabs(app_rel) else os.path.join(root, app_rel)
app_blob = load_bin(app_path, "APP")
bs_blob = default_bootsetting(app_blob)
app_blob = load_bin(app_path, "APP1")
app2_blob = None
if app2_rel:
app2_path = app2_rel if os.path.isabs(app2_rel) else os.path.join(root, app2_rel)
app2_blob = load_bin(app2_path, "APP2")
if app2_version is None:
app2_version = version # APP2 target builds the same release
# extra OTA payload linked for the APP2 bank (not merged into the image)
ota_app2_blob = None
if ota_app2_rel:
ota_app2_path = ota_app2_rel if os.path.isabs(ota_app2_rel) \
else os.path.join(root, ota_app2_rel)
ota_app2_blob = load_bin(ota_app2_path, "OTA APP2 payload")
elif app2_blob:
ota_app2_blob = app2_blob
bs_blob = default_bootsetting(app_blob, app2_blob, version, app2_version)
segments = [
(BOOT_ADDR, boot_blob),
(BOOTSETTING_ADDR, bs_blob),
(APP1_ADDR, app_blob),
]
if with_appdata:
segments.append((APPDATA_ADDR, default_appdata()))
segments.append((APP1_ADDR, app_blob))
if app2_blob:
segments.append((APP2_ADDR, app2_blob))
for addr, blob in segments:
print("0x%08X (%d bytes)" % (addr, len(blob)))
print("bootsetting: active=APP1 start=0x%08X size=%d crc32=0x%08X"
% (APP1_ADDR, len(app_blob), crc32(app_blob)))
print("bootsetting: active=APP1")
print(" bank1: start=0x%08X size=%d crc32=0x%08X ver=0x%08X"
% (APP1_ADDR, len(app_blob), crc32(app_blob), version))
if app2_blob:
print(" bank2: start=0x%08X size=%d crc32=0x%08X ver=0x%08X"
% (APP2_ADDR, len(app2_blob), crc32(app2_blob), app2_version))
out_dir = os.path.join(root, "tools", OUT_DIR)
if not os.path.isdir(out_dir):
@ -163,18 +256,37 @@ def main():
with open(ota_json_path, "w") as f:
json.dump({
"file": os.path.basename(ota_bin_path),
"target_bank": 1,
"size": len(app_blob),
"crc32": "0x%08X" % crc32(app_blob),
"version": version,
}, f, indent=2)
# second OTA payload linked for the APP2 bank (when built)
ota_app2_bin_path = None
if ota_app2_blob:
ota_app2_bin_path = os.path.join(out_dir, out_name + "_ota_app2.bin")
with open(ota_app2_bin_path, "wb") as f:
f.write(ota_app2_blob)
with open(os.path.join(out_dir, out_name + "_ota_app2.json"), "w") as f:
json.dump({
"file": os.path.basename(ota_app2_bin_path),
"target_bank": 2,
"size": len(ota_app2_blob),
"crc32": "0x%08X" % crc32(ota_app2_blob),
"version": version,
}, f, indent=2)
print("")
print("package written:")
print(" %s" % hex_path)
print(" %s (base 0x%08X, %d bytes)" % (bin_path, FLASH_BASE, len(merged)))
print("OTA image written:")
print(" %s (%d bytes, crc32=0x%08X)" % (ota_bin_path, len(app_blob), crc32(app_blob)))
print(" %s (%d bytes, crc32=0x%08X, target bank1)" % (ota_bin_path, len(app_blob), crc32(app_blob)))
print(" %s" % ota_json_path)
if ota_app2_bin_path:
print(" %s (%d bytes, crc32=0x%08X, target bank2)"
% (ota_app2_bin_path, len(ota_app2_blob), crc32(ota_app2_blob)))
return 0

Binary file not shown.

6932
tools/out/caiic_ble_dual.hex Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,7 @@
{
"file": "caiic_ble_dual_ota.bin",
"target_bank": 1,
"size": 54204,
"crc32": "0x08D5A053",
"version": 65553
}

Binary file not shown.

View File

@ -0,0 +1,7 @@
{
"file": "caiic_ble_dual_ota_app2.bin",
"target_bank": 2,
"size": 54504,
"crc32": "0x2229A8CB",
"version": 65553
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,6 +1,7 @@
{
"file": "caiic_ble_full_ota.bin",
"size": 53072,
"crc32": "0x603CBC54",
"version": 65550
"target_bank": 1,
"size": 54204,
"crc32": "0x08D5A053",
"version": 65553
}

Binary file not shown.

View File

@ -0,0 +1,7 @@
{
"file": "caiic_ble_full_ota_app2.bin",
"target_bank": 2,
"size": 54504,
"crc32": "0x2229A8CB",
"version": 65553
}