V1.00.25: UART OTA stream mode + RX DMA ring + SRAM cliff fix
Firmware: - bsp_usart: RX moved from RXDNE byte-queue IRQ to DMA CH2 circular ring (2KB) + IDLE-irq wakeup; bytes keep landing during flash-erase interrupt-off windows (the reason lockstep was needed before) - app_ota: stream mode (ble_protocol.md section 6.7) - 16B BEGIN with flags bit0=STREAM (UART channel only), DATA acked on 4KB sector crossings (original chunk length; two ack-gating bugs fixed during field test), throttled BAD_STATE for go-back-N - FreeRTOS heap 20KB->18KB: the 2KB ring pushed the stack top past the 0x2000C000 SRAM cliff (probed via SWD: accesses above fault on this silicon, usable app SRAM is 32KB) which locked the board at boot; merge_image.py now hard-fails the package when the image initial SP leaves (0x20004000, 0x2000C000] Tools: - ble_ota_update.py: UART stream sender (8KB window, stall watchdog rewind, auto-fallback to lockstep on pre-V1.00.24 firmware, --lockstep to force); case-insensitive option parsing - flash_package.py/bat: stream NSpyocd output live (chunked reads keep the \r progress bar), vendor banner rebranded to CAIIC NSLINK UMP - merge_image.py: initial-SP cliff guard Verified: UART stream OTA both directions, 55.8KB in ~5.9s @9.5kB/s 0 rewinds (lockstep was 39s), PASS after reboot; board boot fixed and verified via SWD. Docs: ble_protocol.md section 6.7, dev log section 47 (+ SRAM cliff post-mortem), AGENTS.md RAM rule rewritten (both cliffs) + V1.00.25
This commit is contained in:
parent
30b5dd908b
commit
f729d16ff2
12
AGENTS.md
12
AGENTS.md
@ -5,7 +5,7 @@
|
|||||||
本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分:
|
本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分:
|
||||||
|
|
||||||
1. **设备固件** — 基于国民技术(Nations)**N32WB031 BLE SoC**(Cortex-M0,64MHz HSI,**板载硅片实测 256KB Flash**(0x01000000~0x0103FFFF,开发日志 §37),RAM 48KB+16KB):
|
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.23**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。
|
- **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/112KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令。当前版本 **V1.00.25**(`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。
|
- **`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 连接设备:查看运行参数(温度/电压/转速/bank/堆等)、CLI 终端、参数读写(appget/appset)、OTA 升级页(e0005 锁步,已恢复)。另有登录/注册/用户信息等云端业务页面。
|
2. **手机 App `SmartAssiter/`** — uni-app **Vue3 + TypeScript** 工程(HBuilderX 项目管理,无 package.json),通过 BLE 连接设备:查看运行参数(温度/电压/转速/bank/堆等)、CLI 终端、参数读写(appget/appset)、OTA 升级页(e0005 锁步,已恢复)。另有登录/注册/用户信息等云端业务页面。
|
||||||
3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。
|
3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。
|
||||||
@ -45,7 +45,7 @@ Flash 布局(256KB,`Boot/src/dfu_layout.h` 为唯一权威定义;Boot/boot
|
|||||||
| APP1 | 0x01008000 | 112KB(0x1C000) |
|
| APP1 | 0x01008000 | 112KB(0x1C000) |
|
||||||
| APP2 | 0x01024000 | 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)` 为准。
|
- **RAM 铁律**:**低 16KB(0x20000000~0x20003FFF)由芯片 ROM/BLE 子系统占用**(rwip 堆描述符、patch 数组、中断中继表,由 ROM 启动代码预备),**任何工程的 IRAM 执行区必须从 0x20004000 起**(含 Boot 和任何小工具),否则 .data/.bss 初始化冲掉 ROM 数据,BLE init 必然 HardFault(实测根因,开发日志 §21)。**顶部同样悬崖(实测 SWD 探明,§47):0x2000C000 起访问即 fault,APP 可用 SRAM 实为 32KB(0x20004000~0x2000BFFF)**——IRAM 配置里的 0xC000(48KB)上限是虚的,**栈顶 `__initial_sp` 必须 < 0x2000C000**;`merge_image.py` 已加硬卡(出包时校验 bin 向量表首字,越线直接失败)。ZI 膨胀(加大缓冲/堆)把栈顶推过线会导致上电即 HardFault/Lockup(V1.00.24 的 2KB DMA ring 事故,heap 已降为 18KB 腾位)。注意: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)。
|
- **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)。
|
||||||
|
|
||||||
## 固件工程(mcm-ddc-ble)
|
## 固件工程(mcm-ddc-ble)
|
||||||
@ -55,7 +55,7 @@ mcm-ddc-ble/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── main.c # 入口:VTOR 预置 → USART+banner → LED → app_ble_init → app_params_init
|
│ ├── main.c # 入口:VTOR 预置 → USART+banner → LED → app_ble_init → app_params_init
|
||||||
│ │ # → ns_sleep_lock_acquire(锁睡眠保 SWD) → 建 BLE 调度/LED/CLI 任务 → vTaskStartScheduler
|
│ │ # → ns_sleep_lock_acquire(锁睡眠保 SWD) → 建 BLE 调度/LED/CLI 任务 → vTaskStartScheduler
|
||||||
│ ├── bsp_usart.c # USART1(PB6/PB7, 115200 8N1):fputc 重定向 + DMA TX(CH1) + RXDNE 中断字节队列 + TX 互斥锁
|
│ ├── bsp_usart.c # USART1(PB6/PB7, 115200 8N1):fputc 重定向 + DMA TX(CH1) + RX DMA 循环环形缓冲(CH2, 2KB, IDLE 中断唤醒;擦扇区关中断期间不丢字节) + TX 互斥锁
|
||||||
│ ├── app_gpio.c # LED1(PB0)/LED2(PA6)
|
│ ├── app_gpio.c # LED1(PB0)/LED2(PA6)
|
||||||
│ ├── app_cli.c # CLI UART 前端:CliTask 行接收/编辑/历史/Tab/Ctrl+Z,提示符 caiic->
|
│ ├── app_cli.c # CLI UART 前端:CliTask 行接收/编辑/历史/Tab/Ctrl+Z,提示符 caiic->
|
||||||
│ ├── cli_core.c # CLI 核心(命令表 s_cmds[]/handler/分词执行),输出经可切换 cli_out_fn
|
│ ├── cli_core.c # CLI 核心(命令表 s_cmds[]/handler/分词执行),输出经可切换 cli_out_fn
|
||||||
@ -95,7 +95,7 @@ mcm-ddc-ble/
|
|||||||
"D:\Keil_v5\UV4\UV4.exe" -b caiic_boot.uvprojx -j0 -o build.log # Boot 增量构建
|
"D:\Keil_v5\UV4\UV4.exe" -b caiic_boot.uvprojx -j0 -o build.log # Boot 增量构建
|
||||||
```
|
```
|
||||||
|
|
||||||
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.22):`Code=48872 RO-data=4980 RW-data=2076 ZI-data=29116`。
|
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.25):`Code=48992 RO-data=4980 RW-data=2088 ZI-data=29120`。
|
||||||
|
|
||||||
**固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
|
**固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ mcm-ddc-ble/
|
|||||||
| `...c72ee005` | Read + Write(带响应) | **OTA 专用特征**(V1.00.19 新增):写携带一帧 OTA 传输帧 → 读同一特征取回 OTA_RSP(轮询至非空),见 ble_protocol.md §6.6 |
|
| `...c72ee005` | Read + Write(带响应) | **OTA 专用特征**(V1.00.19 新增):写携带一帧 OTA 传输帧 → 读同一特征取回 OTA_RSP(轮询至非空),见 ble_protocol.md §6.6 |
|
||||||
|
|
||||||
- **notify 上行已修复(V1.00.19)**:根因是固件帧 CRC16 漏算最后一个 payload 字节(3+payLen vs 协议规定的 4+payLen),所有帧被静默丢弃,与 notify 硬件链路无关(开发日志 §42)。帧协议通道(e0001/e0002)已实测恢复。App(SmartAssiter)走 `...e0003`/`...e0004` 读写特征 + `...e0005` OTA 特征(App 侧参考 `src/mcm/McmOta.ts`,PC 参考 `tools/ble_ota_update.py`)。
|
- **notify 上行已修复(V1.00.19)**:根因是固件帧 CRC16 漏算最后一个 payload 字节(3+payLen vs 协议规定的 4+payLen),所有帧被静默丢弃,与 notify 硬件链路无关(开发日志 §42)。帧协议通道(e0001/e0002)已实测恢复。App(SmartAssiter)走 `...e0003`/`...e0004` 读写特征 + `...e0005` OTA 特征(App 侧参考 `src/mcm/McmOta.ts`,PC 参考 `tools/ble_ota_update.py`)。
|
||||||
- 帧格式:`0xCA | TYPE | SEQ | LEN(LE16) | PAYLOAD | CRC16-CCITT(0x1021/0xFFFF, LE)`,**CRC 覆盖 TYPE..PAYLOAD 共 4+payLen 字节**;OTA 为双 bank 直写、**逐帧锁步 ack**、END 整镜像 zlib CRC32 + 向量表 bank 校验后更新 bootsetting 并复位。OTA 传输层通道无关:BLE e0005 / UART 二进制模式(CLI `ota`)/ 旧帧协议通道,详见 ble_protocol.md §6.6。
|
- 帧格式:`0xCA | TYPE | SEQ | LEN(LE16) | PAYLOAD | CRC16-CCITT(0x1021/0xFFFF, LE)`,**CRC 覆盖 TYPE..PAYLOAD 共 4+payLen 字节**;OTA 为双 bank 直写、END 整镜像 zlib CRC32 + 向量表 bank 校验后更新 bootsetting 并复位。OTA 传输层通道无关:BLE e0005(逐帧锁步)/ UART 二进制模式(CLI `ota`;V1.00.24 起**流式模式**:BEGIN 16B flags、8KB 窗口流水线、扇区边界 ack、go-back-N,旧固件自动回退锁步)/ 旧帧协议通道,详见 ble_protocol.md §6.6/§6.7。
|
||||||
- 连接后设备主动发起 MTU=247 交换;失败退化为默认 20B/包,协议按字节流重组,两种情形都正确。
|
- 连接后设备主动发起 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) / 0x07 当前运行 bank u8(1=APP1,2=APP2,OTA 选包依据)。
|
- 信息项 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 通道,避免自激)。
|
- 调试手段:CLI 命令 `blelog on` 后,串口打印每个收/发协议帧 hex dump、连接/断开、CCCD 订阅事件(走 printf,不经 BLE 通道,避免自激)。
|
||||||
@ -152,7 +152,7 @@ mcm-ddc-ble/
|
|||||||
- `make_package.bat` / `flash_package.bat` — 一键出包 / NSpyocd 整片烧录(脚本会自动找 Python312 全路径,本机 `python` 可能是商店占位 stub)
|
- `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 切换测试
|
- `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_cli_test.py` + `ble_cli.bat` — PC 端 bleak 蓝牙 CLI 测试客户端(帧协议模式 + `-rw` 读写特征模式,交互模式 `-i`,venv 在 `tools/.venv-ble`,首次运行 bat 自动创建)
|
||||||
- `ble_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式:握手标记进入/ABORT 退出/丢帧锁步重传再同步)。**BLE 与 UART 双通道均已实测通过(§42/§44)**
|
- `ble_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式:握手标记进入/ABORT 退出;V1.00.24 起默认流式(8KB 窗口+扇区 ack+go-back-N),`--lockstep` 或旧固件自动回退锁步)。**BLE 与 UART 双通道均已实测通过(§42/§44),流式见 §47**
|
||||||
- `ble_temp_watch.py` — 温度监测小工具
|
- `ble_temp_watch.py` — 温度监测小工具
|
||||||
|
|
||||||
## 实时架构约定(固件)
|
## 实时架构约定(固件)
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
Rebuild target 'caiic_boot'
|
Rebuild target 'caiic_boot'
|
||||||
assembling startup_n32wb03x.s...
|
assembling startup_n32wb03x.s...
|
||||||
compiling boot_crc32.c...
|
compiling boot_crc32.c...
|
||||||
compiling main.c...
|
|
||||||
compiling system_n32wb03x.c...
|
compiling system_n32wb03x.c...
|
||||||
|
compiling main.c...
|
||||||
linking...
|
linking...
|
||||||
Program Size: Code=1320 RO-data=680 RW-data=0 ZI-data=2104
|
Program Size: Code=1320 RO-data=680 RW-data=0 ZI-data=2104
|
||||||
FromELF: creating hex file...
|
FromELF: creating hex file...
|
||||||
|
|||||||
@ -181,6 +181,41 @@ PC 参考实现:`tools\ble_ota_update.py`(`ble_ota.bat`;BLE 默认,
|
|||||||
收到 END 应答;收不到应视为链路异常,但仍可按"等待重启后重连复核 CUR_BANK
|
收到 END 应答;收不到应视为链路异常,但仍可按"等待重启后重连复核 CUR_BANK
|
||||||
与版本"兜底(PC 工具两种处理都保留)。
|
与版本"兜底(PC 工具两种处理都保留)。
|
||||||
|
|
||||||
|
## 6.7 UART 流式传输模式(V1.00.24 起)
|
||||||
|
|
||||||
|
动机:逐帧锁步在 115200 下实测只有 ~1.5 kB/s(每帧一个 RTT + 主机调度
|
||||||
|
抖动)。流式模式让 DATA 帧在窗口内连续下发,**实测 9.5 kB/s(55.8KB
|
||||||
|
约 5.9s,0 重传)**,只受波特率与扇区擦除时间限制。
|
||||||
|
|
||||||
|
**前提(固件侧)**:UART RX 改为 **DMA 环形缓冲**(2KB,循环模式)——
|
||||||
|
DMA 在关中断的扇区擦除窗口内继续接收字节,这是流式不丢包的关键;
|
||||||
|
CLI 文本输入路径不变(同一 `bsp_usart_read_byte()` 接口,行编辑/历史/
|
||||||
|
Tab 行为完全一致),BLE 下行注入 CLI 走独立的注入队列。
|
||||||
|
|
||||||
|
协商:OTA_BEGIN payload 扩展为 **16B** `{total_size, image_crc32,
|
||||||
|
version, flags u32}`,`flags` bit0 = STREAM。仅 UART 通道生效(BLE 通道
|
||||||
|
应答靠读轮询,仍逐帧锁步)。兼容性:
|
||||||
|
|
||||||
|
- 12B 旧格式 BEGIN = 锁步模式(旧主机不受影响);
|
||||||
|
- 旧固件(< V1.00.24)收到 16B BEGIN 回 `bad_frame`(status=1),主机据此
|
||||||
|
自动回退锁步(PC 工具已实现;`--lockstep` 可强制)。
|
||||||
|
|
||||||
|
流式规则:
|
||||||
|
|
||||||
|
- DATA 帧格式不变(payload ≤480B,参考实现用 240B/帧),主机**连续发,
|
||||||
|
不等逐帧 ack**;在途未确认字节数 ≤ **8KB(窗口)**。
|
||||||
|
- 设备 ack 策略:DATA 仅在**跨越 4KB 扇区边界**(该扇区的惰性擦除此时
|
||||||
|
已完成)或到达最后一字节时回 `OTA_RSP(ok, offset)`。
|
||||||
|
- 丢帧/坏帧恢复(go-back-N):重组器静默丢弃坏帧后,下一帧 offset 必然
|
||||||
|
不匹配,设备**立即**回一帧 `BAD_STATE + 期望 offset`(节流:重新对齐
|
||||||
|
前只回一次);主机回退到该 offset 续传,无需重开会话。
|
||||||
|
- ack 整体丢失:主机 **3s 无 ack 进展** → 回退到最后确认的 offset 重发。
|
||||||
|
- END / ABORT 保持每帧必答(锁步语义不变);10s 空闲兜底退出二进制模式、
|
||||||
|
END 应答送达后再复位等既有机制(V1.00.20/21)不受影响。
|
||||||
|
|
||||||
|
主机参考实现:`tools\ble_ota_update.py` 的 `ota_session_uart_stream()`
|
||||||
|
(`--uart` 默认流式,`--lockstep` 回旧模式)。
|
||||||
|
|
||||||
## 6.5 维护命令:bootsetting 与 APP_DATA 参数区(V1.00.09 起,结构化访问;V1.00.10 起参数命令为 appget/appset;V1.00.11 起新增 reset/factory/uartrst/uartinfo)
|
## 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 命令形式提供,
|
bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形式提供,
|
||||||
|
|||||||
60
docs/开发日志.md
60
docs/开发日志.md
@ -1171,3 +1171,63 @@ APP2 链接的镜像发向 APP1 区。设备端 END 校验(向量表 Reset PC
|
|||||||
重传/再同步提示前补换行不与进度条混行;END 总结新增
|
重传/再同步提示前补换行不与进度条混行;END 总结新增
|
||||||
`N resent, N resynced` 统计(链路质量一目了然:BLE 实测 0 次,
|
`N resent, N resynced` 统计(链路质量一目了然:BLE 实测 0 次,
|
||||||
接触不良的串口线 4~6 次)。BEGIN/END 阶段切换增加明示打印。
|
接触不良的串口线 4~6 次)。BEGIN/END 阶段切换增加明示打印。
|
||||||
|
|
||||||
|
## 47. UART OTA 流式传输层 + RX 改 DMA 环形缓冲(2026-09-05,V1.00.24)
|
||||||
|
|
||||||
|
- **动机**:UART OTA 逐帧锁步实测仅 ~1.5 kB/s(55KB 要 39s),瓶颈是每帧
|
||||||
|
一个 RTT(设备处理 + 主机调度抖动),不是波特率。
|
||||||
|
- **UART RX 改 DMA 环形缓冲**(bsp_usart.c):DMA_CH2 循环模式 + 2KB ring,
|
||||||
|
USART1_RX remap;RXDNE 逐字节中断关闭,改开 IDLEF 中断只做信号量唤醒
|
||||||
|
(`bsp_usart_read_byte()` 接口不变,20ms 切片等信号量兜底连续流无 IDLE
|
||||||
|
的情形;BLE 下行注入 CLI 改走独立的 64B 注入队列,优先于 ring 消费)。
|
||||||
|
**关键收益:DMA 在关中断的扇区擦除窗口(数十 ms)内继续收字节**
|
||||||
|
(2KB ≈ 173ms@115200),这是流式不丢包的前提——中断驱动 RX 在擦除期间
|
||||||
|
必丢字节,正是当年只能锁步的根因。CLI 文本模式行为不变。
|
||||||
|
- **流式传输层(ble_protocol.md §6.7)**:OTA_BEGIN 扩展 16B(+flags u32,
|
||||||
|
bit0=STREAM,仅 UART 通道生效);DATA 不再逐帧 ack,只在跨越 4KB 扇区
|
||||||
|
边界(擦除已完成)或最后一字节时回 ack;主机 8KB 窗口流水线发送;
|
||||||
|
丢帧靠既有的 BAD_STATE+期望 offset 立即回执回退续传(go-back-N,设备侧
|
||||||
|
节流为对齐前只报一次);3s 无 ack 进展主机自动回退到最后的 ack 重发;
|
||||||
|
END/ABORT 语义不变。12B BEGIN=锁步,新旧固件/主机任意组合兼容(旧固件
|
||||||
|
对 16B BEGIN 回 bad_frame,PC 工具自动回退;`--lockstep` 强制旧模式)。
|
||||||
|
- PC 工具 `ble_ota_update.py` 新增 `ota_session_uart_stream()`;
|
||||||
|
BLE 路径与 App(e0005)路径完全不受影响。
|
||||||
|
- 预期速率:115200 下 ~10 kB/s(55KB 约 6s,原 39s)。
|
||||||
|
|
||||||
|
### §47 补记:V1.00.24 首版烧录即锁死——SRAM 顶部悬崖(同日下午修复)
|
||||||
|
|
||||||
|
- **现象**:首版 V1.00.24 烧录后板子无 banner 无广播。SWD 连上看:CPU 处于
|
||||||
|
lockup,LR 落在 HardFault_Handler 内(fault 中再 fault)。
|
||||||
|
- **定位**:用 NSpyocd commander 逐地址探测发现 **SRAM 从 0x2000C000 起读即
|
||||||
|
TransferFault**(复位后立刻探也一样),最后一个可访问字是 0x2000BFFC——
|
||||||
|
该 256KB 硅片 APP 可用 SRAM 实为 **32KB(0x20004000~0x2000BFFF)**,
|
||||||
|
顶部 16KB 块并不可用(IRAM 配置里 0xC000=48KB 的上限是虚的)。V1.00.23
|
||||||
|
的栈顶是 0x2000B9D8 恰好在线内;本次新增的 2KB DMA ring 把 ZI 顶高,
|
||||||
|
栈顶漂到 0x2000C1E8 越线 → 上电第一批压栈即炸。**教训:任何 ZI/RW
|
||||||
|
膨胀都必须复查 map 的 `__initial_sp`**。
|
||||||
|
- **修复**:FreeRTOS heap 20KB→18KB(实测余量 12.6KB,够用),栈顶回到
|
||||||
|
0x2000B9E8;`merge_image.py` 增加硬卡:bin 向量表首字(初始 SP)不在
|
||||||
|
(0x20004000, 0x2000C000] 内直接出包失败,距悬崖 <1KB 告警。
|
||||||
|
- **验证**:重烧后 SWD 观测 reset→go→延时,CPU 持续 Running、PC 在 APP1
|
||||||
|
正常代码区推进,HardFault 断点不再命中。
|
||||||
|
- AGENTS.md "RAM 铁律"已改写为双侧悬崖(低 16KB ROM 占用 + 顶部
|
||||||
|
0x2000C000 悬崖)。
|
||||||
|
|
||||||
|
### §47 再补记:流式首测无 ack 的两个叠加 bug(当日下午二修,已实测通过)
|
||||||
|
|
||||||
|
- **现象**:V1.00.25 首版流式 OTA 主机侧 0 ack,反复 stall 回退。
|
||||||
|
- **定位过程**(SWD 在线取证,值得记一笔):commander 读 `s_offset`/
|
||||||
|
`s_erased`/`s_prog` 发现设备把 4800B 突发全部正确收写(环形缓冲内容
|
||||||
|
与主机发送字节流逐字节一致,DMA RX 无丢字节);OTA 状态机正常、erase
|
||||||
|
正常、TX DMA 通道空闲未挂死——唯一没发生的就是 ack 本身。
|
||||||
|
- **根因 1(V1.00.24 版)**:扇区 ack 写成"`s_offset` 恰好是 4096 倍数才
|
||||||
|
ack",240B/帧时偏移序列 4080→4320 永远踩不中边界。
|
||||||
|
- **根因 2(V1.00.25 首版)**:改成跨界判断时用了 `dlen`——但 `dlen`
|
||||||
|
在上面的 4B 对齐暂存逻辑里已被消耗成 0~3 的尾巴,`(s_offset-0)` 与
|
||||||
|
`s_offset` 永远同扇区,ack 永不触发。修复:用帧原始长度 `chunk` 做
|
||||||
|
跨界判断(`app_ota.c ota_on_data`)。
|
||||||
|
- **实测**:双向各跑一次,均 5.9s 完成(**9.5 kB/s,0 rewinds**,锁步时
|
||||||
|
39s/1.4kB/s),复位复核 PASS(APP1→APP2→APP1,版本 V1.00.25)。
|
||||||
|
- 附:flash_package 改 python 包装(tools/flash_package.py)流式转发
|
||||||
|
NSpyocd 输出并替换横幅为 "CAIIC NSLINK UMP";块状读取修进度条 \r
|
||||||
|
不刷新问题;模式替换需在拼接缓冲上做(跨块边界会劈开模式串)。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -33,7 +33,12 @@
|
|||||||
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
|
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
|
||||||
#define configMAX_PRIORITIES ( 7 )
|
#define configMAX_PRIORITIES ( 7 )
|
||||||
#define configMINIMAL_STACK_SIZE ( ( uint16_t ) 128 )
|
#define configMINIMAL_STACK_SIZE ( ( uint16_t ) 128 )
|
||||||
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 20 * 1024 ) )
|
/* Usable app SRAM on this silicon is 32KB (0x20004000..0x2000BFFF; accesses
|
||||||
|
* above fault - the top 16KB block is not available to the APP, see dev log
|
||||||
|
* §47). The stack top (__initial_sp) must stay below 0x2000C000, so
|
||||||
|
* RW+ZI+STACK must fit 32KB; heap sized accordingly (12.6KB free was
|
||||||
|
* observed at V1.00.23 with a 20KB heap, so 2KB less is safe). */
|
||||||
|
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 18 * 1024 ) )
|
||||||
#define configMAX_TASK_NAME_LEN ( 16 )
|
#define configMAX_TASK_NAME_LEN ( 16 )
|
||||||
#define configUSE_TRACE_FACILITY 1
|
#define configUSE_TRACE_FACILITY 1
|
||||||
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
|
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
|
||||||
|
|||||||
@ -9,12 +9,12 @@
|
|||||||
#define __APP_VERSION_H__
|
#define __APP_VERSION_H__
|
||||||
|
|
||||||
#ifndef APP_FW_VERSION
|
#ifndef APP_FW_VERSION
|
||||||
#define APP_FW_VERSION "V1.00.23"
|
#define APP_FW_VERSION "V1.00.25"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
|
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
|
||||||
#ifndef APP_FW_VERSION_NUM
|
#ifndef APP_FW_VERSION_NUM
|
||||||
#define APP_FW_VERSION_NUM 0x00010017u
|
#define APP_FW_VERSION_NUM 0x00010019u
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif /* __APP_VERSION_H__ */
|
#endif /* __APP_VERSION_H__ */
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file bsp_usart.h
|
* @file bsp_usart.h
|
||||||
* @brief USART1 support: printf retarget (polling TX), DMA TX, RX interrupt queue.
|
* @brief USART1 support: printf retarget (polling TX), DMA TX, RX DMA
|
||||||
* TX=PB6, RX=PB7, 115200 8N1.
|
* circular ring + IDLE interrupt. TX=PB6, RX=PB7, 115200 8N1.
|
||||||
*/
|
*/
|
||||||
#ifndef __BSP_USART_H__
|
#ifndef __BSP_USART_H__
|
||||||
#define __BSP_USART_H__
|
#define __BSP_USART_H__
|
||||||
@ -26,6 +26,11 @@ extern "C" {
|
|||||||
#define BSP_USART_TX_DMA_CH DMA_CH1
|
#define BSP_USART_TX_DMA_CH DMA_CH1
|
||||||
#define BSP_USART_TX_DMA_TC DMA_FLAG_TC1
|
#define BSP_USART_TX_DMA_TC DMA_FLAG_TC1
|
||||||
#define BSP_USART_TX_DMA_REMAP DMA_REMAP_USART1_TX
|
#define BSP_USART_TX_DMA_REMAP DMA_REMAP_USART1_TX
|
||||||
|
/* USART1 RX DMA channel: circular mode into a 2KB ring, drained by
|
||||||
|
* bsp_usart_read_byte(). DMA keeps sampling while interrupts are off
|
||||||
|
* (flash erase windows during UART OTA streaming). */
|
||||||
|
#define BSP_USART_RX_DMA_CH DMA_CH2
|
||||||
|
#define BSP_USART_RX_DMA_REMAP DMA_REMAP_USART1_RX
|
||||||
#define BSP_USART_DAT_ADDR (USART1_BASE + 0x04)
|
#define BSP_USART_DAT_ADDR (USART1_BASE + 0x04)
|
||||||
|
|
||||||
/* Pass to bsp_usart_read_byte() to block without timeout */
|
/* Pass to bsp_usart_read_byte() to block without timeout */
|
||||||
@ -40,11 +45,13 @@ void bsp_usart_set_baud(uint32_t baud);
|
|||||||
/* TX: send a buffer through DMA (polled completion) */
|
/* TX: send a buffer through DMA (polled completion) */
|
||||||
void bsp_usart_write_dma(const uint8_t* data, uint16_t len);
|
void bsp_usart_write_dma(const uint8_t* data, uint16_t len);
|
||||||
|
|
||||||
/* RX: fetch one byte received by the RXDNE interrupt; returns 1 on success */
|
/* RX: fetch one received byte. Source order: the inject queue (BLE
|
||||||
|
* downlink feeding the CLI) first, then the DMA RX ring. Returns 1 on
|
||||||
|
* success, 0 on timeout. */
|
||||||
int bsp_usart_read_byte(uint8_t* ch, uint32_t timeout_ms);
|
int bsp_usart_read_byte(uint8_t* ch, uint32_t timeout_ms);
|
||||||
|
|
||||||
/* RX: inject bytes from task context into the same RX queue (BLE downlink
|
/* RX: inject bytes from task context (BLE downlink feeds the CLI through
|
||||||
* feeds the CLI through this). Bytes are dropped when the queue is full. */
|
* this). Bytes are dropped when the inject queue is full. */
|
||||||
void bsp_usart_rx_inject(const uint8_t* data, uint16_t len);
|
void bsp_usart_rx_inject(const uint8_t* data, uint16_t len);
|
||||||
|
|
||||||
/* TX mutex so that DMA bursts and polling printf lines do not interleave.
|
/* TX mutex so that DMA bursts and polling printf lines do not interleave.
|
||||||
|
|||||||
@ -3,11 +3,11 @@
|
|||||||
* @brief OTA receiver implementation (channel-agnostic: BLE OTA
|
* @brief OTA receiver implementation (channel-agnostic: BLE OTA
|
||||||
* characteristic, UART binary mode, legacy notify frame path).
|
* characteristic, UART binary mode, legacy notify frame path).
|
||||||
*
|
*
|
||||||
* Flow: OTA_BEGIN (size/crc/version) -> OTA_DATA* (strictly sequential
|
* Flow: OTA_BEGIN (size/crc/version[/flags]) -> OTA_DATA* (strictly sequential
|
||||||
* offsets, EVERY frame acked with the new offset - lockstep flow control)
|
* offsets; lockstep acks per frame, or sector-boundary acks in UART stream
|
||||||
* -> OTA_END (flush the sub-word tail, verify whole-image CRC32 against
|
* mode - ble_protocol.md §6.7) -> OTA_END (flush the sub-word tail, verify
|
||||||
* flash, verify the vector table targets the right bank, update
|
* whole-image CRC32 against flash, verify the vector table targets the right
|
||||||
* bootsetting, reset). OTA_ABORT aborts the session.
|
* bank, update bootsetting, reset). OTA_ABORT aborts the session.
|
||||||
*
|
*
|
||||||
* Direct-write design (no 4KB staging buffer):
|
* Direct-write design (no 4KB staging buffer):
|
||||||
* - Sectors of the target bank are erased lazily: the first received byte
|
* - Sectors of the target bank are erased lazily: the first received byte
|
||||||
@ -89,6 +89,17 @@ static uint32_t ota_rd32(const uint8_t* p)
|
|||||||
|
|
||||||
static app_ota_rsp_sink_fn s_rsp_sink;
|
static app_ota_rsp_sink_fn s_rsp_sink;
|
||||||
|
|
||||||
|
static void ota_uart_sink(const uint8_t* frame, uint16_t len);
|
||||||
|
|
||||||
|
/* UART streaming mode (ble_protocol.md §6.7): OTA_BEGIN payload extended
|
||||||
|
* to 16B {size, crc32, version, flags}; flags bit0 = stream. In stream mode
|
||||||
|
* DATA frames are acked only at 4KB sector boundaries (the erase has
|
||||||
|
* completed by then) instead of per-frame lockstep; the host pipelines
|
||||||
|
* writes with an 8KB window and rewinds on BAD_STATE. BLE stays lockstep. */
|
||||||
|
#define OTA_BEGIN_FLAG_STREAM 0x01u
|
||||||
|
static uint8_t s_stream; /* current session is streaming */
|
||||||
|
static uint8_t s_bad_state_sent; /* throttle repeated BAD_STATE reports */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief CRC16-CCITT (poly 0x1021, init 0xFFFF) - same as the frame
|
* @brief CRC16-CCITT (poly 0x1021, init 0xFFFF) - same as the frame
|
||||||
* protocol's proto_crc16 (app_ble_proto.c).
|
* protocol's proto_crc16 (app_ble_proto.c).
|
||||||
@ -157,6 +168,8 @@ void app_ota_abort(void)
|
|||||||
s_prog = 0;
|
s_prog = 0;
|
||||||
s_erased = 0;
|
s_erased = 0;
|
||||||
s_word_fill = 0;
|
s_word_fill = 0;
|
||||||
|
s_stream = 0;
|
||||||
|
s_bad_state_sent = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,14 +208,15 @@ static uint8_t ota_program(const uint8_t* data, uint32_t len)
|
|||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief OTA_BEGIN: {total_size u32, image_crc32 u32, version u32}.
|
* @brief OTA_BEGIN: {total_size u32, image_crc32 u32, version u32}
|
||||||
|
* or the extended 16B form {..., flags u32} (ble_protocol.md §6.7).
|
||||||
*/
|
*/
|
||||||
static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||||
{
|
{
|
||||||
uint32_t total, crc, version;
|
uint32_t total, crc, version, flags;
|
||||||
uint32_t self, target;
|
uint32_t self, target;
|
||||||
|
|
||||||
if (len != 12u)
|
if (len != 12u && len != 16u)
|
||||||
{
|
{
|
||||||
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_FRAME, 0);
|
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_FRAME, 0);
|
||||||
return;
|
return;
|
||||||
@ -211,6 +225,7 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
|||||||
total = ota_rd32(p);
|
total = ota_rd32(p);
|
||||||
crc = ota_rd32(p + 4);
|
crc = ota_rd32(p + 4);
|
||||||
version = ota_rd32(p + 8);
|
version = ota_rd32(p + 8);
|
||||||
|
flags = (len == 16u) ? ota_rd32(p + 12) : 0;
|
||||||
|
|
||||||
/* One session at a time across all channels: a second BEGIN (e.g. BLE
|
/* One session at a time across all channels: a second BEGIN (e.g. BLE
|
||||||
* while a UART session runs) is refused instead of corrupting state */
|
* while a UART session runs) is refused instead of corrupting state */
|
||||||
@ -257,6 +272,12 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
|||||||
s_prog = 0;
|
s_prog = 0;
|
||||||
s_erased = 0;
|
s_erased = 0;
|
||||||
s_word_fill = 0;
|
s_word_fill = 0;
|
||||||
|
/* Streaming only on the UART channel: its RSP goes straight out on TX
|
||||||
|
* and the DMA ring buffers inbound bytes during flash erases. The BLE
|
||||||
|
* channel is read-polled and stays per-frame lockstep. */
|
||||||
|
s_stream = ((flags & OTA_BEGIN_FLAG_STREAM) != 0u &&
|
||||||
|
s_rsp_sink == ota_uart_sink) ? 1u : 0u;
|
||||||
|
s_bad_state_sent = 0;
|
||||||
|
|
||||||
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_OK, 0);
|
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_OK, 0);
|
||||||
}
|
}
|
||||||
@ -264,11 +285,13 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
|||||||
/**
|
/**
|
||||||
* @brief OTA_DATA: {offset u32 + data}. Data must arrive strictly in order.
|
* @brief OTA_DATA: {offset u32 + data}. Data must arrive strictly in order.
|
||||||
* Payload is programmed to flash immediately (sectors are erased
|
* Payload is programmed to flash immediately (sectors are erased
|
||||||
* lazily); EVERY frame is acked with the new offset (lockstep flow
|
* lazily). Ack policy depends on the session mode:
|
||||||
* control on the BLE/UART OTA channels; the erase time of a fresh
|
* - lockstep (BLE and legacy hosts): EVERY frame is acked with the
|
||||||
* sector is covered by the ack round trip).
|
* new offset, the ack round trip covers a fresh sector's erase;
|
||||||
* Errors are acked immediately and the state is
|
* - streaming (UART, BEGIN flags bit0): acks only at 4KB sector
|
||||||
* kept so the peer can resend.
|
* boundaries (erase completed) and at the final byte; the host
|
||||||
|
* pipelines frames inside an 8KB window and rewinds to the offset
|
||||||
|
* reported by an immediate BAD_STATE ack when a frame is lost.
|
||||||
*/
|
*/
|
||||||
static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||||
{
|
{
|
||||||
@ -291,11 +314,21 @@ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
|||||||
off = ota_rd32(p);
|
off = ota_rd32(p);
|
||||||
data = p + 4;
|
data = p + 4;
|
||||||
dlen = (uint32_t)len - 4u;
|
dlen = (uint32_t)len - 4u;
|
||||||
|
const uint32_t chunk = dlen; /* dlen is consumed down to the sub-word
|
||||||
|
* tail by the staging logic below; the
|
||||||
|
* stream-ack boundary check needs the
|
||||||
|
* original chunk length */
|
||||||
|
|
||||||
if (off != s_offset)
|
if (off != s_offset)
|
||||||
{
|
{
|
||||||
/* out-of-order: report the expected offset, keep the session */
|
/* out-of-order: report the expected offset, keep the session.
|
||||||
|
* In stream mode the frames after a lost one ALL mismatch until the
|
||||||
|
* host rewinds - report once, not per frame */
|
||||||
|
if (!s_stream || !s_bad_state_sent)
|
||||||
|
{
|
||||||
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, s_offset);
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, s_offset);
|
||||||
|
s_bad_state_sent = 1;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (s_offset + dlen > s_total_size)
|
if (s_offset + dlen > s_total_size)
|
||||||
@ -342,6 +375,22 @@ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
|||||||
app_ota_abort();
|
app_ota_abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
s_bad_state_sent = 0; /* in-order again: re-arm the error report */
|
||||||
|
if (s_stream)
|
||||||
|
{
|
||||||
|
/* Streaming (UART): ack when a 4KB sector boundary was CROSSED by
|
||||||
|
* this chunk (its lazy erase has completed by then) or on the final
|
||||||
|
* byte. A chunk rarely lands exactly on a boundary (e.g. 240B
|
||||||
|
* frames step 4080->4320), so compare sector indices, not equality.
|
||||||
|
* NOTE: use chunk (the original length), not dlen which the staging
|
||||||
|
* logic has consumed to the sub-word tail. */
|
||||||
|
if (((s_offset - chunk) & ~0xFFFu) != (s_offset & ~0xFFFu) ||
|
||||||
|
s_offset == s_total_size)
|
||||||
|
{
|
||||||
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
/* Lockstep ack: every DATA frame is answered with the new offset */
|
/* Lockstep ack: every DATA frame is answered with the new offset */
|
||||||
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
||||||
}
|
}
|
||||||
@ -493,8 +542,6 @@ void app_ota_reset_poll(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ota_uart_sink(const uint8_t* frame, uint16_t len);
|
|
||||||
|
|
||||||
static uint8_t s_uart_exit_req; /* OTA_ABORT on UART: drop back to CLI now */
|
static uint8_t s_uart_exit_req; /* OTA_ABORT on UART: drop back to CLI now */
|
||||||
|
|
||||||
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file bsp_usart.c
|
* @file bsp_usart.c
|
||||||
* @brief USART1 init, printf retarget (fputc, polling TX), DMA TX and
|
* @brief USART1 init, printf retarget (fputc, polling TX), DMA TX and
|
||||||
* RX interrupt byte queue.
|
* RX DMA circular ring (IDLE interrupt wakeup + polled drain).
|
||||||
*/
|
*/
|
||||||
#include "bsp_usart.h"
|
#include "bsp_usart.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@ -11,14 +11,62 @@
|
|||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
#include "semphr.h"
|
#include "semphr.h"
|
||||||
|
|
||||||
#define RX_QUEUE_LENGTH 64
|
#define RX_INJECT_QUEUE_LEN 64 /* BLE downlink -> CLI injection */
|
||||||
|
#define RX_RING_SIZE 2048u /* DMA circular ring: ~173ms @115200,
|
||||||
|
* covers flash-erase interrupt-off
|
||||||
|
* windows during OTA streaming */
|
||||||
|
#define RX_WAIT_SLICE_MS 20u /* semaphore wait granularity (also the
|
||||||
|
* poll period when the IDLE interrupt
|
||||||
|
* does not fire during a back-to-back
|
||||||
|
* byte stream) */
|
||||||
|
|
||||||
static QueueHandle_t s_rxQueue = NULL;
|
static uint8_t s_rxRing[RX_RING_SIZE];
|
||||||
|
static volatile uint16_t s_rxTail; /* software drain position in the ring */
|
||||||
|
static QueueHandle_t s_rxInjectQueue = NULL;
|
||||||
|
static SemaphoreHandle_t s_rxSem = NULL;
|
||||||
static SemaphoreHandle_t s_txMutex = NULL;
|
static SemaphoreHandle_t s_txMutex = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start the USART1 RX DMA circular channel (shared by init and
|
||||||
|
* baud re-apply).
|
||||||
|
*/
|
||||||
|
static void bsp_usart_rx_dma_start(void)
|
||||||
|
{
|
||||||
|
DMA_InitType DMA_InitStructure;
|
||||||
|
|
||||||
|
DMA_EnableChannel(BSP_USART_RX_DMA_CH, DISABLE);
|
||||||
|
DMA_DeInit(BSP_USART_RX_DMA_CH);
|
||||||
|
DMA_RequestRemap(BSP_USART_RX_DMA_REMAP, DMA, BSP_USART_RX_DMA_CH, ENABLE);
|
||||||
|
|
||||||
|
DMA_InitStructure.PeriphAddr = BSP_USART_DAT_ADDR;
|
||||||
|
DMA_InitStructure.MemAddr = (uint32_t)s_rxRing;
|
||||||
|
DMA_InitStructure.Direction = DMA_DIR_PERIPH_SRC;
|
||||||
|
DMA_InitStructure.BufSize = RX_RING_SIZE;
|
||||||
|
DMA_InitStructure.PeriphInc = DMA_PERIPH_INC_DISABLE;
|
||||||
|
DMA_InitStructure.DMA_MemoryInc = DMA_MEM_INC_ENABLE;
|
||||||
|
DMA_InitStructure.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
|
||||||
|
DMA_InitStructure.MemDataSize = DMA_MemoryDataSize_Byte;
|
||||||
|
DMA_InitStructure.CircularMode = DMA_MODE_CIRCULAR;
|
||||||
|
DMA_InitStructure.Priority = DMA_PRIORITY_HIGH;
|
||||||
|
DMA_InitStructure.Mem2Mem = DMA_M2M_DISABLE;
|
||||||
|
DMA_Init(BSP_USART_RX_DMA_CH, &DMA_InitStructure);
|
||||||
|
|
||||||
|
s_rxTail = 0;
|
||||||
|
DMA_EnableChannel(BSP_USART_RX_DMA_CH, ENABLE);
|
||||||
|
USART_EnableDMA(BSP_USARTx, USART_DMAREQ_RX, ENABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief DMA ring head (hardware write position).
|
||||||
|
*/
|
||||||
|
static uint16_t bsp_usart_rx_head(void)
|
||||||
|
{
|
||||||
|
return (uint16_t)(RX_RING_SIZE - DMA_GetCurrDataCounter(BSP_USART_RX_DMA_CH));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initialize USART1 (115200 8N1) on PB6(TX)/PB7(RX),
|
* @brief Initialize USART1 (115200 8N1) on PB6(TX)/PB7(RX),
|
||||||
* DMA TX channel and the RX interrupt queue.
|
* DMA TX channel and the RX DMA ring.
|
||||||
*/
|
*/
|
||||||
void bsp_usart_init(void)
|
void bsp_usart_init(void)
|
||||||
{
|
{
|
||||||
@ -57,11 +105,13 @@ void bsp_usart_init(void)
|
|||||||
DMA_RequestRemap(BSP_USART_TX_DMA_REMAP, DMA, BSP_USART_TX_DMA_CH, ENABLE);
|
DMA_RequestRemap(BSP_USART_TX_DMA_REMAP, DMA, BSP_USART_TX_DMA_CH, ENABLE);
|
||||||
USART_EnableDMA(BSP_USARTx, USART_DMAREQ_TX, ENABLE);
|
USART_EnableDMA(BSP_USARTx, USART_DMAREQ_TX, ENABLE);
|
||||||
|
|
||||||
/* RX byte queue fed by the RXDNE interrupt */
|
/* RX: DMA circular ring + IDLE interrupt wakeup */
|
||||||
s_rxQueue = xQueueCreate(RX_QUEUE_LENGTH, sizeof(uint8_t));
|
s_rxInjectQueue = xQueueCreate(RX_INJECT_QUEUE_LEN, sizeof(uint8_t));
|
||||||
|
s_rxSem = xSemaphoreCreateBinary();
|
||||||
s_txMutex = xSemaphoreCreateMutex();
|
s_txMutex = xSemaphoreCreateMutex();
|
||||||
|
bsp_usart_rx_dma_start();
|
||||||
|
|
||||||
USART_ConfigInt(BSP_USARTx, USART_INT_RXDNE, ENABLE);
|
USART_ConfigInt(BSP_USARTx, USART_INT_IDLEF, ENABLE);
|
||||||
/* Lowest priority: the ISR calls FreeRTOS FromISR APIs */
|
/* Lowest priority: the ISR calls FreeRTOS FromISR APIs */
|
||||||
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
|
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
|
||||||
NVIC_InitStructure.NVIC_IRQChannelPriority = 3;
|
NVIC_InitStructure.NVIC_IRQChannelPriority = 3;
|
||||||
@ -72,10 +122,10 @@ void bsp_usart_init(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Re-apply the baud rate at runtime (keeps RX interrupt/DMA setup).
|
* @brief Re-apply the baud rate at runtime (keeps RX DMA ring setup).
|
||||||
* Waits for TX to drain, re-initializes the USART and flushes the
|
* Waits for TX to drain, re-initializes the USART, restarts the RX
|
||||||
* RX queue (stale bytes may have been sampled at the old baud rate).
|
* DMA ring and flushes the inject queue (stale bytes may have been
|
||||||
* Used by the "uartrst" CLI command.
|
* sampled at the old baud rate). Used by the "uartrst" CLI command.
|
||||||
*/
|
*/
|
||||||
void bsp_usart_set_baud(uint32_t baud)
|
void bsp_usart_set_baud(uint32_t baud)
|
||||||
{
|
{
|
||||||
@ -97,12 +147,13 @@ void bsp_usart_set_baud(uint32_t baud)
|
|||||||
USART_Init(BSP_USARTx, &USART_InitStructure);
|
USART_Init(BSP_USARTx, &USART_InitStructure);
|
||||||
|
|
||||||
/* Drop bytes received at the old baud rate */
|
/* Drop bytes received at the old baud rate */
|
||||||
if (s_rxQueue != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
|
bsp_usart_rx_dma_start();
|
||||||
|
if (s_rxInjectQueue != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
|
||||||
{
|
{
|
||||||
xQueueReset(s_rxQueue);
|
xQueueReset(s_rxInjectQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
USART_ConfigInt(BSP_USARTx, USART_INT_RXDNE, ENABLE);
|
USART_ConfigInt(BSP_USARTx, USART_INT_IDLEF, ENABLE);
|
||||||
USART_Enable(BSP_USARTx, ENABLE);
|
USART_Enable(BSP_USARTx, ENABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,37 +199,71 @@ void bsp_usart_write_dma(const uint8_t* data, uint16_t len)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Fetch one byte received by the RX interrupt.
|
* @brief Fetch one received byte. The BLE-injected queue is drained first,
|
||||||
|
* then the DMA RX ring. Blocking waits use the RX semaphore (poked by
|
||||||
|
* the IDLE interrupt) in RX_WAIT_SLICE_MS slices so a continuous
|
||||||
|
* byte stream without idle gaps is still drained in time.
|
||||||
* @param ch output byte
|
* @param ch output byte
|
||||||
* @param timeout_ms BSP_USART_WAIT_FOREVER or a timeout in ms
|
* @param timeout_ms BSP_USART_WAIT_FOREVER or a timeout in ms
|
||||||
* @return 1 on success, 0 on timeout
|
* @return 1 on success, 0 on timeout
|
||||||
*/
|
*/
|
||||||
int bsp_usart_read_byte(uint8_t* ch, uint32_t timeout_ms)
|
int bsp_usart_read_byte(uint8_t* ch, uint32_t timeout_ms)
|
||||||
{
|
{
|
||||||
TickType_t ticks;
|
uint32_t waited = 0;
|
||||||
|
|
||||||
if (s_rxQueue == NULL)
|
for (;;)
|
||||||
|
{
|
||||||
|
uint16_t head;
|
||||||
|
|
||||||
|
/* Injected bytes (BLE downlink -> CLI) take priority */
|
||||||
|
if (s_rxInjectQueue != NULL &&
|
||||||
|
xTaskGetSchedulerState() == taskSCHEDULER_RUNNING &&
|
||||||
|
xQueueReceive(s_rxInjectQueue, ch, 0) == pdPASS)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
head = bsp_usart_rx_head();
|
||||||
|
if (s_rxTail != head)
|
||||||
|
{
|
||||||
|
*ch = s_rxRing[s_rxTail];
|
||||||
|
s_rxTail = (uint16_t)((s_rxTail + 1u) % RX_RING_SIZE);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout_ms == 0u ||
|
||||||
|
(timeout_ms != BSP_USART_WAIT_FOREVER && waited >= timeout_ms))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (s_rxSem == NULL || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING)
|
||||||
|
{
|
||||||
|
return 0; /* pre-scheduler: non-blocking snapshot only */
|
||||||
|
}
|
||||||
|
|
||||||
ticks = (timeout_ms == BSP_USART_WAIT_FOREVER) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
uint32_t slice = RX_WAIT_SLICE_MS;
|
||||||
return (xQueueReceive(s_rxQueue, ch, ticks) == pdPASS) ? 1 : 0;
|
if (timeout_ms != BSP_USART_WAIT_FOREVER && slice > timeout_ms - waited)
|
||||||
|
{
|
||||||
|
slice = timeout_ms - waited;
|
||||||
|
}
|
||||||
|
(void)xSemaphoreTake(s_rxSem, pdMS_TO_TICKS(slice));
|
||||||
|
waited += slice;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Inject received bytes from task context into the RX queue.
|
* @brief Inject received bytes from task context into the inject queue.
|
||||||
* Used by the BLE downlink to feed the CLI. Drops on full queue.
|
* Used by the BLE downlink to feed the CLI. Drops on full queue.
|
||||||
*/
|
*/
|
||||||
void bsp_usart_rx_inject(const uint8_t* data, uint16_t len)
|
void bsp_usart_rx_inject(const uint8_t* data, uint16_t len)
|
||||||
{
|
{
|
||||||
if (s_rxQueue == NULL || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING)
|
if (s_rxInjectQueue == NULL || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while (len--)
|
while (len--)
|
||||||
{
|
{
|
||||||
if (xQueueSend(s_rxQueue, data++, 0) != pdPASS)
|
if (xQueueSend(s_rxInjectQueue, data++, 0) != pdPASS)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -208,21 +293,23 @@ void bsp_usart_tx_unlock(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief RXDNE interrupt handler, pushes the received byte into the RX queue.
|
* @brief IDLE line interrupt handler: the DMA ring holds the received bytes,
|
||||||
|
* here we only wake a task blocked in bsp_usart_read_byte().
|
||||||
* Called from USART1_IRQHandler (see n32wb03x_it.c).
|
* Called from USART1_IRQHandler (see n32wb03x_it.c).
|
||||||
*/
|
*/
|
||||||
void bsp_usart_rx_isr_handler(void)
|
void bsp_usart_rx_isr_handler(void)
|
||||||
{
|
{
|
||||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||||
|
|
||||||
if (USART_GetIntStatus(BSP_USARTx, USART_INT_RXDNE) != RESET)
|
if (USART_GetIntStatus(BSP_USARTx, USART_INT_IDLEF) != RESET)
|
||||||
{
|
{
|
||||||
/* Reading DAT clears the RXDNE flag */
|
/* IDLEF clears by reading STS (done by GetIntStatus) then DAT;
|
||||||
uint8_t ch = (uint8_t)USART_ReceiveData(BSP_USARTx);
|
* the data register itself is already drained by the DMA */
|
||||||
|
(void)USART_ReceiveData(BSP_USARTx);
|
||||||
|
|
||||||
if (s_rxQueue != NULL)
|
if (s_rxSem != NULL)
|
||||||
{
|
{
|
||||||
xQueueSendFromISR(s_rxQueue, &ch, &xHigherPriorityTaskWoken);
|
xSemaphoreGiveFromISR(s_rxSem, &xHigherPriorityTaskWoken);
|
||||||
}
|
}
|
||||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
ble_ota_update.py - PC-side OTA updater for CAIIC-MCM devices, transport
|
ble_ota_update.py - PC-side OTA updater for CAIIC-MCM devices, transport
|
||||||
protocol per docs/ble_protocol.md section 6.6 (0xCA frames, OTA_BEGIN/DATA/
|
protocol per docs/ble_protocol.md sections 6.6/6.7 (0xCA frames,
|
||||||
END/ABORT -> framed OTA_RSP acks).
|
OTA_BEGIN/DATA/END/ABORT -> framed OTA_RSP acks).
|
||||||
|
|
||||||
Two channels:
|
Two channels:
|
||||||
BLE (default): dedicated OTA characteristic ...e0005 - one frame per
|
BLE (default): dedicated OTA characteristic ...e0005 - one frame per
|
||||||
@ -12,6 +12,10 @@ Two channels:
|
|||||||
UART (--uart): e.g. --uart COM4 - the CLI command "ota" switches the
|
UART (--uart): e.g. --uart COM4 - the CLI command "ota" switches the
|
||||||
serial link to binary frame mode (marker-confirmed
|
serial link to binary frame mode (marker-confirmed
|
||||||
handshake; OTA_ABORT exits); same frames on the wire.
|
handshake; OTA_ABORT exits); same frames on the wire.
|
||||||
|
Firmware V1.00.24+ runs STREAM mode here (pipelined
|
||||||
|
DATA inside an 8KB window, sector-boundary acks,
|
||||||
|
go-back-N on loss); older firmware falls back to
|
||||||
|
lockstep automatically, or force it with --lockstep.
|
||||||
|
|
||||||
Both channels select the payload by CUR_BANK (info item 0x07, read-only
|
Both channels select the payload by CUR_BANK (info item 0x07, read-only
|
||||||
characteristic ...e0004 / CLI "devinfo"): OTA always writes the INACTIVE
|
characteristic ...e0004 / CLI "devinfo"): OTA always writes the INACTIVE
|
||||||
@ -19,7 +23,7 @@ bank with the image linked for it, from the single-file package
|
|||||||
(mothercup_ble_ota.bin, 52B header + both payloads).
|
(mothercup_ble_ota.bin, 52B header + both payloads).
|
||||||
|
|
||||||
usage: ble_ota.bat [mothercup_ble_ota.bin]
|
usage: ble_ota.bat [mothercup_ble_ota.bin]
|
||||||
ble_ota.bat --uart COM4 [mothercup_ble_ota.bin]
|
ble_ota.bat --uart COM4 [--lockstep] [mothercup_ble_ota.bin]
|
||||||
|
|
||||||
Success = the device resets after OTA_END and comes back on the new bank
|
Success = the device resets after OTA_END and comes back on the new bank
|
||||||
with the new version (verified by a second CUR_BANK/FW_VERSION read).
|
with the new version (verified by a second CUR_BANK/FW_VERSION read).
|
||||||
@ -51,8 +55,15 @@ XFER_TRIES = 3 # resend a frame whose ack was lost (flaky links)
|
|||||||
REBOOT_WAIT_S = 20.0
|
REBOOT_WAIT_S = 20.0
|
||||||
|
|
||||||
ST_OK = 0
|
ST_OK = 0
|
||||||
|
ST_BAD_FRAME = 1
|
||||||
ST_BAD_STATE = 2
|
ST_BAD_STATE = 2
|
||||||
|
|
||||||
|
# UART stream mode (firmware V1.00.24+, ble_protocol.md §6.7)
|
||||||
|
BEGIN_FLAG_STREAM = 1
|
||||||
|
STREAM_WINDOW = 8192 # unacked bytes in flight
|
||||||
|
STREAM_CHUNK = 240 # DATA payload per frame (device limit 480+4)
|
||||||
|
STREAM_STALL_S = 3.0 # no ack progress -> rewind to last ack
|
||||||
|
|
||||||
_seq = [0]
|
_seq = [0]
|
||||||
|
|
||||||
|
|
||||||
@ -414,6 +425,107 @@ async def ota_session(xfer, chunk, blob, version, target):
|
|||||||
# Channel frontends
|
# Channel frontends
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def ota_session_uart_stream(transport, blob, version, target):
|
||||||
|
"""UART stream mode (firmware V1.00.24+, ble_protocol.md §6.7):
|
||||||
|
extended BEGIN (16B payload, flags=STREAM), then DATA frames are
|
||||||
|
pipelined inside an 8KB window without per-frame acks; the device acks
|
||||||
|
at 4KB sector boundaries (erase completed) and the final byte.
|
||||||
|
A lost/corrupt frame makes the device report BAD_STATE once with the
|
||||||
|
offset it expects - the host rewinds there (go-back-N). A total ack
|
||||||
|
loss is caught by a stall watchdog that also rewinds to the last ack.
|
||||||
|
Raises RuntimeError with 'status=1' when the device rejects the 16B
|
||||||
|
BEGIN (pre-V1.00.24 firmware) so the caller can fall back to lockstep."""
|
||||||
|
rsp = await transport.xfer(encode_frame(TYPE_OTA_BEGIN,
|
||||||
|
struct.pack("<IIII", len(blob), crc32(blob),
|
||||||
|
version, BEGIN_FLAG_STREAM)))
|
||||||
|
frames = FrameDecoder().feed(rsp)
|
||||||
|
if len(frames) != 1 or frames[0][0] != TYPE_OTA_RSP:
|
||||||
|
raise RuntimeError("BEGIN(stream): bad RSP frame: %s" % rsp.hex(" "))
|
||||||
|
echo, status, offset = parse_rsp(frames[0][2])
|
||||||
|
if status != ST_OK:
|
||||||
|
raise RuntimeError("OTA_BEGIN(stream) rejected, status=%d" % status)
|
||||||
|
print("OTA_BEGIN ok (stream mode), pipelining data ...")
|
||||||
|
|
||||||
|
ser = transport.ser
|
||||||
|
dec = transport.dec
|
||||||
|
sent = 0
|
||||||
|
acked = 0
|
||||||
|
rewinds = 0
|
||||||
|
t0 = time.monotonic()
|
||||||
|
last_progress = t0
|
||||||
|
tty = sys.stdout.isatty()
|
||||||
|
while sent < len(blob) or acked < len(blob):
|
||||||
|
# fill the window
|
||||||
|
while sent < len(blob) and sent - acked < STREAM_WINDOW:
|
||||||
|
n = min(STREAM_CHUNK, len(blob) - sent)
|
||||||
|
ser.write(encode_frame(TYPE_OTA_DATA,
|
||||||
|
struct.pack("<I", sent) + blob[sent:sent + n]))
|
||||||
|
sent += n
|
||||||
|
# collect whatever acks arrived (serial timeout = 0.1s)
|
||||||
|
data = await asyncio.to_thread(ser.read, 256)
|
||||||
|
now = time.monotonic()
|
||||||
|
for ftype, seq, payload in dec.feed(data):
|
||||||
|
if ftype != TYPE_OTA_RSP:
|
||||||
|
continue
|
||||||
|
echo, status, offset = parse_rsp(payload)
|
||||||
|
if echo != TYPE_OTA_DATA:
|
||||||
|
continue
|
||||||
|
if status == ST_OK:
|
||||||
|
if offset > acked:
|
||||||
|
acked = offset
|
||||||
|
last_progress = now
|
||||||
|
elif status == ST_BAD_STATE:
|
||||||
|
if sent != offset:
|
||||||
|
rewinds += 1
|
||||||
|
print("\n DATA: lost frame, rewind %d -> %d"
|
||||||
|
% (sent, offset))
|
||||||
|
sent = offset
|
||||||
|
if acked > offset:
|
||||||
|
acked = offset
|
||||||
|
last_progress = now
|
||||||
|
else:
|
||||||
|
raise RuntimeError("OTA_DATA rejected, status=%d "
|
||||||
|
"(device expects offset %d)"
|
||||||
|
% (status, offset))
|
||||||
|
# progress (throttled on tty, one line per 5% otherwise)
|
||||||
|
pct = 100.0 * acked / len(blob)
|
||||||
|
rate = acked / 1024.0 / max(now - t0, 0.001)
|
||||||
|
if tty:
|
||||||
|
print(" %d / %d bytes (%.0f%%, %.1f kB/s, %d rewinds) "
|
||||||
|
% (acked, len(blob), pct, rate, rewinds),
|
||||||
|
end="\r" if acked != len(blob) else "\n", flush=True)
|
||||||
|
elif acked == len(blob) or int(pct // 5) != getattr(
|
||||||
|
ota_session_uart_stream, "_step", -1):
|
||||||
|
ota_session_uart_stream._step = int(pct // 5)
|
||||||
|
print(" %d / %d bytes (%.0f%%, %.1f kB/s, %d rewinds)"
|
||||||
|
% (acked, len(blob), pct, rate, rewinds))
|
||||||
|
# stall watchdog: no ack progress -> the ack itself was lost,
|
||||||
|
# rewind to the last acked offset and resend from there
|
||||||
|
if now - last_progress > STREAM_STALL_S:
|
||||||
|
if sent != acked:
|
||||||
|
rewinds += 1
|
||||||
|
print("\n DATA: ack stall, rewind %d -> %d" % (sent, acked))
|
||||||
|
sent = acked
|
||||||
|
last_progress = now
|
||||||
|
|
||||||
|
print("OTA_END: verify image + switch bank ...")
|
||||||
|
try:
|
||||||
|
rsp = await transport.xfer(encode_frame(TYPE_OTA_END,
|
||||||
|
struct.pack("<I", crc32(blob))))
|
||||||
|
frames = FrameDecoder().feed(rsp)
|
||||||
|
if len(frames) != 1 or frames[0][0] != TYPE_OTA_RSP:
|
||||||
|
raise RuntimeError("END: bad RSP frame: %s" % rsp.hex(" "))
|
||||||
|
echo, status, offset = parse_rsp(frames[0][2])
|
||||||
|
if status != ST_OK:
|
||||||
|
raise RuntimeError("OTA_END rejected, status=%d "
|
||||||
|
"(4=crc_fail, 6=bank_mismatch)" % status)
|
||||||
|
except (asyncio.TimeoutError, OSError) as exc:
|
||||||
|
print("OTA_END ack lost (%s) - waiting for reboot anyway ..." % exc)
|
||||||
|
print("OTA_END done (%.1fs total, %d rewinds), device reboots into "
|
||||||
|
"APP%d ..." % (time.monotonic() - t0, rewinds, target))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def find_device():
|
async def find_device():
|
||||||
from bleak import BleakScanner
|
from bleak import BleakScanner
|
||||||
print("scanning for %s* ..." % NAME_PREFIX)
|
print("scanning for %s* ..." % NAME_PREFIX)
|
||||||
@ -490,7 +602,7 @@ async def run_ble(pkg_path):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
async def run_uart(port, pkg_path):
|
async def run_uart(port, pkg_path, lockstep=False):
|
||||||
version, banks = parse_combo(pkg_path)
|
version, banks = parse_combo(pkg_path)
|
||||||
print("package: version %s, bank1 %dB / bank2 %dB"
|
print("package: version %s, bank1 %dB / bank2 %dB"
|
||||||
% (fmt_ver(version), len(banks[1]), len(banks[2])))
|
% (fmt_ver(version), len(banks[1]), len(banks[2])))
|
||||||
@ -509,6 +621,16 @@ async def run_uart(port, pkg_path):
|
|||||||
% (target, len(blob), crc32(blob)))
|
% (target, len(blob), crc32(blob)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if lockstep:
|
||||||
|
await ota_session(transport.xfer, 222, blob, version, target)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await ota_session_uart_stream(transport, blob, version, target)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
if "status=1" not in str(exc): # not "bad BEGIN frame"
|
||||||
|
raise
|
||||||
|
print("note: device has no stream mode (pre-V1.00.24), "
|
||||||
|
"falling back to lockstep")
|
||||||
await ota_session(transport.xfer, 222, blob, version, target)
|
await ota_session(transport.xfer, 222, blob, version, target)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("FAIL: %s" % exc)
|
print("FAIL: %s" % exc)
|
||||||
@ -539,12 +661,18 @@ async def run_uart(port, pkg_path):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
argv = sys.argv[1:]
|
||||||
|
lower = [a.lower() for a in argv]
|
||||||
|
lockstep = "--lockstep" in lower
|
||||||
uart = None
|
uart = None
|
||||||
if "--uart" in sys.argv[1:]:
|
skip = -1
|
||||||
i = sys.argv[1:].index("--uart")
|
if "--uart" in lower:
|
||||||
uart = sys.argv[1:][i + 1]
|
i = lower.index("--uart")
|
||||||
args = [a for j, a in enumerate(sys.argv[1:]) if j not in (i, i + 1)]
|
if i + 1 < len(argv):
|
||||||
|
uart = argv[i + 1]
|
||||||
|
skip = i + 1
|
||||||
|
args = [a for j, a in enumerate(argv)
|
||||||
|
if not a.startswith("--") and j != skip]
|
||||||
|
|
||||||
root = os.path.dirname(os.path.abspath(__file__))
|
root = os.path.dirname(os.path.abspath(__file__))
|
||||||
pkg = args[0] if args else os.path.join(root, "out", "mothercup_ble_ota.bin")
|
pkg = args[0] if args else os.path.join(root, "out", "mothercup_ble_ota.bin")
|
||||||
@ -552,7 +680,7 @@ def main():
|
|||||||
print("package not found: %s (run tools\\make_package.bat first)" % pkg)
|
print("package not found: %s (run tools\\make_package.bat first)" % pkg)
|
||||||
return 1
|
return 1
|
||||||
if uart:
|
if uart:
|
||||||
return asyncio.run(run_uart(uart, pkg))
|
return asyncio.run(run_uart(uart, pkg, lockstep))
|
||||||
return asyncio.run(run_ble(pkg))
|
return asyncio.run(run_ble(pkg))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,45 +1,25 @@
|
|||||||
@echo off
|
@echo off
|
||||||
rem ============================================================
|
rem ============================================================
|
||||||
rem flash_package.bat [image] - erase the chip and program the
|
rem flash_package.bat [image] - erase the chip and program the
|
||||||
rem merged package via NS-LINK (NSpyocd).
|
rem merged package via NS-LINK (NSpyocd); thin wrapper over
|
||||||
|
rem flash_package.py (streams output with the CAIIC banner).
|
||||||
rem [image] optional, default: tools\out\mothercup_ble_prod.hex
|
rem [image] optional, default: tools\out\mothercup_ble_prod.hex
|
||||||
rem Connect NS-LINK to SWD (PA4/PA5) and the reset pin first,
|
rem Connect NS-LINK to SWD (PA4/PA5) and the reset pin first,
|
||||||
rem and CLOSE Keil (it holds the probe).
|
rem and CLOSE Keil (it holds the probe).
|
||||||
rem ============================================================
|
rem ============================================================
|
||||||
setlocal
|
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\mothercup_ble_prod.hex
|
|
||||||
if not "%IMAGE:~0,1%"=="\" if not "%IMAGE:~1,1%"==":" set IMAGE=%~dp0%IMAGE%
|
|
||||||
|
|
||||||
if not exist "%IMAGE%" (
|
set PYEXE=
|
||||||
echo %IMAGE% not found - run tools\make_package.bat first
|
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
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
rem -M under-reset: connect while holding the chip in reset
|
%PYEXE% "%~dp0flash_package.py" %*
|
||||||
rem -f 1000000: 1 MHz SWD clock (slow but tolerant of wiring)
|
exit /b %ERRORLEVEL%
|
||||||
echo Erasing chip...
|
|
||||||
%PYOCD% erase --chip --pack %PACK% -M under-reset -f 1000000 -t %TARGET% || goto :fail
|
|
||||||
echo Programming %IMAGE% ...
|
|
||||||
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
|
|
||||||
|
|
||||||
:fail
|
|
||||||
echo.
|
|
||||||
echo *** FLASH FAILED ***
|
|
||||||
echo Troubleshooting:
|
|
||||||
echo 1. Close Keil / any tool holding the probe, replug NS-LINK USB
|
|
||||||
echo 2. List probes: %PYOCD% list
|
|
||||||
echo 3. Check wiring: SWDIO=PA5 SWCLK=PA4 GND and the RESET pin
|
|
||||||
echo 4. If several probes are attached, add -u ^<probe_uid^> to the commands above
|
|
||||||
exit /b 1
|
|
||||||
|
|||||||
111
tools/flash_package.py
Normal file
111
tools/flash_package.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
flash_package.py - erase the chip and program the merged package via
|
||||||
|
NS-LINK (NSpyocd). Streams the vendor tool's output with the vendor
|
||||||
|
banner replaced by the CAIIC one (the banner is baked into the
|
||||||
|
PyInstaller-packed NSpyocd.exe and cannot be edited).
|
||||||
|
|
||||||
|
usage: flash_package.bat [image]
|
||||||
|
[image] optional, default: tools/out/mothercup_ble_prod.hex
|
||||||
|
|
||||||
|
Connect NS-LINK to SWD (PA4/PA5) and the reset pin first, and CLOSE
|
||||||
|
Keil (it holds the probe).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# NSpyocd is a PyInstaller-packed pyocd: its banner is inside the
|
||||||
|
# compressed bundle, so we rebrand it on the fly. The console encoding
|
||||||
|
# varies (UTF-8 vs GBK), match the byte pattern in both.
|
||||||
|
BANNER_PATTERNS = [
|
||||||
|
("国民技术NSLINK上位机".encode("utf-8"), b"CAIIC NSLINK UMP"),
|
||||||
|
("国民技术NSLINK上位机".encode("gbk"), b"CAIIC NSLINK UMP"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
PYOCD = os.path.join(ROOT, "nations-tec", "N32WB03x_SDK_V2.0.0",
|
||||||
|
"utilities", "dfu", "NSpyocd", "NSpyocd.exe")
|
||||||
|
# The builtin "n32wb031" target only maps 256KB of flash (fails at
|
||||||
|
# 0x01040000 on the dual-bank package); the DFP pack target
|
||||||
|
# "n32wb031keq6_2" maps the full 512KB of the KEQ6-2 we actually use.
|
||||||
|
PACK = os.path.join(ROOT, "nations-tec", "N32WB03x_DFP.1.4.0.pack")
|
||||||
|
TARGET = "n32wb031keq6_2"
|
||||||
|
|
||||||
|
|
||||||
|
def run_step(args):
|
||||||
|
"""Run one NSpyocd step, streaming output with the banner replaced.
|
||||||
|
Reads in small chunks (not line-buffered) so the tool's \\r-based
|
||||||
|
progress bar redraws live. Returns the process exit code."""
|
||||||
|
proc = subprocess.Popen([PYOCD] + args,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
out = sys.stdout.buffer
|
||||||
|
keep = max(len(p) for p, _ in BANNER_PATTERNS) - 1
|
||||||
|
tail = b""
|
||||||
|
while True:
|
||||||
|
chunk = proc.stdout.read(256)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buf = tail + chunk
|
||||||
|
# replace on the COMBINED buffer first (a pattern straddling the
|
||||||
|
# chunk boundary only exists contiguously here), then hold back
|
||||||
|
# the last maxpat-1 bytes so a pattern split by the next chunk
|
||||||
|
# is still seen whole
|
||||||
|
for old, new in BANNER_PATTERNS:
|
||||||
|
buf = buf.replace(old, new)
|
||||||
|
emit, tail = buf[:-keep], buf[-keep:]
|
||||||
|
out.write(emit)
|
||||||
|
out.flush()
|
||||||
|
for old, new in BANNER_PATTERNS:
|
||||||
|
tail = tail.replace(old, new)
|
||||||
|
out.write(tail)
|
||||||
|
out.flush()
|
||||||
|
return proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
image = sys.argv[1] if len(sys.argv) > 1 else \
|
||||||
|
os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"out", "mothercup_ble_prod.hex")
|
||||||
|
if not os.path.isfile(image):
|
||||||
|
print("%s not found - run tools\\make_package.bat first" % image)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# -M under-reset: connect while holding the chip in reset
|
||||||
|
# -f 1000000: 1 MHz SWD clock (slow but tolerant of wiring)
|
||||||
|
print("Erasing chip...", flush=True)
|
||||||
|
rc = run_step(["erase", "--chip", "--pack", PACK, "-M", "under-reset",
|
||||||
|
"-f", "1000000", "-t", TARGET])
|
||||||
|
if rc != 0:
|
||||||
|
return fail()
|
||||||
|
|
||||||
|
print("Programming %s ..." % image, flush=True)
|
||||||
|
# -O smart_flash=false: with the pack target the pre-program diff pass
|
||||||
|
# faults reading 0x01000000; programming+verify itself is fine.
|
||||||
|
rc = run_step(["load", "--pack", PACK, "-M", "under-reset",
|
||||||
|
"-f", "1000000", "-t", TARGET,
|
||||||
|
"-O", "smart_flash=false", image])
|
||||||
|
if rc != 0:
|
||||||
|
return fail()
|
||||||
|
|
||||||
|
print("Program Finish!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def fail():
|
||||||
|
print()
|
||||||
|
print("*** FLASH FAILED ***")
|
||||||
|
print("Troubleshooting:")
|
||||||
|
print(" 1. Close Keil / any tool holding the probe, replug NS-LINK USB")
|
||||||
|
print(" 2. List probes: \"%s\" list" % PYOCD)
|
||||||
|
print(" 3. Check wiring: SWDIO=PA5 SWCLK=PA4 GND and the RESET pin")
|
||||||
|
print(" 4. If several probes are attached, add -u <probe_uid> "
|
||||||
|
"to the commands above")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@ -175,6 +175,28 @@ def load_bin(path, desc):
|
|||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
# Usable app SRAM on the 256KB-flash silicon is 32KB: 0x20004000..0x2000BFFF.
|
||||||
|
# Accesses at/above 0x2000C000 fault (dev log §47), so the image's initial
|
||||||
|
# SP (vector[0]) must stay below it - a full-rebuild size drift once pushed
|
||||||
|
# the stack over this cliff and the board locked up at boot.
|
||||||
|
SRAM_STACK_TOP_LIMIT = 0x2000C000
|
||||||
|
|
||||||
|
|
||||||
|
def check_initial_sp(blob, desc):
|
||||||
|
"""Vector table word 0 = initial MSP. Hard-fail when it lands outside
|
||||||
|
the usable SRAM window."""
|
||||||
|
if len(blob) < 8:
|
||||||
|
return
|
||||||
|
sp = struct.unpack_from("<I", blob, 0)[0]
|
||||||
|
if not (0x20004000 < sp <= SRAM_STACK_TOP_LIMIT):
|
||||||
|
raise SystemExit("ERROR: %s initial SP 0x%08X is outside usable SRAM "
|
||||||
|
"(0x20004001..0x%08X) - shrink ZI (heap/buffers); "
|
||||||
|
"see dev log §47" % (desc, sp, SRAM_STACK_TOP_LIMIT))
|
||||||
|
if sp > SRAM_STACK_TOP_LIMIT - 0x400:
|
||||||
|
print("WARNING: %s initial SP 0x%08X is within 1KB of the SRAM cliff"
|
||||||
|
% (desc, sp))
|
||||||
|
|
||||||
|
|
||||||
def read_app_fw_version(root):
|
def read_app_fw_version(root):
|
||||||
"""Extract APP_FW_VERSION_NUM from the APP project's app_version.h.
|
"""Extract APP_FW_VERSION_NUM from the APP project's app_version.h.
|
||||||
Returns None when not found."""
|
Returns None when not found."""
|
||||||
@ -252,6 +274,13 @@ def main():
|
|||||||
elif app2_blob:
|
elif app2_blob:
|
||||||
ota_app2_blob = app2_blob
|
ota_app2_blob = app2_blob
|
||||||
|
|
||||||
|
# Stack-top cliff guard (usable SRAM ends at 0x2000C000, dev log §47)
|
||||||
|
check_initial_sp(app_blob, "APP1")
|
||||||
|
if app2_blob:
|
||||||
|
check_initial_sp(app2_blob, "APP2")
|
||||||
|
if ota_app2_blob:
|
||||||
|
check_initial_sp(ota_app2_blob, "OTA APP2 payload")
|
||||||
|
|
||||||
bs_blob = default_bootsetting(app_blob, app2_blob, version, app2_version)
|
bs_blob = default_bootsetting(app_blob, app2_blob, version, app2_version)
|
||||||
|
|
||||||
segments = [
|
segments = [
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"file": "caiic_ble_dual_ota.bin",
|
|
||||||
"target_bank": 1,
|
|
||||||
"size": 55408,
|
|
||||||
"crc32": "0xA87539ED",
|
|
||||||
"version": 65559
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"file": "caiic_ble_dual_ota_app2.bin",
|
|
||||||
"target_bank": 2,
|
|
||||||
"size": 55708,
|
|
||||||
"crc32": "0x03825A0D",
|
|
||||||
"version": 65559
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"file": "mothercup_ble_prod_ota.bin",
|
"file": "mothercup_ble_prod_ota.bin",
|
||||||
"target_bank": 1,
|
"target_bank": 1,
|
||||||
"size": 55408,
|
"size": 55528,
|
||||||
"crc32": "0xA87539ED",
|
"crc32": "0x83A6B3FA",
|
||||||
"version": 65559
|
"version": 65561
|
||||||
}
|
}
|
||||||
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"file": "mothercup_ble_prod_ota_app2.bin",
|
"file": "mothercup_ble_prod_ota_app2.bin",
|
||||||
"target_bank": 2,
|
"target_bank": 2,
|
||||||
"size": 55708,
|
"size": 55828,
|
||||||
"crc32": "0x03825A0D",
|
"crc32": "0x2DD68645",
|
||||||
"version": 65559
|
"version": 65561
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user