Compare commits
34 Commits
main
...
dev-202609
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32486dc2da | ||
|
|
9b837a2b86 | ||
|
|
28f1dd588d | ||
|
|
ea1dbe4230 | ||
|
|
f34efe9070 | ||
|
|
44172e861a | ||
|
|
03fda56396 | ||
|
|
ed2cf2a29d | ||
|
|
dec34cfe0e | ||
|
|
39e55e867d | ||
|
|
d697885694 | ||
|
|
a4442c14a5 | ||
|
|
c9c1d474a7 | ||
|
|
fb681a4b69 | ||
|
|
10531c2fa8 | ||
|
|
a93d2919a0 | ||
|
|
3d6567819a | ||
|
|
011feee05d | ||
|
|
056790e8d1 | ||
|
|
e74751634b | ||
|
|
5917cafb0c | ||
|
|
8b1744744e | ||
|
|
17e814a883 | ||
|
|
9dad3daf61 | ||
|
|
b38ad3dd5a | ||
|
|
f4faf68ed8 | ||
|
|
15ae64172a | ||
|
|
e0253d1bbf | ||
|
|
f7edfa291d | ||
|
|
1948e4b1ba | ||
|
|
57762e50be | ||
|
|
dd218addce | ||
|
|
8a7cebcf77 | ||
|
|
f729d16ff2 |
5
.gitignore
vendored
@ -5,3 +5,8 @@ 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
|
||||
|
||||
# release zips are regenerable via tools/make_release.bat
|
||||
tools/out/mothercup_tools_*.zip
|
||||
tools/python-embed/Lib/site-packages/__pycache__/
|
||||
tools/python-embed/**/__pycache__/
|
||||
|
||||
31
AGENTS.md
@ -5,14 +5,14 @@
|
||||
本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分:
|
||||
|
||||
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/` 主工程,**已删除**,其功能已全部并入本工程。
|
||||
- **`Boot/`** — 自写精简 bootloader(链接在 `0x01000000`/16KB):校验 bootsetting 记录自身完整性(magic + 结构体 CRC32)后按 active bank 的 `start_address` 直接跳转;**不校验镜像 CRC(CRC 校验在 OTA 升级过程中完成)**;记录无效或地址越界回退 APP1。
|
||||
- **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/112KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令、IWDG 看门狗。当前版本 **V1.00.28**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。
|
||||
- **`Boot/`** — 自写 bootloader(链接在 `0x01000000`/16KB,当前 ~12.9KB,版本 **V1.01.00** `Boot/src/boot_version.h`):校验 bootsetting 记录自身完整性(magic + 结构体 CRC32)后按 active bank 的 `start_address` 直接跳转;**不校验镜像 CRC(CRC 校验在 OTA 升级过程中完成)**;记录无效或地址越界回退 APP1。**V1.01.00 起带 download 模式**:复位后 USART1(460800)banner + 2s 回车窗口进入,或无有效镜像(bootsetting 无效且 APP1 向量不过 sanity)自动进入;模式内为裸机 UART CLI 子集 + OTA 帧升级(共享编译 APP 的 `app_ota.c`/`app_bootset.c`,`BOOT_FIRMWARE` 宏隔离),目标 bank = 非活动区,`tools/ble_ota_update.py --uart` 零改动可用。详见 `docs/ble_protocol.md` §6.8。
|
||||
2. **手机 App `SmartAssiter/`** — uni-app **Vue3 + TypeScript** 工程(HBuilderX 项目管理,无 package.json),通过 BLE 连接设备:查看运行参数(温度/电压/转速/bank/堆等)、CLI 终端、参数读写(appget/appset)、OTA 升级页(e0005 锁步,已恢复)。另有登录/注册/用户信息等云端业务页面。
|
||||
3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。
|
||||
|
||||
关键文档:
|
||||
|
||||
- `docs/开发日志.md` — 固件全程开发日志(45 节,含所有踩坑根因与设计决策,排查问题先查这里)
|
||||
- `docs/开发日志.md` — 固件全程开发日志(52 节,含所有踩坑根因与设计决策,排查问题先查这里)
|
||||
- `docs/ble_protocol.md` — CAIIC BLE 通信协议 V1.1(帧格式/GATT UUID/三类业务流程/维护命令/手机端开发指南)
|
||||
- `SmartAssiter/docs/修改记录_*.md` — App 侧每次改造的详细记录
|
||||
|
||||
@ -29,7 +29,7 @@ mothercup/
|
||||
└── tools/out/ # 出包产物(mothercup_ble_prod.{hex,bin} 生产整包 + mothercup_ble_ota.bin 单文件升级包 + 调试用 _ota.bin/_ota_app2.bin 及 json)
|
||||
```
|
||||
|
||||
SDK 源码(固件库、CMSIS、FreeRTOS V9.0.0、BLE 协议栈/profile/ns_library、启动文件)通过相对路径 `..\..\nations-tec\N32WB03x_SDK_V2.0.0` 被 uvprojx 引用,**不复制进工程**;Bootloader 共享代码(`Boot/src/dfu_layout.h`、`boot_crc32.c`)经 `..\..\Boot\src` 被 APP 工程引用。因此各工程目录不能脱离仓库根单独移动。
|
||||
SDK 源码(固件库、CMSIS、FreeRTOS V9.0.0、BLE 协议栈/profile/ns_library、启动文件)通过相对路径 `..\..\nations-tec\N32WB03x_SDK_V2.0.0` 被 uvprojx 引用,**不复制进工程**;Bootloader 共享代码(`Boot/src/dfu_layout.h`、`boot_crc32.c`、`ota_wire.h`)经 `..\..\Boot\src` 被 APP 工程引用,反向地 Boot 工程也共享编译 APP 的 `mcm-ddc-ble/src/app_ota.c`/`app_bootset.c`(全局宏 `BOOT_FIRMWARE` 隔离 RTOS/BLE 依赖,Boot 侧 `cli_core.h` 为 shim,include 顺序 `..\src` 优先)。因此各工程目录不能脱离仓库根单独移动。
|
||||
|
||||
## Flash / RAM 布局(铁律)
|
||||
|
||||
@ -45,7 +45,7 @@ Flash 布局(256KB,`Boot/src/dfu_layout.h` 为唯一权威定义;Boot/boot
|
||||
| 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)` 为准。
|
||||
- **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)。
|
||||
|
||||
## 固件工程(mcm-ddc-ble)
|
||||
@ -55,7 +55,7 @@ mcm-ddc-ble/
|
||||
├── src/
|
||||
│ ├── main.c # 入口:VTOR 预置 → USART+banner → LED → app_ble_init → app_params_init
|
||||
│ │ # → 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, 460800 8N1, V1.00.26 起;IWDG 看门狗见 §50):fputc 重定向 + DMA TX(CH1) + RX DMA 循环环形缓冲(CH2, 3KB, IDLE 中断唤醒;擦扇区关中断期间不丢字节) + TX 互斥锁
|
||||
│ ├── app_gpio.c # LED1(PB0)/LED2(PA6)
|
||||
│ ├── app_cli.c # CLI UART 前端:CliTask 行接收/编辑/历史/Tab/Ctrl+Z,提示符 caiic->
|
||||
│ ├── 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 增量构建
|
||||
```
|
||||
|
||||
要求保持 **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.28):`Code=49408 RO-data=5132 RW-data=2092 ZI-data=30140`;Boot 基线(V1.01.00):`Code=10254 RO-data=2574 RW-data=96 ZI-data=6648`(bin 12.9KB,16KB 区内;`merge_image.py` 对 boot bin 超 16KB 硬卡)。
|
||||
|
||||
**固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
|
||||
|
||||
@ -113,7 +113,7 @@ mcm-ddc-ble/
|
||||
3. 日常开发调试:Keil F7 编译、F8 下载即可(Boot 不校验镜像 CRC,可直接下载 APP 调试);**Keil 下载选项必须是 "Erase Sectors",整片擦除会杀掉 Boot/bootsetting**(杀掉后需重烧整片包恢复)。板子上烧过其他 0x01000000 起步的程序同样会覆盖 Boot,恢复也是重烧整片包。
|
||||
4. 日常固件升级设计路径是 BLE OTA(手机 App 或 PC bleak 客户端)。
|
||||
|
||||
验证运行:串口助手接 PB6(TX)/PB7(RX),115200 8N1,复位后应看到 banner 与 `caiic->` 提示符(`help` 查看命令);手机应能搜到广播名 `CAIIC-MCM-20260902`。
|
||||
验证运行:串口助手接 PB6(TX)/PB7(RX),460800 8N1(V1.00.26 起),复位后应先看到 boot banner `[boot] CAIIC-BOOT ...`(Boot V1.01.00 起;2s 内按回车进入 download 模式,可 CLI + `ble_ota_update.py --uart` 直接升级,协议见 ble_protocol.md §6.8),随后是 APP banner 与 `caiic->` 提示符(`help` 查看命令);手机应能搜到广播名 `CAIIC-MCM-20260902`。
|
||||
|
||||
## BLE 接口与协议
|
||||
|
||||
@ -128,7 +128,7 @@ mcm-ddc-ble/
|
||||
| `...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`)。
|
||||
- 帧格式:`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/包,协议按字节流重组,两种情形都正确。
|
||||
- 信息项 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 通道,避免自激)。
|
||||
@ -148,17 +148,20 @@ mcm-ddc-ble/
|
||||
|
||||
## PC 工具(tools/)
|
||||
|
||||
- `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)
|
||||
**自包含生产包(§49)**:tools/ 内嵌 Python 运行时(`python-embed/`,bleak+pyserial 预装)+ NSpyocd 烧录器副本 + DFP pack,`make_release.bat` 一键打 zip 到 `out/mothercup_tools_<时间戳>.zip`——解压到任何 Windows 电脑即可用,无需安装 Python/任何环境。三件套不入库(gitignore):`python-embed/`、`NSpyocd/`、`N32WB03x_DFP.1.4.0.pack`;**bat 文件必须保持 CRLF 行尾**(LF 会导致 cmd 解析错乱)。Python 解析优先级:`python-embed` → `.venv-ble`(开发机自动建)→ 系统 Python。
|
||||
|
||||
- `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` 字段);**出包硬卡:镜像初始 SP 必须在 (0x20004000, 0x2000C000](SRAM 悬崖,§47)**
|
||||
- `make_package.bat` / `flash_package.bat` — 一键出包 / NSpyocd 整片烧录(flash_package.py 流式转发输出并把厂商横幅替换为 "CAIIC NSLINK UMP")
|
||||
- `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_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_cli_test.py` + `ble_cli.bat` — PC 端 bleak 蓝牙 CLI 测试客户端(帧协议模式 + `-rw` 读写特征模式,交互模式 `-i`,venv 在 `tools/.venv-ble`,不入库;首次运行 bat 按 `tools/requirements-ble.txt`(bleak+pyserial)自动创建)
|
||||
- `ble_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式(460800 8N1,V1.00.26 起):握手标记进入/ABORT 退出;V1.00.24 起默认流式(8KB 窗口+扇区 ack+go-back-N),`--lockstep` 或旧固件自动回退锁步)。**BLE 与 UART 双通道均已实测通过(§42/§44),流式见 §47/§48**
|
||||
- `ble_temp_watch.py` — 温度监测小工具
|
||||
|
||||
## 实时架构约定(固件)
|
||||
|
||||
- 使用 FreeRTOS V9.0.0 原生 API,**不使用 cmsis_os 封装**。
|
||||
- 任务:BLE 调度任务(栈 512 字,优先级 2,循环 `rwip_schedule(); vTaskDelay(1ms);`——**ke_msg/ke_timer 只允许在该任务上下文运行**);LED 任务(LED1 闪烁半周期读 APP_DATA 参数 `led1_blink_ms`);CliTask(优先级 3);CLI 输出经环形缓冲由 BLE 任务上下文发 notify(`ble_up.c`)。`configTOTAL_HEAP_SIZE = 20KB`(heap_4)。
|
||||
- **IWDG 看门狗(V1.00.27 起)**:4s 超时,idle hook 仅在 3s 内有任务心跳时喂狗(BLE/LED/CLI 任务循环打点 `app_wdt_heartbeat()`);SWD halt 时冻结(DBG_IWDG_STOP);banner 打印复位原因;`hang` 命令可验证点火。新增任务请在循环里打心跳。
|
||||
- 任务:BLE 调度任务(栈 512 字,优先级 2,循环 `rwip_schedule(); vTaskDelay(1ms);`——**ke_msg/ke_timer 只允许在该任务上下文运行**);LED 任务(LED1 闪烁半周期读 APP_DATA 参数 `led1_blink_ms`);CliTask(优先级 3);CLI 输出经环形缓冲由 BLE 任务上下文发 notify(`ble_up.c`)。`configTOTAL_HEAP_SIZE = 18KB`(heap_4;SRAM 悬崖约束见 Flash/RAM 布局铁律)。
|
||||
- SysTick 由 FreeRTOS port 接管:`main` 中不要手动 `SysTick_Config`,`n32wb03x_it.c` 中不要重复定义 SVC/PendSV/SysTick_Handler。
|
||||
- USART1_IRQn 优先级为 3(最低),因 ISR 内调用 FreeRTOS FromISR API,必须 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(=3)。BLE_SW/FIFO IRQ 优先级 0,其 ISR 内禁止调用 FromISR API。
|
||||
- 串口输出并发:多任务/CLI 输出分别持有 `bsp_usart_tx_lock()` 互斥锁;调度器启动前为空操作。不要在中断或多个任务里直接 printf。
|
||||
|
||||
@ -2,10 +2,19 @@
|
||||
Rebuild target 'caiic_boot'
|
||||
assembling startup_n32wb03x.s...
|
||||
compiling boot_crc32.c...
|
||||
compiling main.c...
|
||||
compiling system_n32wb03x.c...
|
||||
compiling boot_cli.c...
|
||||
compiling boot_usart.c...
|
||||
compiling app_bootset.c...
|
||||
compiling n32wb03x_usart.c...
|
||||
compiling main.c...
|
||||
compiling app_ota.c...
|
||||
compiling n32wb03x_gpio.c...
|
||||
compiling n32wb03x_dma.c...
|
||||
compiling n32wb03x_rcc.c...
|
||||
compiling n32wb03x_qflash.c...
|
||||
linking...
|
||||
Program Size: Code=1320 RO-data=680 RW-data=0 ZI-data=2104
|
||||
Program Size: Code=11910 RO-data=2650 RW-data=96 ZI-data=7288
|
||||
FromELF: creating hex file...
|
||||
After Build - User command #1: fromelf --bin --output=.\bin\caiic_boot.bin .\Objects\caiic_boot.axf
|
||||
".\Objects\caiic_boot.axf" - 0 Error(s), 0 Warning(s).
|
||||
|
||||
@ -264,6 +264,130 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>3</GroupNumber>
|
||||
<FileNumber>5</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\src\boot_usart.c</PathWithFileName>
|
||||
<FilenameWithoutPath>boot_usart.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>3</GroupNumber>
|
||||
<FileNumber>6</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\src\boot_cli.c</PathWithFileName>
|
||||
<FilenameWithoutPath>boot_cli.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<GroupName>SHARED_APP</GroupName>
|
||||
<tvExp>1</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<cbSel>0</cbSel>
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>4</GroupNumber>
|
||||
<FileNumber>7</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\mcm-ddc-ble\src\app_ota.c</PathWithFileName>
|
||||
<FilenameWithoutPath>app_ota.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>4</GroupNumber>
|
||||
<FileNumber>8</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\mcm-ddc-ble\src\app_bootset.c</PathWithFileName>
|
||||
<FilenameWithoutPath>app_bootset.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<GroupName>STD_PERIPH</GroupName>
|
||||
<tvExp>1</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<cbSel>0</cbSel>
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>9</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_rcc.c</PathWithFileName>
|
||||
<FilenameWithoutPath>n32wb03x_rcc.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>10</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_gpio.c</PathWithFileName>
|
||||
<FilenameWithoutPath>n32wb03x_gpio.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>11</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_usart.c</PathWithFileName>
|
||||
<FilenameWithoutPath>n32wb03x_usart.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>12</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_dma.c</PathWithFileName>
|
||||
<FilenameWithoutPath>n32wb03x_dma.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>13</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</PathWithFileName>
|
||||
<FilenameWithoutPath>n32wb03x_qflash.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
</Group>
|
||||
|
||||
</ProjectOpt>
|
||||
|
||||
@ -337,9 +337,9 @@
|
||||
<v6Rtti>0</v6Rtti>
|
||||
<VariousControls>
|
||||
<MiscControls></MiscControls>
|
||||
<Define>N32WB03X, USE_STDPERIPH_DRIVER</Define>
|
||||
<Define>N32WB03X, USE_STDPERIPH_DRIVER, BOOT_FIRMWARE</Define>
|
||||
<Undefine></Undefine>
|
||||
<IncludePath>..\src;..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\CMSIS\core;..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\CMSIS\device;..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\inc</IncludePath>
|
||||
<IncludePath>..\src;..\..\mcm-ddc-ble\inc;..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\CMSIS\core;..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\CMSIS\device;..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\inc</IncludePath>
|
||||
</VariousControls>
|
||||
</Cads>
|
||||
<Aads>
|
||||
@ -413,6 +413,61 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\boot_crc32.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>boot_usart.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\boot_usart.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>boot_cli.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\boot_cli.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
<GroupName>SHARED_APP</GroupName>
|
||||
<Files>
|
||||
<File>
|
||||
<FileName>app_ota.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\mcm-ddc-ble\src\app_ota.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_bootset.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\mcm-ddc-ble\src\app_bootset.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
<GroupName>STD_PERIPH</GroupName>
|
||||
<Files>
|
||||
<File>
|
||||
<FileName>n32wb03x_rcc.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_rcc.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_gpio.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_gpio.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_usart.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_usart.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_dma.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_dma.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_qflash.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
</Groups>
|
||||
|
||||
788
Boot/src/boot_cli.c
Normal file
@ -0,0 +1,788 @@
|
||||
/**
|
||||
* @file boot_cli.c
|
||||
* @brief Bootloader download-mode CLI: bare-metal polled line editor
|
||||
* (echo, backspace, TAB command-name completion, UP/DOWN history)
|
||||
* + command table, and the UART OTA binary frame mode.
|
||||
*
|
||||
* Wire-compatible with the application's UART CLI so that
|
||||
* tools/ble_ota_update.py works unchanged against the bootloader:
|
||||
* - prompt "caiic->" (the script's PROMPT marker)
|
||||
* - "devinfo" prints "cur bank: APP<n>" (the boot's OTA target is the
|
||||
* OTHER bank, exactly like the app's "write the inactive bank" rule)
|
||||
* - "ota" prints the "[ota] binary mode ON" go-ahead marker, then the
|
||||
* stream carries 0xCA OTA frames (engine: shared app_ota.c)
|
||||
*
|
||||
* No libc printf: cli_printf() below is a minimal formatter (%% %c %s %d
|
||||
* %u %x %X, width, '-'/'0' pads, 'l' ignored - all integers are 32-bit).
|
||||
*/
|
||||
#include "cli_core.h" /* bootloader shim (Boot/src) */
|
||||
#include "boot_usart.h"
|
||||
#include "boot_version.h"
|
||||
#include "app_bootset.h"
|
||||
#include "app_ota.h"
|
||||
#include "dfu_layout.h"
|
||||
#include "boot_crc32.h"
|
||||
#include "n32wb03x.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#define CLI_TX_BUF_SIZE 96
|
||||
#define CLI_LINE_SIZE 80
|
||||
#define CLI_MAX_ARGS 8
|
||||
#define OTA_IDLE_EXIT_MS 3000u /* binary mode fallback exit */
|
||||
#define CLI_KEY_TAB 0x09 /* TAB: command-name completion */
|
||||
#define CLI_KEY_ESC 0x1B /* ESC: arrow-key escape sequence start */
|
||||
#define CLI_HISTORY_SIZE 8 /* number of remembered command lines */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Output (cli_core.h shim implementation) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void cli_write(const char* str)
|
||||
{
|
||||
boot_usart_write((const uint8_t*)str, (uint16_t)strlen(str));
|
||||
}
|
||||
|
||||
/** Format v into out (NUL-terminated), least-significant first reversed. */
|
||||
static void u32_to_str(uint32_t v, char* out, uint8_t base, int neg)
|
||||
{
|
||||
char tmp[11]; /* 10 digits + sign */
|
||||
uint8_t n = 0;
|
||||
|
||||
do
|
||||
{
|
||||
uint32_t d = v % base;
|
||||
tmp[n++] = (char)((d < 10u) ? ('0' + d) : ('A' + d - 10u));
|
||||
v /= base;
|
||||
} while (v != 0u);
|
||||
if (neg)
|
||||
{
|
||||
tmp[n++] = '-';
|
||||
}
|
||||
while (n != 0u)
|
||||
{
|
||||
*out++ = tmp[--n];
|
||||
}
|
||||
*out = '\0';
|
||||
}
|
||||
|
||||
static char* str_pad(char* out, const char* s, int width, uint8_t left, char pad)
|
||||
{
|
||||
int len = (int)strlen(s);
|
||||
|
||||
if (!left)
|
||||
{
|
||||
while (width-- > len)
|
||||
{
|
||||
*out++ = pad;
|
||||
}
|
||||
}
|
||||
while (*s != '\0')
|
||||
{
|
||||
*out++ = *s++;
|
||||
}
|
||||
while (width-- > len)
|
||||
{
|
||||
*out++ = ' ';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void cli_printf(const char* fmt, ...)
|
||||
{
|
||||
static char s_txBuf[CLI_TX_BUF_SIZE];
|
||||
char* out = s_txBuf;
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
while (*fmt != '\0' && out < s_txBuf + CLI_TX_BUF_SIZE - 12)
|
||||
{
|
||||
char c = *fmt++;
|
||||
|
||||
if (c != '%')
|
||||
{
|
||||
*out++ = c;
|
||||
continue;
|
||||
}
|
||||
{
|
||||
uint8_t left = 0;
|
||||
char pad = ' ';
|
||||
int width = 0;
|
||||
char num[12];
|
||||
char spec;
|
||||
|
||||
if (*fmt == '-')
|
||||
{
|
||||
left = 1;
|
||||
fmt++;
|
||||
}
|
||||
if (*fmt == '0')
|
||||
{
|
||||
pad = '0';
|
||||
fmt++;
|
||||
}
|
||||
while (*fmt >= '0' && *fmt <= '9')
|
||||
{
|
||||
width = width * 10 + (*fmt++ - '0');
|
||||
}
|
||||
if (*fmt == 'l')
|
||||
{
|
||||
fmt++; /* 32-bit already: %lu == %u here */
|
||||
}
|
||||
spec = *fmt;
|
||||
if (spec != '\0')
|
||||
{
|
||||
fmt++;
|
||||
}
|
||||
switch (spec)
|
||||
{
|
||||
case 's':
|
||||
out = str_pad(out, va_arg(args, const char*), width, left, ' ');
|
||||
break;
|
||||
case 'c':
|
||||
*out++ = (char)va_arg(args, int);
|
||||
break;
|
||||
case 'd':
|
||||
{
|
||||
long v = va_arg(args, long);
|
||||
u32_to_str((uint32_t)((v < 0) ? -v : v), num, 10, v < 0);
|
||||
out = str_pad(out, num, width, left, pad);
|
||||
break;
|
||||
}
|
||||
case 'u':
|
||||
u32_to_str(va_arg(args, unsigned long), num, 10, 0);
|
||||
out = str_pad(out, num, width, left, pad);
|
||||
break;
|
||||
case 'x':
|
||||
case 'X':
|
||||
u32_to_str(va_arg(args, unsigned long), num, 16, 0);
|
||||
out = str_pad(out, num, width, left, pad);
|
||||
break;
|
||||
case '%':
|
||||
*out++ = '%';
|
||||
break;
|
||||
default:
|
||||
*out++ = '%';
|
||||
if (spec != '\0')
|
||||
{
|
||||
*out++ = spec;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
va_end(args);
|
||||
*out = '\0';
|
||||
cli_write(s_txBuf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Commands */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef void (*CliCmdHandler_t)(int argc, char* argv[]);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char* name;
|
||||
const char* help;
|
||||
CliCmdHandler_t handler;
|
||||
} CliCmd_t;
|
||||
|
||||
static uint8_t s_otaMode; /* 1 = OTA binary frame mode */
|
||||
|
||||
static void CmdHelp(int argc, char* argv[]);
|
||||
|
||||
static void CmdVersion(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
cli_printf("bootloader version: %s (download mode)\r\n", BOOT_FW_VERSION);
|
||||
cli_printf("SystemCoreClock = %lu Hz\r\n", (unsigned long)SystemCoreClock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "devinfo": report the bank the NEXT OTA would replace, using the
|
||||
* same "cur bank: APP<n>" line the application prints - the PC
|
||||
* updater picks the payload for the other bank, which is exactly
|
||||
* the boot's OTA target rule (inactive bank; APP1 when the
|
||||
* bootsetting record is invalid).
|
||||
*/
|
||||
static void CmdDevInfo(int argc, char* argv[])
|
||||
{
|
||||
const caiic_bootsetting_t* bs = (const caiic_bootsetting_t*)CAIIC_BOOTSETTING_ADDR;
|
||||
uint32_t active;
|
||||
uint8_t bs_ok;
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
bs_ok = (bs->magic == CAIIC_BOOT_MAGIC &&
|
||||
bs->crc32 == caiic_crc32((const uint8_t*)bs,
|
||||
(uint32_t)offsetof(caiic_bootsetting_t, crc32))) ? 1 : 0;
|
||||
active = bs->active_bank;
|
||||
|
||||
cli_printf("bootloader: %s, download mode\r\n", BOOT_FW_VERSION);
|
||||
if (!bs_ok)
|
||||
{
|
||||
cli_write("bootsetting: INVALID - OTA targets APP1\r\n");
|
||||
active = CAIIC_ACTIVE_BANK2; /* report the bank NOT targeted */
|
||||
}
|
||||
cli_printf("cur bank: APP%lu\r\n", (unsigned long)active);
|
||||
cli_printf("uptime: %lu s\r\n", (unsigned long)(boot_millis() / 1000u));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "ota": enter the OTA binary frame mode (0xCA frames on the raw
|
||||
* stream, framed RSP on TX). The marker line is the host's go-ahead.
|
||||
* Exits: OTA_ABORT frame, 3 s idle, or reset after OTA_END.
|
||||
*/
|
||||
static void CmdOta(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
cli_write("[ota] binary mode ON - send 0xCA OTA frames now\r\n"
|
||||
"(ble_protocol.md section 6.6; OTA_ABORT or 3s idle exits)\r\n");
|
||||
s_otaMode = 1;
|
||||
}
|
||||
|
||||
static void CmdReset(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
cli_write("resetting...\r\n");
|
||||
boot_delay_ms(200u);
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
static void CmdUartInfo(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
cli_write("usart1: TX=PB6 RX=PB7 (AF4)\r\n");
|
||||
cli_write("baud: 460800, 8 data bits, no parity, 1 stop bit\r\n");
|
||||
cli_write("flow control: none; tx: polled; rx: DMA ring (3KB, polled)\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a "pxN" pin name (e.g. "pb0", "PA3", "PB13") into a GPIO port
|
||||
* and pin mask. @return 1 on success, 0 on bad name/out-of-range pin.
|
||||
*/
|
||||
static int pinset_parse(const char* name, GPIO_Module** port, uint16_t* mask)
|
||||
{
|
||||
const char* p;
|
||||
int pin = 0;
|
||||
|
||||
if (name[0] != 'p' && name[0] != 'P')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (name[1] == 'a' || name[1] == 'A')
|
||||
{
|
||||
*port = GPIOA;
|
||||
}
|
||||
else if (name[1] == 'b' || name[1] == 'B')
|
||||
{
|
||||
*port = GPIOB;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
p = &name[2];
|
||||
if (*p < '0' || *p > '9')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
while (*p >= '0' && *p <= '9')
|
||||
{
|
||||
pin = pin * 10 + (*p - '0');
|
||||
if (pin > 15)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
if (*p != '\0')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (*port == GPIOA && pin > 6) /* PA7 not bonded on this chip */
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (*port == GPIOB && pin > 13) /* PB14/PB15 not bonded */
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
*mask = (uint16_t)(1u << pin);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "pinset <pxN> [0|1]": set a GPIO output high/low, or read its level.
|
||||
* e.g. "pinset pb0 1", "pinset pa0 0", "pinset pb0" (read level).
|
||||
*/
|
||||
static void CmdPinSet(int argc, char* argv[])
|
||||
{
|
||||
GPIO_Module* port = NULL;
|
||||
uint16_t mask = 0;
|
||||
GPIO_InitType s;
|
||||
|
||||
if (argc < 2)
|
||||
{
|
||||
cli_write("usage: pinset <pxN> [0|1] (e.g. pinset pb0 1)\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pinset_parse(argv[1], &port, &mask))
|
||||
{
|
||||
cli_printf("bad pin: %s (use pa0..pa6 / pb0..pb13)\r\n", argv[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (port == GPIOA)
|
||||
{
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA, ENABLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
|
||||
}
|
||||
|
||||
if (argc >= 3)
|
||||
{
|
||||
/* reserved-pin guard: warn before reconfiguring SWD / console pins */
|
||||
if ((port == GPIOA && (mask & (GPIO_PIN_4 | GPIO_PIN_5)) != 0u) ||
|
||||
(port == GPIOB && (mask & (GPIO_PIN_6 | GPIO_PIN_7)) != 0u))
|
||||
{
|
||||
cli_write("warning: SWD/console pin - reconfigure may break the link\r\n");
|
||||
}
|
||||
|
||||
GPIO_InitStruct(&s);
|
||||
s.Pin = mask;
|
||||
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
|
||||
GPIO_InitPeripheral(port, &s);
|
||||
|
||||
if (argv[2][0] == '1' && argv[2][1] == '\0')
|
||||
{
|
||||
GPIO_SetBits(port, mask);
|
||||
cli_printf("%s = 1\r\n", argv[1]);
|
||||
}
|
||||
else if (argv[2][0] == '0' && argv[2][1] == '\0')
|
||||
{
|
||||
GPIO_ResetBits(port, mask);
|
||||
cli_printf("%s = 0\r\n", argv[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
cli_write("usage: pinset <pxN> [0|1] (0=low, 1=high)\r\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* read: the input data register reflects the actual pin level */
|
||||
cli_printf("%s = %u\r\n", argv[1],
|
||||
(unsigned)(GPIO_ReadInputDataBit(port, mask) ? 1u : 0u));
|
||||
}
|
||||
}
|
||||
|
||||
static const CliCmd_t s_cmds[] = {
|
||||
{"help", "help [cmd]: list commands / show one command's help", CmdHelp},
|
||||
{"version", "show bootloader version", CmdVersion},
|
||||
{"devinfo", "show boot info + cur bank (OTA writes the OTHER bank)", CmdDevInfo},
|
||||
{"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},
|
||||
{"ota", "ota: UART enters OTA binary frame mode (see help ota)", CmdOta},
|
||||
{"reset", "reset: system reset (reboot)", CmdReset},
|
||||
{"pinset", "pinset <pxN> [0|1]: set/read GPIO level (pa0-6/pb0-13)", CmdPinSet},
|
||||
{"uartinfo", "uartinfo: show USART1 pins/baud/format", CmdUartInfo},
|
||||
};
|
||||
|
||||
#define CLI_CMD_COUNT (sizeof(s_cmds) / sizeof(s_cmds[0]))
|
||||
|
||||
static void CmdHelp(int argc, char* argv[])
|
||||
{
|
||||
uint32_t i;
|
||||
|
||||
if (argc >= 2)
|
||||
{
|
||||
for (i = 0; i < CLI_CMD_COUNT; i++)
|
||||
{
|
||||
if (strcmp(argv[1], s_cmds[i].name) == 0)
|
||||
{
|
||||
cli_printf(" %-8s %s\r\n", s_cmds[i].name, s_cmds[i].help);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cli_printf("unknown command: %s (try 'help')\r\n", argv[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
cli_write("mode: BOOT download\r\n");
|
||||
cli_write("commands:\r\n");
|
||||
for (i = 0; i < CLI_CMD_COUNT; i++)
|
||||
{
|
||||
cli_printf(" %-8s %s\r\n", s_cmds[i].name, s_cmds[i].help);
|
||||
}
|
||||
cli_write("keys: TAB = complete, UP/DOWN = history\r\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Line parsing / dispatch */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int cli_exec_line(char* line)
|
||||
{
|
||||
char* argv[CLI_MAX_ARGS];
|
||||
int argc = 0;
|
||||
char* p = line;
|
||||
uint32_t i;
|
||||
|
||||
while (*p != '\0' && argc < CLI_MAX_ARGS)
|
||||
{
|
||||
while (*p == ' ' || *p == '\t')
|
||||
{
|
||||
p++;
|
||||
}
|
||||
if (*p == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
argv[argc++] = p;
|
||||
while (*p != '\0' && *p != ' ' && *p != '\t')
|
||||
{
|
||||
p++;
|
||||
}
|
||||
if (*p != '\0')
|
||||
{
|
||||
*p++ = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
if (argc == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < CLI_CMD_COUNT; i++)
|
||||
{
|
||||
if (strcmp(argv[0], s_cmds[i].name) == 0)
|
||||
{
|
||||
s_cmds[i].handler(argc, argv);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
cli_printf("unknown command: %s (try 'help')\r\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Download-mode superloop */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define BOOT_CLI_PROMPT "caiic->" /* wire-compatible with the app CLI */
|
||||
|
||||
/**
|
||||
* @brief Redraw the input line: erase `shownLen` chars, print prompt + line.
|
||||
* @return new shown length (in the boot CLI this always equals strlen(line)).
|
||||
*/
|
||||
static uint16_t boot_cli_redraw(const char* line, uint16_t shownLen)
|
||||
{
|
||||
uint16_t i;
|
||||
uint16_t len = (uint16_t)strlen(line);
|
||||
|
||||
cli_write("\r" BOOT_CLI_PROMPT);
|
||||
for (i = 0; i < shownLen; i++)
|
||||
{
|
||||
cli_write(" ");
|
||||
}
|
||||
cli_write("\r" BOOT_CLI_PROMPT);
|
||||
if (len > 0u)
|
||||
{
|
||||
boot_usart_write((const uint8_t*)line, len);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TAB completion on the first token of the line (command name only).
|
||||
* Single match: complete it and append a space.
|
||||
* Multiple matches: extend to the common prefix, or list candidates.
|
||||
* Ported from the application CLI (mcm-ddc-ble/src/app_cli.c).
|
||||
*/
|
||||
static uint16_t boot_cli_tab_complete(char* line, uint16_t len)
|
||||
{
|
||||
char common[CLI_LINE_SIZE];
|
||||
uint16_t commonLen = 0;
|
||||
int matchCount = 0;
|
||||
uint32_t i;
|
||||
|
||||
/* Only complete the first token (no space typed yet) */
|
||||
if (memchr(line, ' ', len) != NULL)
|
||||
{
|
||||
return len;
|
||||
}
|
||||
line[len] = '\0';
|
||||
|
||||
for (i = 0; i < CLI_CMD_COUNT; i++)
|
||||
{
|
||||
const char* name = s_cmds[i].name;
|
||||
if (strncmp(name, line, len) == 0)
|
||||
{
|
||||
if (matchCount == 0)
|
||||
{
|
||||
strcpy(common, name);
|
||||
commonLen = (uint16_t)strlen(common);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* shrink common to the shared prefix */
|
||||
while (commonLen > len && strncmp(common, name, commonLen) != 0)
|
||||
{
|
||||
commonLen--;
|
||||
}
|
||||
common[commonLen] = '\0';
|
||||
}
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchCount == 0)
|
||||
{
|
||||
return len;
|
||||
}
|
||||
|
||||
if (matchCount == 1)
|
||||
{
|
||||
/* complete the command name + one space */
|
||||
while (len < CLI_LINE_SIZE - 1u && len < commonLen)
|
||||
{
|
||||
boot_usart_write((const uint8_t*)&common[len], 1);
|
||||
line[len] = common[len];
|
||||
len++;
|
||||
}
|
||||
if (len < CLI_LINE_SIZE - 1u)
|
||||
{
|
||||
line[len] = ' ';
|
||||
len++;
|
||||
cli_write(" ");
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
if (commonLen > len)
|
||||
{
|
||||
/* extend to the common prefix */
|
||||
while (len < CLI_LINE_SIZE - 1u && len < commonLen)
|
||||
{
|
||||
boot_usart_write((const uint8_t*)&common[len], 1);
|
||||
line[len] = common[len];
|
||||
len++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* ambiguous: list the candidates and redraw */
|
||||
cli_write("\r\n");
|
||||
for (i = 0; i < CLI_CMD_COUNT; i++)
|
||||
{
|
||||
const char* name = s_cmds[i].name;
|
||||
if (strncmp(name, line, len) == 0)
|
||||
{
|
||||
cli_write(name);
|
||||
cli_write(" ");
|
||||
}
|
||||
}
|
||||
cli_write("\r\n");
|
||||
boot_cli_redraw(line, len);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Command history (ring buffer) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static char s_history[CLI_HISTORY_SIZE][CLI_LINE_SIZE];
|
||||
static uint8_t s_histCount = 0; /* number of valid entries */
|
||||
static uint8_t s_histHead = 0; /* next write position */
|
||||
|
||||
/**
|
||||
* @brief Store a command line in the history ring (skips empty lines and
|
||||
* repeats of the most recent entry).
|
||||
*/
|
||||
static void cli_history_add(const char* line)
|
||||
{
|
||||
if (line[0] == '\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (s_histCount > 0u)
|
||||
{
|
||||
uint8_t last = (uint8_t)((s_histHead + CLI_HISTORY_SIZE - 1u) % CLI_HISTORY_SIZE);
|
||||
if (strcmp(s_history[last], line) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
strcpy(s_history[s_histHead], line);
|
||||
s_histHead = (uint8_t)((s_histHead + 1u) % CLI_HISTORY_SIZE);
|
||||
if (s_histCount < CLI_HISTORY_SIZE)
|
||||
{
|
||||
s_histCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get a history entry; nav = 0 is the newest entry.
|
||||
*/
|
||||
static const char* cli_history_get(int nav)
|
||||
{
|
||||
int idx = ((int)s_histHead - 1 - nav + CLI_HISTORY_SIZE) % CLI_HISTORY_SIZE;
|
||||
return s_history[idx];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Download-mode main loop. Never returns (leaves via reset only).
|
||||
*/
|
||||
void boot_cli_run(void)
|
||||
{
|
||||
static char s_line[CLI_LINE_SIZE];
|
||||
uint16_t len = 0;
|
||||
uint16_t shownLen = 0; /* chars currently displayed after the prompt */
|
||||
uint8_t escState = 0; /* 0=normal, 1=got ESC, 2=got ESC '[' */
|
||||
int histNav = -1; /* -1 = editing, 0.. = history entry (0=newest) */
|
||||
uint32_t otaLastRx = 0;
|
||||
|
||||
cli_write("\r\nCLI ready (bootloader download mode). Type 'help' for commands.\r\n"
|
||||
BOOT_CLI_PROMPT);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
uint8_t ch;
|
||||
|
||||
/* deferred post-OTA_END reset (waits until the RSP drained) */
|
||||
app_ota_reset_poll();
|
||||
|
||||
/* OTA binary-mode fallback exit after 3 s of silence */
|
||||
if (s_otaMode && (boot_millis() - otaLastRx) > OTA_IDLE_EXIT_MS)
|
||||
{
|
||||
s_otaMode = 0;
|
||||
app_ota_chan_idle();
|
||||
cli_write("\r\n[ota] binary mode OFF (idle)\r\n" BOOT_CLI_PROMPT);
|
||||
len = 0;
|
||||
shownLen = 0;
|
||||
}
|
||||
|
||||
if (boot_usart_read(&ch) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s_otaMode)
|
||||
{
|
||||
otaLastRx = boot_millis();
|
||||
if (app_ota_chan_rx_uart(ch) != 0)
|
||||
{
|
||||
s_otaMode = 0;
|
||||
cli_write("\r\n[ota] binary mode OFF (abort)\r\n" BOOT_CLI_PROMPT);
|
||||
len = 0;
|
||||
shownLen = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Escape sequences (arrow keys: ESC '[' 'A'/'B') */
|
||||
if (escState == 1)
|
||||
{
|
||||
escState = (ch == '[') ? 2 : 0;
|
||||
continue;
|
||||
}
|
||||
if (escState == 2)
|
||||
{
|
||||
escState = 0;
|
||||
if (ch == 'A') /* UP: older history entry */
|
||||
{
|
||||
if (histNav + 1 < (int)s_histCount)
|
||||
{
|
||||
histNav++;
|
||||
strcpy(s_line, cli_history_get(histNav));
|
||||
shownLen = boot_cli_redraw(s_line, shownLen);
|
||||
len = (uint16_t)strlen(s_line);
|
||||
}
|
||||
}
|
||||
else if (ch == 'B') /* DOWN: newer entry / back to empty line */
|
||||
{
|
||||
if (histNav >= 0)
|
||||
{
|
||||
histNav--;
|
||||
if (histNav >= 0)
|
||||
{
|
||||
strcpy(s_line, cli_history_get(histNav));
|
||||
}
|
||||
else
|
||||
{
|
||||
s_line[0] = '\0';
|
||||
}
|
||||
shownLen = boot_cli_redraw(s_line, shownLen);
|
||||
len = (uint16_t)strlen(s_line);
|
||||
}
|
||||
}
|
||||
/* other escape sequences are ignored */
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case CLI_KEY_ESC:
|
||||
escState = 1;
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
case '\n':
|
||||
cli_write("\r\n");
|
||||
s_line[len] = '\0';
|
||||
cli_history_add(s_line); /* copy before dispatch tokenizes in place */
|
||||
(void)cli_exec_line(s_line);
|
||||
len = 0;
|
||||
shownLen = 0;
|
||||
histNav = -1;
|
||||
cli_write(BOOT_CLI_PROMPT);
|
||||
break;
|
||||
|
||||
case '\b':
|
||||
case 0x7F:
|
||||
if (len != 0u)
|
||||
{
|
||||
len--;
|
||||
cli_write("\b \b");
|
||||
shownLen = len;
|
||||
}
|
||||
break;
|
||||
|
||||
case CLI_KEY_TAB:
|
||||
len = boot_cli_tab_complete(s_line, len);
|
||||
shownLen = len;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (ch >= 0x20u && ch < 0x7Fu && len < CLI_LINE_SIZE - 1u)
|
||||
{
|
||||
s_line[len++] = (char)ch;
|
||||
boot_usart_write(&ch, 1); /* echo */
|
||||
shownLen = len;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Boot/src/boot_usart.c
Normal file
@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @file boot_usart.c
|
||||
* @brief Bootloader USART1 (bare metal): polling TX, RX DMA circular ring
|
||||
* (polled, no interrupts) and a SysTick-polled millisecond time base.
|
||||
*
|
||||
* The RX DMA ring matters during OTA: Qflash erase/write disables interrupts
|
||||
* for tens of ms per sector; at 460800 Bd that is ~2KB of inbound bytes the
|
||||
* DMA keeps sampling while the CPU sits in the flash algorithm.
|
||||
*/
|
||||
#include "boot_usart.h"
|
||||
#include "n32wb03x.h"
|
||||
|
||||
#define BOOT_USARTx USART1
|
||||
#define BOOT_USART_CLK RCC_APB2_PERIPH_USART1
|
||||
#define BOOT_USART_GPIO GPIOB
|
||||
#define BOOT_USART_GPIO_CLK RCC_APB2_PERIPH_GPIOB
|
||||
#define BOOT_USART_TX_PIN GPIO_PIN_6
|
||||
#define BOOT_USART_RX_PIN GPIO_PIN_7
|
||||
#define BOOT_USART_TX_GPIO_AF GPIO_AF4_USART1
|
||||
#define BOOT_USART_RX_GPIO_AF GPIO_AF4_USART1
|
||||
#define BOOT_USART_BAUDRATE 460800u
|
||||
|
||||
/* RX DMA channel: circular into the ring (remap required on this chip) */
|
||||
#define BOOT_USART_RX_DMA_CH DMA_CH2
|
||||
#define BOOT_USART_RX_DMA_REMAP DMA_REMAP_USART1_RX
|
||||
#define BOOT_USART_DAT_ADDR (USART1_BASE + 0x04)
|
||||
|
||||
#define RX_RING_SIZE 3072u /* ~69ms @460800, covers flash erase */
|
||||
|
||||
static uint8_t s_rxRing[RX_RING_SIZE];
|
||||
static volatile uint16_t s_rxTail; /* software drain position */
|
||||
|
||||
/* SysTick-polled millisecond base (no interrupt: the superloop polls
|
||||
* boot_millis() far more often than 1ms, COUNTFLAG latches each wrap) */
|
||||
static volatile uint32_t s_millis;
|
||||
|
||||
uint32_t boot_millis(void)
|
||||
{
|
||||
if ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) != 0u)
|
||||
{
|
||||
/* reading CTRL clears COUNTFLAG; a slow poll (flash erase) can
|
||||
* undercount - acceptable for the timeouts this drives */
|
||||
s_millis++;
|
||||
}
|
||||
return s_millis;
|
||||
}
|
||||
|
||||
void boot_delay_ms(uint32_t ms)
|
||||
{
|
||||
uint32_t start = boot_millis();
|
||||
|
||||
while ((boot_millis() - start) < ms)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief DMA ring head (hardware write position).
|
||||
*/
|
||||
static uint16_t boot_usart_rx_head(void)
|
||||
{
|
||||
return (uint16_t)(RX_RING_SIZE - DMA_GetCurrDataCounter(BOOT_USART_RX_DMA_CH));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize USART1 (460800 8N1) on PB6(TX)/PB7(RX), the RX DMA
|
||||
* circular ring and the SysTick millisecond base.
|
||||
*/
|
||||
void boot_usart_init(void)
|
||||
{
|
||||
GPIO_InitType GPIO_InitStructure;
|
||||
USART_InitType USART_InitStructure;
|
||||
DMA_InitType DMA_InitStructure;
|
||||
|
||||
RCC_EnableAPB2PeriphClk(BOOT_USART_GPIO_CLK, ENABLE);
|
||||
RCC_EnableAPB2PeriphClk(BOOT_USART_CLK, ENABLE);
|
||||
|
||||
GPIO_InitStruct(&GPIO_InitStructure);
|
||||
GPIO_InitStructure.Pin = BOOT_USART_TX_PIN;
|
||||
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_AF_PP;
|
||||
GPIO_InitStructure.GPIO_Alternate = BOOT_USART_TX_GPIO_AF;
|
||||
GPIO_InitPeripheral(BOOT_USART_GPIO, &GPIO_InitStructure);
|
||||
|
||||
GPIO_InitStructure.Pin = BOOT_USART_RX_PIN;
|
||||
GPIO_InitStructure.GPIO_Alternate = BOOT_USART_RX_GPIO_AF;
|
||||
GPIO_InitPeripheral(BOOT_USART_GPIO, &GPIO_InitStructure);
|
||||
|
||||
USART_InitStructure.BaudRate = BOOT_USART_BAUDRATE;
|
||||
USART_InitStructure.WordLength = USART_WL_8B;
|
||||
USART_InitStructure.StopBits = USART_STPB_1;
|
||||
USART_InitStructure.Parity = USART_PE_NO;
|
||||
USART_InitStructure.HardwareFlowControl = USART_HFCTRL_NONE;
|
||||
USART_InitStructure.Mode = USART_MODE_RX | USART_MODE_TX;
|
||||
USART_Init(BOOT_USARTx, &USART_InitStructure);
|
||||
|
||||
/* RX: DMA circular ring, drained by polling (no IDLE interrupt here) */
|
||||
RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_DMA, ENABLE);
|
||||
DMA_EnableChannel(BOOT_USART_RX_DMA_CH, DISABLE);
|
||||
DMA_DeInit(BOOT_USART_RX_DMA_CH);
|
||||
DMA_RequestRemap(BOOT_USART_RX_DMA_REMAP, DMA, BOOT_USART_RX_DMA_CH, ENABLE);
|
||||
|
||||
DMA_InitStructure.PeriphAddr = BOOT_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(BOOT_USART_RX_DMA_CH, &DMA_InitStructure);
|
||||
|
||||
s_rxTail = 0;
|
||||
DMA_EnableChannel(BOOT_USART_RX_DMA_CH, ENABLE);
|
||||
USART_EnableDMA(BOOT_USARTx, USART_DMAREQ_RX, ENABLE);
|
||||
|
||||
USART_Enable(BOOT_USARTx, ENABLE);
|
||||
|
||||
/* 1ms tick: 64MHz / 1000; polled via COUNTFLAG, no interrupt */
|
||||
SysTick->LOAD = 64000u - 1u;
|
||||
SysTick->VAL = 0u;
|
||||
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
|
||||
s_millis = 0;
|
||||
}
|
||||
|
||||
void boot_usart_write(const uint8_t* data, uint16_t len)
|
||||
{
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0; i < len; i++)
|
||||
{
|
||||
USART_SendData(BOOT_USARTx, data[i]);
|
||||
while (USART_GetFlagStatus(BOOT_USARTx, USART_FLAG_TXDE) == RESET)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int boot_usart_read(uint8_t* ch)
|
||||
{
|
||||
uint16_t head = boot_usart_rx_head();
|
||||
|
||||
if (s_rxTail == head)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
*ch = s_rxRing[s_rxTail];
|
||||
s_rxTail = (uint16_t)((s_rxTail + 1u) % RX_RING_SIZE);
|
||||
return 1;
|
||||
}
|
||||
38
Boot/src/boot_usart.h
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @file boot_usart.h
|
||||
* @brief Bootloader USART1 support (bare metal, no RTOS, no interrupts):
|
||||
* polling TX, RX via DMA circular ring polled through the DMA write
|
||||
* pointer (bytes keep landing while Qflash erases with interrupts
|
||||
* disabled), plus a SysTick-polled millisecond time base.
|
||||
* TX=PB6, RX=PB7, 460800 8N1 (same as the application CLI).
|
||||
*/
|
||||
#ifndef __BOOT_USART_H__
|
||||
#define __BOOT_USART_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** USART1 + RX DMA ring + SysTick 1ms time base. */
|
||||
void boot_usart_init(void);
|
||||
|
||||
/** Blocking polled TX of a buffer. */
|
||||
void boot_usart_write(const uint8_t* data, uint16_t len);
|
||||
|
||||
/** Non-blocking RX: fetch one byte from the DMA ring. @return 1 on success. */
|
||||
int boot_usart_read(uint8_t* ch);
|
||||
|
||||
/** Milliseconds since boot_usart_init() (SysTick COUNTFLAG polled; call
|
||||
* often - the superloop and the download-mode window both poll it). */
|
||||
uint32_t boot_millis(void);
|
||||
|
||||
/** Busy-wait delay in ms (uses boot_millis()). */
|
||||
void boot_delay_ms(uint32_t ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __BOOT_USART_H__ */
|
||||
11
Boot/src/boot_version.h
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @file boot_version.h
|
||||
* @brief Bootloader version (independent from the application version in
|
||||
* mcm-ddc-ble/inc/app_version.h; bump when the boot code changes).
|
||||
*/
|
||||
#ifndef __BOOT_VERSION_H__
|
||||
#define __BOOT_VERSION_H__
|
||||
|
||||
#define BOOT_FW_VERSION "V1.00.07"
|
||||
|
||||
#endif /* __BOOT_VERSION_H__ */
|
||||
24
Boot/src/cli_core.h
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @file cli_core.h
|
||||
* @brief Bootloader-side shim of the application's CLI output interface
|
||||
* (mcm-ddc-ble/inc/cli_core.h). Only the output helpers exist here;
|
||||
* the download-mode line editor and command table live in
|
||||
* boot_cli.c. Lets shared modules (app_bootset.c, app_ota.c) compile
|
||||
* unchanged in the BOOT_FIRMWARE build.
|
||||
*/
|
||||
#ifndef __CLI_CORE_H__
|
||||
#define __CLI_CORE_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Output helpers: straight out on USART1 (polling TX, boot_usart.c) */
|
||||
void cli_write(const char* str);
|
||||
void cli_printf(const char* fmt, ...);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __CLI_CORE_H__ */
|
||||
@ -1,21 +1,43 @@
|
||||
/**
|
||||
* @file main.c
|
||||
* @brief CAIIC minimal bootloader: reads the jump address from the
|
||||
* @brief CAIIC bootloader: normally reads the jump address from the
|
||||
* bootsetting record, remaps vectors and jumps to the application.
|
||||
* With this release it also brings up USART1 and offers a
|
||||
* DOWNLOAD MODE (UART CLI + OTA frame upgrade, see boot_cli.c) for
|
||||
* production download and bricked-device recovery.
|
||||
*
|
||||
* No interrupts, no peripherals: flash is read through the memory map only.
|
||||
* Download mode entry:
|
||||
* 0. press Enter within the 2 s window printed in the boot banner, or
|
||||
* 1. automatic when there is no jumpable image: the bootsetting record
|
||||
* is invalid AND the APP1 vector table fails sanity (SP outside the
|
||||
* usable SRAM window / reset PC outside APP1) - the recovery path.
|
||||
* The former PB5 strap-pin entry was removed (the pin is needed by the
|
||||
* display); the two remaining entries still cover download and bricked
|
||||
* device recovery.
|
||||
*
|
||||
* Policy: the application image CRC is verified during the OTA process
|
||||
* (app_ota.c) BEFORE bootsetting is switched - boot itself never validates
|
||||
* image contents, it only trusts the bootsetting sector integrity
|
||||
* (app_ota.c) BEFORE bootsetting is switched - the normal boot path never
|
||||
* validates image contents, it only trusts the bootsetting sector integrity
|
||||
* (magic + own CRC32) and jumps straight to the recorded start_address.
|
||||
* A blank/invalid record or an out-of-range address falls back to APP1.
|
||||
* A blank/invalid record falls back to APP1.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "n32wb03x.h"
|
||||
|
||||
#include "dfu_layout.h"
|
||||
#include "boot_crc32.h"
|
||||
#include "boot_usart.h"
|
||||
#include "boot_version.h"
|
||||
|
||||
/* Download-mode entry window after the banner (ms) */
|
||||
#define BOOT_DL_WINDOW_MS 2000u
|
||||
|
||||
/* Usable SRAM on this silicon: 0x20004000..0x2000BFFF (dev log §47) */
|
||||
#define SRAM_SP_MIN 0x20004001u
|
||||
#define SRAM_SP_MAX 0x2000C000u
|
||||
|
||||
extern void boot_cli_run(void); /* boot_cli.c - never returns */
|
||||
|
||||
/**
|
||||
* @brief Remap the vector table to the app and jump to it. Never returns.
|
||||
@ -42,6 +64,27 @@ static void jump_to_app(uint32_t base)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Vector-table sanity: word0 = initial SP inside the usable SRAM
|
||||
* window, word1 = reset PC inside [base, base + bank size).
|
||||
*/
|
||||
static int image_sane(uint32_t base)
|
||||
{
|
||||
uint32_t sp = *(volatile uint32_t*)base;
|
||||
uint32_t pc = *(volatile uint32_t*)(base + 4);
|
||||
|
||||
return (sp >= SRAM_SP_MIN && sp <= SRAM_SP_MAX &&
|
||||
pc >= base + 8u && pc < base + CAIIC_APP_BANK_SIZE) ? 1 : 0;
|
||||
}
|
||||
|
||||
static void puts_boot(const char* s)
|
||||
{
|
||||
while (*s != '\0')
|
||||
{
|
||||
boot_usart_write((const uint8_t*)s++, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Main program.
|
||||
*/
|
||||
@ -49,6 +92,9 @@ int main(void)
|
||||
{
|
||||
const caiic_bootsetting_t* bs = (const caiic_bootsetting_t*)CAIIC_BOOTSETTING_ADDR;
|
||||
uint32_t target = CAIIC_APP1_BASE; /* default when bootsetting invalid */
|
||||
uint8_t bs_ok = 0;
|
||||
uint32_t window_start;
|
||||
uint8_t enter_dl = 0;
|
||||
|
||||
/* The jump address comes from the bootsetting record. Only the record's
|
||||
* own integrity is checked (magic + its own CRC32); the app image itself
|
||||
@ -59,13 +105,46 @@ int main(void)
|
||||
{
|
||||
const caiic_bank_t* bank = (bs->active_bank == CAIIC_ACTIVE_BANK2) ? &bs->bank2
|
||||
: &bs->bank1;
|
||||
bs_ok = 1;
|
||||
if (bank->start_address >= CAIIC_APP1_BASE &&
|
||||
bank->start_address < CAIIC_IMAGE_UPDATE_BASE + CAIIC_IMAGE_UPDATE_SIZE)
|
||||
bank->start_address < CAIIC_APP2_BASE + CAIIC_APP_BANK_SIZE)
|
||||
{
|
||||
target = bank->start_address;
|
||||
}
|
||||
}
|
||||
|
||||
boot_usart_init();
|
||||
puts_boot("\r\n[boot] CAIIC-BOOT " BOOT_FW_VERSION
|
||||
" - Enter within 2s: download mode\r\n");
|
||||
|
||||
/* Entry 0: Enter key inside the 2 s window */
|
||||
if (!enter_dl)
|
||||
{
|
||||
window_start = boot_millis();
|
||||
while ((boot_millis() - window_start) < BOOT_DL_WINDOW_MS)
|
||||
{
|
||||
uint8_t ch;
|
||||
if (boot_usart_read(&ch) != 0 && (ch == '\r' || ch == '\n'))
|
||||
{
|
||||
enter_dl = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Entry 1 (recovery): no jumpable image at all */
|
||||
if (!enter_dl && !bs_ok && !image_sane(CAIIC_APP1_BASE))
|
||||
{
|
||||
enter_dl = 1;
|
||||
puts_boot("[boot] no valid app image, forced download mode\r\n");
|
||||
}
|
||||
|
||||
if (enter_dl)
|
||||
{
|
||||
puts_boot("[boot] DOWNLOAD MODE (ota/cli over UART, 'help' for commands)\r\n");
|
||||
boot_cli_run(); /* never returns */
|
||||
}
|
||||
|
||||
jump_to_app(target);
|
||||
|
||||
for (;;)
|
||||
|
||||
42
Boot/src/ota_wire.h
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file ota_wire.h
|
||||
* @brief 0xCA frame protocol wire constants, shared by the application
|
||||
* (mcm-ddc-ble, see app_ble_proto.h) and the bootloader download
|
||||
* mode (Boot/). Single source of truth: values are protocol-fixed
|
||||
* by docs/ble_protocol.md - never change a value, only add new ones.
|
||||
*/
|
||||
#ifndef __OTA_WIRE_H__
|
||||
#define __OTA_WIRE_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Frame types */
|
||||
#define BLE_FRAME_SOF 0xCA /* frame start byte */
|
||||
#define BLE_FRAME_HDR_LEN 5u /* SOF+TYPE+SEQ+LEN(2) */
|
||||
#define BLE_FRAME_CLI_REQ 0x01 /* phone -> device: one command line */
|
||||
#define BLE_FRAME_CLI_RSP 0x02 /* device -> phone: response text chunk */
|
||||
#define BLE_FRAME_CLI_RSP_END 0x03 /* device -> phone: {status u8} */
|
||||
#define BLE_FRAME_OTA_BEGIN 0x10 /* phone -> device: {size, crc32, version} */
|
||||
#define BLE_FRAME_OTA_DATA 0x11 /* phone -> device: {offset, data...} */
|
||||
#define BLE_FRAME_OTA_END 0x12 /* phone -> device: {crc32} */
|
||||
#define BLE_FRAME_OTA_ABORT 0x13 /* phone -> device: abort session (no payload) */
|
||||
#define BLE_FRAME_OTA_RSP 0x1F /* device -> phone: {cmd, status, offset} */
|
||||
#define BLE_FRAME_INFO_QUERY 0x20 /* phone -> device: {item_id}*n (empty/0xFF = all) */
|
||||
#define BLE_FRAME_INFO_RSP 0x21 /* device -> phone: TLV {id, len, value}*n */
|
||||
|
||||
/* OTA_RSP / generic status codes */
|
||||
#define BLE_OTA_ST_OK 0
|
||||
#define BLE_OTA_ST_BAD_FRAME 1
|
||||
#define BLE_OTA_ST_BAD_STATE 2 /* bad state / bad offset sequence */
|
||||
#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 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __OTA_WIRE_H__ */
|
||||
@ -3,7 +3,7 @@
|
||||
"configurations" : [
|
||||
{
|
||||
"customPlaygroundType" : "local",
|
||||
"playground" : "standard",
|
||||
"playground" : "custom",
|
||||
"type" : "uni-app:app-android"
|
||||
},
|
||||
{
|
||||
|
||||
116
SmartAssiter/.icon_build/make_icon.py
Normal file
@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 生成 App 图标:制冷杯 + 蓝牙徽标
|
||||
import math, sys
|
||||
sys.path.insert(0, '.icon_build')
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
S = 4 # 超采样倍数
|
||||
N = 1024 # 目标尺寸
|
||||
W = N * S
|
||||
|
||||
def sc(v):
|
||||
return int(round(v * S))
|
||||
|
||||
# ---------- 背景:冰蓝纵向渐变 ----------
|
||||
top = (110, 200, 235) # 浅冰蓝
|
||||
bot = (24, 82, 152) # 深蓝
|
||||
grad = Image.new('RGB', (1, 2))
|
||||
grad.putpixel((0, 0), top)
|
||||
grad.putpixel((0, 1), bot)
|
||||
grad = grad.resize((W, W), Image.BICUBIC)
|
||||
|
||||
img = Image.new('RGBA', (W, W), (0, 0, 0, 0))
|
||||
|
||||
# 圆角矩形蒙版(角透明,与旧图标一致)
|
||||
mask = Image.new('L', (W, W), 0)
|
||||
md = ImageDraw.Draw(mask)
|
||||
md.rounded_rectangle([0, 0, W - 1, W - 1], radius=sc(230), fill=255)
|
||||
img.paste(grad, (0, 0), mask)
|
||||
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
def thick_line(pts, color, width):
|
||||
d.line(pts, fill=color, width=width, joint='curve')
|
||||
r = width // 2
|
||||
for (x, y) in (pts[0], pts[-1]):
|
||||
d.ellipse([x - r, y - r, x + r, y + r], fill=color)
|
||||
|
||||
def rpoly(pts, r, color):
|
||||
"""多边形 + 顶点圆角"""
|
||||
d.polygon(pts, fill=color)
|
||||
for (x, y) in pts:
|
||||
d.ellipse([x - r, y - r, x + r, y + r], fill=color)
|
||||
|
||||
# ---------- 杯子 ----------
|
||||
WHITE = (255, 255, 255, 255)
|
||||
cx = 462
|
||||
|
||||
# 杯盖(圆角矩形,略宽于杯身)
|
||||
lid = [sc(cx - 138), sc(240), sc(cx + 138), sc(330)]
|
||||
d.rounded_rectangle(lid, radius=sc(32), fill=WHITE)
|
||||
# 盖顶小凸起(冷感模块)
|
||||
d.rounded_rectangle([sc(cx - 46), sc(196), sc(cx + 46), sc(250)],
|
||||
radius=sc(22), fill=WHITE)
|
||||
|
||||
# 杯身(梯形,上宽下窄;仅顶部顶点补圆角,底部由圆角矩形收边)
|
||||
body = [(sc(cx - 128), sc(316)),
|
||||
(sc(cx + 128), sc(316)),
|
||||
(sc(cx + 96), sc(712)),
|
||||
(sc(cx - 96), sc(712))]
|
||||
d.polygon(body, fill=WHITE)
|
||||
tr = sc(14)
|
||||
for (x, y) in (body[0], body[1]):
|
||||
d.ellipse([x - tr, y - tr, x + tr, y + tr], fill=WHITE)
|
||||
|
||||
# 杯底圆角收边
|
||||
d.rounded_rectangle([sc(cx - 100), sc(648), sc(cx + 100), sc(716)],
|
||||
radius=sc(30), fill=WHITE)
|
||||
|
||||
# ---------- 雪花标志(杯身正面) ----------
|
||||
SNOW = (39, 122, 190, 255)
|
||||
fx, fy = sc(cx), sc(500)
|
||||
arm = sc(78)
|
||||
lw = sc(17)
|
||||
br = sc(36) # 分叉长度
|
||||
bpos = 0.62 # 分叉位置(沿臂比例)
|
||||
|
||||
# 中心圆点
|
||||
cr = sc(14)
|
||||
d.ellipse([fx - cr, fy - cr, fx + cr, fy + cr], fill=SNOW)
|
||||
|
||||
for k in range(6):
|
||||
a = math.radians(k * 60 + 90)
|
||||
dx, dy = math.cos(a), math.sin(a)
|
||||
ex, ey = fx + dx * arm, fy + dy * arm
|
||||
thick_line([(fx, fy), (ex, ey)], SNOW, lw)
|
||||
# 每个臂上分叉
|
||||
bx0, by0 = fx + dx * arm * bpos, fy + dy * arm * bpos
|
||||
for sgn in (-1, 1):
|
||||
ba = a + sgn * math.radians(55)
|
||||
bex, bey = bx0 + math.cos(ba) * br, by0 + math.sin(ba) * br
|
||||
thick_line([(bx0, by0), (bex, bey)], SNOW, lw)
|
||||
|
||||
# ---------- 蓝牙徽标(右下角白圆) ----------
|
||||
bcx, bcy = sc(738), sc(742)
|
||||
brad = sc(182)
|
||||
d.ellipse([bcx - brad, bcy - brad, bcx + brad, bcy + brad], fill=WHITE)
|
||||
|
||||
RUNE = (24, 82, 152, 255)
|
||||
a_ = sc(100) # 半高
|
||||
d_ = sc(58) # 右伸距离
|
||||
lw2 = sc(32)
|
||||
p_top = (bcx, bcy - a_)
|
||||
p_mid = (bcx, bcy)
|
||||
p_bot = (bcx, bcy + a_)
|
||||
p_ru = (bcx + d_, bcy - a_ // 2)
|
||||
p_rl = (bcx + d_, bcy + a_ // 2)
|
||||
p_lu = (bcx - d_, bcy - a_ // 2)
|
||||
p_ll = (bcx - d_, bcy + a_ // 2)
|
||||
for seg in ([p_top, p_bot], [p_top, p_ru, p_mid], [p_mid, p_rl, p_bot],
|
||||
[p_lu, p_rl], [p_ll, p_ru]):
|
||||
thick_line(seg, RUNE, lw2)
|
||||
|
||||
# ---------- 输出 ----------
|
||||
out = img.resize((N, N), Image.LANCZOS)
|
||||
out.save('static/appIcon.png')
|
||||
print('saved static/appIcon.png')
|
||||
BIN
SmartAssiter/.icon_build/reg_backup/AeDebug_32_backup.reg
Normal file
BIN
SmartAssiter/.icon_build/reg_backup/AeDebug_64_backup.reg
Normal file
30
SmartAssiter/docs/修改记录_2026-09-09_App图标更换为制冷杯主题.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 修改记录 2026-09-09:App 图标更换为制冷杯主题
|
||||
|
||||
## 背景
|
||||
|
||||
原图标(奶瓶 + 蓝牙徽标)仍沿用母婴产品早期设计,现产品定位为**制冷杯**,图标需体现该概念。
|
||||
|
||||
## 改动内容
|
||||
|
||||
- `static/appIcon.png`:重新绘制 1024×1024 图标。
|
||||
- 背景:冰蓝纵向渐变(浅冰蓝 → 深蓝),圆角矩形外形与旧图标一致;
|
||||
- 主体:白色制冷杯剪影(杯盖 + 顶部模块凸起 + 上宽下窄杯身),杯身正面为蓝色雪花标志,表达"制冷";
|
||||
- 右下角保留蓝牙徽标(白圆 + 蓝牙符文),延续 App 的 BLE 连接属性。
|
||||
- `manifest.json` 无需改动:Android 各 dpi 与 iOS appstore 均指向 `/static/appIcon.png`,直接替换文件即可生效。
|
||||
|
||||
## 生成方式
|
||||
|
||||
图标由脚本 `.icon_build/make_icon.py` 程序化绘制(Pillow,4 倍超采样抗锯齿)。
|
||||
本机无 Pillow 时重新生成需先执行:
|
||||
|
||||
```bash
|
||||
python -m pip install --target .icon_build Pillow
|
||||
python .icon_build/make_icon.py
|
||||
```
|
||||
|
||||
(`.icon_build/` 仅为图标生成临时目录,不参与 App 构建;Pillow 依赖已清理,仅保留脚本。)
|
||||
|
||||
## 追加:自定义基座云打包配置修复
|
||||
|
||||
- `manifest.json`:`name` 改为 `SmartAssister`;`app-plus.modules` 增加 `Bluetooth`(云打包/自定义基座必须显式声明蓝牙模块,标准基座默认全模块故此前未暴露)。
|
||||
- 勾选 Bluetooth 模块后,云端会自动注入 `BLUETOOTH`/`BLUETOOTH_ADMIN`/`BLUETOOTH_SCAN(neverForLocation)`/`BLUETOOTH_CONNECT` 权限,与 `distribute.android.permissions` 中手工声明的重复,导致 AndroidManifest 合并冲突(`BLUETOOTH_SCAN duplicated`)打包失败。**已删除手工声明的 4 个蓝牙权限**(模块注入版本自带 `neverForLocation`,行为不变),位置/WiFi 权限保留。
|
||||
@ -1,81 +1,83 @@
|
||||
{
|
||||
"name": "smartAssister",
|
||||
"appid": "__UNI__884AC41",
|
||||
"description": "",
|
||||
"versionName": "1.0.1",
|
||||
"versionCode": "110",
|
||||
"name" : "smartAssister",
|
||||
"appid" : "__UNI__884AC41",
|
||||
"description" : "smartAssister",
|
||||
"versionName" : "1.0.2",
|
||||
"versionCode" : 111,
|
||||
/* 快应用特有相关 */
|
||||
"quickapp": {},
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin": {
|
||||
"appid": "wx50ffc3eab938ff4c",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"minified": true
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx50ffc3eab938ff4c",
|
||||
"setting" : {
|
||||
"urlCheck" : true,
|
||||
"minified" : true
|
||||
},
|
||||
"usingComponents": true,
|
||||
"permission": {}
|
||||
"usingComponents" : true,
|
||||
"permission" : {}
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion": "3",
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"modules": {},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"vueVersion" : "3",
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"modules" : {
|
||||
"Barcode" : {},
|
||||
"Bluetooth" : {},
|
||||
"Camera" : {},
|
||||
"Fingerprint" : {}
|
||||
},
|
||||
"distribute" : {
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
|
||||
"<uses-permission android:name=\"android.permission.BLUETOOTH\"/>",
|
||||
"<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>",
|
||||
"<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" android:usesPermissionFlags=\"neverForLocation\"/>",
|
||||
"<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>"
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>"
|
||||
]
|
||||
},
|
||||
"ios": {
|
||||
"privacyDescription": {
|
||||
"NSBluetoothPeripheralUsageDescription": "Please allow Bluetooth access. It is used to connect to the mother cup device: displaying runtime parameters such as temperature and fan speed, querying device system status, and upgrading the device firmware over Bluetooth.",
|
||||
"NSBluetoothAlwaysUsageDescription": "Please allow Bluetooth access. It is used to connect to the mother cup device: displaying runtime parameters such as temperature and fan speed, querying device system status, and upgrading the device firmware over Bluetooth.",
|
||||
"NSLocationWhenInUseUsageDescription": "Please allow the use of location services to identify the position of charging stations. This app promises that this function will not be used for any other purposes.",
|
||||
"NSLocalNetworkUsageDescription": "Please allow the use of network connectivity for communication between the app and the cloud service backend to enable charging-related features. This app promises that it will only be used to complete charging-related services with the cloud backend and will not be utilized for any other purposes.",
|
||||
"NSCameraUsageDescription": "Please allow the use of the camera function. This function is solely used for scanning the QR codes on charging piles to obtain information corresponding to the QR codes. This app promises that the function will not be used for any other purposes."
|
||||
"ios" : {
|
||||
"privacyDescription" : {
|
||||
"NSBluetoothPeripheralUsageDescription" : "Please allow Bluetooth access. It is used to connect to the mother cup device: displaying runtime parameters such as temperature and fan speed, querying device system status, and upgrading the device firmware over Bluetooth.",
|
||||
"NSBluetoothAlwaysUsageDescription" : "Please allow Bluetooth access. It is used to connect to the mother cup device: displaying runtime parameters such as temperature and fan speed, querying device system status, and upgrading the device firmware over Bluetooth.",
|
||||
"NSLocationWhenInUseUsageDescription" : "Please allow the use of location services to identify the position of charging stations. This app promises that this function will not be used for any other purposes.",
|
||||
"NSLocalNetworkUsageDescription" : "Please allow the use of network connectivity for communication between the app and the cloud service backend to enable charging-related features. This app promises that it will only be used to complete charging-related services with the cloud backend and will not be utilized for any other purposes.",
|
||||
"NSCameraUsageDescription" : "Please allow the use of the camera function. This function is solely used for scanning the QR codes on charging piles to obtain information corresponding to the QR codes. This app promises that the function will not be used for any other purposes."
|
||||
},
|
||||
"dSYMs" : false
|
||||
},
|
||||
"icons" : {
|
||||
"android" : {
|
||||
"hdpi" : "/static/appIcon.png",
|
||||
"xhdpi" : "/static/appIcon.png",
|
||||
"xxhdpi" : "/static/appIcon.png",
|
||||
"xxxhdpi" : "/static/appIcon.png"
|
||||
},
|
||||
"ios" : {
|
||||
"appstore" : "/static/appIcon.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"android": {
|
||||
"hdpi": "/static/appIcon.png",
|
||||
"xhdpi": "/static/appIcon.png",
|
||||
"xxhdpi": "/static/appIcon.png",
|
||||
"xxxhdpi": "/static/appIcon.png"
|
||||
},
|
||||
"ios": {
|
||||
"appstore": "/static/appIcon.png"
|
||||
}
|
||||
},
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"router": {
|
||||
"mode": ""
|
||||
"web" : {
|
||||
"router" : {
|
||||
"mode" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 604 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 604 KiB |
|
Before Width: | Height: | Size: 38 KiB |
227
docs/YF30137_驱动真值表.md
Normal file
@ -0,0 +1,227 @@
|
||||
# YF30137 七脚数码屏驱动真值表
|
||||
|
||||
> 依据《数码屏YF30137.dwg》引脚定义图还原。7 个 IO 脚,Charlieplexing(两两复用电极)驱动。
|
||||
> 段位结构:DIG1~DIG5 各 7 段(A~G)= 35 段,DIG6 两段(A6=°F、B6=小数点)= 2 段,共 37 段。
|
||||
|
||||
## 约定
|
||||
|
||||
- **脚号对应**(脚 N = 网络 LED_N = MCU 引脚):
|
||||
- LED_1 = PB0 LED_2 = PB1 LED_3 = PA0 LED_4 = PA1 LED_5 = PB11 LED_6 = PB13 LED_7 = PA6
|
||||
- `x→y` = **脚 x 拉高、脚 y 拉低**,其余 5 个脚全部**高阻(输入)**。
|
||||
- 逐段点亮:点一段 → 全高阻 → 点下一段,同一数字的多段**不能同时点**(脚共用)。
|
||||
- 若实测段「反相不亮」,把每个脚对的高/低对调即可(段位对应不变)。
|
||||
|
||||
## 一、段 → 脚 总表
|
||||
|
||||
| 段 | DIG1 | DIG2 | DIG3 | DIG4 | DIG5 | DIG6 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| A | 2→1 | 3→2 | 4→3 | 5→4 | 6→5 | 7→6(°F) |
|
||||
| B | 3→1 | 4→2 | 5→3 | 6→4 | 7→5 | 1→7(小数点) |
|
||||
| C | 4→1 | 5→2 | 6→3 | 7→4 | 1→6 | — |
|
||||
| D | 5→1 | 6→2 | 7→3 | 1→5 | 2→6 | — |
|
||||
| E | 6→1 | 7→2 | 1→4 | 2→5 | 3→6 | — |
|
||||
| F | 7→1 | 1→3 | 2→4 | 3→5 | 4→6 | — |
|
||||
| G | 1→2 | 2→3 | 3→4 | 4→5 | 5→6 | — |
|
||||
|
||||
## 二、字符段码表(哪个字符亮哪些段)
|
||||
|
||||
| 字符 | 亮段 | 字符 | 亮段 | 字符 | 亮段 |
|
||||
|---|---|---|---|---|---|
|
||||
| **0** | ABCDEF | **1** | BC | **2** | ABDEG |
|
||||
| **3** | ABCDG | **4** | BCFG | **5** | ACDFG |
|
||||
| **6** | ACDEFG | **7** | ABC | **8** | ABCDEFG |
|
||||
| **9** | ABCDFG | **A** | ABCEFG | **b** | CDEFG |
|
||||
| **C** | ADEF | **d** | BCDEG | **E** | ADEFG |
|
||||
| **F** | AEFG | **H** | BCEFG | **L** | DEF |
|
||||
| **P** | ABEFG | **U** | BCDEF | **o** | CDEG |
|
||||
| **n** | CEG | **r** | EG | **y** | BCDFG |
|
||||
| **t** | DEFG | | | | |
|
||||
|
||||
> 说明:`b`、`d`、`o`、`n`、`r`、`y`、`t` 为小写字母(避免与数字 8/0 混淆);
|
||||
> `A`、`C`、`E`、`F`、`H`、`L`、`P`、`U` 为大写。
|
||||
|
||||
## 三、DIG1 组合表(数字 + 字母)
|
||||
|
||||
| 字符 | 亮段 | 脚对(高→低) |
|
||||
|---|---|---|
|
||||
| **0** | ABCDEF | 2→1,3→1,4→1,5→1,6→1,7→1 |
|
||||
| **1** | BC | 3→1,4→1 |
|
||||
| **2** | ABDEG | 2→1,3→1,5→1,6→1,1→2 |
|
||||
| **3** | ABCDG | 2→1,3→1,4→1,5→1,1→2 |
|
||||
| **4** | BCFG | 3→1,4→1,7→1,1→2 |
|
||||
| **5** | ACDFG | 2→1,4→1,5→1,7→1,1→2 |
|
||||
| **6** | ACDEFG | 2→1,4→1,5→1,6→1,7→1,1→2 |
|
||||
| **7** | ABC | 2→1,3→1,4→1 |
|
||||
| **8** | ABCDEFG | 2→1,3→1,4→1,5→1,6→1,7→1,1→2 |
|
||||
| **9** | ABCDFG | 2→1,3→1,4→1,5→1,7→1,1→2 |
|
||||
| **A** | ABCEFG | 2→1,3→1,4→1,6→1,7→1,1→2 |
|
||||
| **b** | CDEFG | 4→1,5→1,6→1,7→1,1→2 |
|
||||
| **C** | ADEF | 2→1,5→1,6→1,7→1 |
|
||||
| **d** | BCDEG | 3→1,4→1,5→1,6→1,1→2 |
|
||||
| **E** | ADEFG | 2→1,5→1,6→1,7→1,1→2 |
|
||||
| **F** | AEFG | 2→1,6→1,7→1,1→2 |
|
||||
| **H** | BCEFG | 3→1,4→1,6→1,7→1,1→2 |
|
||||
| **L** | DEF | 5→1,6→1,7→1 |
|
||||
| **P** | ABEFG | 2→1,3→1,6→1,7→1,1→2 |
|
||||
| **U** | BCDEF | 3→1,4→1,5→1,6→1,7→1 |
|
||||
| **o** | CDEG | 4→1,5→1,6→1,1→2 |
|
||||
| **n** | CEG | 4→1,6→1,1→2 |
|
||||
| **r** | EG | 6→1,1→2 |
|
||||
| **y** | BCDFG | 3→1,4→1,5→1,7→1,1→2 |
|
||||
| **t** | DEFG | 5→1,6→1,7→1,1→2 |
|
||||
|
||||
## 三、DIG2 组合表(数字 + 字母)
|
||||
|
||||
| 字符 | 亮段 | 脚对(高→低) |
|
||||
|---|---|---|
|
||||
| **0** | ABCDEF | 3→2,4→2,5→2,6→2,7→2,1→3 |
|
||||
| **1** | BC | 4→2,5→2 |
|
||||
| **2** | ABDEG | 3→2,4→2,6→2,7→2,2→3 |
|
||||
| **3** | ABCDG | 3→2,4→2,5→2,6→2,2→3 |
|
||||
| **4** | BCFG | 4→2,5→2,1→3,2→3 |
|
||||
| **5** | ACDFG | 3→2,5→2,6→2,1→3,2→3 |
|
||||
| **6** | ACDEFG | 3→2,5→2,6→2,7→2,1→3,2→3 |
|
||||
| **7** | ABC | 3→2,4→2,5→2 |
|
||||
| **8** | ABCDEFG | 3→2,4→2,5→2,6→2,7→2,1→3,2→3 |
|
||||
| **9** | ABCDFG | 3→2,4→2,5→2,6→2,1→3,2→3 |
|
||||
| **A** | ABCEFG | 3→2,4→2,5→2,7→2,1→3,2→3 |
|
||||
| **b** | CDEFG | 5→2,6→2,7→2,1→3,2→3 |
|
||||
| **C** | ADEF | 3→2,6→2,7→2,1→3 |
|
||||
| **d** | BCDEG | 4→2,5→2,6→2,7→2,2→3 |
|
||||
| **E** | ADEFG | 3→2,6→2,7→2,1→3,2→3 |
|
||||
| **F** | AEFG | 3→2,7→2,1→3,2→3 |
|
||||
| **H** | BCEFG | 4→2,5→2,7→2,1→3,2→3 |
|
||||
| **L** | DEF | 6→2,7→2,1→3 |
|
||||
| **P** | ABEFG | 3→2,4→2,7→2,1→3,2→3 |
|
||||
| **U** | BCDEF | 4→2,5→2,6→2,7→2,1→3 |
|
||||
| **o** | CDEG | 5→2,6→2,7→2,2→3 |
|
||||
| **n** | CEG | 5→2,7→2,2→3 |
|
||||
| **r** | EG | 7→2,2→3 |
|
||||
| **y** | BCDFG | 4→2,5→2,6→2,1→3,2→3 |
|
||||
| **t** | DEFG | 6→2,7→2,1→3,2→3 |
|
||||
|
||||
## 三、DIG3 组合表(数字 + 字母)
|
||||
|
||||
| 字符 | 亮段 | 脚对(高→低) |
|
||||
|---|---|---|
|
||||
| **0** | ABCDEF | 4→3,5→3,6→3,7→3,1→4,2→4 |
|
||||
| **1** | BC | 5→3,6→3 |
|
||||
| **2** | ABDEG | 4→3,5→3,7→3,1→4,3→4 |
|
||||
| **3** | ABCDG | 4→3,5→3,6→3,7→3,3→4 |
|
||||
| **4** | BCFG | 5→3,6→3,2→4,3→4 |
|
||||
| **5** | ACDFG | 4→3,6→3,7→3,2→4,3→4 |
|
||||
| **6** | ACDEFG | 4→3,6→3,7→3,1→4,2→4,3→4 |
|
||||
| **7** | ABC | 4→3,5→3,6→3 |
|
||||
| **8** | ABCDEFG | 4→3,5→3,6→3,7→3,1→4,2→4,3→4 |
|
||||
| **9** | ABCDFG | 4→3,5→3,6→3,7→3,2→4,3→4 |
|
||||
| **A** | ABCEFG | 4→3,5→3,6→3,1→4,2→4,3→4 |
|
||||
| **b** | CDEFG | 6→3,7→3,1→4,2→4,3→4 |
|
||||
| **C** | ADEF | 4→3,7→3,1→4,2→4 |
|
||||
| **d** | BCDEG | 5→3,6→3,7→3,1→4,3→4 |
|
||||
| **E** | ADEFG | 4→3,7→3,1→4,2→4,3→4 |
|
||||
| **F** | AEFG | 4→3,1→4,2→4,3→4 |
|
||||
| **H** | BCEFG | 5→3,6→3,1→4,2→4,3→4 |
|
||||
| **L** | DEF | 7→3,1→4,2→4 |
|
||||
| **P** | ABEFG | 4→3,5→3,1→4,2→4,3→4 |
|
||||
| **U** | BCDEF | 5→3,6→3,7→3,1→4,2→4 |
|
||||
| **o** | CDEG | 6→3,7→3,1→4,3→4 |
|
||||
| **n** | CEG | 6→3,1→4,3→4 |
|
||||
| **r** | EG | 1→4,3→4 |
|
||||
| **y** | BCDFG | 5→3,6→3,7→3,2→4,3→4 |
|
||||
| **t** | DEFG | 7→3,1→4,2→4,3→4 |
|
||||
|
||||
## 三、DIG4 组合表(数字 + 字母)
|
||||
|
||||
| 字符 | 亮段 | 脚对(高→低) |
|
||||
|---|---|---|
|
||||
| **0** | ABCDEF | 5→4,6→4,7→4,1→5,2→5,3→5 |
|
||||
| **1** | BC | 6→4,7→4 |
|
||||
| **2** | ABDEG | 5→4,6→4,1→5,2→5,4→5 |
|
||||
| **3** | ABCDG | 5→4,6→4,7→4,1→5,4→5 |
|
||||
| **4** | BCFG | 6→4,7→4,3→5,4→5 |
|
||||
| **5** | ACDFG | 5→4,7→4,1→5,3→5,4→5 |
|
||||
| **6** | ACDEFG | 5→4,7→4,1→5,2→5,3→5,4→5 |
|
||||
| **7** | ABC | 5→4,6→4,7→4 |
|
||||
| **8** | ABCDEFG | 5→4,6→4,7→4,1→5,2→5,3→5,4→5 |
|
||||
| **9** | ABCDFG | 5→4,6→4,7→4,1→5,3→5,4→5 |
|
||||
| **A** | ABCEFG | 5→4,6→4,7→4,2→5,3→5,4→5 |
|
||||
| **b** | CDEFG | 7→4,1→5,2→5,3→5,4→5 |
|
||||
| **C** | ADEF | 5→4,1→5,2→5,3→5 |
|
||||
| **d** | BCDEG | 6→4,7→4,1→5,2→5,4→5 |
|
||||
| **E** | ADEFG | 5→4,1→5,2→5,3→5,4→5 |
|
||||
| **F** | AEFG | 5→4,2→5,3→5,4→5 |
|
||||
| **H** | BCEFG | 6→4,7→4,2→5,3→5,4→5 |
|
||||
| **L** | DEF | 1→5,2→5,3→5 |
|
||||
| **P** | ABEFG | 5→4,6→4,2→5,3→5,4→5 |
|
||||
| **U** | BCDEF | 6→4,7→4,1→5,2→5,3→5 |
|
||||
| **o** | CDEG | 7→4,1→5,2→5,4→5 |
|
||||
| **n** | CEG | 7→4,2→5,4→5 |
|
||||
| **r** | EG | 2→5,4→5 |
|
||||
| **y** | BCDFG | 6→4,7→4,1→5,3→5,4→5 |
|
||||
| **t** | DEFG | 1→5,2→5,3→5,4→5 |
|
||||
|
||||
## 三、DIG5 组合表(数字 + 字母)
|
||||
|
||||
| 字符 | 亮段 | 脚对(高→低) |
|
||||
|---|---|---|
|
||||
| **0** | ABCDEF | 6→5,7→5,1→6,2→6,3→6,4→6 |
|
||||
| **1** | BC | 7→5,1→6 |
|
||||
| **2** | ABDEG | 6→5,7→5,2→6,3→6,5→6 |
|
||||
| **3** | ABCDG | 6→5,7→5,1→6,2→6,5→6 |
|
||||
| **4** | BCFG | 7→5,1→6,4→6,5→6 |
|
||||
| **5** | ACDFG | 6→5,1→6,2→6,4→6,5→6 |
|
||||
| **6** | ACDEFG | 6→5,1→6,2→6,3→6,4→6,5→6 |
|
||||
| **7** | ABC | 6→5,7→5,1→6 |
|
||||
| **8** | ABCDEFG | 6→5,7→5,1→6,2→6,3→6,4→6,5→6 |
|
||||
| **9** | ABCDFG | 6→5,7→5,1→6,2→6,4→6,5→6 |
|
||||
| **A** | ABCEFG | 6→5,7→5,1→6,3→6,4→6,5→6 |
|
||||
| **b** | CDEFG | 1→6,2→6,3→6,4→6,5→6 |
|
||||
| **C** | ADEF | 6→5,2→6,3→6,4→6 |
|
||||
| **d** | BCDEG | 7→5,1→6,2→6,3→6,5→6 |
|
||||
| **E** | ADEFG | 6→5,2→6,3→6,4→6,5→6 |
|
||||
| **F** | AEFG | 6→5,3→6,4→6,5→6 |
|
||||
| **H** | BCEFG | 7→5,1→6,3→6,4→6,5→6 |
|
||||
| **L** | DEF | 2→6,3→6,4→6 |
|
||||
| **P** | ABEFG | 6→5,7→5,3→6,4→6,5→6 |
|
||||
| **U** | BCDEF | 7→5,1→6,2→6,3→6,4→6 |
|
||||
| **o** | CDEG | 1→6,2→6,3→6,5→6 |
|
||||
| **n** | CEG | 1→6,3→6,5→6 |
|
||||
| **r** | EG | 3→6,5→6 |
|
||||
| **y** | BCDFG | 7→5,1→6,2→6,4→6,5→6 |
|
||||
| **t** | DEFG | 2→6,3→6,4→6,5→6 |
|
||||
|
||||
## 四、DIG6 指示段(不是数字位)
|
||||
|
||||
| 段 | 功能 | 点亮方式 |
|
||||
|---|---|---|
|
||||
| A6(上面) | 华氏度 °F 符号 | 7→6 |
|
||||
| B6(下面) | 小数点 DP | 1→7 |
|
||||
|
||||
> DIG6 只有这两段,无 C~G。
|
||||
|
||||
## 五、固件换算公式
|
||||
|
||||
设段 `S`(A=0…G=6)、位 `D`(DIG1=1…DIG5=5):
|
||||
|
||||
```
|
||||
HIGH脚 = ((S + D) % 7) + 1
|
||||
LOW脚 = D + (S + D) / 7 // 整除
|
||||
```
|
||||
|
||||
DIG6 两个固定脚对:`°F = 7高6低`,`小数点 = 1高7低`。
|
||||
|
||||
## 六、控制要点
|
||||
|
||||
1. **7 个脚必须支持三态**(高/低/高阻):点段前先把 7 脚全切输入,再只把目标 2 脚切输出。
|
||||
2. **限流电阻**:若 YF30137 内部无电阻,每脚(或每段)串 100~330Ω(看亮度),否则烧段/烧 IO。
|
||||
3. **防鬼影**:点完一段立即切回全高阻再点下一段。
|
||||
4. **刷新率**:温度用 3 位(DIG1~3)+ 小数点,共 21+1 段,全扫一遍 ≥ 50~60Hz(每段约 0.7ms)。
|
||||
5. **扫描实现**:建议用定时器中断(TIM3 或 SysTick)逐段点亮,别用主循环阻塞。
|
||||
|
||||
## 七、温度显示示例(8.0~36.7℃,格式 XX.X)
|
||||
|
||||
| 温度 | DIG1 | DIG2 | DIG3 | 小数点(B6) | °F(A6) |
|
||||
|---|---|---|---|---|---|
|
||||
| 36.7 | 3 | 6 | 7 | 1→7 | 灭(摄氏) |
|
||||
| 8.0 | (灭/空白) | 8 | 0 | 1→7 | 灭(摄氏) |
|
||||
| 25.0 | 2 | 5 | 0 | 1→7 | 灭(摄氏) |
|
||||
@ -149,7 +149,7 @@ OTA 会话(§6 的 BEGIN/DATA/END/ABORT → OTA_RSP)跑在 0xCA 帧上,帧
|
||||
| 通道 | 下行(主机→设备) | 上行(设备→主机) |
|
||||
|---|---|---|
|
||||
| **BLE OTA 特征** `...e0005` | 每次"写带响应"携带一帧(≤ att_mtu−3) | 写完**读同一特征**取回 OTA_RSP 帧;读应答可能先于设备处理完成返回空,**读需轮询至非空**(擦扇区时数十 ms) |
|
||||
| **UART 串口**(115200 8N1) | CLI 先发文本命令 `ota`,**等到设备回标记行 `[ota] binary mode ON` 再发帧**(握手,防帧被当文本命令吃掉),随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;**OTA_ABORT 立即退出**回 CLI,10s 无帧兜底退出 |
|
||||
| **UART 串口**(460800 8N1,V1.00.26 起;之前为 115200) | CLI 先发文本命令 `ota`,**等到设备回标记行 `[ota] binary mode ON` 再发帧**(握手,防帧被当文本命令吃掉),随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;**OTA_ABORT 立即退出**回 CLI,10s 无帧兜底退出 |
|
||||
|
||||
UART 模式切换与容错(V1.00.21 起):
|
||||
|
||||
@ -181,6 +181,80 @@ PC 参考实现:`tools\ble_ota_update.py`(`ble_ota.bat`;BLE 默认,
|
||||
收到 END 应答;收不到应视为链路异常,但仍可按"等待重启后重连复核 CUR_BANK
|
||||
与版本"兜底(PC 工具两种处理都保留)。
|
||||
|
||||
## 6.7 UART 流式传输模式(V1.00.24 起)
|
||||
|
||||
动机:逐帧锁步在 115200 下实测只有 ~1.5 kB/s(每帧一个 RTT + 主机调度
|
||||
抖动)。流式模式让 DATA 帧在窗口内连续下发:**115200 下实测 9.5 kB/s
|
||||
(55.8KB 约 5.9s,0 重传);460800(V1.00.26 起默认)下实测 23 kB/s
|
||||
(约 2.6s)**,只受波特率与扇区擦除时间限制。
|
||||
|
||||
**前提(固件侧)**: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.8 Bootloader download 模式(Boot V1.01.00 起,搭配 APP ≥ V1.00.28)
|
||||
|
||||
Boot 不再只是跳转器:复位后初始化 USART1(460800 8N1,与 APP 一致),
|
||||
打印 boot banner 并给 **2s 窗口**;窗口内收到回车即进入 **download 模式**。
|
||||
此外当 bootsetting 无效 **且** APP1 向量表 sanity 检查(SP 在可用 SRAM
|
||||
窗口内、复位 PC 落在 APP1 bank 内)不过时**自动进入**(救砖路径)。
|
||||
|
||||
download 模式为裸机 superloop(无 RTOS/BLE),提供 CLI 子集:
|
||||
|
||||
| 命令 | 说明 |
|
||||
|---|---|
|
||||
| `help` / `version` / `uartinfo` | 与 APP 同义(version 报 boot 自身版本) |
|
||||
| `devinfo` | 打印 `cur bank: APP<n>`:n = bootsetting 的 active bank(记录无效时报告 APP2,表示 OTA 将写 APP1) |
|
||||
| `bsdump` / `bsset` / `appsw` | 与 APP 完全相同(复用 app_bootset.c) |
|
||||
| `ota` | 进入 0xCA 二进制帧模式,语义与 APP 完全一致:先打印 `[ota] binary mode ON` 标记行;OTA_ABORT 立即退出 / 3s 空闲退出 / OTA_END 成功且应答送达后复位 |
|
||||
| `reset` | 复位 |
|
||||
|
||||
**与 APP 侧的唯一语义差异**——目标 bank 选择:Boot 自身不运行在任一
|
||||
bank,OTA 目标 = bootsetting 的**非活动区**(OTA_END 后翻转 active 指向
|
||||
新区);bootsetting 无效时目标 = APP1。`devinfo` 按同一规则报告
|
||||
"cur bank",因此 **`tools\ble_ota_update.py --uart` 对 Boot 零改动可用**:
|
||||
脚本读到的 cur bank 的"另一侧"恰是 boot 的写入目标。
|
||||
|
||||
实现要点(对后续维护者):
|
||||
|
||||
- OTA 引擎直接编译共享 `mcm-ddc-ble/src/app_ota.c`(`BOOT_FIRMWARE` 宏隔离
|
||||
FreeRTOS/BLE 依赖,tick 用 SysTick 轮询 millis,UART sink 用阻塞轮询 TX);
|
||||
帧常量单一来源为 `Boot/src/ota_wire.h`(APP 的 `app_ble_proto.h` 包含它);
|
||||
- RX 为 DMA 环形缓冲(3KB,裸机轮询 DMA 写指针,无中断),保证扇区擦除
|
||||
关中断窗口内不丢字节(§6.7 的同一前提);流式模式在 Boot 同样生效;
|
||||
- Boot 尺寸预算 16KB:当前 ~12.9KB;`merge_image.py` 对 boot bin 超 16KB
|
||||
硬卡(出包失败);
|
||||
- Boot 不启用看门狗(避免 APP 启动早期被 Boot 遗留的 IWDG 误杀,APP 的
|
||||
IWDG 由 §50 自管);download 模式为有人值守场景,卡死靠断电重启。
|
||||
|
||||
注意:**Boot 不能被 OTA 更新**(OTA 只写 APP bank);升级 Boot 只能 SWD
|
||||
烧整片包。已出厂设备的旧 Boot(无 download 模式)不受影响,照常跳转。
|
||||
|
||||
## 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 命令形式提供,
|
||||
@ -196,8 +270,9 @@ bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形
|
||||
| `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 队列 |
|
||||
| `uartrst` | 复位串口:USART1 按 460800 8N1 重新初始化并清空 RX 队列 |
|
||||
| `uartinfo` | 查询串口参数:引脚(PB6/PB7 AF4)、波特率/数据位/校验/停止位、TX DMA/RX 中断形态 |
|
||||
| `hang` | 挂死 CLI 任务(IWDG 看门狗点火测试,V1.00.27 起;设备 ~4s 后复位) |
|
||||
|
||||
参数记录(APP_DATA 区头,52B):magic `0xCA12DA7A` + layout_ver + 参数字段 +
|
||||
reserved[8] + CRC32;记录无效时固件以缺省值运行,`appset` 时落盘。
|
||||
|
||||
527
docs/开发日志.md
@ -1171,3 +1171,530 @@ APP2 链接的镜像发向 APP1 区。设备端 END 校验(向量表 Reset PC
|
||||
重传/再同步提示前补换行不与进度条混行;END 总结新增
|
||||
`N resent, N resynced` 统计(链路质量一目了然:BLE 实测 0 次,
|
||||
接触不良的串口线 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
|
||||
不刷新问题;模式替换需在拼接缓冲上做(跨块边界会劈开模式串)。
|
||||
|
||||
## 48. UART 波特率提升 115200→460800(2026-09-05,V1.00.26)
|
||||
|
||||
- 固件 `BSP_USART_BAUDRATE` 460800(64MHz/460800 分频误差 +0.03%);
|
||||
`uartrst`/`uartinfo` 走宏自动跟随,帮助文本同步。
|
||||
- **环形缓冲 2KB→3KB**:460800 下 2KB 只能覆盖 44ms,小于擦扇区关中断
|
||||
窗口(~45ms);3KB ≈ 69ms。注意 SRAM 悬崖(§47):栈顶上移 1KB 至
|
||||
0x2000BDE8,merge_image.py 硬卡给出 <1KB 告警(属预期内)。
|
||||
- PC 工具默认波特率同步(ble_ota_update.py UartTransport、uart_cap.py)。
|
||||
- 实测:流式 OTA 55832B **2.6s(23 kB/s)**,途中丢 1 帧由 go-back-N
|
||||
自动回退续传( rewind 45360→38160),复位复核 PASS;CLI 文本模式
|
||||
在 460800 下正常(devinfo/ota 握手/复位后复核全走文本 CLI)。
|
||||
- 对比:115200 锁步 39s → 115200 流式 5.9s → 460800 流式 2.6s。
|
||||
- 注意:串口终端/工具必须同步改 460800,否则 CLI 全是乱码。
|
||||
|
||||
## 49. tools 自包含生产包(2026-09-05,无固件改动)
|
||||
|
||||
- 目标:tools/ 打包即可发布到任何 Windows 产线电脑,无需安装任何环境。
|
||||
- **内嵌 Python 运行时**:`tools/python-embed/`(python-3.12.10-embed-amd64,
|
||||
华为云镜像下载;python312._pth 启用 Lib/site-packages + import site,
|
||||
bleak 3.0.2/pyserial 3.5 用 pip --target 预装,29MB)。bat 优先级:
|
||||
python-embed → .venv-ble(开发机自动建)→ 系统 Python。
|
||||
- **烧录器收编**:NSpyocd.exe + DFP pack 复制进 tools/(flash_package.py
|
||||
优先用 tools 内副本,找不到才回退仓库 nations-tec 路径)。
|
||||
- **一键打包**:`make_release.bat`(PowerShell Compress-Archive,排除
|
||||
.venv-ble/__pycache__,zip 暂存 %TEMP% 再移入 out/——直接写 out/ 会
|
||||
自读自写报错)。
|
||||
- **坑**:bat 必须 CRLF 行尾——LF-only 时 cmd 对括号块/%~dp0 的解析会
|
||||
错乱(standalone 复测时 venv 分支被误触发且 PYREAL 为空),本次全部
|
||||
转 CRLF。zip 用 Windows 资源管理器/Expand-Archive 解压(Info-ZIP
|
||||
unzip 对 Compress-Archive 的反斜杠分隔符不兼容)。
|
||||
- 验证:zip 解到仓库外独立目录,ble_ota.bat --uart COM4 全流程 PASS
|
||||
(2.2s,27 kB/s,0 重传);flash_package.bat 用包内 NSpyocd 烧录 PASS。
|
||||
|
||||
## 50. 独立看门狗 IWDG(2026-09-05,V1.00.27)
|
||||
|
||||
- 起因:此前 CLI 卡死事故中 `reset` 命令无从执行(任务已死),系统无自救
|
||||
手段。固件原本没有看门狗。
|
||||
- 复位路径复查:当前固件 `reset`/`factory`/`appsw`/OTA 延迟复位均实测正常
|
||||
(NVIC_SystemReset 有效);用户报告的"复位没生效"发生于 CLI 卡死状态,
|
||||
命令根本无法到达执行路径——这正是看门狗的场景。
|
||||
- 设计(main.c / app_wdt.h,驱动 n32wb03x_iwdg.c 已加入全部 4 个 target):
|
||||
- IWDG:LSI 32kHz /128 → 250Hz,reload 1000 = **4s 超时**;
|
||||
- **喂养策略**:FreeRTOS idle hook 只在"3s 内有任务心跳"时喂狗;BLE 调度/
|
||||
LED/CLI 三个任务循环里打点 `app_wdt_heartbeat()`。任何任务死锁/死循环
|
||||
(CLI 独占 CPU 饿死 idle,或全体阻塞 idle 空转无心跳)都会在 ≤4s 复位;
|
||||
- `DBG_ConfigPeriph(DBG_IWDG_STOP, ENABLE)`:SWD halt 时冻结 IWDG,
|
||||
调试会话不会被看门狗打断;
|
||||
- 启动 banner 打印复位原因(RCC IWDGRSTF)并清标志。
|
||||
- 新增调试命令 `hang`(CLI 任务忙等挂起,验证看门狗点火用)。
|
||||
- 实测:正常喂狗系统稳定(12s+ 无重启、CLI 正常);`hang` 后 4.4s 复位,
|
||||
新 banner 打印 "Last reset: IWDG watchdog!"。
|
||||
- 附带修正:`uartinfo` 输出里 RX 描述更新为 DMA ring(此前还是 RXDNE 队列
|
||||
的旧文案)。
|
||||
|
||||
## 51. Boot download 模式:设计定稿(2026-09-05,代码未落地)
|
||||
|
||||
- 目标:Boot 增加 USART1 功能,把 APP 的 CLI 与 UART OTA 能力移植进 Boot,
|
||||
支持 **download 模式**(该模式下有 CLI + OTA 帧升级),用于救砖与生产
|
||||
下载。本节先记录摸底结论与设计决策,实现见后续章节。
|
||||
- 摸底结论(复用性评估):
|
||||
- Boot 现状:仅 4 个源文件(startup/system/main/boot_crc32),ROM 占用
|
||||
2KB,预算 16KB(`CAIIC_BOOT_SIZE`),IRAM 0x20004000 起(栈顶须远离
|
||||
0x2000C000 SRAM 悬崖,§47);
|
||||
- `app_ota.c`(741 行)**通道无关**:帧重组/惰性擦扇区/4B 暂存/CRC32 校验/
|
||||
bootsetting 更新全部可复用;耦合点仅 FreeRTOS tick(延迟复位)、
|
||||
`app_ble_proto.h`(帧常量 + legacy notify 应答)、`bsp_usart.h`
|
||||
(UART sink)。用 `BOOT_FIRMWARE` 宏做双构建隔离,**不复制副本**,
|
||||
避免两份 OTA 引擎日后漂移;
|
||||
- `app_bootset.c`(bsdump/bsset/appsw)几乎无耦合一并复用;
|
||||
`cli_core.c`/`app_cli.c` 与 FreeRTOS/BLE/各 app 模块深耦合,**不搬**,
|
||||
Boot 侧写一个精简的轮询行编辑器 + 命令子集;
|
||||
- Qflash 驱动 = 常量数组 memcpy 到 RAM(0x324B)+ 关中断执行,裸机可用;
|
||||
- flash 擦除关中断数十 ms,**RX 必须用 DMA 环形缓冲**(同 APP 方案,
|
||||
裸机轮询 DMA 写指针即可,无需 IDLE 中断),否则 460800 下擦除窗口丢字节。
|
||||
- PC 脚本兼容面(`tools/ble_ota_update.py` UART 流程,决定 Boot CLI 协议面):
|
||||
提示符 `caiic->`;`devinfo` 输出须含 `cur bank: APP<n>`(脚本据此选
|
||||
非活动区载荷);`ota` 命令须打印 `[ota] binary mode ON` 标记行后进入
|
||||
二进制帧模式。**Boot CLI 全部照此实现,脚本零改动即可对 Boot 升级**。
|
||||
- download 模式设计:
|
||||
- 入口:① 每次复位后 Boot 初始化 USART1(460800 8N1)打印 boot banner,
|
||||
**2s 窗口**内收到字节即进入;② bootsetting 无效且 APP1 向量表
|
||||
(SP/复位 PC) sanity 检查不过 → 无镜像可跳,自动进入(救砖路径);
|
||||
- 模式内:裸机 superloop(无 RTOS),SysTick 1ms 时基;命令子集:
|
||||
help / version / devinfo / bsdump / bsset / appsw / ota / reset /
|
||||
uartinfo;`ota` 进入 0xCA 二进制帧模式(复用 app_ota 引擎,语义与
|
||||
APP 完全一致:OTA_ABORT 立即退出 / 3s 空闲退出 / OTA_END 成功后延迟
|
||||
复位——应答被取走 +300ms 或 2s 超时兜底);
|
||||
- **目标 bank 规则**:写 bootsetting 非活动区并翻转到新区;bootsetting
|
||||
无效时写 APP1 并置活动。boot 的 `devinfo` 按此规则报告 "cur bank"
|
||||
(无效记录时报告 APP2,使脚本选 APP1 载荷,与 boot 目标一致);
|
||||
- download 模式内开 IWDG 兜底(superloop 喂养),跳转 APP 前不动 IWDG
|
||||
(APP 自理,§50)。
|
||||
- 风险提示:Boot 16KB 尺寸预算紧张(printf + USART/DMA/GPIO/RCC 驱动 +
|
||||
qflash + OTA 引擎 + CLI),构建后核对 map 是否越界,越界则裁剪命令。
|
||||
|
||||
## 52. Boot download 模式:实现与实测(2026-09-05,Boot V1.01.00 / APP V1.00.28)
|
||||
|
||||
- 实现(设计见 §51,一处变更:Boot 内**不启用 IWDG**——download 模式遗留的
|
||||
IWDG 会在 APP 启动早期(app_wdt_init 之前)误杀,APP 看门狗由 §50 自管;
|
||||
download 为有人值守场景,卡死靠断电):
|
||||
- `Boot/src/boot_usart.c/h`:USART1 460800 8N1 裸机驱动——TX 轮询、RX
|
||||
**DMA CH2 环形 3KB**(裸机轮询 DMA 写指针,无中断;扇区擦除关中断窗口
|
||||
不丢字节,流式 OTA 的前提)、SysTick 轮询 millis(COUNTFLAG,无中断);
|
||||
- `Boot/src/ota_wire.h`:0xCA 帧类型/OTA 状态码的**单一来源**共享头,
|
||||
APP 的 `app_ble_proto.h` 改为包含它(协议常量不再两处定义);
|
||||
- `mcm-ddc-ble/src/app_ota.c`、`app_bootset.c`:**不复制副本**,加
|
||||
`BOOT_FIRMWARE` 宏双构建——隔离 FreeRTOS tick(boot 用 millis)/
|
||||
BLE legacy 应答路径/BLE 通道函数/bsp_usart(boot 用阻塞轮询 TX);
|
||||
Boot 下 OTA 目标 bank = bootsetting 非活动区(记录无效时 APP1);
|
||||
- `Boot/src/cli_core.h`(shim,仅 cli_write/cli_printf)+ `boot_cli.c`:
|
||||
轮询行编辑器(回显/退格)+ 迷你 printf(无 libc printf 依赖,支持
|
||||
%s/%-8s/%08lX 等)+ 命令子集(help/version/devinfo/bsdump/bsset/appsw/
|
||||
ota/reset/uartinfo,后三个复用 app_bootset.c);
|
||||
- `Boot/src/main.c`:banner(`[boot] CAIIC-BOOT V1.01.00 - Enter within
|
||||
2s: download mode`)→ 2s 回车窗口;bootsetting 无效**且** APP1 向量表
|
||||
sanity(SP∈0x20004001~0x2000C000、复位 PC∈APP1 bank)不过 → 自动进入
|
||||
download 模式;否则按原逻辑跳转;
|
||||
- uvprojx:加 boot_usart/boot_cli + 共享 app_ota/app_bootset + 5 个 SDK
|
||||
驱动(rcc/gpio/usart/dma/qflash),宏 `BOOT_FIRMWARE`,include 路径
|
||||
`..\src` 先于 `..\..\mcm-ddc-ble\inc`(shim 生效);
|
||||
- `merge_image.py`:boot bin 超 16KB 硬卡 + boot 栈顶悬崖校验。
|
||||
- **PC 脚本零改动兼容**的关键:boot CLI 提示符同为 `caiic->`、`devinfo`
|
||||
按"OTA 目标的对侧"打印 `cur bank: APP<n>`、`ota` 打印同一
|
||||
`[ota] binary mode ON` 标记行——`ble_ota_update.py --uart` 的探活/选包/
|
||||
握手流程原样命中。
|
||||
- 尺寸:Boot `Code=10254 RO-data=2574 RW-data=96 ZI-data=6648`,bin 12924B
|
||||
(16KB 区余 3.4KB);`__initial_sp=0x20005A58`,远离 0x2000C000 悬崖。
|
||||
APP 行为不变(V1.00.28,基线与 V1.00.27 相同)。
|
||||
- 实测(COM4 460800 + SWD)全部 PASS:
|
||||
1. 正常启动:窗口超时自动跳 APP(SWD 读 xTickCount 持续增长);
|
||||
2. 窗口内回车 → download 模式,help/version/devinfo/bsdump 全部正常;
|
||||
3. download 模式下 `ble_ota_update.py --uart` 流式写 **APP2**:
|
||||
27.3 kB/s、2.3s、0 回退,复位后运行 APP2(PASS);
|
||||
4. 再次 download 模式写 **APP1**(往返):同样 PASS,板子回到 APP1;
|
||||
5. **救砖路径**:SWD 擦除 bootsetting + APP1 首扇区模拟变砖 → 复位后
|
||||
自动进 download 模式(devinfo 报 "bootsetting: INVALID - OTA targets
|
||||
APP1")→ 脚本写入 APP1 并重建 bootsetting → 复位进 APP1(PASS)。
|
||||
- 协议文档:`docs/ble_protocol.md` §6.8。注意 **Boot 本身不能被 OTA 更新**
|
||||
(OTA 只写 APP bank),升级 Boot 只能 SWD 整片包;已出厂设备的旧 Boot
|
||||
无 download 模式,功能不受影响。
|
||||
|
||||
## 53. Boot 新增 3.3V 拉高下载 strapping 机制(2026-09-06,Boot V1.01.01)
|
||||
|
||||
- 需求:在不动原有 2s 回车窗口 / 救砖自动进入两条入口的前提下,**新增一条
|
||||
纯硬件入口**——上电/复位时把某一 IO 脚接 3.3V(板载 VDD 轨),Boot 即进入
|
||||
download 模式,无需 PC 串口在 2s 内回车,方便产线/现场用一根跳线强制进入。
|
||||
- 引脚选型(依据 STB 开发板原理图 `nations-tec/N32WB031_STB_V1.3` + SDK
|
||||
EVAL 例程按键定义):
|
||||
- 占用脚(必须避开):SWD=PA4/PA5、LED1=PB0(D1)、LED2=PA6(D2)、
|
||||
USART1=PB6(TX)/PB7(RX)、32.768K 晶振=PB8/PB9、用户键 KEY1=PB1/KEY2=PB2
|
||||
(SDK `central_hogprh`/`EXTI/KeyInterrupt` 等例程均定义 PB1/PB2);
|
||||
PA0 为 WKUP 唤醒脚,也避开。
|
||||
- **选定 PB5**:空闲、纯 GPIO、无板载按键/LED/晶振/SWD/串口占用,且在
|
||||
排针 J3 上与 PB6(TX)/PB7(RX) 相邻,接跳线最方便。
|
||||
- 实现(`Boot/src/main.c`):
|
||||
- 新增宏 `BOOT_DL_STRAP_GPIO=GPIOB` / `BOOT_DL_STRAP_PIN=GPIO_PIN_5` /
|
||||
`BOOT_DL_STRAP_CLK=RCC_APB2_PERIPH_GPIOB`;
|
||||
- `boot_dl_strap_active()`:使能 GPIOB 时钟 → 配为**输入 + 内部下拉**
|
||||
(`GPIO_MODE_INPUT` + `GPIO_PULL_DOWN`)→ 读 `GPIO_ReadInputDataBit`,
|
||||
为高返回 1。不接跳线时内部弱下拉把脚拉低 → 正常启动;外接 3.3V 强驱动
|
||||
覆盖弱下拉 → 读高 → 进 download;
|
||||
- 主流程:banner 后**先查 strap**(置 enter_dl 并打印
|
||||
`[boot] strap pin HIGH (3V3): download mode`),strap 生效时**跳过 2s
|
||||
回车窗口**直接进 download;未生效则继续走原有 2s 窗口 + 救砖判断,
|
||||
两条旧入口行为不变。
|
||||
- 版本:`Boot/src/boot_version.h` `BOOT_FW_VERSION` V1.01.00 → V1.01.01。
|
||||
- 用法:板子断电 → 用跳线把 J3 的 PB5 接到 3.3V(J3 上有 VDD_U1/3.3V 脚)→
|
||||
上电/复位 → 看到 `strap pin HIGH (3V3)` 即进入 download 模式(CLI + OTA)。
|
||||
- 注意:PB5 与 LPUART 无冲突(本工程未用 LPUART 日志);若日后产品板复用
|
||||
PB5,只需改上述三个宏即可换脚。
|
||||
|
||||
## 54. Boot download CLI 移植 TAB 命令补全(2026-09-06,Boot V1.01.02)
|
||||
|
||||
- 从 APP 的 `mcm-ddc-ble/src/app_cli.c` 把 **TAB 命令名自动补全**移植到
|
||||
`Boot/src/boot_cli.c`(Boot 侧为裸机轮询行编辑器,命令表是本地 `s_cmds[]`,
|
||||
直接引用本地表而非 cli_core 的枚举函数):
|
||||
- `boot_cli_tab_complete()`:只补全首 token(已含空格则不补);单匹配→补全
|
||||
命令名并追加一个空格;多匹配→先扩展到公共前缀,再按 TAB 列出候选;
|
||||
- `boot_cli_redraw()`:多匹配列出候选后重画 `caiic->` 提示符 + 当前行
|
||||
(Boot 无历史导航,`shownLen` 恒等于当前行长);
|
||||
- `boot_cli_run()`:switch 新增 `CLI_KEY_TAB(0x09)` 分支,`len` 由
|
||||
`uint8_t` 改 `uint16_t`;ready banner 提示 `(TAB = complete)`。
|
||||
- 行为与 APP 一致:`b`+TAB → 扩到 `bs`(bsdump/bsset 公共前缀),再 TAB 列出
|
||||
`bsdump bsset`;`d`+TAB → `devinfo `;空行+TAB → 列出全部命令。
|
||||
- 版本:`Boot/src/boot_version.h` V1.01.01 → V1.01.02;构建 0E/0W,
|
||||
`Code=10894 RO-data=2590 RW-data=96 ZI-data=6648`,bin 13580B(16KB 区内)。
|
||||
|
||||
## 55. Boot download CLI `help` 补全提示串对齐 APP(2026-09-06,Boot V1.01.03)
|
||||
|
||||
- 问题:APP 的 `help` 末尾有一行 `keys: TAB = complete, UP/DOWN = history`
|
||||
(`cli_core.c` 的 CmdHelp 打印,是 APP 运行时显示的字符串);Boot 的 `help`
|
||||
只有命令表,没有这行提示,与 APP 不一致。
|
||||
- 修改(`Boot/src/boot_cli.c`):
|
||||
- `CmdHelp` 无参数分支在命令表后追加 `keys: TAB = complete`(Boot 只移植了
|
||||
TAB 补全,无 UP/DOWN 历史,故提示串如实只写 TAB);
|
||||
- 撤回 ready banner 里临时加的 `(TAB = complete)`,保持与 APP banner 风格
|
||||
一致(提示只放在 `help` 里)。
|
||||
- 版本:`Boot/src/boot_version.h` V1.01.02 → V1.01.03;构建 0E/0W,
|
||||
`Code=10922 RO-data=2574 RW-data=96 ZI-data=6648`。
|
||||
|
||||
## 56. Boot download CLI 移植 UP/DOWN 历史(2026-09-06,Boot V1.01.04)
|
||||
|
||||
- 从 APP 的 `mcm-ddc-ble/src/app_cli.c` 把 **UP/DOWN 箭头历史导航**移植到
|
||||
`Boot/src/boot_cli.c`,与 TAB 补全一起使 Boot CLI 行编辑能力与 APP 对齐:
|
||||
- 命令历史环形缓冲 `s_history[CLI_HISTORY_SIZE=8][CLI_LINE_SIZE=80]` +
|
||||
`cli_history_add()`(跳过空行/相邻重复)/ `cli_history_get(nav)`;
|
||||
- `boot_cli_run()`:新增 `escState`(ESC / ESC '[' 两级状态机,识别
|
||||
`ESC [ A`=上、`ESC [ B`=下)、`histNav`(-1=编辑,0..=历史索引)、
|
||||
`shownLen`(屏显行长,用于重画);UP 取更早历史、DOWN 取更新/回空行,
|
||||
切换时 `boot_cli_redraw()` 重画提示符+整行;
|
||||
- 回车执行前 `cli_history_add()` 入栈并复位 `histNav/shownLen`;退格/回显/
|
||||
TAB 均同步维护 `shownLen`;普通字符回显加 `ch < 0x7F` 过滤(对齐 APP);
|
||||
- `help` 提示串由 `keys: TAB = complete` 更新为
|
||||
`keys: TAB = complete, UP/DOWN = history`(与 APP 一致)。
|
||||
- 版本:`Boot/src/boot_version.h` V1.01.03 → V1.01.04;构建 0E/0W,
|
||||
`Code=11302 RO-data=2574 RW-data=96 ZI-data=7288`(+640B 历史环),
|
||||
bin 13972B(16KB 区内),`__initial_sp=0x20005CD8`(远离 0x2000C000 悬崖)。
|
||||
|
||||
## 57. CLI `help` 增加运行模式标识 + Boot 版本号改 1.00.XX(2026-09-06)
|
||||
|
||||
- `help` 输出首行增加当前运行模式标识,Boot/APP 均加,便于区分:
|
||||
- `Boot/src/boot_cli.c` CmdHelp:命令表前打印 `mode: BOOT download`;
|
||||
- `mcm-ddc-ble/src/cli_core.c` CmdHelp:命令表前打印 `mode: APP`。
|
||||
- Boot 版本号格式由 `V1.01.XX` 改为 `V1.00.XX`(与 APP 的 `V1.00.XX`
|
||||
命名一致):`Boot/src/boot_version.h` `V1.01.04` → `V1.00.05`。
|
||||
- APP 版本递增:`mcm-ddc-ble/inc/app_version.h` `V1.00.28` → `V1.00.29`
|
||||
(`APP_FW_VERSION_NUM` 0x0001001C → 0x0001001D)。
|
||||
- 构建/出包:Boot bin 14004B;APP1 bin 56120B(crc 0x747E4248,ver 0x0001001D),
|
||||
APP2 56420B;OTA 载荷随 APP 一并更新。
|
||||
|
||||
## 58. YF30137 七脚数码管显示驱动 + 取消 Boot strap 下载检测(2026-09-06,APP V1.00.30 / Boot V1.00.06)
|
||||
|
||||
- 新增 YF30137 七脚数码管显示驱动(Charlieplexing,7 个 GPIO 直接扫描,
|
||||
段位真值表见 `docs/YF30137_驱动真值表.md`):
|
||||
- `mcm-ddc-ble/src/app_display.c` + `inc/app_display.h`:独立 FreeRTOS 任务
|
||||
`display_task`(优先级 1、栈 256 字、20ms 帧周期)逐段点亮;
|
||||
- 7 脚映射(脚1~7 = 网络 LED_1~7):PB0 / PB1 / PA0 / PA1 / PB11 / PB13 / PA6;
|
||||
- 段→脚公式 `HIGH=((S+D)%7)+1`、`LOW=D+(S+D)/7`(S=A0..G6,D=DIG1..5),
|
||||
DIG6 两指示段:A6=°F(7高6低)、B6=小数点(1高7低);
|
||||
- 三态驱动:点段时高脚拉高/低脚拉低、其余 5 脚高阻,点完立即回高阻(防鬼影);
|
||||
- 显示接口:`volatile` 全局变量(float 数值 / type 类型 / base 进制)+
|
||||
无锁 `display_set(value, type, base)`;`base=10` 十进制(1 位小数)、
|
||||
`base=16` 十六进制(整数按 hex);`type` 控制 °F 指示灯;负号用 G 段;
|
||||
- 显示能力:5 位数字 + 小数点 + °F,段码含 0~9 / A~F / H L P U o n r y t。
|
||||
- 原 LED1(PB0)/LED2(PA6) 运行/连接指示灯移除(两脚让给数码管):
|
||||
- `main.c` 删 `led_task` 与 `LedInit(LED1/LED2)`,改创建 `display_task`;
|
||||
- `app_ble.c` 删 `app_ble_connected/disconnected` 里的 LED2 `LedOn/LedOff`;
|
||||
- `mcm-ddc-ble.uvprojx` USER 组加入 `app_display.c`(3 个 target)。
|
||||
- Boot 取消 PB5 strap 引脚进 download 模式(管脚让出):
|
||||
- `Boot/src/main.c` 删 `boot_dl_strap_active()` 与 strap 检测分支,保留
|
||||
USART1 2s 回车窗口 + 无有效镜像自动进 download 两条路径(功能仍在)。
|
||||
- 版本:APP `V1.00.29` → `V1.00.30`(NUM 0x0001001D → 0x0001001E);
|
||||
Boot `V1.00.05` → `V1.00.06`。
|
||||
- 构建/出包:APP1 bin 56584B(ver 0x0001001E)、APP2 56888B;Boot bin 13856B
|
||||
(16KB 区内);整包 + OTA 包已更新。APP ZI 不变(30140),`__initial_sp`
|
||||
0x2000BDF0 距 0x2000C000 悬崖 528B(merge_image.py 报 within 1KB 警告,
|
||||
为既有状态,非本次引入,后续加 ZI 需注意)。
|
||||
|
||||
## 59. CLI 新增 `pinset` 管脚高低电平命令(2026-09-06,APP V1.00.31 / Boot V1.00.07)
|
||||
|
||||
- 需求:加一条 CLI 命令直接设置/读取 GPIO 高低电平,便于硬件联调:
|
||||
`pinset pb0 1` / `pinset pa0 0`(设置输出),`pinset pb0`(读当前电平)。
|
||||
- 实现(`Boot/src/boot_cli.c` 与 `mcm-ddc-ble/src/cli_core.c` 各加一份,
|
||||
逻辑相同):
|
||||
- `pinset_parse()`:解析 `pxN`(大小写不敏感,p+a/b+数字),映射到
|
||||
`GPIOA/GPIOB` + `1<<n` 位掩码;范围校验 PA0~6(PA7 未引出)、PB0~13
|
||||
(PB14/15 未引出),非法名/越界报错;
|
||||
- `CmdPinSet`:带值 → 使能端口时钟 + 配输出推挽 + `GPIO_SetBits/ResetBits`;
|
||||
不带值 → 读 `GPIO_ReadInputDataBit`(输入数据寄存器反映实际电平);
|
||||
对 PA4/PA5(SWD)、PB6/PB7(USART1 控制台)在重配前打印 warning;
|
||||
- 命令表注册:`pinset <pxN> [0|1]: set/read GPIO level (pa0-6/pb0-13)`。
|
||||
- 与 §58 数码管驱动的共存:数码管占用 PB0/PB1/PA0/PA1/PB11/PB13/PA6,
|
||||
APP 下 `pinset` 点这些脚会被 `display_task`(20ms 周期)覆盖,属预期
|
||||
(调试工具);其余空闲脚(PB2~5/PB10/PB12/PA2/PA3)可稳定读写。
|
||||
- 版本:Boot `V1.00.06` → `V1.00.07`;APP `V1.00.30` → `V1.00.31`
|
||||
(`APP_FW_VERSION_NUM` 0x0001001E → 0x0001001F)。
|
||||
|
||||
## 60. CLI 新增 `disp` 数码管显示值设置命令(2026-09-06,APP V1.00.32,仅 APP)
|
||||
|
||||
- 背景:§58 的数码管驱动只有 `display_task` 扫描,全局值
|
||||
`g_disp_value/g_disp_type/g_disp_base` 初值 0.0/°C/十进制,**全程无任何
|
||||
任务调用 `display_set()`**(链接 map 显示 `i.display_set` 被裁掉),
|
||||
数码管一直显示静态的"0"(小数点常亮)。本次加 `disp` 命令提供修改入口。
|
||||
- 实现(`mcm-ddc-ble/src/cli_core.c`,仅 APP):
|
||||
- `cli_parse_float()`:手写十进制浮点解析(符号/整数/1 位小数,不依赖
|
||||
MicroLIB 的 atof/strtod);
|
||||
- `cli_parse_hex()`:手写十六进制整数解析(可带 `0x` 前缀);
|
||||
- `CmdDisp`:`disp <value> [f] [hex]`——默认十进制 °C;`f` 点亮 °F 指示段
|
||||
(DIG6 A6);`hex` 按十六进制整数显示;先扫模式标志再按进制解析数值,
|
||||
调 `display_set(value, type, base)` 后回显;
|
||||
- 命令表注册:`disp <value> [f] [hex]: set 7-seg display value`。
|
||||
- 版本:APP `V1.00.31` → `V1.00.32`(`APP_FW_VERSION_NUM` 0x0001001F →
|
||||
0x00010020)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 61. 数码管手动管脚控制:`dispauto` 开关 + `dispset` 命令(2026-09-06,APP V1.00.33,仅 APP)
|
||||
|
||||
- 需求:① 加开关可关掉自动显示扫描,用户手工控制数码管 1~7 管脚;② 加
|
||||
`dispset pin1-7 0/1`,pin1~7 直接映射到数码管对应的 CPU IO。
|
||||
- 管脚映射(同 §58,权威表在 `app_display.c` 的 `s_disp_pin[]`):
|
||||
`pin1=PB0 pin2=PB1 pin3=PA0 pin4=PA1 pin5=PB11 pin6=PB13 pin7=PA6`。
|
||||
- 实现:
|
||||
- `app_display.c/h`:
|
||||
- 新增 `g_disp_auto`(默认 1=自动)+ `display_auto_set/get()`;
|
||||
- `display_task`:自动关(`g_disp_auto==0`)时把 7 脚一次性置高阻
|
||||
(`all_pins_input()`,用 `last_auto` 沿检测只做一次),随后仅喂狗+
|
||||
20ms 让出,不碰管脚;自动开时恢复正常扫描;
|
||||
- `display_pin_map(pin_no 1..7, &port, &pin)`:管脚号→GPIO 端口+掩码。
|
||||
- `cli_core.c`:
|
||||
- `dispauto [on|off]`:开关自动显示并回显状态;
|
||||
- `dispset <pin1-7> [0|1]`:手工驱动管脚——`0` 拉低 / `1` 拉高
|
||||
(输出推挽),`z` 置高阻(Charlieplexing 手工驱动必需),不带值读电平;
|
||||
自动扫描未关时打印 warning 提示先 `dispauto off`;回显带 CPU IO 名
|
||||
(如 `pin1 (PB0) = 1`),用 `cli_pin_str()` 由 port+pin 生成;
|
||||
- `cli_parse_disp_pin()`:解析 `pin1`~`pin7`(或裸 `1`~`7`)。
|
||||
- 版本:APP `V1.00.32` → `V1.00.33`(`APP_FW_VERSION_NUM` 0x00010020 →
|
||||
0x00010021)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 62. `dispset` 增加 `all` 一键全管脚设置(2026-09-06,APP V1.00.34,仅 APP)
|
||||
|
||||
- 需求:`dispset all 0/1` 一次把数码管 7 个管脚全部拉高/拉低(也支持
|
||||
`dispset all z` 全置高阻)。
|
||||
- 实现(`mcm-ddc-ble/src/cli_core.c`):
|
||||
- 抽出 `cli_disp_set_pin(pin_no, val)`(单脚驱动 0/1/z),单脚与 all 共用;
|
||||
- `CmdDispSet` 顶部识别 `all`(大小写不敏感)→ 循环 pin1~7 逐个
|
||||
`cli_disp_set_pin()`,回显 `all 7 display pins = 0/1/high-Z`;
|
||||
- help 串更新为 `dispset <pin1-7|all> [0|1|z]`。
|
||||
- 版本:APP `V1.00.33` → `V1.00.34`(`APP_FW_VERSION_NUM` 0x00010021 →
|
||||
0x00010022)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 63. 修复 PA0/PA1(数码管 pin3/pin4)拉高无输出:GPIOA 时钟未使能(2026-09-06,APP V1.00.35,仅 APP)
|
||||
|
||||
- 现象:`dispset pin3 1`(PA0)/ `dispset pin4 1`(PA1)拉高后管脚无输出;
|
||||
GPIOB 侧的 pin1/pin2 等正常。
|
||||
- 根因:§58 数码管驱动把原 `LedInit(LED1/LED2)` 从 `main.c` 删除后,
|
||||
**GPIOA 时钟(`RCC_APB2_PERIPH_GPIOA`)再无人使能**——原 `app_gpio.c`
|
||||
`LedInit()` 里那两行 `RCC_EnableAPB2PeriphClk(GPIOA/GPIOB)` 是唯一使能点。
|
||||
GPIOB 因 `bsp_usart.c`(USART1 PB6/PB7)仍会使能,故只有 GPIOA 的
|
||||
PA0/PA1/PA6 失效。`GPIO_InitPeripheral`/`GPIO_SetBits` 对时钟未开的端口
|
||||
写寄存器无效,管脚不动作(模式本身 `GPIO_MODE_OUTPUT_PP` 是对的)。
|
||||
- 修复:
|
||||
- `app_display.c` `display_task` 开头显式使能 GPIOA + GPIOB 时钟;
|
||||
- `cli_core.c` `cli_disp_set_pin()` 里按端口再使能一次(防御,独立于
|
||||
display_task 也能用)。
|
||||
- 版本:APP `V1.00.34` → `V1.00.35`(`APP_FW_VERSION_NUM` 0x00010022 →
|
||||
0x00010023)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 64. 数码管代码复核 + 新增 `disptest` 自动化显示自检(2026-09-06,APP V1.00.36,仅 APP)
|
||||
|
||||
- 代码复核(结合用户已确认的硬件/管脚对应):
|
||||
- 管脚表 `s_disp_pin[7]`(pin1=PB0 … pin7=PA6)与段位公式 `seg_on()` 的
|
||||
`h=(s+d)%7` / `l=(d-1)+(s+d)/7` 一致,且恒有 h≠l(无自配对),符合
|
||||
YF30137 Charlieplexing 映射;
|
||||
- DIG6 指示段 `degF(pin7高/pin6低)`、`dot(pin1高/pin7低)` 与表一致;
|
||||
- `display_decode` 十进制(×10 一位小数 + 前导零消隐 + 负号)与十六进制
|
||||
(5 位 hex + 前导零消隐)逻辑正确;小数点/°F 为 DIG6 单指示段(硬件
|
||||
特性,非逐位小数点);
|
||||
- 结论:除 §63 的 GPIOA 时钟外,无其它缺陷。
|
||||
- 新增 `disptest` 命令(`cli_core.c`):自动化显示自检,逐项显示
|
||||
`0.0~9.0` → `88888`(全段)→ `ABCDE`(hex)→ `72.0F`(°F 指示)→
|
||||
`-123.4`(负号),每项 0.5~1.2s;测试期间强制开自动扫描、结束后恢复
|
||||
原 auto 状态并回到 0.0。
|
||||
- 版本:APP `V1.00.35` → `V1.00.36`(`APP_FW_VERSION_NUM` 0x00010023 →
|
||||
0x00010024)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 65. 修正数码管位序:DIG4 为固定 F、DIG5 为小数位(2026-09-06,APP V1.00.37,仅 APP)
|
||||
|
||||
- 用户实测反馈修正数码管位序(原代码把 DIG1~5 当 5 个全功能数字位是错的):
|
||||
- **DIG1 = 百位、DIG2 = 十位、DIG3 = 个位、DIG4 = 固定标志 F、
|
||||
DIG5 = 温度小数位(十分位)**;DIG6-A = 华氏标志(°F)、
|
||||
DIG6-B = 小数点。
|
||||
- 修改:
|
||||
- `app_display.c` `display_decode()` 重写为温度位序:value 钳位 0.0~999.9,
|
||||
×10 后按 `[百][十][个][F][小数]` 落位(百/十前导零消隐,DIG4 恒写
|
||||
`0x0F`→seg_of_char 出 "F"),`dp_flag` 恒 1(小数位恒显示);删除
|
||||
hex 解码分支;
|
||||
- `cli_core.c` `CmdDisp` 去掉 `hex` 模式(`disp <value> [f]`,0~999.9
|
||||
一位小数),删除 `cli_parse_hex()`;`CmdDispTest` 自检序列改为
|
||||
`0.0~9.0` → `12.3/456.7/999.9` → `36.5F/36.5C`;
|
||||
- `app_display.h` 头部注释与 `display_set` 说明更新为温度位序。
|
||||
- 版本:APP `V1.00.36` → `V1.00.37`(`APP_FW_VERSION_NUM` 0x00010024 →
|
||||
0x00010025)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 66. DIG4 单位字母改 F/C(华氏 F、摄氏 C)(2026-09-06,APP V1.00.38,仅 APP)
|
||||
|
||||
- 用户确认:DIG4 不是固定 F,而是**华氏显示 F、摄氏显示 C**。
|
||||
- 修改:
|
||||
- `app_display.c` `display_decode()` 增加 `type` 入参,DIG4 按类型写
|
||||
`0x0F`(F,FAHRENHEIT)或 `0x0C`(C,CELSIUS);`display_task` 调用处
|
||||
传入 `type`;
|
||||
- `app_display.h` 头部注释同步为 "DIG4 = unit letter (F/C)"。
|
||||
- 版本:APP `V1.00.37` → `V1.00.38`(`APP_FW_VERSION_NUM` 0x00010025 →
|
||||
0x00010026)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 67. 新增 `apptest`:全字符(0-9 / A-F)显示自检(2026-09-06,APP V1.00.39,仅 APP)
|
||||
|
||||
- 需求:`apptest` 把数码管所有可显示字符(0-9、A-F)都呈现一遍,肉眼核对
|
||||
每个段位字符。
|
||||
- 实现:
|
||||
- `app_display.c/h` 新增 `display_set_raw(digits[5], type)` + 裸码模式
|
||||
(`g_disp_raw`/`g_disp_raw_digits[]`):直接把 5 个段位字符码交给扫描,
|
||||
绕过温度解码;`display_set()` 会清掉裸码模式回到数值解码;`display_task`
|
||||
按 `g_disp_raw` 分流(裸码直接拷入 `digits[]`,`dp_flag` 恒 1);
|
||||
- `cli_core.c` 新增 `CmdAppTest`:循环 c=0..15,四个数字位(百/十/个/小数)
|
||||
全部显示同一字符(DIG4 固定写 `C`),0-9 打印数字、10-15 打印 A-F,每项
|
||||
500ms;结束后恢复 auto 状态并回 0.0;命令表注册 `apptest`。
|
||||
- 版本:APP `V1.00.38` → `V1.00.39`(`APP_FW_VERSION_NUM` 0x00010026 →
|
||||
0x00010027)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 68. `apptest` 改为温度量程扫测(0.1~999.0,摄氏+华氏)(2026-09-06,APP V1.00.40,仅 APP)
|
||||
|
||||
- 用户澄清:`apptest` 不是"全字符",而是要**把 0.1~999.0 整个温度量程在
|
||||
摄氏、华氏各扫一遍**,覆盖所有数字位(百/十/个/小数)与 F/C 单位字母、
|
||||
DIG6-A 指示。
|
||||
- 修改(`cli_core.c` `CmdAppTest`):
|
||||
- 删掉上一版的 0-9/A-F 裸码循环(`display_set_raw` 保留,供后续状态
|
||||
显示如 Err/Lo/Hi 用,不再被 apptest 调用);
|
||||
- 改为两轮扫测(先摄氏后华氏):`0.1~9.9`(步 0.1)→ `10~99`(步 1)→
|
||||
`100~990`(步 10)→ `999.0`,每值 100ms、末值 300ms;全程强制自动扫描,
|
||||
结束恢复 auto 状态并回 0.0;
|
||||
- 命令 help 更新为 `apptest: sweep display 0.1-999.0 in C and F`。
|
||||
- 注:显示驱动**不做 F/C 数值换算**,华氏/摄氏只影响 DIG4 字母与 DIG6-A,
|
||||
数值原样显示。
|
||||
- 版本:APP `V1.00.39` → `V1.00.40`(`APP_FW_VERSION_NUM` 0x00010027 →
|
||||
0x00010028)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 69. 整理《数码管显示设计说明书》(2026-09-06,仅文档)
|
||||
|
||||
- 新增 `docs/数码管显示设计说明书.md`:汇总当日数码管联调成果(§58~§68),
|
||||
涵盖硬件脚号映射、修正后的位序(DIG1百/DIG2十/DIG3个/DIG4单位F-C/DIG5小数/
|
||||
DIG6-A°F/DIG6-B小数点)、Charlieplexing 段→脚公式、字符段码表、温度解码、
|
||||
软件接口、CLI 命令、踩坑记录与显示示例。无代码改动。
|
||||
|
||||
## 70. `apptest` 删除,功能合入 `disptest`(2026-09-06,APP V1.00.41,仅 APP)
|
||||
|
||||
- 需求:去掉 `apptest` 命令,把它的温度量程扫测功能并入 `disptest`。
|
||||
- 修改(`cli_core.c`):
|
||||
- `CmdDispTest` 合并为两段——① 快速位/字符检查(0.0~9.0 → 12.3/456.7/999.9
|
||||
→ 36.5F/36.5C);② 全量程扫测(0.1~9.9 步 0.1 → 10~99 步 1 → 100~990
|
||||
步 10 → 999.0,摄氏+华氏各一轮);
|
||||
- 删除 `CmdAppTest` 与 `apptest` 命令表条目;`disptest` help 更新。
|
||||
- 版本:APP `V1.00.40` → `V1.00.41`(`APP_FW_VERSION_NUM` 0x00010028 →
|
||||
0x00010029)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 71. 状态灯:LED1(PB1) 运行心跳 + LED2(PB2) 蓝牙连接指示(2026-09-06,APP V1.00.42,仅 APP)
|
||||
|
||||
- 用户外接:PB1→LED1、PB2→LED2;需求:LED1 作 APP 运行状态灯、LED2 作
|
||||
蓝牙连接指示。
|
||||
- 修改:
|
||||
- `app_gpio.h`:LED1 改 `GPIOB/PIN_1`(PB1)、LED2 改 `GPIOB/PIN_2`(PB2)
|
||||
(原 PB0/PA6 已让给数码管);
|
||||
- `main.c`:新增 `led_task`(优先级 1、栈 128 字)——`LedBlink(LED1)` +
|
||||
以 `app_params_get()->led1_blink_ms`(默认 500ms)为半周期闪烁 + 喂狗;
|
||||
启动时 `LedInit(LED1/LED2)`、`LedOff(LED2)`,并 `xTaskCreate(led_task)`;
|
||||
- `app_ble.c`:`app_ble_connected()` 里 `LedOn(LED2)`、`app_ble_disconnected()`
|
||||
里 `LedOff(LED2)`。
|
||||
- 版本:APP `V1.00.41` → `V1.00.42`(`APP_FW_VERSION_NUM` 0x00010029 →
|
||||
0x0001002A)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
## 72. 状态灯换脚:LED1→PB2、LED2→PB3(2026-09-06,APP V1.00.43,仅 APP)
|
||||
|
||||
- 用户要求避开 PB1(数码管 pin2 冲突):LED1 从 PB1 改 **PB2**(运行心跳)、
|
||||
LED2 从 PB2 改 **PB3**(蓝牙连接指示)。
|
||||
- 修改:仅 `app_gpio.h` 的 `LED1_PIN=GPIO_PIN_2`、`LED2_PIN=GPIO_PIN_3`
|
||||
(端口均为 GPIOB),其余逻辑不变。
|
||||
- 版本:APP `V1.00.42` → `V1.00.43`(`APP_FW_VERSION_NUM` 0x0001002A →
|
||||
0x0001002B)。Boot 不动(仍 V1.00.07)。
|
||||
|
||||
181
docs/数码管显示设计说明书.md
Normal file
@ -0,0 +1,181 @@
|
||||
# YF30137 数码管显示设计说明书
|
||||
|
||||
> 版本:V1.1(2026-09-06,融合当日数码管联调的位序修正与 GPIOA 时钟修复)
|
||||
> 关联文档:`docs/YF30137_驱动真值表.md`(完整段→脚真值表)、`docs/开发日志.md` §58~§68
|
||||
> 固件:`mcm-ddc-ble/src/app_display.c/h`(驱动)、`mcm-ddc-ble/src/cli_core.c`(CLI 命令)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
数码屏为 **YF30137 七脚 Charlieplexing 温度显示模块**:7 个 IO 脚两两复用电极,点亮 5 个数字位 + 2 个指示段。驱动由独立的 FreeRTOS 任务 `display_task` 逐段扫描完成。
|
||||
|
||||
温度显示格式:**`[百][十][个][F/C][小数]` + `°F` + 小数点**,量程 **0.0 ~ 999.9**(1 位小数)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 硬件连接(脚号 → MCU 引脚)
|
||||
|
||||
数码管脚 1~7(网络 `LED_1`~`LED_7`)与 MCU 引脚一一对应:
|
||||
|
||||
| 数码管脚 | 网络 | MCU 引脚 | | 数码管脚 | 网络 | MCU 引脚 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| pin1 | LED_1 | **PB0** | | pin5 | LED_5 | **PB11** |
|
||||
| pin2 | LED_2 | **PB1** | | pin6 | LED_6 | **PB13** |
|
||||
| pin3 | LED_3 | **PA0** | | pin7 | LED_7 | **PA6** |
|
||||
| pin4 | LED_4 | **PA1** | | | | |
|
||||
|
||||
- 7 个脚分布在 **GPIOA**(PA0/PA1/PA6)与 **GPIOB**(PB0/PB1/PB11/PB13)两个端口。
|
||||
- ⚠️ **两个端口时钟都必须使能**(`RCC_APB2_PERIPH_GPIOA` / `RCC_APB2_PERIPH_GPIOB`),否则 PA0/PA1/PA6 无输出(见 §9 踩坑 1)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 显示位序定义(重要:当日修正)
|
||||
|
||||
| 位 | 含义 | 说明 |
|
||||
|---|---|---|
|
||||
| **DIG1** | 百位 | 数字 0~9,前导零消隐 |
|
||||
| **DIG2** | 十位 | 数字 0~9,前导零消隐 |
|
||||
| **DIG3** | 个位 | 数字 0~9,恒显示 |
|
||||
| **DIG4** | 单位字母 | **华氏显 `F`、摄氏显 `C`**(非数字位、非固定 F)|
|
||||
| **DIG5** | 小数位(十分位) | 数字 0~9,恒显示 |
|
||||
| **DIG6-A** | 华氏标志 °F | 华氏点亮、摄氏熄灭 |
|
||||
| **DIG6-B** | 小数点 | 恒点亮(小数位恒显示) |
|
||||
|
||||
> 旧代码曾把 DIG1~5 当作 5 个全功能数字位、DIG4 当普通数字位——**已修正**。
|
||||
> 注意 DIG6-A/DIG6-B 是**两个独立的单指示段**,不是逐位小数点(硬件特性)。
|
||||
|
||||
---
|
||||
|
||||
## 4. Charlieplexing 驱动原理
|
||||
|
||||
**段 S(A=0…G=6)在位 D(DIG1=1…DIG5=5)点亮**,由一对脚 `高→低` 驱动,其余 5 脚高阻:
|
||||
|
||||
```
|
||||
高脚索引 h = (S + D) % 7 → 脚号 = h + 1
|
||||
低脚索引 l = (D - 1) + (S + D) / 7 → 脚号 = l + 1
|
||||
```
|
||||
|
||||
**DIG6 两个固定脚对**:
|
||||
|
||||
| 段 | 功能 | 点亮方式 |
|
||||
|---|---|---|
|
||||
| A6 | 华氏 °F | 脚7 高、脚6 低(`PA6 高 → PB13 低`)|
|
||||
| B6 | 小数点 | 脚1 高、脚7 低(`PB0 高 → PA6 低`)|
|
||||
|
||||
**扫描要点**:逐段点亮(每段 250µs)→ 点完立即把两脚切回高阻(输入)→ 点下一段;同一数字的多段不能同时点(脚共用,否则鬼影)。一个完整扫描帧约 20ms。
|
||||
|
||||
### 4.1 三态驱动与防鬼影(核心)
|
||||
|
||||
点亮某一段时,7 个脚里**只有 2 个是输出(一高一低),其余 5 个脚必须全部「高阻」(输入浮空,`GPIO_MODE_INPUT` + `GPIO_NO_PULL`)**——既不是置高、也不是置低。
|
||||
|
||||
**为什么其它脚不能置高/置低(鬼影机理)**:以「脚6 高、脚1 低」点亮段 `6→1` 为例:
|
||||
|
||||
| 其它脚状态 | 后果 |
|
||||
|---|---|
|
||||
| 脚3 也**置低** | 6 高 + 3 低 → 段 `6→3` 也被点亮(多余段)|
|
||||
| 脚2 也**置高** | 2 高 + 1 低 → 段 `2→1` 也被点亮(多余段)|
|
||||
| 脚7 置高、脚5 置低 | 段 `7→5` 亮,甚至形成 `6→5`、`7→1` 等一串串扰 |
|
||||
|
||||
即:任一其它脚为高就会变成「电流源」,为低就会变成「电流汇」,都会和当前高低脚对构成寄生回路点亮别的段。**唯一安全状态是浮空高阻**(既不供流也不吸流)。
|
||||
|
||||
**代码实现(`app_display.c`)**:
|
||||
|
||||
```c
|
||||
pin_input(idx) → GPIO_MODE_INPUT + GPIO_NO_PULL /* 高阻(浮空输入)*/
|
||||
pin_high(idx) → GPIO_MODE_OUTPUT_PP + GPIO_SetBits /* 推挽输出高 */
|
||||
pin_low(idx) → GPIO_MODE_OUTPUT_PP + GPIO_ResetBits/* 推挽输出低 */
|
||||
|
||||
static void seg_pair_on(uint8_t h, uint8_t l)
|
||||
{
|
||||
pin_high(h); /* 1. 只把高脚切输出高 */
|
||||
pin_low(l); /* 2. 只把低脚切输出低 —— 其余 5 脚保持高阻不动 */
|
||||
delay_n_10us(25); /* 3. 点亮 250µs */
|
||||
pin_input(h); /* 4. 点完立刻把两脚放回高阻 */
|
||||
pin_input(l);
|
||||
}
|
||||
```
|
||||
|
||||
支撑前提:`display_task` 每帧先 `all_pins_input()` 把 7 脚全部清成高阻,再逐段 `seg_pair_on`——因此任意时刻**最多 2 个脚是输出,其余恒为高阻**,杜绝串段。
|
||||
|
||||
**为什么必须逐段点(时间复用)**:7 脚复用,同一数字的多段脚对互相牵连、**不能同时点亮**。数字 8(7 段)就是 7×250µs 依次点亮,20ms 一帧内扫完,靠人眼视觉暂留看起来是"整字同时亮"。
|
||||
|
||||
---
|
||||
|
||||
## 5. 字符段码表(bit0=A … bit6=G)
|
||||
|
||||
| 字符 | 段码 | 字符 | 段码 | 字符 | 段码 |
|
||||
|---|---|---|---|---|---|
|
||||
| 0 | 0x3F | 1 | 0x06 | 2 | 0x5B |
|
||||
| 3 | 0x4F | 4 | 0x66 | 5 | 0x6D |
|
||||
| 6 | 0x7D | 7 | 0x07 | 8 | 0x7F |
|
||||
| 9 | 0x6F | A | 0x77 | b | 0x7C |
|
||||
| **C** | **0x39** | d | 0x5E | E | 0x79 |
|
||||
| **F** | **0x71** | — | 负号 G 段 | 0x40 | |
|
||||
|
||||
- `F`(单位字母)= **0x71**(AEFG)、`C`(单位字母)= **0x39**(ADEF),用于 DIG4。
|
||||
- 完整每个数字位的段→脚真值表见 `docs/YF30137_驱动真值表.md`(段→脚公式不变,只是位语义按 §3 修正)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 温度解码(`display_decode`)
|
||||
|
||||
输入温度 `value`(float),输出 5 个位字符码:
|
||||
|
||||
1. 钳位:`value < 0 → 0`;`value > 999.9 → 999.9`;
|
||||
2. `scaled = (int)(value × 10 + 0.5)`(×10 转 0.1℃ 分辨率);
|
||||
3. 落位:`DIG5(小数)=scaled%10` → `DIG3(个)=(scaled/10)%10` → `DIG2(十)=(scaled/100)%10` → `DIG1(百)=(scaled/1000)%10`;
|
||||
4. 前导零消隐:百位为 0 消隐,百位已消隐且十位为 0 再消隐十位;个位与小数位恒显示;
|
||||
5. **DIG4 = 华氏 `0x0F`(F) / 摄氏 `0x0C`(C)**;
|
||||
6. 小数点(DIG6-B)恒亮;华氏标志(DIG6-A)随华氏/摄氏开关。
|
||||
|
||||
> 驱动**不做 F/C 数值换算**:华氏/摄氏只切换 DIG4 字母与 DIG6-A 指示,数值原样显示(单位换算由上层完成)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 软件架构与接口(`app_display.c/h`)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| `void display_set(float value, uint8_t type, uint8_t base)` | 设温度值 0.0~999.9;`type` 华氏/摄氏;`base` 已废弃(兼容保留)|
|
||||
| `void display_set_raw(const uint8_t digits[5], uint8_t type)` | 直接写 5 个位字符码(绕过温度解码,用于 `Err`/`Lo`/`Hi` 等状态字)|
|
||||
| `void display_auto_set(uint8_t on)` / `uint8_t display_auto_get(void)` | 自动扫描开关(关后 7 脚置高阻,交出手动控制)|
|
||||
| `int display_pin_map(uint8_t pin_no, GPIO_Module**, uint16_t*)` | 数码管脚号 1~7 → GPIO 端口+掩码 |
|
||||
| `void display_task(void*)` | 扫描任务:优先级 1、栈 256 字、20ms/帧、每段 250µs,循环喂狗 |
|
||||
|
||||
内部状态:`g_disp_value/g_disp_type/g_disp_base`(数值)、`g_disp_raw/g_disp_raw_digits`(裸码模式)、`g_disp_auto`(自动开关)。
|
||||
|
||||
---
|
||||
|
||||
## 8. CLI 命令(`cli_core.c`)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|---|---|
|
||||
| `disp <value> [f]` | 设温度显示;`f` = 华氏(DIG4 显 F + DIG6-A 亮),默认摄氏 |
|
||||
| `dispauto [on|off]` | 自动扫描开关(查询/设置)|
|
||||
| `dispset <pin1-7\|all> [0\|1\|z]` | 手动驱动数码管脚:`0` 低 / `1` 高 / `z` 高阻;`all` 一键全脚 |
|
||||
| `disptest` | 显示自检:① 0.0~9.0 → 12.3/456.7/999.9 → 36.5F/36.5C;② 全量程扫测 0.1~9.9(步0.1)→10~99(步1)→100~990(步10)→999.0,摄氏+华氏各一轮 |
|
||||
| `pinset <pxN> [0\|1]` | 通用 GPIO 读写(非数码管专用)|
|
||||
|
||||
---
|
||||
|
||||
## 9. 关键踩坑与修正记录(当日)
|
||||
|
||||
1. **GPIOA 时钟未使能 → PA0/PA1/PA6 无输出**:数码管驱动删掉 `LedInit` 后 GPIOA 时钟失去使能点(GPIOB 因 USART 仍在使能)。修复:`display_task` 开头显式使能 GPIOA+GPIOB,`dispset` 内再防御性使能一次。
|
||||
2. **位序理解错误**:旧代码把 DIG1~5 当 5 个全功能数字位、DIG4 当数字位。修正:DIG4 是单位字母、DIG5 是小数位。
|
||||
3. **DIG4 从"固定 F"改为"F/C"**:华氏显 F、摄氏显 C。
|
||||
4. **量程**:0.0~999.9(百十个小·数共 4 位数字 + 1 位单位),非 9999.9。
|
||||
5. **裸码模式**:`display_set_raw` 用于显示 0~9/A~F 之外的状态字符(如 Err),`display_set` 会清掉裸码回到数值解码。
|
||||
|
||||
---
|
||||
|
||||
## 10. 温度显示示例
|
||||
|
||||
| 温度 | DIG1(百) | DIG2(十) | DIG3(个) | DIG4 | DIG5(小数) | 小数点 | °F |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 0.0 ℃ | (空) | (空) | 0 | C | 0 | 亮 | 灭 |
|
||||
| 8.5 ℃ | (空) | (空) | 8 | C | 5 | 亮 | 灭 |
|
||||
| 36.5 ℃ | (空) | 3 | 6 | C | 5 | 亮 | 灭 |
|
||||
| 36.5 ℉ | (空) | 3 | 6 | **F** | 5 | 亮 | **亮** |
|
||||
| 100.0 ℃ | 1 | 0 | 0 | C | 0 | 亮 | 灭 |
|
||||
| 999.9 ℉ | 9 | 9 | 9 | **F** | 9 | 亮 | **亮** |
|
||||
@ -38,7 +38,7 @@ V1.0 / 2026-09-05
|
||||
- **RAM 铁律**:低 16KB(0x20000000~0x20003FFF)被芯片 ROM/BLE 子系统占用,
|
||||
任何工程 IRAM 执行区必须从 **0x20004000** 起(否则 BLE init 必 HardFault)
|
||||
- **SWD 引脚 SWCLK=PA4 / SWDIO=PA5,应用程序绝对禁止占用**
|
||||
- 板载 LED:LED1=PB0、LED2=PA6;USART1:TX=PB6 / RX=PB7(AF4,115200 8N1)
|
||||
- 板载 LED:LED1=PB0、LED2=PA6;USART1:TX=PB6 / RX=PB7(AF4,460800 8N1,V1.00.26 起)
|
||||
- 新增 GPIO 前核对开发日志 §4 引脚分配表
|
||||
- **不要直接写寄存器**;勘误修复必须先在 SDK 源码交叉验证地址用途
|
||||
(曾按勘误表写 AFEC 寄存器导致 BLE 射频挂死,开发日志 §28)
|
||||
|
||||
11
hardware_design/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# AI 工具链 / 临时产物(不入库)
|
||||
easyeda-api/
|
||||
run-api-gateway_*.eext
|
||||
BackupProjects_*.zip
|
||||
|
||||
# 与 nations-tec/N32WB031_STB_V1.3/ 重复的 STB 板文件副本(以厂商原始文件为准)
|
||||
N32WB031_STB_V1.3_20220930.pcb
|
||||
N32WB031_STB_V1.3_20220930.sch
|
||||
|
||||
# 与 docs/YF30137_驱动真值表.md 重复(以 docs/ 下的为准)
|
||||
YF30137_驱动真值表.md
|
||||
59
hardware_design/_analyze.mjs
Normal file
@ -0,0 +1,59 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
const { compData, wireData, pinData } = JSON.parse(readFileSync('H:/caiic_workspace/mothercup/hardware_design/_dump.json', 'utf8'));
|
||||
|
||||
function connPoint(p) {
|
||||
const len = p.len || 20;
|
||||
switch (p.rot) {
|
||||
case 0: return { x: p.x - len, y: p.y }; // pin points left
|
||||
case 90: return { x: p.x, y: p.y - len }; // pin points up
|
||||
case 180: return { x: p.x + len, y: p.y }; // pin points right
|
||||
case 270: return { x: p.x, y: p.y + len }; // pin points down
|
||||
default: return { x: p.x, y: p.y };
|
||||
}
|
||||
}
|
||||
|
||||
function onSeg(px, py, ax, ay, bx, by, tol = 4) {
|
||||
const cr = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
|
||||
if (Math.abs(cr) > tol * 20) return false;
|
||||
return px >= Math.min(ax, bx) - tol && px <= Math.max(ax, bx) + tol &&
|
||||
py >= Math.min(ay, by) - tol && py <= Math.max(ay, by) + tol;
|
||||
}
|
||||
function wpts(line) {
|
||||
if (!Array.isArray(line)) return [];
|
||||
if (Array.isArray(line[0])) return line.flat();
|
||||
return line;
|
||||
}
|
||||
|
||||
// net flags (components with no designator but a name)
|
||||
const netflags = compData.filter(c => (!c.d || c.d.trim()==='') && c.n);
|
||||
|
||||
function netsAt(pt) {
|
||||
const s = new Set();
|
||||
for (const w of wireData) {
|
||||
const p = wpts(w.line);
|
||||
for (let i = 0; i + 3 < p.length; i += 2) {
|
||||
if (onSeg(pt.x, pt.y, p[i], p[i+1], p[i+2], p[i+3])) if (w.net) s.add(w.net);
|
||||
}
|
||||
}
|
||||
for (const f of netflags) {
|
||||
if (Math.abs(f.x - pt.x) <= 8 && Math.abs(f.y - pt.y) <= 8 && f.n) s.add(f.n);
|
||||
}
|
||||
return [...s];
|
||||
}
|
||||
|
||||
console.log('=== key component pin -> net ===');
|
||||
for (const k of Object.keys(pinData)) {
|
||||
const pd = pinData[k];
|
||||
if (!pd) { console.log(`${k}: (not found)`); continue; }
|
||||
console.log(`${k} (${pd.name}) @ (${pd.pos.x},${pd.pos.y})`);
|
||||
for (const p of pd.pins) {
|
||||
const pt = connPoint(p);
|
||||
const nets = netsAt(pt);
|
||||
console.log(` pin ${p.num} ${p.name||''} rot=${p.rot} len=${p.len} -> ${JSON.stringify(nets)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// all components with positions (sorted by x then y) to see layout
|
||||
console.log('\n=== all components (d:name @x,y) ===');
|
||||
const sorted = [...compData].filter(c=>c.d && c.d.trim()).sort((a,b)=>a.x-b.x);
|
||||
for (const c of sorted) console.log(` ${c.d}:${c.n||''} @ (${c.x},${c.y})`);
|
||||
25
hardware_design/_diag.mjs
Normal file
@ -0,0 +1,25 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
const { compData, wireData, pinData } = JSON.parse(readFileSync('H:/caiic_workspace/mothercup/hardware_design/_dump.json', 'utf8'));
|
||||
|
||||
console.log('=== SW1/Q1/Q7/Q4/Q2/Q3/D1 pins (x,y,rot,len) ===');
|
||||
for (const k of ['SW1','Q1','Q7','Q4','Q2','Q3','D1']) {
|
||||
const pd = pinData[k];
|
||||
if (!pd) continue;
|
||||
console.log(`${k} @ (${pd.pos.x},${pd.pos.y})`);
|
||||
for (const p of pd.pins) console.log(` pin${p.num} ${p.name} (${p.x},${p.y}) rot=${p.rot} len=${p.len}`);
|
||||
}
|
||||
|
||||
console.log('\n=== wires near power-switch region (x -2000..-1500, y -1100..-400) ===');
|
||||
let cnt = 0;
|
||||
for (const w of wireData) {
|
||||
const p = Array.isArray(w.line) ? (Array.isArray(w.line[0]) ? w.line.flat() : w.line) : [];
|
||||
let near = false, pts = [];
|
||||
for (let i=0;i+1<p.length;i+=2) {
|
||||
if (p[i]>=-2000 && p[i]<=-1500 && p[i+1]>=-1100 && p[i+1]<=-400) { near=true; pts.push([p[i],p[i+1]]); }
|
||||
}
|
||||
if (near && cnt < 60) {
|
||||
cnt++;
|
||||
console.log(` net=${w.net} pts=${JSON.stringify(pts.slice(0,10))}`);
|
||||
}
|
||||
}
|
||||
console.log('total near wires:', cnt);
|
||||
33
hardware_design/_dump.js
Normal file
@ -0,0 +1,33 @@
|
||||
const comps = await eda.sch_PrimitiveComponent.getAll();
|
||||
const wires = await eda.sch_PrimitiveWire.getAll();
|
||||
|
||||
const compData = comps.map(c => {
|
||||
let d=null,n=null,x=null,y=null;
|
||||
try{d=c.getState_Designator();}catch(e){}
|
||||
try{n=c.getState_Name();}catch(e){}
|
||||
try{x=c.getState_X();y=c.getState_Y();}catch(e){}
|
||||
return {d,n,x,y};
|
||||
});
|
||||
|
||||
const wireData = wires.map(w => {
|
||||
let net=null,line=null;
|
||||
try{net=w.getState_Net();}catch(e){}
|
||||
try{line=w.getState_Line();}catch(e){}
|
||||
return {net,line};
|
||||
});
|
||||
|
||||
// pins of key power-switch components
|
||||
const key = ['SW1','Q1','Q7','Q4','Q2','Q3','D1','U1','U3','U5','BAT'];
|
||||
const pinData = {};
|
||||
for (const k of key) {
|
||||
const c = comps.find(x => { try{return x.getState_Designator()===k;}catch(e){return false;} });
|
||||
if (!c) { pinData[k]=null; continue; }
|
||||
const pins = await eda.sch_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
|
||||
pinData[k] = {
|
||||
pos: { x: c.getState_X(), y: c.getState_Y() },
|
||||
name: c.getState_Name(),
|
||||
pins: pins.map(p => ({ num:p.pinNumber, name:p.pinName, x:p.x, y:p.y, rot:p.rotation, len:p.pinLength }))
|
||||
};
|
||||
}
|
||||
|
||||
return { compData, wireData, pinData };
|
||||
1
hardware_design/_dump.json
Normal file
14
hardware_design/_dump_run.mjs
Normal file
@ -0,0 +1,14 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { request } from 'node:http';
|
||||
const code = readFileSync('H:/caiic_workspace/mothercup/hardware_design/_dump.js', 'utf8');
|
||||
const body = JSON.stringify({ code });
|
||||
const req = request({ host: '127.0.0.1', port: 49620, path: '/execute', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, res => {
|
||||
let d = '';
|
||||
res.on('data', c => d += c);
|
||||
res.on('end', () => {
|
||||
const j = JSON.parse(d);
|
||||
if (j.success) { writeFileSync('H:/caiic_workspace/mothercup/hardware_design/_dump.json', JSON.stringify(j.result)); console.log('saved', JSON.stringify(j.result).length); }
|
||||
else console.log('FAIL', d);
|
||||
});
|
||||
});
|
||||
req.write(body); req.end();
|
||||
63
hardware_design/_netlist.mjs
Normal file
@ -0,0 +1,63 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
const { compData, wireData, pinData } = JSON.parse(readFileSync('H:/caiic_workspace/mothercup/hardware_design/_dump.json', 'utf8'));
|
||||
|
||||
// Build a connectivity graph from wire segments. Snap to ~1 unit grid.
|
||||
const key = (x,y) => `${Math.round(x)},${Math.round(y)}`;
|
||||
const graph = new Map(); // node -> {neighbors:Set, label:null}
|
||||
|
||||
function node(k) { if(!graph.has(k)) graph.set(k,{n:new Set(),label:null}); return graph.get(k); }
|
||||
function link(a,b){ const A=node(a),B=node(b); A.n.add(b); B.n.add(a); }
|
||||
|
||||
for (const w of wireData) {
|
||||
const p = Array.isArray(w.line) ? (Array.isArray(w.line[0]) ? w.line.flat() : w.line) : [];
|
||||
const pts = [];
|
||||
for (let i=0;i+1<p.length;i+=2) pts.push(key(p[i],p[i+1]));
|
||||
// connect consecutive points
|
||||
for (let i=0;i+1<pts.length;i++) link(pts[i],pts[i+1]);
|
||||
// if a wire has a net label, mark ALL its points with the label
|
||||
if (w.net) for (const k of pts) node(k).label = w.net;
|
||||
}
|
||||
|
||||
// net flags: components with no designator but a name -> mark nearest node
|
||||
for (const f of compData.filter(c=>(!c.d||c.d.trim()==='')&&c.n)) {
|
||||
const k = key(f.x,f.y);
|
||||
const nd = node(k);
|
||||
if (!nd.label) nd.label = f.n;
|
||||
}
|
||||
|
||||
// Find connected components and collect labels
|
||||
const visited = new Set();
|
||||
function component(start) {
|
||||
const comp = new Set(); const labels = new Set();
|
||||
const stack=[start]; visited.add(start);
|
||||
while(stack.length){
|
||||
const k=stack.pop(); comp.add(k);
|
||||
const nd=graph.get(k); if(nd.label) labels.add(nd.label);
|
||||
for(const nb of nd.n) if(!visited.has(nb)){visited.add(nb);stack.push(nb);}
|
||||
}
|
||||
return {comp,labels:[...labels]};
|
||||
}
|
||||
|
||||
const components=[];
|
||||
for(const k of graph.keys()){ if(!visited.has(k)) components.push(component(k)); }
|
||||
|
||||
function compOf(x,y){
|
||||
const k=key(x,y);
|
||||
for(const c of components) if(c.comp.has(k)) return c;
|
||||
// nearest node within 2 units
|
||||
for(const c of components) for(const kk of c.comp){ const [a,b]=kk.split(',').map(Number); if(Math.abs(a-x)<=2&&Math.abs(b-y)<=2) return c; }
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('=== key pin -> net (connectivity) ===');
|
||||
for (const k of ['SW1','Q1','Q7','Q4','Q2','Q3','D1','U5','U1']) {
|
||||
const pd=pinData[k]; if(!pd){console.log(k+': n/a');continue;}
|
||||
console.log(`${k} (${pd.name})`);
|
||||
for(const p of pd.pins){
|
||||
const c=compOf(p.x,p.y);
|
||||
console.log(` pin${p.num} ${p.name||''} @(${p.x.toFixed(0)},${p.y.toFixed(0)}) -> ${c?JSON.stringify(c.labels):'??'} (${c?c.comp.size:0} nodes)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== labeled nets (component size >= 2) ===');
|
||||
for(const c of components) if(c.labels.length && c.comp.size>=2) console.log(` ${JSON.stringify(c.labels)} : ${c.comp.size} nodes`);
|
||||
43
hardware_design/_trace.mjs
Normal file
@ -0,0 +1,43 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
const { compData, wireData, pinData } = JSON.parse(readFileSync('H:/caiic_workspace/mothercup/hardware_design/_dump.json', 'utf8'));
|
||||
|
||||
function onSeg(px, py, ax, ay, bx, by, tol = 4) {
|
||||
const cr = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
|
||||
if (Math.abs(cr) > tol * 20) return false;
|
||||
return px >= Math.min(ax, bx) - tol && px <= Math.max(ax, bx) + tol &&
|
||||
py >= Math.min(ay, by) - tol && py <= Math.max(ay, by) + tol;
|
||||
}
|
||||
function wpts(line) { if (!Array.isArray(line)) return []; if (Array.isArray(line[0])) return line.flat(); return line; }
|
||||
|
||||
// net flags
|
||||
const netflags = compData.filter(c => (!c.d || c.d.trim()==='') && c.n);
|
||||
|
||||
// For a pin at (px,py), find nets of wires touching it (pin connects at its own x,y)
|
||||
function netsAt(px, py) {
|
||||
const s = new Set();
|
||||
for (const w of wireData) {
|
||||
const p = wpts(w.line);
|
||||
for (let i=0;i+3<p.length;i+=2) {
|
||||
if (onSeg(px,py,p[i],p[i+1],p[i+2],p[i+3])) if (w.net) s.add(w.net);
|
||||
}
|
||||
}
|
||||
for (const f of netflags) if (Math.abs(f.x-px)<=6 && Math.abs(f.y-py)<=6 && f.n) s.add(f.n);
|
||||
return [...s];
|
||||
}
|
||||
|
||||
// map each key pin to nets (pin connects at its own x,y)
|
||||
console.log('=== key pin -> net (pin at own x,y) ===');
|
||||
for (const k of ['SW1','Q1','Q7','Q4','Q2','Q3','D1','Q5','U1','U3','U5']) {
|
||||
const pd = pinData[k];
|
||||
if (!pd) { console.log(`${k}: not found`); continue; }
|
||||
console.log(`${k} (${pd.name}) @(${pd.pos.x.toFixed(0)},${pd.pos.y.toFixed(0)})`);
|
||||
for (const p of pd.pins) {
|
||||
console.log(` pin${p.num} ${p.name||''} @(${p.x.toFixed(0)},${p.y.toFixed(0)}) -> ${JSON.stringify(netsAt(p.x,p.y))}`);
|
||||
}
|
||||
}
|
||||
|
||||
// also dump components near the power-switch region with positions
|
||||
console.log('\n=== components x -2000..-1500, y -1100..-400 ===');
|
||||
for (const c of compData.filter(c=>c.d&&c.d.trim()).sort((a,b)=>a.x-b.x)) {
|
||||
if (c.x>=-2000 && c.x<=-1500 && c.y>=-1100 && c.y<=-400) console.log(` ${c.d}:${c.n||''} @(${c.x.toFixed(0)},${c.y.toFixed(0)})`);
|
||||
}
|
||||
95
hardware_design/mothercup_原理图验证报告.md
Normal file
@ -0,0 +1,95 @@
|
||||
# MotherCup 原理图验证报告(嘉立创EDA 实读)
|
||||
|
||||
> 读取对象:工程 `MotherCup_Main` → 原理图 `mothercup_main`(图页 `MotherCup_Main`)
|
||||
> 方法:通过 easyeda-api Bridge 调用 `eda.sch_*` 接口逐器件、逐引脚、逐网络读回并交叉核对。
|
||||
> 结论:**该原理图已完整覆盖全部 7 项需求,无需补画**;本文档记录实测的引脚分配与器件清单,供后续固件/维护对照。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论速览
|
||||
|
||||
| # | 需求 | 实现 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 1 | 电源充电可控 | BQ25792 升降压充电 IC(I2C 可编程)+ `CHG_CE` 使能 + HY2120 电池保护 | ✅ |
|
||||
| 2 | 采样制冷片温度 | 外部 NTC(J8 端子)+ R21/R23 分压 → `ADC_NTC`(PB10) | ✅ |
|
||||
| 3 | 制冷片 PWM | `TEC_PWM`(PB4) → Q3 CRTD055N03L 低边 MOS + D6 SS54 续流 | ✅ |
|
||||
| 4 | 留出 UART | `UART_TX/UART_RX`(PB6/PB7),经 J5/J6 排针引出 | ✅ |
|
||||
| 5 | Download 模式 IO | 按键 S1(KEY6X6,一侧 GND、一侧上拉接 MCU IO) | ✅ |
|
||||
| 6 | SWD 口 | `SWDCLK/SWDIO`(PA4/PA5),经 J5/J6 排针引出 | ✅ |
|
||||
| 7 | 三数码管显示 | TM1650 数码管驱动(I2C `TM_SDA/TM_SCL`,PA0/PA1)+ J7 FPC-14P | ✅ |
|
||||
|
||||
额外还画了:**风扇 PWM**(FAN_PWM/PB5)、**VBUS/电池电压采样**(ADC_VBUS/PB8、ADC_VBAT/PB9)、充电状态灯(LED_G/PB11、LED_R/PB13)、充电中断/状态(CHG_INT/PB12、CHG_STAT/PB2)。
|
||||
|
||||
---
|
||||
|
||||
## 2. N32WB031(U1)实测引脚网络映射
|
||||
|
||||
| 引脚 | 名称 | 连接网络 | 功能 |
|
||||
|---|---|---|---|
|
||||
| 31 | PA0 | TM_SDA | 数码管 TM1650 数据 |
|
||||
| 32 | PA1 | TM_SCL | 数码管 TM1650 时钟 |
|
||||
| 1 | PA2 | I2C_CHG_SDA | 充电 IC BQ25792 I2C 数据 |
|
||||
| 2 | PA3 | I2C_CHG_SCL | 充电 IC BQ25792 I2C 时钟 |
|
||||
| 5 | PA4 | SWDCLK | SWD 时钟 |
|
||||
| 6 | PA5 | SWDIO | SWD 数据 |
|
||||
| 7 | PA6 | (局部/待确认) | 备用(固件原 LED2) |
|
||||
| 4 | PB0 | (局部,接 S1 按键) | 下载/按键输入(待确认) |
|
||||
| 11 | PB1 | CHG_CE | 充电使能(电源充电可控) |
|
||||
| 12 | PB2 | CHG_STAT | 充电状态 |
|
||||
| 13 | PB3 | (局部) | 备用/按键 |
|
||||
| 14 | PB4 | TEC_PWM | 制冷片 PWM(TIM3_CH1) |
|
||||
| 15 | PB5 | FAN_PWM | 风扇 PWM |
|
||||
| 16 | PB6 | UART_TX | USART1 发送 |
|
||||
| 17 | PB7 | UART_RX | USART1 接收 |
|
||||
| 9 | PB8 | ADC_VBUS | VBUS 电压采样 |
|
||||
| 10 | PB9 | ADC_VBAT | 电池电压采样 |
|
||||
| 18 | PB10 | ADC_NTC | **制冷片温度采样** |
|
||||
| 20 | PB11 | LED_G | 绿 LED |
|
||||
| 19 | PB12 | CHG_INT | 充电中断 |
|
||||
| 21 | PB13 | LED_R | 红 LED |
|
||||
| 22 | VCC | 3V3 | 电源 |
|
||||
| 28 | VCCRF | 3V3 | RF 电源 |
|
||||
| 33 | GND | GND | 地(底部大焊盘) |
|
||||
|
||||
> 晶振(32MHz,Y1)、RFIOP 天线匹配、VDCDC/VDCDCRF/VDD_PA 等按硬件指南接局部退耦/匹配元件。
|
||||
|
||||
---
|
||||
|
||||
## 3. 器件清单(实测,98 个真实器件)
|
||||
|
||||
| 类别 | 器件 |
|
||||
|---|---|
|
||||
| MCU | U1 = N32WB031K8Q7 |
|
||||
| 充电 | U2 = BQ25792RQMR(升降压充电,I2C) |
|
||||
| 电池保护 | U3 = HY2120-CB + Q1 = FS8205A(双 MOS) |
|
||||
| 数码管驱动 | U4 = TM1650(I2C) |
|
||||
| LDO | U6 = ME6231A33PG(3.3V) |
|
||||
| 制冷片 MOS | Q3 = CRTD055N03L + D6 = SS54(续流) |
|
||||
| 辅助 MOS | Q2 = AO3400 + D2 = SS14 |
|
||||
| 晶振 | Y1 = 32MHz 12pF |
|
||||
| 接口 | J1 USB-C、J2/J3/J4/J8 XH2.54 端子、J5/J6 HDR2.54 排针、J7 FPC-14P |
|
||||
| 其它 | 按键 S1、LED D3/D4/D5、TVS D1(SMBJ24A)、电感 L1/L2、阻容若干 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 与早期设计文档的差异(重要)
|
||||
|
||||
我之前写的 `mothercup_硬件电路设计.md` 是基于 TP4056 + IO 直驱数码管的**简化方案**,与实际已有原理图**不一致**。实测实际设计更先进:
|
||||
|
||||
| 项 | 我的简化文档 | 实际原理图 |
|
||||
|---|---|---|
|
||||
| 充电 | TP4056(1A 线性) | BQ25792(升降压 + 电源路径 + I2C 可编程) |
|
||||
| 电池保护 | 无 | HY2120 + FS8205A |
|
||||
| 数码管 | 8 段+3 位选 11 个 IO 直驱 | TM1650(2 线 I2C,仅 PA0/PA1) |
|
||||
| 温度采样 | PB8 | PB10(ADC_NTC) |
|
||||
| 额外功能 | 无 | 风扇 PWM、VBUS/VBAT 采样、充电状态灯 |
|
||||
|
||||
**建议以实际原理图为准**,`mothercup_硬件电路设计.md` 仅作早期方案记录,引脚部分已被本报告取代。
|
||||
|
||||
---
|
||||
|
||||
## 5. 待确认/可选
|
||||
|
||||
1. **PA6、PB0、PB3** 三个脚在图中为“局部网络”(未挂全局网络名),其中 S1 按键一侧接 GND、另一侧经 10K 上拉接某一 IO——建议确认哪个脚是「下载模式输入」,以及 RESET 是否需要补一个复位按键(当前 RESET 只有 10K 上拉 + 100nF,无按键)。
|
||||
2. 如需,我可在原理图上补:复位按键、或给 `CHG_CE/下载按键` 加网络标签,便于固件对照。
|
||||
3. 需要的话我可以把 `mothercup_硬件电路设计.md` 的引脚分配章节更新为本报告的实测版本。
|
||||
277
hardware_design/mothercup_硬件电路设计.md
Normal file
@ -0,0 +1,277 @@
|
||||
# 母乳杯(MotherCup)制冷杯硬件电路设计
|
||||
|
||||
> 基于国民技术 N32WB031(QFN32)BLE SoC,参考 `nations-tec/N32WB031_STB_V1.3` 开发板原理图
|
||||
> 及《N32WB03x 系列芯片硬件设计指南 V1.4》《N32WB03X 系列用户手册 V1.5》。
|
||||
> 固件侧对齐 `mcm-ddc-ble`(USART1=PB6/PB7,BLE 低时钟用内部 LSI,**不使用外部 32.768K 晶振**)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求 → 方案映射
|
||||
|
||||
| # | 需求 | 实现方案 |
|
||||
|---|---|---|
|
||||
| 1 | 电源充电可控 | Type-C 5V → 高边 P-MOSFET 开关(GPIO 控制)→ TP4056 充电 → 锂电池;GPIO 可关断充电 |
|
||||
| 2 | 采样制冷片温度 | 10K NTC(B3950) + 10K 分压 → PB8(ADC 直接检测 0~3.6V) |
|
||||
| 3 | 制冷片 PWM 控制 | PB4 = TIM3_CH1 → N-MOSFET(低边)开关 TEC,PWM 调占空比 |
|
||||
| 4 | 留出 UART | PB6(USART1_TX)/PB7(USART1_RX),4 针排针引出(3V3/TX/RX/GND) |
|
||||
| 5 | 进入 Download 模式 IO | PB3 作 BOOT 拉低按键(复位时按住 → 引导程序进 download 模式) |
|
||||
| 6 | SWD 口 | PA4(SWDCLK,10K 下拉)/PA5(SWDIO,10K 上拉),4 针排针引出 |
|
||||
| 7 | 三个 7 段数码管(IO 控制) | 3 位共阴数码管,8 段 + 3 位选 = 11 个 IO 直接动态扫描驱动 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统框图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
USB Type-C(5V,VBUS) ──┤ D1(SS34) ────────────── VSYS(≈4.7V) │
|
||||
│ │ │ │
|
||||
│ └─ Q2 P-MOS ─ TP4056 ── BAT+ (锂电 3.7V) │
|
||||
│ ↑gate │ │
|
||||
│ CHG_EN(PB0)→Q3 N-MOS 控制 D2(SS34)──┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────┴─────┐ │
|
||||
│ │ VSYS ── LDO3.3V ── VCC_3V3 (MCU/外设) │
|
||||
│ │ VSYS ── TEC+ ──[TEC]── TEC- ── Q1 NMOS ─ GND │
|
||||
│ │ ↑gate=TEC_PWM(PB4) │
|
||||
└────────────────────────┴──────────────────────────────────────────┘
|
||||
|
||||
N32WB031:
|
||||
PA0/PA1 ── 32MHz 晶振(XO32MM/XO32MP)
|
||||
PA4/PA5 ── SWD
|
||||
PB6/PB7 ── USART1 TX/RX
|
||||
PB8 ── NTC 温度采样(ADC)
|
||||
PB4 ── TEC PWM(TIM3_CH1)
|
||||
PB0 ── CHG_EN(充电使能)
|
||||
PB3 ── BOOT 下载模式按键
|
||||
PA2/PA3/PA6/PB1/PB2/PB5/PA0/PA1 ── 8 条段选 SEG_A..DP
|
||||
PB9/PB10/PB11 ── 3 条位选 DIG1..3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. N32WB031 引脚分配(关键)
|
||||
|
||||
> 芯片引脚号见 STB 原理图 QFN32 封装(33 个焊盘,含底部 GND 大焊盘)。
|
||||
|
||||
### 3.1 固定/系统引脚
|
||||
|
||||
| 引脚 | 名称 | 电路连接 |
|
||||
|---|---|---|
|
||||
| 29 | XO32MM_OUT | 32MHz 晶振 Y2(8pF,±10ppm),C15=10pF |
|
||||
| 30 | XO32MP_IN | 32MHz 晶振 Y2,C16=12pF |
|
||||
| 22 | VCC | 3.3V;C13=1uF + C5=0.1uF(近脚)+ C4=10uF |
|
||||
| 28 | VCCRF | 直连 VCC(R3 去掉);C9=1uF |
|
||||
| 8 | VDD_FLASH | 仅退耦,不外接电源;C1=2.2uF + C2=0.1uF |
|
||||
| 24 | VDCDC | C6=1uF;与 23(SWITCH) 间串 L3=4.7uH 功率电感 |
|
||||
| 23 | SWITCH | 经 L3(4.7uH) 接 VDCDC(DCDC 供电模式) |
|
||||
| 27 | VDCDCRF | 直连 VDCDC;C38=1uF |
|
||||
| 26 | VDD_PA | RF 偏置;L1=1.6nH + C18=1uF + C10=47pF |
|
||||
| 25 | RFIOP | 天线匹配:C12=1.2pF/C8=1.2pF/L2=1nH/C14=10pF + ANT |
|
||||
| 33 | VSS | 底部大焊盘,9 过孔接地 |
|
||||
| 3 | RESET | 10K 上拉到 3.3V + 100nF 到 GND + 复位按键 |
|
||||
|
||||
### 3.2 功能引脚分配
|
||||
|
||||
| 引脚 | 功能 | 方向 | 复用/说明 |
|
||||
|---|---|---|---|
|
||||
| PA4 (5) | SWDCLK | — | **10K 下拉**(硬件指南要求) |
|
||||
| PA5 (6) | SWDIO | — | **10K 上拉**(硬件指南要求) |
|
||||
| PB6 (16) | USART1_TX | 输出 | AF4 |
|
||||
| PB7 (17) | USART1_RX | 输入 | AF4 |
|
||||
| PB8 (9) | NTC_ADC(制冷片温度) | 模拟输入 | ADC 直接检测,0~3.6V,360KΩ |
|
||||
| PB4 (14) | TEC_PWM | 输出 | TIM3_CH1(AF2),接 N-MOSFET 栅极 |
|
||||
| PB0 (4) | CHG_EN(充电使能) | 输出 | 高=充电使能,经 Q3 控制 Q2 P-MOS 栅极 |
|
||||
| PB3 (13) | BOOT_DL(下载模式按键) | 输入 | 按键到 GND,10K 上拉;复位时按住=进 download |
|
||||
| PA2 (1) | SEG_A | 输出 | 数码管段 A |
|
||||
| PA3 (2) | SEG_B | 输出 | 数码管段 B |
|
||||
| PA6 (7) | SEG_C | 输出 | 数码管段 C |
|
||||
| PB1 (11) | SEG_D | 输出 | 数码管段 D |
|
||||
| PB2 (12) | SEG_E | 输出 | 数码管段 E |
|
||||
| PB5 (15) | SEG_F | 输出 | 数码管段 F |
|
||||
| PA0 (31) | SEG_G | 输出 | 数码管段 G |
|
||||
| PA1 (32) | SEG_DP | 输出 | 小数点 DP |
|
||||
| PB9 (10) | DIG1(位选 1) | 输出 | 经 NPN 驱动数码管第 1 位阴极 |
|
||||
| PB10 (18) | DIG2(位选 2) | 输出 | 经 NPN 驱动数码管第 2 位阴极 |
|
||||
| PB11 (20) | DIG3(位选 3) | 输出 | 经 NPN 驱动数码管第 3 位阴极 |
|
||||
| PB12 (19) | 备用 | — | 预留(可作充电状态 LED / 蜂鸣器) |
|
||||
| PB13 (21) | 备用 | — | 预留 |
|
||||
|
||||
> 引脚占用:外设共 15 个 IO + SWD/UART 4 个 = 19;芯片 GPIO 共 21(PA0~PA6 7 个 + PB0~PB13 14 个),
|
||||
> 剩余 PB12、PB13 两个 IO 备用。PA0/PA1 为独立 GPIO(32MHz 晶振在专用 29/30 脚),可放心作段选。
|
||||
|
||||
---
|
||||
|
||||
## 4. 各功能模块电路
|
||||
|
||||
### 4.1 电源输入 + 充电(电源充电可控)
|
||||
|
||||
```
|
||||
VBUS(5V) ──┬── D1(SS34) ────────────► VSYS
|
||||
│
|
||||
└── Q2(AO3401 P-MOS) 源极
|
||||
│漏极
|
||||
├── TP4056 VCC(4) ──┬─ BAT(5) ──► BAT+ (锂电 3.7V)
|
||||
│ ├─ PROG(2) ── Rprog=1.2K → GND (≈1A 充电电流)
|
||||
│ ├─ TEMP(1) ── GND (不使用温度检测)
|
||||
│ ├─ CHRG(7) ── LED_RED (充电指示,可选)
|
||||
│ ├─ STDBY(6)── LED_GREEN(充满指示,可选)
|
||||
│ └─ CE(8) ── GND (常使能,使能由 Q2 控制)
|
||||
│
|
||||
Q2 栅极 ──┬── R_pu 10K → VBUS
|
||||
└── Q3(2N7002 N-MOS) 漏极
|
||||
│栅极 ← Rg 10K ← CHG_EN(PB0)
|
||||
│源极 → GND
|
||||
|
||||
BAT+ ── D2(SS34) ─────────────► VSYS (电池供电通路,与 VBUS 二选一/自动切换)
|
||||
|
||||
VSYS ── C_bulk 100uF + C 100nF → GND
|
||||
VSYS ── U_LDO(ME6211C33) ──► VCC_3V3 ── C 1uF + C 100nF → GND
|
||||
```
|
||||
|
||||
要点:
|
||||
- **充电可控**:`CHG_EN` 高电平 → Q3 导通 → Q2(P-MOS) 栅极拉低 → Q2 导通 → 充电开启;
|
||||
低电平 → Q2 关断 → 切断充电(可在大电流制冷时关断充电,保护弱 USB 电源)。
|
||||
- **D1/D2 肖特基 OR-ing**:插 USB 时系统走 5V(≈4.7V),拔掉后自动切到电池(≈3.4V),无切换死区。
|
||||
- TP4056 为 1A 线性锂电充电器;充电电流 `I = 1000 / Rprog`,Rprog=1.2K ≈ 0.83A(保守,避免与 TEC 抢电)。
|
||||
- VCC_3V3 用低压差 LDO(ME6211C33,dropout≈40mV@100mA);电池低压到 3.2V 时 MCU 仍可工作(N32WB031 VCC 1.8~3.6V)。
|
||||
|
||||
### 4.2 制冷片 TEC PWM 控制
|
||||
|
||||
```
|
||||
VSYS ── C1 100uF ── GND
|
||||
VSYS ──┬─────────────► TEC+ (制冷片正极)
|
||||
│
|
||||
└── D_fw(SS34) 阴极
|
||||
│阳极 ─────────► TEC- (制冷片负极)
|
||||
TEC- ──┬── Q1(AO3400/IRLML6344 N-MOS) 漏极
|
||||
│ │栅极 ← Rg 22Ω ← TEC_PWM(PB4)
|
||||
│ │栅极 ── R_pd 10K → GND
|
||||
│ │源极 → GND
|
||||
└── C2 100uF(可选,平滑 PWM) ── GND
|
||||
```
|
||||
|
||||
要点:
|
||||
- **低边 N-MOS 开关**:PB4 输出 PWM(TIM3_CH1),占空比 0~100% 调节制冷功率。
|
||||
- Q1 需耐压≥20V、导通电阻低、栅极阈值≤2V(3.3V 逻辑直接驱动)。AO3400/IRLML6244/SI2300 均可,按 TEC 峰值电流(1~3A)选型。
|
||||
- **D_fw 续流肖特基**(SS34)跨 TEC,泄放导线/接线电感反峰,保护 MOS。
|
||||
- C2(100uF)可选:平滑 PWM 纹波、降低 TEC 热冲击(推荐加,PWM 频率 10~50kHz)。
|
||||
- 若需更平滑直流(延长 TEC 寿命),可在 Q1 与 TEC 间串 10~22uH 电感 + 大电容做 LC 滤波,代价是体积。
|
||||
|
||||
### 4.3 制冷片温度采样(NTC)
|
||||
|
||||
```
|
||||
VCC_3V3 ── R_ref 10K(1%) ──┬──► PB8(NTC_ADC)
|
||||
│
|
||||
NTC 10K B3950
|
||||
│
|
||||
GND
|
||||
PB8 ── C_filt 100nF ── GND (抗噪)
|
||||
```
|
||||
|
||||
要点:
|
||||
- 分压点电压 `V = 3.3 × R_NTC / (R_ref + R_NTC)`。10K/10K 在 25℃ 时为 1.65V,全温区落在 0~3.6V,配合 PB8 直接检测(输入阻抗 360KΩ,分压源内阻 ~5KΩ,无精度损失)。
|
||||
- NTC 推荐 **10K B3950**(如 NCP18XH103F03RB / 常规 10K 3950 玻璃封装,贴制冷片铝板或冷面)。
|
||||
- 固件用查表(Steinhart-Hart 或 B 值公式)换算温度;`app_info.c` 现有内部温度/电压 ADC 逻辑可复用,新增 PB8 单端采样通道。
|
||||
|
||||
### 4.4 三数码管显示(IO 直接动态扫描)
|
||||
|
||||
```
|
||||
8 条段选(MCU 输出,各串 330Ω 限流):
|
||||
SEG_A(PA2) ── 330Ω ── DS1/DS2/DS3 的 A 阳极并联
|
||||
SEG_B(PA3) ── 330Ω ── ... B
|
||||
SEG_C(PA6) ── 330Ω ── ... C
|
||||
SEG_D(PB1) ── 330Ω ── ... D
|
||||
SEG_E(PB2) ── 330Ω ── ... E
|
||||
SEG_F(PB5) ── 330Ω ── ... F
|
||||
SEG_G(PA0) ── 330Ω ── ... G
|
||||
SEG_DP(PA1)── 330Ω ── ... DP
|
||||
|
||||
3 条位选(MCU 输出 → NPN 基极 → 共阴阴极到地):
|
||||
DIG1(PB9) ── 1K ── Q_d1(MMBT3904) 基极,集电极→DS1 公共阴极,发射极→GND
|
||||
DIG2(PB10)── 1K ── Q_d2 ... → DS2 公共阴极
|
||||
DIG3(PB11)── 1K ── Q_d3 ... → DS3 公共阴极
|
||||
```
|
||||
|
||||
要点:
|
||||
- **共阴数码管**(3 位,如 0.36/0.56 英寸红色共阴)。段选由 MCU 直接拉高(每段 ~4mA),位选经 NPN(MMBT3904)灌电流(最多 8 段×4mA≈32mA,NPN 足够)。
|
||||
- 动态扫描:每位点亮 1/3 周期(如 2~3ms),三位轮询,刷新率 ≥100Hz 无闪烁。
|
||||
- 显示内容例:温度 `37.5`(第 2 位点 DP),或 `25`℃。段码表由固件按 `SEG_A..DP` 位序生成。
|
||||
- 若希望少占 IO,可改用 74HC595(3 线)或 TM1650(2 线 I2C)驱动,但用户要求「IO 管脚控制」,本方案用 11 个 IO 直驱。
|
||||
|
||||
### 4.5 UART / SWD / Download 模式接口
|
||||
|
||||
- **UART(4P 排针)**:`3V3 | PB6_TX | PB7_RX | GND`,460800 8N1(与固件一致)。外部用 3.3V TTL USB-UART 小板。
|
||||
- **SWD(4P 排针)**:`3V3 | PA4_SWDCLK | PA5_SWDIO | GND`;板上 PA4 加 10K 下拉、PA5 加 10K 上拉。
|
||||
- **Download 模式(按键)**:`PB3` 串 10K 上拉到 3.3V,按键一端接 PB3、另一端接 GND。
|
||||
- 复位/上电时按住该键(PB3=低),Bootloader 检测后进入 download 模式(UART 升级)。
|
||||
- **需固件配合**:`Boot/src` 引导代码增加「复位后读 PB3,低电平则进入 download」分支(当前 Boot V1.01.00 仅靠 USART1 2s 回车窗口 / 无有效镜像进入,需补这一条 GPIO 判定)。
|
||||
|
||||
---
|
||||
|
||||
## 5. BOM(关键器件)
|
||||
|
||||
| 位号 | 器件 | 参数/型号 | 封装 | 数量 |
|
||||
|---|---|---|---|---|
|
||||
| U1 | N32WB031 | BLE SoC | QFN32 | 1 |
|
||||
| U2 | TP4056 | 1A 锂电充电 IC | SOP8/ESOP8 | 1 |
|
||||
| U3 | ME6211C33 | 3.3V 低压差 LDO | SOT23-5 | 1 |
|
||||
| Y2 | 32MHz 晶振 | 8pF, ±10ppm | 1612(4P) | 1 |
|
||||
| L3 | 4.7uH 功率电感 | RDC≤250mΩ, 800mA | 0805 | 1 |
|
||||
| L1 | 1.6nH 高频电感 | SDCL1005C1N6STDF | 0402 | 1 |
|
||||
| L2 | 1nH 高频电感 | RF 匹配 | 0402 | 1 |
|
||||
| Q1 | N-MOS | 制冷片开关 AO3400/IRLML6244 | SOT23 | 1 |
|
||||
| Q2 | P-MOS | 充电开关 AO3401 | SOT23 | 1 |
|
||||
| Q3 | N-MOS | 充电开关驱动 2N7002 | SOT23 | 1 |
|
||||
| Q_d1..3 | NPN | 位选驱动 MMBT3904 | SOT23 | 3 |
|
||||
| D1,D2,D_fw | 肖特基 | SS34(3A/40V) | SMA | 3 |
|
||||
| NTC | NTC 热敏 | 10K B3950 1% | 0603/玻璃 | 1 |
|
||||
| R_ref | 电阻 | 10K 1% | 0402 | 1 |
|
||||
| Rprog | 电阻 | 1.2K 1% | 0402 | 1 |
|
||||
| 段限流 | 电阻 | 330Ω | 0402 | 8 |
|
||||
| 位选基极 | 电阻 | 1K | 0402 | 3 |
|
||||
| 栅极/上拉 | 电阻 | 10K / 22Ω / 1K 若干 | 0402 | ~10 |
|
||||
| DS1..3 | 数码管 | 3 位共阴红色 | 0.36/0.56" | 1(3位) |
|
||||
| J | 排针 | 2×4P UART+SWD、电池、TEC、NTC | 2.54mm | 若干 |
|
||||
| 电容 | 退耦/滤波 | 见下表 | 0402/0603/0805 | 若干 |
|
||||
|
||||
退耦电容(按硬件指南最小系统):1uF×4、0.1uF×12、2.2uF×1、4.7uF×1、10uF×5、10pF×2、12pF×2、47pF×1;另加电源 100uF 电解×1、100nF×若干。
|
||||
|
||||
---
|
||||
|
||||
## 6. 固件兼容性说明(重要)
|
||||
|
||||
| 项 | 现状 | 需要的改动 |
|
||||
|---|---|---|
|
||||
| UART | 已是 PB6/PB7 (AF4) | 无 |
|
||||
| SWD | PA4/PA5 | 无(注意 PA4 下拉/PA5 上拉硬件接法) |
|
||||
| 32.768K 晶振 | 固件用内部 LSI,未用 LSE | **本设计不焊 32.768K 晶振**,PB8/PB9 复用为 ADC/位选 |
|
||||
| 制冷片温度 | 固件现用内部温度/电压 ADC | 新增 PB8 单端 NTC 采样 + 查表换算(`app_info.c` 扩展) |
|
||||
| TEC PWM | 无 | 新增 TIM3_CH1(PB4) PWM 输出 + 占空比控制命令(`appset` 增项) |
|
||||
| 充电使能 | 无 | 新增 PB0 GPIO 输出控制 CHG_EN |
|
||||
| 下载模式按键 | Boot 仅 USART 窗口/无镜像进入 | `Boot/src` 增 PB3 读引脚判定进入 download |
|
||||
| 数码管 | 无 | 新增定时器中断动态扫描 + 段码表(或复用 LED 任务) |
|
||||
|
||||
---
|
||||
|
||||
## 7. PCB Layout 要点(摘自硬件指南 V1.4)
|
||||
|
||||
- **RFIOP(25)**:走线短而直、50Ω、四周包地、下方不走线;天线周边 ≥3mm 净空;天线口建议加 TVS 防静电。
|
||||
- **32MHz 晶振(29/30)**:两条走线短且等长、四周包地;下方(尤其 VDCDCRF 走线)不走线;远离大电流/高速线。
|
||||
- **芯片 GND**:底部大焊盘,中心 0.6/0.4mm + 四周 8×0.5/0.3mm 共 9 个过孔接地。
|
||||
- **电源**:走线粗短,小电容近芯片脚;VDCDC(24) 与 SWITCH(23) 间的 4.7uH 电感贴近放置。
|
||||
- **TEC 大电流**:VSYS/TEC-/MOS 走线加宽(1A 每 1mm 线宽经验值),MOS 与肖特基尽量贴近 TEC 端子。
|
||||
|
||||
---
|
||||
|
||||
## 8. 待确认/可选
|
||||
|
||||
1. **电池规格**(容量/尺寸:软包 or 18650)→ 决定 TP4056 充电电流与结构。
|
||||
2. **TEC 规格**(TEC1-03103 等)→ 决定 Q1 选型与是否加 LC 平滑。
|
||||
3. **数码管尺寸/颜色**(0.28/0.36/0.56 英寸、红/白)→ 决定限流电阻与结构开窗。
|
||||
4. 是否显示 **°C 符号/小数点**(本设计已预留 DP 段)。
|
||||
5. 是否需要 **充电状态灯**(可占用备用 PB12/PB13 + TP4056 CHRG/STDBY)。
|
||||
|
||||
---
|
||||
|
||||
*文档生成:基于 N32WB031 STB V1.3 原理图 + 硬件设计指南 V1.4 + 用户手册 V1.5(引脚 AF 表/ADC 通道/时钟)。*
|
||||
144
hardware_design/开关机电路设计方案.md
Normal file
@ -0,0 +1,144 @@
|
||||
# 软开关机电路设计方案(长按 2 秒开机 + 自动关机)
|
||||
|
||||
> 适用于 MotherCup 整机:电池供电,按键软开关,防误触开机,无操作自动断电。
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求
|
||||
|
||||
| 项 | 要求 |
|
||||
|---|---|
|
||||
| 开机 | **长按按键 2 秒**才开机(防误触,短按不开机) |
|
||||
| 关机 | **自动关机**:无操作超时后自动断电(固件定时,超时值可配) |
|
||||
| 掉电后 | 整机彻底断电(VSYS=0),待机电流仅 P-MOS 漏电流(<1µA 级) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 电路设计
|
||||
|
||||
```
|
||||
VBAT(电池 3.0~4.2V)
|
||||
│
|
||||
│ S
|
||||
Q1 ─────┤ P-MOS (AO3401)
|
||||
│ D
|
||||
├─────────────────────── VSYS(整机电源,去 LDO/升压/制冷片)
|
||||
│ G(栅极节点 G)
|
||||
│
|
||||
R2 100K ─────────┼────┬──────────────────┬─────────────────────┐
|
||||
(接 VBAT) │ │ │ │
|
||||
│ │D │D │
|
||||
│ Q2(N-MOS 2N7002) Q3(N-MOS 2N7002) │
|
||||
│ │G │G │
|
||||
│ │ │ │
|
||||
│ ├─ R3 10K ─┐ ├── C1 3.3µF ── GND │
|
||||
│ │ │ │ │
|
||||
│ ├─ R5 100K ┴ GND ├── R4 10M ── GND │
|
||||
│ │ (PWR_HOLD 下拉,MCU 掉电时确保 Q2 关) │
|
||||
│ │ │ │
|
||||
│ └──── PWR_HOLD (MCU 输出) │
|
||||
│ R1 1M │
|
||||
│ │ │
|
||||
│ SW1 ──── VBAT │
|
||||
│ │ │
|
||||
│ (可选) KEY_DET (MCU 输入) │
|
||||
│ Q2/Q3 源极 ── GND Q2/Q3 源极 ── GND
|
||||
```
|
||||
|
||||
> 说明:Q2 的漏极与 Q3 的漏极都并到栅极节点 G,任一路导通都能把 Q1 栅极拉低。
|
||||
|
||||
---
|
||||
|
||||
## 3. 工作原理
|
||||
|
||||
### 3.1 开机(长按 2 秒)
|
||||
|
||||
1. 初始:`PWR_HOLD` 悬空(MCU 未上电),R5 把 Q2 栅极拉低 → Q2 关断;
|
||||
R2 把 Q1 栅极拉高到 VBAT → Q1 关断 → VSYS=0,整机断电。
|
||||
2. 按住 SW1:VBAT 经 SW1 → R1 给 C1 充电(τ = R1×C1 ≈ 3.3s)。
|
||||
3. 约 **2.1 秒**后 C1 电压达到 Q3(2N7002) 阈值(~2V) → Q3 导通 → 拉低 Q1 栅极 → Q1 导通 → VSYS 得电。
|
||||
4. MCU 上电启动,**第一时间置 `PWR_HOLD=高`** → Q2 导通 → 持续拉低 Q1 栅极(锁存开机)。
|
||||
5. 松开 SW1:C1 经 R4 缓慢泄放,Q3 逐渐关断,但 Q2 已锁存,不影响供电。
|
||||
6. 若按的时间不足 2 秒就松手:C1 没充到阈值,Q3 不导通,Q1 不开机(防误触)。
|
||||
|
||||
### 3.2 自动关机
|
||||
|
||||
1. MCU 运行中检测「无操作」超时(BLE 无指令 / 按键无动作,超时值可配,见 §6)。
|
||||
2. 超时到 → 置 `PWR_HOLD=低` → Q2 关断 → R2 把 Q1 栅极拉高 → Q1 关断 → VSYS=0 → 整机断电。
|
||||
|
||||
---
|
||||
|
||||
## 4. 器件清单(BOM)
|
||||
|
||||
| 位号 | 器件 | 参数/型号 | 封装 | 数量 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| Q1 | P-MOSFET | AO3401(-30V/-4A,低压开启) | SOT23 | 1 | 高边主开关 |
|
||||
| Q2 | N-MOSFET | 2N7002(Vth≈2V) | SOT23 | 1 | PWR_HOLD 锁存 |
|
||||
| Q3 | N-MOSFET | 2N7002(Vth≈2V) | SOT23 | 1 | 长按开机触发 |
|
||||
| R1 | 电阻 | 1MΩ ±5% | 0402 | 1 | C1 充电电阻(定 2s 延时) |
|
||||
| R2 | 电阻 | 100KΩ | 0402 | 1 | Q1 栅极上拉(默认关机) |
|
||||
| R3 | 电阻 | 10KΩ | 0402 | 1 | PWR_HOLD → Q2 栅极 |
|
||||
| R4 | 电阻 | 10MΩ | 0402 | 1 | C1 泄放 |
|
||||
| R5 | 电阻 | 100KΩ | 0402 | 1 | Q2 栅极下拉(MCU 掉电时确保 Q2 关) |
|
||||
| C1 | 电容 | 3.3µF / 10V X7R | 0603 | 1 | 延时电容 |
|
||||
| SW1 | 轻触按键 | 6×6 侧按/顶按 | — | 1 | 电源键 |
|
||||
| (可选) D1 | 二极管 | BAT54 | SOT23 | 1 | 如 KEY_DET 需隔离时用 |
|
||||
|
||||
---
|
||||
|
||||
## 5. RC 延时计算
|
||||
|
||||
- 充电:`Vc(t) = VBAT × (1 − e^(−t/RC))`,RC = R1×C1 = 1M×3.3µF = 3.3s。
|
||||
- 2N7002 阈值约 2V,取 VBAT=4.2V:到达 2V 的时间
|
||||
`t = −RC·ln(1 − 2/4.2) = −3.3·ln(0.524) ≈ 2.13s` ✓(约 2 秒)。
|
||||
- 若需更精确 2.0s,可调 R1=910K 或 C1=3.0µF;**最终以实测为准微调 R1/C1**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 固件配合
|
||||
|
||||
### 6.1 GPIO
|
||||
|
||||
| 引脚 | 方向 | 说明 |
|
||||
|---|---|---|
|
||||
| `PWR_HOLD` | 输出 | **`main()` 最开头就置高**(先于 banner/初始化),锁存开机;自动关机时置低 |
|
||||
| `KEY_DET`(可选) | 输入 | 接 C1 节点/按键节点,运行中检测按键,用于复位自动关机定时器 |
|
||||
|
||||
> 注意:`PWR_HOLD` 必须在 MCU 复位后**立即**拉高,否则用户一松键就掉电。
|
||||
|
||||
### 6.2 自动关机定时(伪代码)
|
||||
|
||||
```c
|
||||
// 空闲超时值(可做成 APP_DATA 参数,appset 配置)
|
||||
#define AUTO_OFF_TIMEOUT_MS 120000 /* 默认 2 分钟,可配 */
|
||||
|
||||
void activity_heartbeat(void) { // 任何活动(BLE 指令/按键)都调
|
||||
s_last_activity = xTaskGetTickCount();
|
||||
}
|
||||
|
||||
// 在某个常驻任务里(如 BLE 调度任务)周期性检查:
|
||||
if ((xTaskGetTickCount() - s_last_activity) >= pdMS_TO_TICKS(AUTO_OFF_TIMEOUT_MS)) {
|
||||
display_set(0, DISP_TYPE_CELSIUS, DISP_BASE_DECIMAL); // 可先清屏/提示
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
PWR_HOLD_LOW(); // 断电
|
||||
for(;;) {} // 等电源跌落
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. **开机时序**:`PWR_HOLD` 必须在 MCU 启动后立即置高,且 R5 保证 MCU 掉电时 Q2 可靠关断。
|
||||
2. **C1 泄放**:R4 取 10M 时泄放 τ≈33s;若担心「短按后快速再长按」会把上次残余电荷算进去,可把 R4 调小(如 4.7M)或加受控泄放。
|
||||
3. **待机功耗**:关机时只有 R2(100K→VBAT) 与 P-MOS 漏电流,约 0.04µA + 漏电流,可满足低待机要求。
|
||||
4. **VBAT 下限**:Q3(2N7002) 阈值 2V,电池低到 ~2.5V 时 C1 充不到阈值会导致开不了机;若需支持更低电压,换低压开启的 N-MOS(如 SI2302,Vth<1V)并重算 RC。
|
||||
5. **栅极保护**:Q1 栅极如需加 TVS/钳位(防静电/浪涌),可在 G 节点对 VBAT 加 4.7~6.8V 稳压管(可选)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 待确认
|
||||
|
||||
1. **自动关机超时值**:本文默认 2 分钟(`AUTO_OFF_TIMEOUT_MS`),你之前写的是「2 秒」——请确认用多少。
|
||||
2. 是否需要 **KEY_DET**(按键复位自动关机定时 / 手动关机)。
|
||||
3. 电池最低电压(决定 Q3 选型与 RC 是否需重算)。
|
||||
BIN
hardware_design/数码屏YF30137.dwg
Normal file
@ -194,6 +194,147 @@
|
||||
</TargetOption>
|
||||
</Target>
|
||||
|
||||
<Target>
|
||||
<TargetName>APP2</TargetName>
|
||||
<ToolsetNumber>0x4</ToolsetNumber>
|
||||
<ToolsetName>ARM-ADS</ToolsetName>
|
||||
<TargetOption>
|
||||
<CLKADS>64000000</CLKADS>
|
||||
<OPTTT>
|
||||
<gFlags>0</gFlags>
|
||||
<BeepAtEnd>1</BeepAtEnd>
|
||||
<RunSim>1</RunSim>
|
||||
<RunTarget>0</RunTarget>
|
||||
<RunAbUc>0</RunAbUc>
|
||||
</OPTTT>
|
||||
<OPTHX>
|
||||
<HexSelection>1</HexSelection>
|
||||
<FlashByte>65535</FlashByte>
|
||||
<HexRangeLowAddress>0</HexRangeLowAddress>
|
||||
<HexRangeHighAddress>0</HexRangeHighAddress>
|
||||
<HexOffset>0</HexOffset>
|
||||
</OPTHX>
|
||||
<OPTLEX>
|
||||
<PageWidth>79</PageWidth>
|
||||
<PageLength>66</PageLength>
|
||||
<TabStop>8</TabStop>
|
||||
<ListingPath>.\Listings-app2\</ListingPath>
|
||||
</OPTLEX>
|
||||
<ListingPage>
|
||||
<CreateCListing>1</CreateCListing>
|
||||
<CreateAListing>1</CreateAListing>
|
||||
<CreateLListing>1</CreateLListing>
|
||||
<CreateIListing>0</CreateIListing>
|
||||
<AsmCond>1</AsmCond>
|
||||
<AsmSymb>1</AsmSymb>
|
||||
<AsmXref>0</AsmXref>
|
||||
<CCond>1</CCond>
|
||||
<CCode>0</CCode>
|
||||
<CListInc>0</CListInc>
|
||||
<CSymb>0</CSymb>
|
||||
<LinkerCodeListing>0</LinkerCodeListing>
|
||||
</ListingPage>
|
||||
<OPTXL>
|
||||
<LMap>1</LMap>
|
||||
<LComments>1</LComments>
|
||||
<LGenerateSymbols>1</LGenerateSymbols>
|
||||
<LLibSym>1</LLibSym>
|
||||
<LLines>1</LLines>
|
||||
<LLocSym>1</LLocSym>
|
||||
<LPubSym>1</LPubSym>
|
||||
<LXref>0</LXref>
|
||||
<LExpSel>0</LExpSel>
|
||||
</OPTXL>
|
||||
<OPTFL>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<IsCurrentTarget>0</IsCurrentTarget>
|
||||
</OPTFL>
|
||||
<CpuCode>255</CpuCode>
|
||||
<DebugOpt>
|
||||
<uSim>1</uSim>
|
||||
<uTrg>0</uTrg>
|
||||
<sLdApp>1</sLdApp>
|
||||
<sGomain>1</sGomain>
|
||||
<sRbreak>1</sRbreak>
|
||||
<sRwatch>1</sRwatch>
|
||||
<sRmem>1</sRmem>
|
||||
<sRfunc>1</sRfunc>
|
||||
<sRbox>1</sRbox>
|
||||
<tLdApp>1</tLdApp>
|
||||
<tGomain>0</tGomain>
|
||||
<tRbreak>1</tRbreak>
|
||||
<tRwatch>1</tRwatch>
|
||||
<tRmem>1</tRmem>
|
||||
<tRfunc>0</tRfunc>
|
||||
<tRbox>1</tRbox>
|
||||
<tRtrace>1</tRtrace>
|
||||
<sRSysVw>1</sRSysVw>
|
||||
<tRSysVw>1</tRSysVw>
|
||||
<sRunDeb>0</sRunDeb>
|
||||
<sLrtime>0</sLrtime>
|
||||
<bEvRecOn>1</bEvRecOn>
|
||||
<bSchkAxf>0</bSchkAxf>
|
||||
<bTchkAxf>0</bTchkAxf>
|
||||
<nTsel>-1</nTsel>
|
||||
<sDll></sDll>
|
||||
<sDllPa></sDllPa>
|
||||
<sDlgDll></sDlgDll>
|
||||
<sDlgPa></sDlgPa>
|
||||
<sIfile></sIfile>
|
||||
<tDll></tDll>
|
||||
<tDllPa></tDllPa>
|
||||
<tDlgDll></tDlgDll>
|
||||
<tDlgPa></tDlgPa>
|
||||
<tIfile></tIfile>
|
||||
<pMon></pMon>
|
||||
</DebugOpt>
|
||||
<Breakpoint/>
|
||||
<Tracepoint>
|
||||
<THDelay>0</THDelay>
|
||||
</Tracepoint>
|
||||
<DebugFlag>
|
||||
<trace>0</trace>
|
||||
<periodic>0</periodic>
|
||||
<aLwin>0</aLwin>
|
||||
<aCover>0</aCover>
|
||||
<aSer1>0</aSer1>
|
||||
<aSer2>0</aSer2>
|
||||
<aPa>0</aPa>
|
||||
<viewmode>0</viewmode>
|
||||
<vrSel>0</vrSel>
|
||||
<aSym>0</aSym>
|
||||
<aTbox>0</aTbox>
|
||||
<AscS1>0</AscS1>
|
||||
<AscS2>0</AscS2>
|
||||
<AscS3>0</AscS3>
|
||||
<aSer3>0</aSer3>
|
||||
<eProf>0</eProf>
|
||||
<aLa>0</aLa>
|
||||
<aPa1>0</aPa1>
|
||||
<AscS4>0</AscS4>
|
||||
<aSer4>0</aSer4>
|
||||
<StkLoc>0</StkLoc>
|
||||
<TrcWin>0</TrcWin>
|
||||
<newCpu>0</newCpu>
|
||||
<uProt>0</uProt>
|
||||
</DebugFlag>
|
||||
<LintExecutable></LintExecutable>
|
||||
<LintConfigFile></LintConfigFile>
|
||||
<bLintAuto>0</bLintAuto>
|
||||
<bAutoGenD>0</bAutoGenD>
|
||||
<LntExFlags>0</LntExFlags>
|
||||
<pMisraName></pMisraName>
|
||||
<pszMrule></pszMrule>
|
||||
<pSingCmds></pSingCmds>
|
||||
<pMultCmds></pMultCmds>
|
||||
<pMisraNamep></pMisraNamep>
|
||||
<pszMrulep></pszMrulep>
|
||||
<pSingCmdsp></pSingCmdsp>
|
||||
<pMultCmdsp></pMultCmdsp>
|
||||
</TargetOption>
|
||||
</Target>
|
||||
|
||||
<Target>
|
||||
<TargetName>OTA_IMG_1</TargetName>
|
||||
<ToolsetNumber>0x4</ToolsetNumber>
|
||||
@ -478,7 +619,7 @@
|
||||
|
||||
<Group>
|
||||
<GroupName>STARTUP</GroupName>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExp>1</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<cbSel>0</cbSel>
|
||||
<RteFlg>0</RteFlg>
|
||||
@ -666,6 +807,18 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>3</GroupNumber>
|
||||
<FileNumber>15</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</PathWithFileName>
|
||||
<FilenameWithoutPath>n32wb03x_iwdg.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
@ -676,7 +829,7 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>4</GroupNumber>
|
||||
<FileNumber>15</FileNumber>
|
||||
<FileNumber>16</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -688,7 +841,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>4</GroupNumber>
|
||||
<FileNumber>16</FileNumber>
|
||||
<FileNumber>17</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -700,7 +853,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>4</GroupNumber>
|
||||
<FileNumber>17</FileNumber>
|
||||
<FileNumber>18</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -720,7 +873,7 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>18</FileNumber>
|
||||
<FileNumber>19</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -732,7 +885,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>19</FileNumber>
|
||||
<FileNumber>20</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -744,7 +897,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>20</FileNumber>
|
||||
<FileNumber>21</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -756,7 +909,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>21</FileNumber>
|
||||
<FileNumber>22</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -768,7 +921,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>22</FileNumber>
|
||||
<FileNumber>23</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -780,7 +933,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>23</FileNumber>
|
||||
<FileNumber>24</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -792,7 +945,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>24</FileNumber>
|
||||
<FileNumber>25</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -804,7 +957,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>25</FileNumber>
|
||||
<FileNumber>26</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -816,7 +969,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>26</FileNumber>
|
||||
<FileNumber>27</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -828,7 +981,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>27</FileNumber>
|
||||
<FileNumber>28</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -840,7 +993,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>5</GroupNumber>
|
||||
<FileNumber>28</FileNumber>
|
||||
<FileNumber>29</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -860,7 +1013,7 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>6</GroupNumber>
|
||||
<FileNumber>29</FileNumber>
|
||||
<FileNumber>30</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -872,7 +1025,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>6</GroupNumber>
|
||||
<FileNumber>30</FileNumber>
|
||||
<FileNumber>31</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -884,7 +1037,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>6</GroupNumber>
|
||||
<FileNumber>31</FileNumber>
|
||||
<FileNumber>32</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -896,7 +1049,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>6</GroupNumber>
|
||||
<FileNumber>32</FileNumber>
|
||||
<FileNumber>33</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -908,7 +1061,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>6</GroupNumber>
|
||||
<FileNumber>33</FileNumber>
|
||||
<FileNumber>34</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -928,7 +1081,7 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>7</GroupNumber>
|
||||
<FileNumber>34</FileNumber>
|
||||
<FileNumber>35</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -940,7 +1093,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>7</GroupNumber>
|
||||
<FileNumber>35</FileNumber>
|
||||
<FileNumber>36</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -952,7 +1105,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>7</GroupNumber>
|
||||
<FileNumber>36</FileNumber>
|
||||
<FileNumber>37</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -972,7 +1125,7 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>8</GroupNumber>
|
||||
<FileNumber>37</FileNumber>
|
||||
<FileNumber>38</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -984,7 +1137,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>8</GroupNumber>
|
||||
<FileNumber>38</FileNumber>
|
||||
<FileNumber>39</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -996,7 +1149,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>8</GroupNumber>
|
||||
<FileNumber>39</FileNumber>
|
||||
<FileNumber>40</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1008,7 +1161,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>8</GroupNumber>
|
||||
<FileNumber>40</FileNumber>
|
||||
<FileNumber>41</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1020,7 +1173,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>8</GroupNumber>
|
||||
<FileNumber>41</FileNumber>
|
||||
<FileNumber>42</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1032,7 +1185,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>8</GroupNumber>
|
||||
<FileNumber>42</FileNumber>
|
||||
<FileNumber>43</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1052,7 +1205,7 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>9</GroupNumber>
|
||||
<FileNumber>43</FileNumber>
|
||||
<FileNumber>44</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1064,7 +1217,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>9</GroupNumber>
|
||||
<FileNumber>44</FileNumber>
|
||||
<FileNumber>45</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1076,7 +1229,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>9</GroupNumber>
|
||||
<FileNumber>45</FileNumber>
|
||||
<FileNumber>46</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1088,7 +1241,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>9</GroupNumber>
|
||||
<FileNumber>46</FileNumber>
|
||||
<FileNumber>47</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1100,7 +1253,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>9</GroupNumber>
|
||||
<FileNumber>47</FileNumber>
|
||||
<FileNumber>48</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1120,7 +1273,7 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>48</FileNumber>
|
||||
<FileNumber>49</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1132,7 +1285,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>49</FileNumber>
|
||||
<FileNumber>50</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1144,7 +1297,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>50</FileNumber>
|
||||
<FileNumber>51</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1156,7 +1309,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>51</FileNumber>
|
||||
<FileNumber>52</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1168,7 +1321,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>52</FileNumber>
|
||||
<FileNumber>53</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1180,7 +1333,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>53</FileNumber>
|
||||
<FileNumber>54</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1192,7 +1345,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>54</FileNumber>
|
||||
<FileNumber>55</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1204,7 +1357,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>55</FileNumber>
|
||||
<FileNumber>56</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1216,7 +1369,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>56</FileNumber>
|
||||
<FileNumber>57</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1228,7 +1381,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>57</FileNumber>
|
||||
<FileNumber>58</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1240,7 +1393,7 @@
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>58</FileNumber>
|
||||
<FileNumber>59</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1250,17 +1403,85 @@
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>60</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\src\app_display.c</PathWithFileName>
|
||||
<FilenameWithoutPath>app_display.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>61</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\src\app_params.c</PathWithFileName>
|
||||
<FilenameWithoutPath>app_params.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>10</GroupNumber>
|
||||
<FileNumber>62</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\src\app_bootset.c</PathWithFileName>
|
||||
<FilenameWithoutPath>app_bootset.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<GroupName>CONFIG</GroupName>
|
||||
<tvExp>1</tvExp>
|
||||
<GroupName>infra</GroupName>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<cbSel>0</cbSel>
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>11</GroupNumber>
|
||||
<FileNumber>59</FileNumber>
|
||||
<FileNumber>63</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\src\infra\cJSON.c</PathWithFileName>
|
||||
<FilenameWithoutPath>cJSON.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>11</GroupNumber>
|
||||
<FileNumber>64</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<bDave2>0</bDave2>
|
||||
<PathWithFileName>..\src\infra\infra_cjson.c</PathWithFileName>
|
||||
<FilenameWithoutPath>infra_cjson.c</FilenameWithoutPath>
|
||||
<RteFlg>0</RteFlg>
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<GroupName>CONFIG</GroupName>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<cbSel>0</cbSel>
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>12</GroupNumber>
|
||||
<FileNumber>65</FileNumber>
|
||||
<FileType>5</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1274,13 +1495,13 @@
|
||||
|
||||
<Group>
|
||||
<GroupName>DOC</GroupName>
|
||||
<tvExp>1</tvExp>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
<cbSel>0</cbSel>
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>12</GroupNumber>
|
||||
<FileNumber>60</FileNumber>
|
||||
<GroupNumber>13</GroupNumber>
|
||||
<FileNumber>66</FileNumber>
|
||||
<FileType>5</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1299,8 +1520,8 @@
|
||||
<cbSel>0</cbSel>
|
||||
<RteFlg>0</RteFlg>
|
||||
<File>
|
||||
<GroupNumber>13</GroupNumber>
|
||||
<FileNumber>61</FileNumber>
|
||||
<GroupNumber>14</GroupNumber>
|
||||
<FileNumber>67</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1311,8 +1532,8 @@
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>13</GroupNumber>
|
||||
<FileNumber>62</FileNumber>
|
||||
<GroupNumber>14</GroupNumber>
|
||||
<FileNumber>68</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1323,8 +1544,8 @@
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>13</GroupNumber>
|
||||
<FileNumber>63</FileNumber>
|
||||
<GroupNumber>14</GroupNumber>
|
||||
<FileNumber>69</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1335,8 +1556,8 @@
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>13</GroupNumber>
|
||||
<FileNumber>64</FileNumber>
|
||||
<GroupNumber>14</GroupNumber>
|
||||
<FileNumber>70</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1347,8 +1568,8 @@
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>13</GroupNumber>
|
||||
<FileNumber>65</FileNumber>
|
||||
<GroupNumber>14</GroupNumber>
|
||||
<FileNumber>71</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
@ -1359,8 +1580,8 @@
|
||||
<bShared>0</bShared>
|
||||
</File>
|
||||
<File>
|
||||
<GroupNumber>13</GroupNumber>
|
||||
<FileNumber>66</FileNumber>
|
||||
<GroupNumber>14</GroupNumber>
|
||||
<FileNumber>72</FileNumber>
|
||||
<FileType>1</FileType>
|
||||
<tvExp>0</tvExp>
|
||||
<tvExpOptDlg>0</tvExpOptDlg>
|
||||
|
||||
@ -251,7 +251,7 @@
|
||||
<IROM>
|
||||
<Type>1</Type>
|
||||
<StartAddress>0x1008000</StartAddress>
|
||||
<Size>0x1c000</Size>
|
||||
<Size>0x38000</Size>
|
||||
</IROM>
|
||||
<XRAM>
|
||||
<Type>0</Type>
|
||||
@ -532,6 +532,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@ -1078,6 +1083,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_gpio.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_display.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_display.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_params.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
@ -1406,8 +1416,8 @@
|
||||
</IRAM>
|
||||
<IROM>
|
||||
<Type>1</Type>
|
||||
<StartAddress>0x1024000</StartAddress>
|
||||
<Size>0x1c000</Size>
|
||||
<StartAddress>0x1008000</StartAddress>
|
||||
<Size>0x38000</Size>
|
||||
</IROM>
|
||||
<XRAM>
|
||||
<Type>0</Type>
|
||||
@ -1688,6 +1698,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@ -2234,6 +2249,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_gpio.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_display.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_display.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_params.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
@ -2844,6 +2864,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@ -3099,6 +3124,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_gpio.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_display.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_display.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_params.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
@ -3709,6 +3739,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@ -3964,6 +3999,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_gpio.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_display.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\src\app_display.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>app_params.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
|
||||
@ -27,13 +27,18 @@
|
||||
|
||||
|
||||
#define configUSE_PREEMPTION 1
|
||||
#define configUSE_IDLE_HOOK 0
|
||||
#define configUSE_IDLE_HOOK 1
|
||||
#define configUSE_TICK_HOOK 0
|
||||
#define configCPU_CLOCK_HZ ( SystemCoreClock )
|
||||
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
|
||||
#define configMAX_PRIORITIES ( 7 )
|
||||
#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 configUSE_TRACE_FACILITY 1
|
||||
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
|
||||
|
||||
@ -23,28 +23,9 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Frame types */
|
||||
#define BLE_FRAME_SOF 0xCA /* frame start byte */
|
||||
#define BLE_FRAME_HDR_LEN 5u /* SOF+TYPE+SEQ+LEN(2) */
|
||||
#define BLE_FRAME_CLI_REQ 0x01 /* phone -> device: one command line */
|
||||
#define BLE_FRAME_CLI_RSP 0x02 /* device -> phone: response text chunk */
|
||||
#define BLE_FRAME_CLI_RSP_END 0x03 /* device -> phone: {status u8} */
|
||||
#define BLE_FRAME_OTA_BEGIN 0x10 /* phone -> device: {size, crc32, version} */
|
||||
#define BLE_FRAME_OTA_DATA 0x11 /* phone -> device: {offset, data...} */
|
||||
#define BLE_FRAME_OTA_END 0x12 /* phone -> device: {crc32} */
|
||||
#define BLE_FRAME_OTA_ABORT 0x13 /* phone -> device: abort session (no payload) */
|
||||
#define BLE_FRAME_OTA_RSP 0x1F /* device -> phone: {cmd, status, offset} */
|
||||
#define BLE_FRAME_INFO_QUERY 0x20 /* phone -> device: {item_id}*n (empty/0xFF = all) */
|
||||
#define BLE_FRAME_INFO_RSP 0x21 /* device -> phone: TLV {id, len, value}*n */
|
||||
|
||||
/* OTA_RSP / generic status codes */
|
||||
#define BLE_OTA_ST_OK 0
|
||||
#define BLE_OTA_ST_BAD_FRAME 1
|
||||
#define BLE_OTA_ST_BAD_STATE 2 /* bad state / bad offset sequence */
|
||||
#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 */
|
||||
/* Frame type / OTA status constants moved to the shared wire header
|
||||
* (Boot/src/ota_wire.h, also compiled into the bootloader download mode) */
|
||||
#include "ota_wire.h"
|
||||
|
||||
/**
|
||||
* @brief Reset the protocol state machine.
|
||||
|
||||
80
mcm-ddc-ble/inc/app_display.h
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @file app_display.h
|
||||
* @brief YF30137 7-segment display driver.
|
||||
*
|
||||
* The YF30137 is a 7-pin Charlieplexed temperature display:
|
||||
* - DIG1 = hundreds, DIG2 = tens, DIG3 = ones,
|
||||
* DIG4 = unit letter ("F" Fahrenheit / "C" Celsius), DIG5 = tenths
|
||||
* - DIG6 holds two single indicators: A6 = "deg F", B6 = decimal point
|
||||
*
|
||||
* A dedicated FreeRTOS task (display_task) scans the module continuously.
|
||||
* Every other task updates the display simply by calling display_set();
|
||||
* the scan task only reads the global state and never blocks callers.
|
||||
*/
|
||||
#ifndef __APP_DISPLAY_H__
|
||||
#define __APP_DISPLAY_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include "n32wb03x.h"
|
||||
|
||||
/* Display type / unit (controls the DIG6 "deg F" indicator A6) */
|
||||
#define DISP_TYPE_CELSIUS 0u /* degree C: A6 off */
|
||||
#define DISP_TYPE_FAHRENHEIT 1u /* degree F: A6 on */
|
||||
|
||||
/* Number base for the displayed value */
|
||||
#define DISP_BASE_DECIMAL 10u
|
||||
#define DISP_BASE_HEX 16u
|
||||
|
||||
/**
|
||||
* @brief Update what the display should show (read by the scan task).
|
||||
* @param value temperature to display, clamped to 0.0..999.9 (1 decimal)
|
||||
* @param type DISP_TYPE_CELSIUS / DISP_TYPE_FAHRENHEIT (DIG6-A indicator)
|
||||
* @param base unused (kept for API compatibility)
|
||||
* @note Non-blocking; may be called from any task context.
|
||||
*/
|
||||
void display_set(float value, uint8_t type, uint8_t base);
|
||||
|
||||
/**
|
||||
* @brief Show raw digit codes directly (bypasses the temperature decode).
|
||||
* Used by the display self-test to render every segment character.
|
||||
* @param digits 5 codes: 0..9, 0xA..0xF (A..F), CHAR_BLANK(0xFF),
|
||||
* CHAR_MINUS(0x10); index 3 is the DIG4 unit letter
|
||||
* @param type DISP_TYPE_CELSIUS / DISP_TYPE_FAHRENHEIT (DIG6-A indicator)
|
||||
*/
|
||||
void display_set_raw(const uint8_t digits[5], uint8_t type);
|
||||
|
||||
/**
|
||||
* @brief Display driver FreeRTOS task: Charlieplexing scan loop, never returns.
|
||||
*/
|
||||
void display_task(void *pvParameters);
|
||||
|
||||
/**
|
||||
* @brief Enable (1) / disable (0) the automatic Charlieplexing scan.
|
||||
* When disabled the scan task releases all 7 pins to high-impedance
|
||||
* so another task (e.g. the "dispset" CLI) can drive them manually.
|
||||
*/
|
||||
void display_auto_set(uint8_t on);
|
||||
|
||||
/**
|
||||
* @brief @return 1 when the automatic scan is enabled.
|
||||
*/
|
||||
uint8_t display_auto_get(void);
|
||||
|
||||
/**
|
||||
* @brief Map a display pin number (1..7) to its GPIO port + pin mask.
|
||||
* @param pin_no display pin number 1..7 (pin1=PB0 ... pin7=PA6)
|
||||
* @param port out: GPIO port
|
||||
* @param pin out: GPIO pin mask
|
||||
* @return 1 on success, 0 when pin_no is out of range.
|
||||
*/
|
||||
int display_pin_map(uint8_t pin_no, GPIO_Module** port, uint16_t* pin);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __APP_DISPLAY_H__ */
|
||||
@ -63,9 +63,9 @@ extern "C" {
|
||||
#include "n32wb03x.h"
|
||||
|
||||
#define LED1_PORT GPIOB
|
||||
#define LED1_PIN GPIO_PIN_0
|
||||
#define LED2_PORT GPIOA
|
||||
#define LED2_PIN GPIO_PIN_6
|
||||
#define LED1_PIN GPIO_PIN_2 /* PB2: APP running status LED (heartbeat blink) */
|
||||
#define LED2_PORT GPIOB
|
||||
#define LED2_PIN GPIO_PIN_3 /* PB3: BLE connected indicator LED */
|
||||
|
||||
|
||||
void LedInit(GPIO_Module* GPIOx, uint16_t Pin);
|
||||
|
||||
@ -9,12 +9,12 @@
|
||||
#define __APP_VERSION_H__
|
||||
|
||||
#ifndef APP_FW_VERSION
|
||||
#define APP_FW_VERSION "V1.00.23"
|
||||
#define APP_FW_VERSION "V1.00.43"
|
||||
#endif
|
||||
|
||||
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
|
||||
#ifndef APP_FW_VERSION_NUM
|
||||
#define APP_FW_VERSION_NUM 0x00010017u
|
||||
#define APP_FW_VERSION_NUM 0x0001002Bu
|
||||
#endif
|
||||
|
||||
#endif /* __APP_VERSION_H__ */
|
||||
|
||||
32
mcm-ddc-ble/inc/app_wdt.h
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @file app_wdt.h
|
||||
* @brief Independent watchdog (IWDG, LSI 32kHz): 4s timeout, fed from the
|
||||
* FreeRTOS idle hook only while at least one application task has
|
||||
* heartbeated within the last 3s. Frozen while the core is halted
|
||||
* (DBG_IWDG_STOP) so SWD debug sessions do not trigger resets.
|
||||
*/
|
||||
#ifndef __APP_WDT_H__
|
||||
#define __APP_WDT_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Report the reset cause (banner) and start the IWDG.
|
||||
* Call once from main() after the USART banner.
|
||||
*/
|
||||
void app_wdt_init(void);
|
||||
|
||||
/**
|
||||
* @brief Mark a task as alive. Called from the BLE schedule / LED / CLI
|
||||
* task loops; the idle hook stops feeding the IWDG when no task
|
||||
* heartbeated for APP_WDT_HB_TIMEOUT_MS.
|
||||
*/
|
||||
void app_wdt_heartbeat(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __APP_WDT_H__ */
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file bsp_usart.h
|
||||
* @brief USART1 support: printf retarget (polling TX), DMA TX, RX interrupt queue.
|
||||
* TX=PB6, RX=PB7, 115200 8N1.
|
||||
* @brief USART1 support: printf retarget (polling TX), DMA TX, RX DMA
|
||||
* circular ring + IDLE interrupt. TX=PB6, RX=PB7, 460800 8N1.
|
||||
*/
|
||||
#ifndef __BSP_USART_H__
|
||||
#define __BSP_USART_H__
|
||||
@ -20,12 +20,17 @@ extern "C" {
|
||||
#define BSP_USART_RX_PIN GPIO_PIN_7
|
||||
#define BSP_USART_TX_GPIO_AF GPIO_AF4_USART1
|
||||
#define BSP_USART_RX_GPIO_AF GPIO_AF4_USART1
|
||||
#define BSP_USART_BAUDRATE 115200
|
||||
#define BSP_USART_BAUDRATE 460800
|
||||
|
||||
/* USART1 TX DMA channel (remap required on this chip) */
|
||||
#define BSP_USART_TX_DMA_CH DMA_CH1
|
||||
#define BSP_USART_TX_DMA_TC DMA_FLAG_TC1
|
||||
#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)
|
||||
|
||||
/* 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) */
|
||||
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);
|
||||
|
||||
/* RX: inject bytes from task context into the same RX queue (BLE downlink
|
||||
* feeds the CLI through this). Bytes are dropped when the queue is full. */
|
||||
/* RX: inject bytes from task context (BLE downlink feeds the CLI through
|
||||
* this). Bytes are dropped when the inject queue is full. */
|
||||
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.
|
||||
|
||||
@ -314,8 +314,8 @@ void app_ble_init(void)
|
||||
*/
|
||||
void app_ble_connected(void)
|
||||
{
|
||||
LedOn(LED2_PORT,LED2_PIN);
|
||||
notify_en = 0;
|
||||
LedOn(LED2_PORT, LED2_PIN); /* PB2: BLE connected indicator on */
|
||||
if (app_ble_proto_log_get())
|
||||
{
|
||||
printf("[BLE] connected\r\n");
|
||||
@ -341,8 +341,8 @@ void app_ble_disconnected(void)
|
||||
ns_ble_adv_start();
|
||||
|
||||
app_ble_proto_on_disconnect(); /* drop half RX frame + uplink buffer */
|
||||
LedOff(LED2_PORT,LED2_PIN);
|
||||
notify_en = 0;
|
||||
LedOff(LED2_PORT, LED2_PIN); /* PB2: BLE connected indicator off */
|
||||
if (app_ble_proto_log_get())
|
||||
{
|
||||
printf("[BLE] disconnected, advertising restarted\r\n");
|
||||
|
||||
@ -23,8 +23,19 @@
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef BOOT_FIRMWARE
|
||||
#include "boot_usart.h" /* boot_delay_ms */
|
||||
#else
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#endif
|
||||
|
||||
/* Let the final reply drain before the reboot (ms) */
|
||||
#ifdef BOOT_FIRMWARE
|
||||
#define bootset_reset_drain() boot_delay_ms(200u)
|
||||
#else
|
||||
#define bootset_reset_drain() vTaskDelay(pdMS_TO_TICKS(200))
|
||||
#endif
|
||||
|
||||
static uint8_t s_qflash_ready;
|
||||
|
||||
@ -291,6 +302,6 @@ void AppBootset_CmdAppswitch(int argc, char* argv[])
|
||||
}
|
||||
|
||||
cli_write("appsw: done, resetting...\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
bootset_reset_drain();
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
#include "cli_core.h"
|
||||
#include "bsp_usart.h"
|
||||
#include "app_ota.h"
|
||||
#include "app_wdt.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@ -245,12 +246,14 @@ static void CliTask(void* argument)
|
||||
|
||||
for (;;)
|
||||
{
|
||||
app_wdt_heartbeat();
|
||||
/* OTA binary frame mode: raw bytes to the OTA channel.
|
||||
* otaIdleMs is (re)armed on every entry - a leftover count from a
|
||||
* previous session must not trip an instant idle timeout. */
|
||||
otaIdleMs = 0;
|
||||
while (s_otaMode)
|
||||
{
|
||||
app_wdt_heartbeat();
|
||||
if (bsp_usart_read_byte(&ch, OTA_MODE_POLL_MS) != 0)
|
||||
{
|
||||
if (app_ota_chan_rx_uart(ch) != 0)
|
||||
|
||||
460
mcm-ddc-ble/src/app_display.c
Normal file
@ -0,0 +1,460 @@
|
||||
/**
|
||||
* @file app_display.c
|
||||
* @brief YF30137 7-segment display driver (Charlieplexing, 7 GPIOs).
|
||||
*
|
||||
* The YF30137 is a 7-pin Charlieplexed display: every segment is a LED wired
|
||||
* between a unique ordered pair of the 7 pins. Lighting one segment means
|
||||
* driving its "high" pin HIGH, its "low" pin LOW and leaving the other five
|
||||
* pins high-impedance, one segment at a time (the shared pins forbid lighting
|
||||
* several segments of one digit simultaneously).
|
||||
*
|
||||
* Pin mapping (net name -> MCU pin):
|
||||
* pin1 = LED_1 = PB0 pin2 = LED_2 = PB1 pin3 = LED_3 = PA0
|
||||
* pin4 = LED_4 = PA1 pin5 = LED_5 = PB11 pin6 = LED_6 = PB13
|
||||
* pin7 = LED_7 = PA6
|
||||
*
|
||||
* Segment (s) of digit (d) is lit by pin HIGH -> pin LOW:
|
||||
* HIGH = ((s + d) % 7) + 1
|
||||
* LOW = d + (s + d) / 7 (s: A=0..G=6, d: DIG1=1..DIG5=5)
|
||||
*
|
||||
* DIG6 indicators:
|
||||
* A6 (deg F) = pin7 HIGH, pin6 LOW
|
||||
* B6 (dot) = pin1 HIGH, pin7 LOW
|
||||
*
|
||||
* Other tasks update the display via display_set(); this task only reads the
|
||||
* volatile globals and scans, so it never blocks the callers.
|
||||
*/
|
||||
#include "app_display.h"
|
||||
#include "n32wb03x.h"
|
||||
#include "ns_delay.h"
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#include "app_wdt.h"
|
||||
|
||||
/** @addtogroup
|
||||
* @{
|
||||
*/
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
|
||||
/** One display pin (GPIO port + pin). */
|
||||
typedef struct
|
||||
{
|
||||
GPIO_Module *port;
|
||||
uint16_t pin;
|
||||
} disp_pin_t;
|
||||
|
||||
/* Private define ------------------------------------------------------------*/
|
||||
|
||||
/** Per-segment on-time in us (sets brightness and scan rate). */
|
||||
#define DISP_SEG_ON_US 250u
|
||||
/** Number of full seven-segment digits. */
|
||||
#define DISP_DIGITS 5u
|
||||
|
||||
/* Segment bit mask, bit0=A .. bit6=G. */
|
||||
#define SEG_BLANK 0x00u
|
||||
#define SEG_MINUS 0x40u /* '-' = G only */
|
||||
|
||||
/* Decode marker codes (not a real digit). */
|
||||
#define CHAR_MINUS 0x10u
|
||||
#define CHAR_BLANK 0xFFu
|
||||
|
||||
/* Private constants ----------------------------------------------------------*/
|
||||
|
||||
/** Display pin table, index 0..6 = display pin 1..7. */
|
||||
static const disp_pin_t s_disp_pin[7] =
|
||||
{
|
||||
{ GPIOB, GPIO_PIN_0 }, /* pin1 = LED_1 = PB0 */
|
||||
{ GPIOB, GPIO_PIN_1 }, /* pin2 = LED_2 = PB1 */
|
||||
{ GPIOA, GPIO_PIN_0 }, /* pin3 = LED_3 = PA0 */
|
||||
{ GPIOA, GPIO_PIN_1 }, /* pin4 = LED_4 = PA1 */
|
||||
{ GPIOB, GPIO_PIN_11 }, /* pin5 = LED_5 = PB11 */
|
||||
{ GPIOB, GPIO_PIN_13 }, /* pin6 = LED_6 = PB13 */
|
||||
{ GPIOA, GPIO_PIN_6 }, /* pin7 = LED_7 = PA6 */
|
||||
};
|
||||
|
||||
/* Seven-segment codes, bit0=A .. bit6=G. */
|
||||
static const uint8_t s_seg_digit[10] =
|
||||
{
|
||||
0x3Fu, /* 0 = ABCDEF */
|
||||
0x06u, /* 1 = BC */
|
||||
0x5Bu, /* 2 = ABDEG */
|
||||
0x4Fu, /* 3 = ABCDG */
|
||||
0x66u, /* 4 = BCFG */
|
||||
0x6Du, /* 5 = ACDFG */
|
||||
0x7Du, /* 6 = ACDEFG */
|
||||
0x07u, /* 7 = ABC */
|
||||
0x7Fu, /* 8 = ABCDEFG */
|
||||
0x6Fu, /* 9 = ABCDFG */
|
||||
};
|
||||
|
||||
/* Hex digit codes, index 0..5 = A..F. */
|
||||
static const uint8_t s_seg_hex[6] =
|
||||
{
|
||||
0x77u, /* A = ABCEFG */
|
||||
0x7Cu, /* b = CDEFG */
|
||||
0x39u, /* C = ADEF */
|
||||
0x5Eu, /* d = BCDEG */
|
||||
0x79u, /* E = ADEFG */
|
||||
0x71u, /* F = AEFG */
|
||||
};
|
||||
|
||||
/* Private variables ----------------------------------------------------------*/
|
||||
|
||||
/** Display value written by other tasks through display_set(). */
|
||||
static volatile float g_disp_value = 0.0f;
|
||||
/** Display unit: DISP_TYPE_CELSIUS / DISP_TYPE_FAHRENHEIT. */
|
||||
static volatile uint8_t g_disp_type = DISP_TYPE_CELSIUS;
|
||||
/** Number base: DISP_BASE_DECIMAL / DISP_BASE_HEX. */
|
||||
static volatile uint8_t g_disp_base = DISP_BASE_DECIMAL;
|
||||
/** Automatic scan enable: 1 = auto scan, 0 = manual pin control. */
|
||||
static volatile uint8_t g_disp_auto = 1u;
|
||||
/** Raw-mode: 1 = show g_disp_raw_digits[] instead of decoding g_disp_value. */
|
||||
static volatile uint8_t g_disp_raw = 0u;
|
||||
/** Raw digit codes written by display_set_raw(). */
|
||||
static volatile uint8_t g_disp_raw_digits[DISP_DIGITS];
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
/* Private functions ----------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @brief Set a display pin to input (high-impedance).
|
||||
*/
|
||||
static void pin_input(uint8_t idx)
|
||||
{
|
||||
GPIO_InitType s;
|
||||
|
||||
GPIO_InitStruct(&s);
|
||||
s.Pin = s_disp_pin[idx].pin;
|
||||
s.GPIO_Mode = GPIO_MODE_INPUT;
|
||||
s.GPIO_Pull = GPIO_NO_PULL;
|
||||
GPIO_InitPeripheral(s_disp_pin[idx].port, &s);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set a display pin to push-pull output HIGH.
|
||||
*/
|
||||
static void pin_high(uint8_t idx)
|
||||
{
|
||||
GPIO_InitType s;
|
||||
|
||||
GPIO_InitStruct(&s);
|
||||
s.Pin = s_disp_pin[idx].pin;
|
||||
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
|
||||
GPIO_InitPeripheral(s_disp_pin[idx].port, &s);
|
||||
GPIO_SetBits(s_disp_pin[idx].port, s_disp_pin[idx].pin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set a display pin to push-pull output LOW.
|
||||
*/
|
||||
static void pin_low(uint8_t idx)
|
||||
{
|
||||
GPIO_InitType s;
|
||||
|
||||
GPIO_InitStruct(&s);
|
||||
s.Pin = s_disp_pin[idx].pin;
|
||||
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
|
||||
GPIO_InitPeripheral(s_disp_pin[idx].port, &s);
|
||||
GPIO_ResetBits(s_disp_pin[idx].port, s_disp_pin[idx].pin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Put all seven display pins into high-impedance (input).
|
||||
*/
|
||||
static void all_pins_input(void)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < 7; i++)
|
||||
{
|
||||
pin_input(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Light one segment: pin h HIGH, pin l LOW, others stay high-Z, then
|
||||
* release both pins back to high-Z.
|
||||
*/
|
||||
static void seg_pair_on(uint8_t h, uint8_t l)
|
||||
{
|
||||
pin_high(h);
|
||||
pin_low(l);
|
||||
delay_n_10us(DISP_SEG_ON_US / 10u);
|
||||
pin_input(h);
|
||||
pin_input(l);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Light segment s (A=0..G=6) of digit d (1..5).
|
||||
*/
|
||||
static void seg_on(uint8_t s, uint8_t d)
|
||||
{
|
||||
uint8_t h = (uint8_t)((s + d) % 7u);
|
||||
uint8_t l = (uint8_t)((d - 1u) + (s + d) / 7u);
|
||||
|
||||
seg_pair_on(h, l);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Light the DIG6 "deg F" indicator (A6).
|
||||
*/
|
||||
static void dig6_degf_on(void)
|
||||
{
|
||||
seg_pair_on(6u, 5u); /* pin7 HIGH, pin6 LOW */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Light the DIG6 decimal-point indicator (B6).
|
||||
*/
|
||||
static void dig6_dp_on(void)
|
||||
{
|
||||
seg_pair_on(0u, 6u); /* pin1 HIGH, pin7 LOW */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Map a decode character to its segment bit mask.
|
||||
*/
|
||||
static uint8_t seg_of_char(uint8_t c)
|
||||
{
|
||||
if (c <= 9u)
|
||||
{
|
||||
return s_seg_digit[c];
|
||||
}
|
||||
if (c < 0x10u)
|
||||
{
|
||||
return s_seg_hex[c - 0xAu];
|
||||
}
|
||||
if (c == CHAR_MINUS)
|
||||
{
|
||||
return SEG_MINUS;
|
||||
}
|
||||
return SEG_BLANK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decode the temperature value into the display layout.
|
||||
*
|
||||
* Layout (left to right):
|
||||
* DIG1 = hundreds (百位)
|
||||
* DIG2 = tens (十位)
|
||||
* DIG3 = ones (个位)
|
||||
* DIG4 = unit letter: "F" (Fahrenheit) / "C" (Celsius)
|
||||
* DIG5 = tenths (小数位)
|
||||
* DIG6-A (degF indicator) and DIG6-B (decimal point) are handled by
|
||||
* the scan loop, not here.
|
||||
*
|
||||
* @param value temperature to display, clamped to 0.0..999.9
|
||||
* @param type DISP_TYPE_CELSIUS / DISP_TYPE_FAHRENHEIT (DIG4 letter)
|
||||
* @param base unused (kept for API compatibility)
|
||||
* @param digits out: 5 codes (0..9, 0xF="F"/0xC="C", CHAR_BLANK)
|
||||
* @param dp_flag out: 1 = light the DIG6 decimal point (always 1)
|
||||
*/
|
||||
static void display_decode(float value, uint8_t type, uint8_t base,
|
||||
uint8_t *digits, uint8_t *dp_flag)
|
||||
{
|
||||
int32_t scaled;
|
||||
uint8_t i;
|
||||
|
||||
(void)base;
|
||||
|
||||
for (i = 0; i < DISP_DIGITS; i++)
|
||||
{
|
||||
digits[i] = CHAR_BLANK;
|
||||
}
|
||||
*dp_flag = 1u; /* the tenths digit is always shown, so dot is always on */
|
||||
|
||||
if (value < 0.0f)
|
||||
{
|
||||
value = 0.0f;
|
||||
}
|
||||
if (value > 999.9f)
|
||||
{
|
||||
value = 999.9f;
|
||||
}
|
||||
|
||||
/* value * 10 -> [hundreds][tens][ones][-][tenths] */
|
||||
scaled = (int32_t)(value * 10.0f + 0.5f);
|
||||
|
||||
digits[4] = (uint8_t)(scaled % 10); /* DIG5 = tenths */
|
||||
scaled /= 10;
|
||||
digits[2] = (uint8_t)(scaled % 10); /* DIG3 = ones */
|
||||
scaled /= 10;
|
||||
digits[1] = (uint8_t)(scaled % 10); /* DIG2 = tens */
|
||||
scaled /= 10;
|
||||
digits[0] = (uint8_t)(scaled % 10); /* DIG1 = hundreds */
|
||||
|
||||
/* blank leading zeros (hundreds, then tens) */
|
||||
if (digits[0] == 0u)
|
||||
{
|
||||
digits[0] = CHAR_BLANK;
|
||||
}
|
||||
if (digits[0] == CHAR_BLANK && digits[1] == 0u)
|
||||
{
|
||||
digits[1] = CHAR_BLANK;
|
||||
}
|
||||
|
||||
/* DIG4 = unit letter: F for Fahrenheit, C for Celsius */
|
||||
digits[3] = (type == DISP_TYPE_FAHRENHEIT) ? 0x0Fu : 0x0Cu;
|
||||
}
|
||||
|
||||
/* Public functions -----------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @brief Update the display content.
|
||||
* @param value floating value to display
|
||||
* @param type DISP_TYPE_CELSIUS / DISP_TYPE_FAHRENHEIT
|
||||
* @param base DISP_BASE_DECIMAL / DISP_BASE_HEX
|
||||
*/
|
||||
void display_set(float value, uint8_t type, uint8_t base)
|
||||
{
|
||||
/* A single scan frame may observe a partially-updated triple; that is
|
||||
* harmless for a display and keeps the setter lock-free. */
|
||||
g_disp_value = value;
|
||||
g_disp_type = type;
|
||||
g_disp_base = base;
|
||||
g_disp_raw = 0u; /* back to decoded-value mode */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show raw digit codes directly (bypasses the temperature decode).
|
||||
*/
|
||||
void display_set_raw(const uint8_t digits[5], uint8_t type)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < DISP_DIGITS; i++)
|
||||
{
|
||||
g_disp_raw_digits[i] = digits[i];
|
||||
}
|
||||
g_disp_type = type;
|
||||
g_disp_raw = 1u;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enable (1) / disable (0) the automatic scan.
|
||||
*/
|
||||
void display_auto_set(uint8_t on)
|
||||
{
|
||||
g_disp_auto = (on != 0u) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief @return 1 when the automatic scan is enabled.
|
||||
*/
|
||||
uint8_t display_auto_get(void)
|
||||
{
|
||||
return g_disp_auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Map a display pin number (1..7) to its GPIO port + pin mask.
|
||||
*/
|
||||
int display_pin_map(uint8_t pin_no, GPIO_Module** port, uint16_t* pin)
|
||||
{
|
||||
if (pin_no < 1u || pin_no > 7u)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
*port = s_disp_pin[pin_no - 1u].port;
|
||||
*pin = s_disp_pin[pin_no - 1u].pin;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Display driver task: decode the global value and scan every lit
|
||||
* segment, one at a time, forever.
|
||||
*/
|
||||
void display_task(void *pvParameters)
|
||||
{
|
||||
uint8_t last_auto = 1u; /* auto scan is on at boot */
|
||||
|
||||
(void)pvParameters;
|
||||
|
||||
/* The display spans GPIOA (PA0/PA1/PA6) and GPIOB (PB0/PB1/PB11/PB13);
|
||||
* the app no longer calls LedInit (which used to enable these clocks),
|
||||
* so turn both on here - otherwise the port-A pins never drive. */
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA, ENABLE);
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
|
||||
|
||||
all_pins_input();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
float value;
|
||||
uint8_t type;
|
||||
uint8_t base;
|
||||
uint8_t digits[DISP_DIGITS];
|
||||
uint8_t dp_flag;
|
||||
uint8_t d, s;
|
||||
uint8_t segmask;
|
||||
|
||||
app_wdt_heartbeat();
|
||||
|
||||
/* Manual mode (auto disabled): release every pin once, then keep the
|
||||
* task alive and leave the pins for the caller to drive. */
|
||||
if (g_disp_auto == 0u)
|
||||
{
|
||||
if (last_auto)
|
||||
{
|
||||
all_pins_input();
|
||||
last_auto = 0u;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
continue;
|
||||
}
|
||||
last_auto = 1u;
|
||||
|
||||
type = g_disp_type;
|
||||
|
||||
if (g_disp_raw)
|
||||
{
|
||||
/* raw mode: copy the digit codes straight through */
|
||||
uint8_t i;
|
||||
for (i = 0; i < DISP_DIGITS; i++)
|
||||
{
|
||||
digits[i] = g_disp_raw_digits[i];
|
||||
}
|
||||
dp_flag = 1u;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = g_disp_value;
|
||||
base = g_disp_base;
|
||||
display_decode(value, type, base, digits, &dp_flag);
|
||||
}
|
||||
|
||||
/* DIG1..DIG5 */
|
||||
for (d = 1u; d <= DISP_DIGITS; d++)
|
||||
{
|
||||
segmask = seg_of_char(digits[d - 1u]);
|
||||
if (segmask == SEG_BLANK)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (s = 0; s < 7u; s++)
|
||||
{
|
||||
if (segmask & (1u << s))
|
||||
{
|
||||
seg_on(s, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* DIG6 indicators */
|
||||
if (type != DISP_TYPE_CELSIUS)
|
||||
{
|
||||
dig6_degf_on();
|
||||
}
|
||||
if (dp_flag)
|
||||
{
|
||||
dig6_dp_on();
|
||||
}
|
||||
|
||||
/* one scan frame done: yield; a 20 ms refresh is enough for the display */
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@ -3,11 +3,11 @@
|
||||
* @brief OTA receiver implementation (channel-agnostic: BLE OTA
|
||||
* characteristic, UART binary mode, legacy notify frame path).
|
||||
*
|
||||
* Flow: OTA_BEGIN (size/crc/version) -> OTA_DATA* (strictly sequential
|
||||
* offsets, EVERY frame acked with the new offset - lockstep flow control)
|
||||
* -> OTA_END (flush the sub-word tail, verify whole-image CRC32 against
|
||||
* flash, verify the vector table targets the right bank, update
|
||||
* bootsetting, reset). OTA_ABORT aborts the session.
|
||||
* Flow: OTA_BEGIN (size/crc/version[/flags]) -> OTA_DATA* (strictly sequential
|
||||
* offsets; lockstep acks per frame, or sector-boundary acks in UART stream
|
||||
* mode - ble_protocol.md §6.7) -> OTA_END (flush the sub-word tail, verify
|
||||
* whole-image CRC32 against flash, verify the vector table targets the right
|
||||
* bank, update bootsetting, reset). OTA_ABORT aborts the session.
|
||||
*
|
||||
* Direct-write design (no 4KB staging buffer):
|
||||
* - Sectors of the target bank are erased lazily: the first received byte
|
||||
@ -23,8 +23,7 @@
|
||||
* survives this via the 5 s connection supervision timeout.
|
||||
*/
|
||||
#include "app_ota.h"
|
||||
#include "app_ble_proto.h"
|
||||
#include "bsp_usart.h"
|
||||
#include "ota_wire.h"
|
||||
#include "dfu_layout.h"
|
||||
#include "boot_crc32.h"
|
||||
#include "n32wb03x.h"
|
||||
@ -33,8 +32,23 @@
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef BOOT_FIRMWARE
|
||||
/* Bootloader download mode: bare metal, no RTOS, no BLE. The millisecond
|
||||
* base and the blocking UART TX come from boot_usart.c. */
|
||||
#include "boot_usart.h"
|
||||
#define ota_tick_t uint32_t
|
||||
#define ota_tick_get() boot_millis()
|
||||
#define OTA_MS_TO_TICKS(ms) (ms) /* boot tick is 1ms */
|
||||
#else
|
||||
#include "app_ble_proto.h"
|
||||
#include "bsp_usart.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#define ota_tick_t TickType_t
|
||||
#define ota_tick_get() xTaskGetTickCount()
|
||||
#define OTA_MS_TO_TICKS(ms) pdMS_TO_TICKS(ms)
|
||||
#endif
|
||||
|
||||
/* armlink execution-region base symbol: address of this running image */
|
||||
extern uint32_t Image$$ER_IROM1$$Base;
|
||||
@ -65,7 +79,7 @@ static uint8_t s_qflash_ready;
|
||||
/* Deferred post-OTA_END reset (see OTA_RESET_GRACE_MS above) */
|
||||
static volatile uint8_t s_rsp_consumed; /* END RSP taken by the host */
|
||||
static volatile uint8_t s_reset_pending; /* reset scheduled, polled by the BLE task */
|
||||
static TickType_t s_reset_stamp;
|
||||
static ota_tick_t s_reset_stamp;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
@ -89,6 +103,17 @@ static uint32_t ota_rd32(const uint8_t* p)
|
||||
|
||||
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
|
||||
* protocol's proto_crc16 (app_ble_proto.c).
|
||||
@ -142,7 +167,11 @@ static void ota_respond(uint8_t seq, uint8_t cmd_echo, uint8_t status, uint32_t
|
||||
s_rsp_sink(fr, (uint16_t)sizeof(fr));
|
||||
return;
|
||||
}
|
||||
#ifdef BOOT_FIRMWARE
|
||||
/* no legacy notify path in the bootloader - the sink is always set */
|
||||
#else
|
||||
(void)app_ble_proto_send_frame(BLE_FRAME_OTA_RSP, seq, p, sizeof(p));
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t app_ota_current_bank_base(void)
|
||||
@ -157,6 +186,8 @@ void app_ota_abort(void)
|
||||
s_prog = 0;
|
||||
s_erased = 0;
|
||||
s_word_fill = 0;
|
||||
s_stream = 0;
|
||||
s_bad_state_sent = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,14 +226,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)
|
||||
{
|
||||
uint32_t total, crc, version;
|
||||
uint32_t total, crc, version, flags;
|
||||
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);
|
||||
return;
|
||||
@ -211,6 +243,7 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
total = ota_rd32(p);
|
||||
crc = ota_rd32(p + 4);
|
||||
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
|
||||
* while a UART session runs) is refused instead of corrupting state */
|
||||
@ -220,6 +253,20 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef BOOT_FIRMWARE
|
||||
/* The bootloader runs from neither bank: update the INACTIVE bank per
|
||||
* the bootsetting record (then flip active at OTA_END). An invalid
|
||||
* record (bricked/blank device recovery) targets APP1. */
|
||||
{
|
||||
const caiic_bootsetting_t* bs = (const caiic_bootsetting_t*)CAIIC_BOOTSETTING_ADDR;
|
||||
uint8_t bs_ok = (bs->magic == CAIIC_BOOT_MAGIC &&
|
||||
bs->crc32 == caiic_crc32((const uint8_t*)bs,
|
||||
(uint32_t)offsetof(caiic_bootsetting_t, crc32))) ? 1 : 0;
|
||||
target = (bs_ok && bs->active_bank == CAIIC_ACTIVE_BANK1) ? CAIIC_APP2_BASE
|
||||
: CAIIC_APP1_BASE;
|
||||
}
|
||||
(void)self;
|
||||
#else
|
||||
/* Pick the opposite bank as the update target */
|
||||
self = app_ota_current_bank_base();
|
||||
if (self == CAIIC_APP1_BASE)
|
||||
@ -235,6 +282,7 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_STATE, 0);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (total < OTA_MIN_IMAGE_SIZE || total > CAIIC_APP_BANK_SIZE)
|
||||
{
|
||||
@ -257,6 +305,12 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
s_prog = 0;
|
||||
s_erased = 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);
|
||||
}
|
||||
@ -264,11 +318,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.
|
||||
* Payload is programmed to flash immediately (sectors are erased
|
||||
* lazily); EVERY frame is acked with the new offset (lockstep flow
|
||||
* control on the BLE/UART OTA channels; the erase time of a fresh
|
||||
* sector is covered by the ack round trip).
|
||||
* Errors are acked immediately and the state is
|
||||
* kept so the peer can resend.
|
||||
* lazily). Ack policy depends on the session mode:
|
||||
* - lockstep (BLE and legacy hosts): EVERY frame is acked with the
|
||||
* new offset, the ack round trip covers a fresh sector's erase;
|
||||
* - streaming (UART, BEGIN flags bit0): acks only at 4KB sector
|
||||
* 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)
|
||||
{
|
||||
@ -291,11 +347,21 @@ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
off = ota_rd32(p);
|
||||
data = p + 4;
|
||||
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)
|
||||
{
|
||||
/* 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);
|
||||
s_bad_state_sent = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (s_offset + dlen > s_total_size)
|
||||
@ -342,6 +408,22 @@ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
app_ota_abort();
|
||||
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 */
|
||||
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
||||
}
|
||||
@ -468,9 +550,9 @@ static void ota_on_end(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_OK, s_total_size);
|
||||
|
||||
/* Defer the reset until the host has fetched the END response (polled
|
||||
* from the BLE schedule task, which keeps running meanwhile so the ATT
|
||||
* read / notify flush can actually happen) */
|
||||
s_reset_stamp = xTaskGetTickCount();
|
||||
* from the BLE schedule task / boot superloop, which keeps running
|
||||
* meanwhile so the ATT read / UART drain can actually happen) */
|
||||
s_reset_stamp = ota_tick_get();
|
||||
s_reset_pending = 1;
|
||||
}
|
||||
|
||||
@ -483,18 +565,16 @@ void app_ota_reset_poll(void)
|
||||
{
|
||||
if (s_reset_pending)
|
||||
{
|
||||
TickType_t elapsed = xTaskGetTickCount() - s_reset_stamp;
|
||||
ota_tick_t elapsed = ota_tick_get() - s_reset_stamp;
|
||||
|
||||
if ((s_rsp_consumed && elapsed >= pdMS_TO_TICKS(OTA_RESET_GRACE_MS)) ||
|
||||
elapsed >= pdMS_TO_TICKS(OTA_RESET_TIMEOUT_MS))
|
||||
if ((s_rsp_consumed && elapsed >= OTA_MS_TO_TICKS(OTA_RESET_GRACE_MS)) ||
|
||||
elapsed >= OTA_MS_TO_TICKS(OTA_RESET_TIMEOUT_MS))
|
||||
{
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 */
|
||||
|
||||
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
||||
@ -629,7 +709,9 @@ void app_ota_chan_rx(const uint8_t* data, uint16_t len, app_ota_rsp_sink_fn sink
|
||||
}
|
||||
}
|
||||
|
||||
/* --- BLE OTA characteristic (...e0005): write frames in, read RSP out --- */
|
||||
/* --- BLE OTA characteristic (...e0005): write frames in, read RSP out ---
|
||||
* (application firmware only; the bootloader has no BLE) */
|
||||
#ifndef BOOT_FIRMWARE
|
||||
|
||||
#define OTA_BLE_RSP_BUF_SIZE 16u /* largest RSP frame = 7B hdr + 6B */
|
||||
|
||||
@ -660,15 +742,20 @@ const uint8_t* app_ota_chan_rsp_ble(uint16_t* out_len)
|
||||
s_bleRspLen = 0; /* consumed by the read */
|
||||
return s_bleRsp;
|
||||
}
|
||||
#endif /* !BOOT_FIRMWARE */
|
||||
|
||||
/* --- UART binary mode (CLI "ota"): RSP frames straight out on TX --- */
|
||||
|
||||
static void ota_uart_sink(const uint8_t* frame, uint16_t len)
|
||||
{
|
||||
#ifdef BOOT_FIRMWARE
|
||||
boot_usart_write(frame, len); /* blocking: frame physically out below */
|
||||
#else
|
||||
bsp_usart_tx_lock();
|
||||
bsp_usart_write_dma(frame, len);
|
||||
bsp_usart_tx_unlock();
|
||||
/* Blocking DMA TX: the frame is physically out when we get here */
|
||||
#endif
|
||||
s_rsp_consumed = 1;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file bsp_usart.c
|
||||
* @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 <stdio.h>
|
||||
@ -11,14 +11,65 @@
|
||||
#include "queue.h"
|
||||
#include "semphr.h"
|
||||
|
||||
#define RX_QUEUE_LENGTH 64
|
||||
#define RX_INJECT_QUEUE_LEN 64 /* BLE downlink -> CLI injection */
|
||||
#define RX_RING_SIZE 3072u /* DMA circular ring: ~69ms @460800,
|
||||
* covers flash-erase interrupt-off
|
||||
* windows (~45ms) during OTA streaming.
|
||||
* NOTE: keep the stack top below the
|
||||
* 0x2000C000 SRAM cliff (dev log §47) -
|
||||
* merge_image.py hard-fails otherwise */
|
||||
#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;
|
||||
|
||||
/**
|
||||
* @brief Initialize USART1 (115200 8N1) on PB6(TX)/PB7(RX),
|
||||
* DMA TX channel and the RX interrupt queue.
|
||||
* @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 (460800 8N1) on PB6(TX)/PB7(RX),
|
||||
* DMA TX channel and the RX DMA ring.
|
||||
*/
|
||||
void bsp_usart_init(void)
|
||||
{
|
||||
@ -57,11 +108,13 @@ void bsp_usart_init(void)
|
||||
DMA_RequestRemap(BSP_USART_TX_DMA_REMAP, DMA, BSP_USART_TX_DMA_CH, ENABLE);
|
||||
USART_EnableDMA(BSP_USARTx, USART_DMAREQ_TX, ENABLE);
|
||||
|
||||
/* RX byte queue fed by the RXDNE interrupt */
|
||||
s_rxQueue = xQueueCreate(RX_QUEUE_LENGTH, sizeof(uint8_t));
|
||||
/* RX: DMA circular ring + IDLE interrupt wakeup */
|
||||
s_rxInjectQueue = xQueueCreate(RX_INJECT_QUEUE_LEN, sizeof(uint8_t));
|
||||
s_rxSem = xSemaphoreCreateBinary();
|
||||
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 */
|
||||
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
|
||||
NVIC_InitStructure.NVIC_IRQChannelPriority = 3;
|
||||
@ -72,10 +125,10 @@ void bsp_usart_init(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Re-apply the baud rate at runtime (keeps RX interrupt/DMA setup).
|
||||
* Waits for TX to drain, re-initializes the USART and flushes the
|
||||
* RX queue (stale bytes may have been sampled at the old baud rate).
|
||||
* Used by the "uartrst" CLI command.
|
||||
* @brief Re-apply the baud rate at runtime (keeps RX DMA ring setup).
|
||||
* Waits for TX to drain, re-initializes the USART, restarts the RX
|
||||
* DMA ring and flushes the inject queue (stale bytes may have been
|
||||
* sampled at the old baud rate). Used by the "uartrst" CLI command.
|
||||
*/
|
||||
void bsp_usart_set_baud(uint32_t baud)
|
||||
{
|
||||
@ -97,12 +150,13 @@ void bsp_usart_set_baud(uint32_t baud)
|
||||
USART_Init(BSP_USARTx, &USART_InitStructure);
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
@ -148,37 +202,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 timeout_ms BSP_USART_WAIT_FOREVER or a timeout in ms
|
||||
* @return 1 on success, 0 on timeout
|
||||
*/
|
||||
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;
|
||||
}
|
||||
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);
|
||||
return (xQueueReceive(s_rxQueue, ch, ticks) == pdPASS) ? 1 : 0;
|
||||
uint32_t slice = RX_WAIT_SLICE_MS;
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
while (len--)
|
||||
{
|
||||
if (xQueueSend(s_rxQueue, data++, 0) != pdPASS)
|
||||
if (xQueueSend(s_rxInjectQueue, data++, 0) != pdPASS)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -208,21 +296,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).
|
||||
*/
|
||||
void bsp_usart_rx_isr_handler(void)
|
||||
{
|
||||
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 */
|
||||
uint8_t ch = (uint8_t)USART_ReceiveData(BSP_USARTx);
|
||||
/* IDLEF clears by reading STS (done by GetIntStatus) then DAT;
|
||||
* 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);
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
#include "app_params.h"
|
||||
#include "app_bootset.h"
|
||||
#include "app_cli.h"
|
||||
#include "app_display.h"
|
||||
#include "n32wb03x.h" /* SystemTrimValueGet (temp debug) */
|
||||
|
||||
#include <stdio.h>
|
||||
@ -277,6 +278,24 @@ static void CmdFactory(int argc, char* argv[])
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "hang": wedge the CLI task in a busy loop so the idle hook stops
|
||||
* feeding the IWDG - watchdog fire test (device resets in ~4s and
|
||||
* the next banner shows "Last reset: IWDG watchdog!").
|
||||
*/
|
||||
static void CmdHang(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
cli_write("hanging now - watchdog should reset in ~4s ...\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(CLI_RESET_DELAY_MS));
|
||||
for (;;)
|
||||
{
|
||||
/* busy: idle task starves, IWDG unfed */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "uartrst": re-initialize USART1 at the default baud rate and
|
||||
* flush the RX queue (recovery from a wedged/misconfigured UART).
|
||||
@ -302,7 +321,7 @@ static void CmdUartInfo(int argc, char* argv[])
|
||||
cli_write("usart1: TX=PB6 RX=PB7 (AF4)\r\n");
|
||||
cli_printf("baud: %lu, 8 data bits, no parity, 1 stop bit\r\n",
|
||||
(unsigned long)BSP_USART_BAUDRATE);
|
||||
cli_write("flow control: none; tx: DMA CH1 (polled); rx: RXDNE irq queue\r\n");
|
||||
cli_write("flow control: none; tx: DMA CH1 (polled); rx: DMA CH2 ring (3KB, idle irq)\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -324,6 +343,539 @@ static void CmdOta(int argc, char* argv[])
|
||||
app_cli_ota_mode_enter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a "pxN" pin name (e.g. "pb0", "PA3", "PB13") into a GPIO port
|
||||
* and pin mask. @return 1 on success, 0 on bad name/out-of-range pin.
|
||||
*/
|
||||
static int pinset_parse(const char* name, GPIO_Module** port, uint16_t* mask)
|
||||
{
|
||||
const char* p;
|
||||
int pin = 0;
|
||||
|
||||
if (name[0] != 'p' && name[0] != 'P')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (name[1] == 'a' || name[1] == 'A')
|
||||
{
|
||||
*port = GPIOA;
|
||||
}
|
||||
else if (name[1] == 'b' || name[1] == 'B')
|
||||
{
|
||||
*port = GPIOB;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
p = &name[2];
|
||||
if (*p < '0' || *p > '9')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
while (*p >= '0' && *p <= '9')
|
||||
{
|
||||
pin = pin * 10 + (*p - '0');
|
||||
if (pin > 15)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
if (*p != '\0')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (*port == GPIOA && pin > 6) /* PA7 not bonded on this chip */
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (*port == GPIOB && pin > 13) /* PB14/PB15 not bonded */
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
*mask = (uint16_t)(1u << pin);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "pinset <pxN> [0|1]": set a GPIO output high/low, or read its level.
|
||||
* e.g. "pinset pb0 1", "pinset pa0 0", "pinset pb0" (read level).
|
||||
*/
|
||||
static void CmdPinSet(int argc, char* argv[])
|
||||
{
|
||||
GPIO_Module* port = NULL;
|
||||
uint16_t mask = 0;
|
||||
GPIO_InitType s;
|
||||
|
||||
if (argc < 2)
|
||||
{
|
||||
cli_write("usage: pinset <pxN> [0|1] (e.g. pinset pb0 1)\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pinset_parse(argv[1], &port, &mask))
|
||||
{
|
||||
cli_printf("bad pin: %s (use pa0..pa6 / pb0..pb13)\r\n", argv[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (port == GPIOA)
|
||||
{
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA, ENABLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
|
||||
}
|
||||
|
||||
if (argc >= 3)
|
||||
{
|
||||
/* reserved-pin guard: warn before reconfiguring SWD / console pins */
|
||||
if ((port == GPIOA && (mask & (GPIO_PIN_4 | GPIO_PIN_5)) != 0u) ||
|
||||
(port == GPIOB && (mask & (GPIO_PIN_6 | GPIO_PIN_7)) != 0u))
|
||||
{
|
||||
cli_write("warning: SWD/console pin - reconfigure may break the link\r\n");
|
||||
}
|
||||
|
||||
GPIO_InitStruct(&s);
|
||||
s.Pin = mask;
|
||||
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
|
||||
GPIO_InitPeripheral(port, &s);
|
||||
|
||||
if (argv[2][0] == '1' && argv[2][1] == '\0')
|
||||
{
|
||||
GPIO_SetBits(port, mask);
|
||||
cli_printf("%s = 1\r\n", argv[1]);
|
||||
}
|
||||
else if (argv[2][0] == '0' && argv[2][1] == '\0')
|
||||
{
|
||||
GPIO_ResetBits(port, mask);
|
||||
cli_printf("%s = 0\r\n", argv[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
cli_write("usage: pinset <pxN> [0|1] (0=low, 1=high)\r\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* read: the input data register reflects the actual pin level */
|
||||
cli_printf("%s = %u\r\n", argv[1],
|
||||
(unsigned)(GPIO_ReadInputDataBit(port, mask) ? 1u : 0u));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a decimal floating-point number ("36.5", "-1.2", "88").
|
||||
* @return 1 on success, 0 on bad input.
|
||||
*/
|
||||
static int cli_parse_float(const char* s, float* out)
|
||||
{
|
||||
float v = 0.0f;
|
||||
float frac = 0.1f;
|
||||
int neg = 0;
|
||||
int any = 0;
|
||||
|
||||
if (*s == '-')
|
||||
{
|
||||
neg = 1;
|
||||
s++;
|
||||
}
|
||||
else if (*s == '+')
|
||||
{
|
||||
s++;
|
||||
}
|
||||
|
||||
while (*s >= '0' && *s <= '9')
|
||||
{
|
||||
v = v * 10.0f + (float)(*s - '0');
|
||||
s++;
|
||||
any = 1;
|
||||
}
|
||||
if (*s == '.')
|
||||
{
|
||||
s++;
|
||||
while (*s >= '0' && *s <= '9')
|
||||
{
|
||||
v += frac * (float)(*s - '0');
|
||||
frac *= 0.1f;
|
||||
s++;
|
||||
any = 1;
|
||||
}
|
||||
}
|
||||
if (!any || *s != '\0')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
*out = neg ? -v : v;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a hexadecimal integer ("ff", "0xFF", "10").
|
||||
* @return 1 on success, 0 on bad input.
|
||||
*/
|
||||
/**
|
||||
* @brief "disp <value> [f]": set the temperature display.
|
||||
* value 0.0..999.9 is shown as [hundreds][tens][ones][F][tenths];
|
||||
* "f" turns on the DIG6-A degF indicator.
|
||||
*/
|
||||
static void CmdDisp(int argc, char* argv[])
|
||||
{
|
||||
uint8_t type = DISP_TYPE_CELSIUS;
|
||||
float value;
|
||||
int i;
|
||||
|
||||
if (argc < 2)
|
||||
{
|
||||
cli_write("usage: disp <value> [f] (e.g. disp 36.5, disp 36.5 f)\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 2; i < argc; i++)
|
||||
{
|
||||
if (strcmp(argv[i], "f") == 0)
|
||||
{
|
||||
type = DISP_TYPE_FAHRENHEIT;
|
||||
}
|
||||
else
|
||||
{
|
||||
cli_printf("bad mode: %s (use f)\r\n", argv[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cli_parse_float(argv[1], &value))
|
||||
{
|
||||
cli_printf("bad value: %s\r\n", argv[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
display_set(value, type, DISP_BASE_DECIMAL);
|
||||
|
||||
/* echo the (clamped) value actually shown */
|
||||
{
|
||||
float shown = value;
|
||||
int scaled, ip, fp;
|
||||
|
||||
if (shown < 0.0f)
|
||||
{
|
||||
shown = 0.0f;
|
||||
}
|
||||
if (shown > 999.9f)
|
||||
{
|
||||
shown = 999.9f;
|
||||
}
|
||||
scaled = (int)(shown * 10.0f + 0.5f);
|
||||
ip = scaled / 10;
|
||||
fp = scaled - ip * 10;
|
||||
cli_printf("display = %d.%d %c\r\n", ip, fp,
|
||||
(type == DISP_TYPE_FAHRENHEIT) ? 'F' : 'C');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a display pin selector "pin1".."pin7" (or bare "1".."7").
|
||||
* @return 1 on success, 0 on bad input.
|
||||
*/
|
||||
static int cli_parse_disp_pin(const char* s, uint8_t* pin_no)
|
||||
{
|
||||
if (s[0] == 'p' || s[0] == 'P')
|
||||
{
|
||||
if ((s[1] != 'i' && s[1] != 'I') || (s[2] != 'n' && s[2] != 'N'))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
s += 3;
|
||||
}
|
||||
if (s[0] < '1' || s[0] > '7' || s[1] != '\0')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
*pin_no = (uint8_t)(s[0] - '0');
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Format a GPIO port + pin mask as a pin name ("PA6"/"PB0"/"PB11").
|
||||
*/
|
||||
static void cli_pin_str(GPIO_Module* port, uint16_t pin, char* out)
|
||||
{
|
||||
char pc = (port == GPIOA) ? 'A' : 'B';
|
||||
uint8_t n = 0u;
|
||||
|
||||
while ((pin & 1u) == 0u)
|
||||
{
|
||||
pin >>= 1;
|
||||
n++;
|
||||
}
|
||||
if (n >= 10u)
|
||||
{
|
||||
out[0] = 'P';
|
||||
out[1] = pc;
|
||||
out[2] = '1';
|
||||
out[3] = (char)('0' + (n - 10u));
|
||||
out[4] = '\0';
|
||||
}
|
||||
else
|
||||
{
|
||||
out[0] = 'P';
|
||||
out[1] = pc;
|
||||
out[2] = (char)('0' + n);
|
||||
out[3] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "dispauto [on|off]": enable/disable the automatic display scan.
|
||||
* Turning it off releases all 7 display pins to high-impedance so
|
||||
* "dispset" can drive them manually.
|
||||
*/
|
||||
static void CmdDispAuto(int argc, char* argv[])
|
||||
{
|
||||
if (argc >= 2)
|
||||
{
|
||||
if (strcmp(argv[1], "on") == 0)
|
||||
{
|
||||
display_auto_set(1u);
|
||||
}
|
||||
else if (strcmp(argv[1], "off") == 0)
|
||||
{
|
||||
display_auto_set(0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
cli_write("usage: dispauto [on|off]\r\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
cli_printf("auto display: %s\r\n", display_auto_get() ? "on" : "off");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Drive one display pin (1..7) to 0 / 1 / high-Z.
|
||||
* @return 1 on success, 0 on bad value, -1 on bad pin number.
|
||||
*/
|
||||
static int cli_disp_set_pin(uint8_t pin_no, char val)
|
||||
{
|
||||
GPIO_Module* port;
|
||||
uint16_t pin;
|
||||
GPIO_InitType s;
|
||||
|
||||
if (!display_pin_map(pin_no, &port, &pin))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Make sure the port clock is on before touching the pin (defensive;
|
||||
* display_task also enables both clocks at boot). */
|
||||
if (port == GPIOA)
|
||||
{
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA, ENABLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
|
||||
}
|
||||
|
||||
GPIO_InitStruct(&s);
|
||||
s.Pin = pin;
|
||||
if (val == '1')
|
||||
{
|
||||
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
|
||||
GPIO_InitPeripheral(port, &s);
|
||||
GPIO_SetBits(port, pin);
|
||||
}
|
||||
else if (val == '0')
|
||||
{
|
||||
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
|
||||
GPIO_InitPeripheral(port, &s);
|
||||
GPIO_ResetBits(port, pin);
|
||||
}
|
||||
else if (val == 'z' || val == 'Z')
|
||||
{
|
||||
s.GPIO_Mode = GPIO_MODE_INPUT;
|
||||
s.GPIO_Pull = GPIO_NO_PULL;
|
||||
GPIO_InitPeripheral(port, &s);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "dispset <pin1-7|all> [0|1|z]": drive display pin(s) manually.
|
||||
* pin1..pin7 map to PB0/PB1/PA0/PA1/PB11/PB13/PA6; "all" hits all 7.
|
||||
* "z" = high-Z. No value reads one pin's level. Use after "dispauto off".
|
||||
*/
|
||||
static void CmdDispSet(int argc, char* argv[])
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
cli_write("usage: dispset <pin1-7|all> [0|1|z]\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* "all": apply the value to all 7 display pins */
|
||||
if (strcmp(argv[1], "all") == 0 || strcmp(argv[1], "ALL") == 0)
|
||||
{
|
||||
uint8_t p;
|
||||
|
||||
if (argc < 3 || argv[2][1] != '\0')
|
||||
{
|
||||
cli_write("usage: dispset all [0|1|z]\r\n");
|
||||
return;
|
||||
}
|
||||
if (display_auto_get())
|
||||
{
|
||||
cli_write("warning: auto display is ON - use 'dispauto off' first\r\n");
|
||||
}
|
||||
for (p = 1u; p <= 7u; p++)
|
||||
{
|
||||
if (cli_disp_set_pin(p, argv[2][0]) != 1)
|
||||
{
|
||||
cli_write("usage: dispset all [0|1|z]\r\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
cli_printf("all 7 display pins = %s\r\n",
|
||||
(argv[2][0] == 'z' || argv[2][0] == 'Z') ? "high-Z" : argv[2]);
|
||||
return;
|
||||
}
|
||||
|
||||
/* single pin */
|
||||
{
|
||||
GPIO_Module* port;
|
||||
uint16_t pin;
|
||||
uint8_t pin_no;
|
||||
char name[8];
|
||||
|
||||
if (!cli_parse_disp_pin(argv[1], &pin_no) ||
|
||||
!display_pin_map(pin_no, &port, &pin))
|
||||
{
|
||||
cli_printf("bad pin: %s (use pin1..pin7 or all)\r\n", argv[1]);
|
||||
return;
|
||||
}
|
||||
cli_pin_str(port, pin, name);
|
||||
|
||||
if (argc >= 3)
|
||||
{
|
||||
if (display_auto_get())
|
||||
{
|
||||
cli_write("warning: auto display is ON - use 'dispauto off' first\r\n");
|
||||
}
|
||||
if (argv[2][1] == '\0' && cli_disp_set_pin(pin_no, argv[2][0]) == 1)
|
||||
{
|
||||
if (argv[2][0] == 'z' || argv[2][0] == 'Z')
|
||||
{
|
||||
cli_printf("pin%u (%s) = high-Z\r\n", (unsigned)pin_no, name);
|
||||
}
|
||||
else
|
||||
{
|
||||
cli_printf("pin%u (%s) = %c\r\n", (unsigned)pin_no, name, argv[2][0]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cli_write("usage: dispset <pin1-7> [0|1|z]\r\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cli_printf("pin%u (%s) = %u\r\n", (unsigned)pin_no, name,
|
||||
(unsigned)(GPIO_ReadInputDataBit(port, pin) ? 1u : 0u));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "disptest": automated display self-test. First a quick digit/position
|
||||
* check (0..9, multi-digit, max 999.9, degF toggle), then a full sweep
|
||||
* of the temperature range 0.1..999.0 in both Celsius and Fahrenheit.
|
||||
* Restores auto-scan + 0.0 when done.
|
||||
*/
|
||||
static void CmdDispTest(int argc, char* argv[])
|
||||
{
|
||||
static const float s_digits[10] = { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f,
|
||||
5.0f, 6.0f, 7.0f, 8.0f, 9.0f };
|
||||
uint8_t prev_auto = display_auto_get();
|
||||
uint16_t i;
|
||||
uint8_t unit;
|
||||
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
/* the test drives values through display_set(), so auto must be on */
|
||||
display_auto_set(1u);
|
||||
|
||||
/* --- 1. quick digit / position check --- */
|
||||
cli_write("disptest: 0.0..9.0 -> 12.3/456.7/999.9 -> 36.5F/C\r\n");
|
||||
|
||||
for (i = 0u; i < 10u; i++)
|
||||
{
|
||||
display_set(s_digits[i], DISP_TYPE_CELSIUS, DISP_BASE_DECIMAL);
|
||||
cli_printf(" %u.0\r\n", (unsigned)i);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
|
||||
display_set(12.3f, DISP_TYPE_CELSIUS, DISP_BASE_DECIMAL);
|
||||
cli_write(" 12.3\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
display_set(456.7f, DISP_TYPE_CELSIUS, DISP_BASE_DECIMAL);
|
||||
cli_write(" 456.7\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
display_set(999.9f, DISP_TYPE_CELSIUS, DISP_BASE_DECIMAL);
|
||||
cli_write(" 999.9 (max)\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
display_set(36.5f, DISP_TYPE_FAHRENHEIT, DISP_BASE_DECIMAL);
|
||||
cli_write(" 36.5 F (degF indicator on)\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(1200));
|
||||
|
||||
display_set(36.5f, DISP_TYPE_CELSIUS, DISP_BASE_DECIMAL);
|
||||
cli_write(" 36.5 C (degF indicator off)\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(1200));
|
||||
|
||||
/* --- 2. full temperature sweep, Celsius then Fahrenheit --- */
|
||||
for (unit = 0u; unit < 2u; unit++)
|
||||
{
|
||||
uint8_t type = (unit == 0u) ? DISP_TYPE_CELSIUS : DISP_TYPE_FAHRENHEIT;
|
||||
|
||||
cli_printf("disptest sweep %s: 0.1..9.9 -> 10..99 -> 100..999\r\n",
|
||||
(unit == 0u) ? "C" : "F");
|
||||
|
||||
for (i = 1u; i <= 99u; i++) /* 0.1 .. 9.9 step 0.1 */
|
||||
{
|
||||
display_set((float)i * 0.1f, type, DISP_BASE_DECIMAL);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
for (i = 10u; i <= 99u; i++) /* 10 .. 99 step 1 */
|
||||
{
|
||||
display_set((float)i, type, DISP_BASE_DECIMAL);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
for (i = 100u; i <= 990u; i += 10u) /* 100 .. 990 step 10 */
|
||||
{
|
||||
display_set((float)i, type, DISP_BASE_DECIMAL);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
display_set(999.0f, type, DISP_BASE_DECIMAL); /* 999.0 */
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
}
|
||||
|
||||
display_set(0.0f, DISP_TYPE_CELSIUS, DISP_BASE_DECIMAL);
|
||||
display_auto_set(prev_auto);
|
||||
cli_write("disptest done\r\n");
|
||||
}
|
||||
|
||||
/* Command table; add new commands here */
|
||||
static const CliCmd_t s_cmds[] = {
|
||||
{"help", "help [cmd]: list commands / show one command's help", CmdHelp},
|
||||
@ -339,9 +891,15 @@ static const CliCmd_t s_cmds[] = {
|
||||
{"appsw", "appsw [1|2]: switch boot bank (crc verified) + reboot", AppBootset_CmdAppswitch},
|
||||
{"ota", "ota: UART enters OTA binary frame mode (see help ota)", CmdOta},
|
||||
{"reset", "reset: system reset (reboot)", CmdReset},
|
||||
{"pinset", "pinset <pxN> [0|1]: set/read GPIO level (pa0-6/pb0-13)", CmdPinSet},
|
||||
{"disp", "disp <value> [f]: set temperature display (0-999.9, 1 dec)", CmdDisp},
|
||||
{"dispauto", "dispauto [on|off]: enable/disable auto display scan", CmdDispAuto},
|
||||
{"dispset", "dispset <pin1-7|all> [0|1|z]: drive display pin(s)", CmdDispSet},
|
||||
{"disptest", "disptest: display self-test + full 0.1-999.0 sweep (C/F)", CmdDispTest},
|
||||
{"factory", "factory: restore params to defaults and reboot", CmdFactory},
|
||||
{"uartrst", "uartrst: re-init USART1 (115200 8N1), flush rx queue", CmdUartRst},
|
||||
{"uartrst", "uartrst: re-init USART1 (460800 8N1), flush rx queue", CmdUartRst},
|
||||
{"uartinfo", "uartinfo: show USART1 pins/baud/format", CmdUartInfo},
|
||||
{"hang", "hang: wedge the CLI task (IWDG watchdog fire test)", CmdHang},
|
||||
};
|
||||
|
||||
#define CLI_CMD_COUNT (sizeof(s_cmds) / sizeof(s_cmds[0]))
|
||||
@ -370,6 +928,7 @@ static void CmdHelp(int argc, char* argv[])
|
||||
return;
|
||||
}
|
||||
|
||||
cli_write("mode: APP\r\n");
|
||||
cli_write("commands:\r\n");
|
||||
for (i = 0; i < CLI_CMD_COUNT; i++)
|
||||
{
|
||||
|
||||
@ -84,6 +84,9 @@
|
||||
#include "app_ota.h"
|
||||
#include "app_version.h"
|
||||
#include "app_user_config.h"
|
||||
#include "app_wdt.h"
|
||||
#include "app_display.h"
|
||||
#include "n32wb03x_iwdg.h"
|
||||
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
/* Private define ------------------------------------------------------------*/
|
||||
@ -102,11 +105,67 @@
|
||||
|
||||
#define BLE_SCHED_TASK_STACK (512) /* stack in words */
|
||||
#define BLE_SCHED_TASK_PRIORITY (2)
|
||||
#define DISP_TASK_STACK (256) /* stack in words */
|
||||
#define DISP_TASK_PRIORITY (1)
|
||||
#define LED_TASK_STACK (128) /* stack in words */
|
||||
#define LED_TASK_PRIORITY (1)
|
||||
|
||||
/* Private constants ---------------------------------------------------------*/
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
/* Private constants ----------------------------------------------------------*/
|
||||
/* Private variables ----------------------------------------------------------*/
|
||||
|
||||
/* IWDG: LSI 32kHz, /128 prescaler -> 250Hz counter; 1000 counts = 4s */
|
||||
#define APP_WDT_RELOAD 1000u
|
||||
/* the idle hook feeds the IWDG only while some task heartbeated this long */
|
||||
#define APP_WDT_HB_TIMEOUT_MS 3000u
|
||||
|
||||
static volatile TickType_t s_wdt_last_hb;
|
||||
|
||||
/**
|
||||
* @brief Report the reset cause and start the independent watchdog.
|
||||
* IWDG is frozen while the core is halted (debug-friendly).
|
||||
*/
|
||||
void app_wdt_init(void)
|
||||
{
|
||||
if (RCC_GetFlagStatus(RCC_CTRLSTS_FLAG_IWDGRSTF) != RESET)
|
||||
{
|
||||
printf(" Last reset: IWDG watchdog!\n");
|
||||
}
|
||||
RCC_ClrFlag();
|
||||
|
||||
/* Freeze IWDG while the core is halted (SWD debug sessions) */
|
||||
RCC_EnableAPB1PeriphClk(RCC_APB1_PERIPH_PWR, ENABLE);
|
||||
DBG_ConfigPeriph(DBG_IWDG_STOP, ENABLE);
|
||||
|
||||
IWDG_WriteConfig(IWDG_WRITE_ENABLE);
|
||||
IWDG_SetPrescalerDiv(IWDG_PRESCALER_DIV128);
|
||||
IWDG_CntReload(APP_WDT_RELOAD);
|
||||
IWDG_ReloadKey();
|
||||
IWDG_Enable(); /* LSI is enabled by hardware */
|
||||
|
||||
s_wdt_last_hb = 0;
|
||||
}
|
||||
|
||||
void app_wdt_heartbeat(void)
|
||||
{
|
||||
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
|
||||
{
|
||||
s_wdt_last_hb = xTaskGetTickCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief FreeRTOS idle hook: feed the IWDG only while some application task
|
||||
* heartbeated recently. A wedged/deadlocked system (idle spins but no
|
||||
* task advances) resets within the IWDG timeout.
|
||||
*/
|
||||
void vApplicationIdleHook(void)
|
||||
{
|
||||
if ((xTaskGetTickCount() - s_wdt_last_hb) < pdMS_TO_TICKS(APP_WDT_HB_TIMEOUT_MS))
|
||||
{
|
||||
IWDG_ReloadKey();
|
||||
}
|
||||
}
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
/* Private functions ---------------------------------------------------------*/
|
||||
|
||||
@ -121,6 +180,7 @@ static void ble_schedule_task(void *pvParameters)
|
||||
(void)pvParameters;
|
||||
for (;;)
|
||||
{
|
||||
app_wdt_heartbeat();
|
||||
/*schedule all pending events*/
|
||||
rwip_schedule();
|
||||
/*flush pending CLI uplink frames (notify, paced by cfm)*/
|
||||
@ -132,23 +192,17 @@ static void ble_schedule_task(void *pvParameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief LED blink task: toggle LED1 (PB0) as a run indicator; the half
|
||||
* period comes from the APP_DATA parameter record (led1_blink_ms,
|
||||
* "appset led <50..10000>"). LED2 (PA6) stays owned by
|
||||
* app_ble.c as the connection state LED.
|
||||
* @brief LED1 (PB1) "running" heartbeat: blinks at the APP_DATA parameter
|
||||
* led1_blink_ms half-period so a live board is visually obvious.
|
||||
*/
|
||||
static void led_task(void *pvParameters)
|
||||
{
|
||||
(void)pvParameters;
|
||||
for (;;)
|
||||
{
|
||||
uint16_t period = app_params_get()->led1_blink_ms;
|
||||
if (period < PARAM_LED1_BLINK_MS_MIN)
|
||||
{
|
||||
period = PARAM_LED1_BLINK_MS_DEF;
|
||||
}
|
||||
app_wdt_heartbeat();
|
||||
LedBlink(LED1_PORT, LED1_PIN);
|
||||
vTaskDelay(pdMS_TO_TICKS(period));
|
||||
vTaskDelay(pdMS_TO_TICKS(app_params_get()->led1_blink_ms));
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,6 +231,9 @@ int main(void)
|
||||
printf(" BLE enabled, advertising as \"%s\"\n", CUSTOM_DEVICE_NAME);
|
||||
printf("========================================\n");
|
||||
|
||||
/* reset-cause report + start the independent watchdog (idle-hook fed) */
|
||||
app_wdt_init();
|
||||
|
||||
#if (CFG_APP_NS_IUS)
|
||||
if(CURRENT_APP_START_ADDRESS == NS_APP1_START_ADDRESS){
|
||||
NS_LOG_INFO("application 1 start new ...\r\n");
|
||||
@ -189,10 +246,6 @@ int main(void)
|
||||
/* frame protocol frontend: CLI-over-BLE (rdtss characteristics) */
|
||||
app_ble_proto_init();
|
||||
|
||||
// periph init
|
||||
LedInit(LED1_PORT,LED1_PIN); // power led
|
||||
LedInit(LED2_PORT,LED2_PIN); //connection state
|
||||
|
||||
/* Load APP_DATA parameter record (defaults in RAM when invalid) */
|
||||
app_params_init();
|
||||
|
||||
@ -200,10 +253,18 @@ int main(void)
|
||||
* deep sleep conflicts with the FreeRTOS tick). */
|
||||
ns_sleep_lock_acquire();
|
||||
|
||||
/* status LEDs: LED1(PB1) running heartbeat, LED2(PB2) BLE link */
|
||||
LedInit(LED1_PORT, LED1_PIN);
|
||||
LedInit(LED2_PORT, LED2_PIN);
|
||||
LedOff(LED2_PORT, LED2_PIN);
|
||||
|
||||
/* BLE kernel schedule task (replaces the bare-metal while loop) */
|
||||
xTaskCreate(ble_schedule_task, "BLE", BLE_SCHED_TASK_STACK, NULL,
|
||||
BLE_SCHED_TASK_PRIORITY, NULL);
|
||||
/* LED1 blink task */
|
||||
/* YF30137 7-seg display scan task (Charlieplexing over 7 GPIOs) */
|
||||
xTaskCreate(display_task, "DISP", DISP_TASK_STACK, NULL,
|
||||
DISP_TASK_PRIORITY, NULL);
|
||||
/* LED1 running heartbeat blink task */
|
||||
xTaskCreate(led_task, "LED", LED_TASK_STACK, NULL,
|
||||
LED_TASK_PRIORITY, NULL);
|
||||
/* CLI task (command parsing/execution/response over UART + BLE) */
|
||||
|
||||
BIN
tools/N32WB03x_DFP.1.4.0.pack
Normal file
BIN
tools/NSpyocd/NSpyocd.exe
Normal file
@ -13,8 +13,10 @@ rem Requires tools\.venv-ble (created automatically on first run).
|
||||
rem ============================================================
|
||||
setlocal
|
||||
set TOOLS=%~dp0
|
||||
set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
|
||||
rem 优先使用随包内嵌的 Python 运行时(tools\python-embed,免安装);
|
||||
rem 没有则回退到自动创建的 .venv-ble(开发机)
|
||||
set PYEXE=%TOOLS%python-embed\python.exe
|
||||
if not exist "%PYEXE%" set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
if not exist "%PYEXE%" (
|
||||
echo [setup] creating venv %TOOLS%.venv-ble ...
|
||||
set PYREAL=
|
||||
@ -24,7 +26,7 @@ if not exist "%PYEXE%" (
|
||||
)
|
||||
if not defined PYREAL set PYREAL=python
|
||||
%PYREAL% -m venv "%TOOLS%.venv-ble" || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet bleak || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet -r "%TOOLS%requirements-ble.txt" || goto :fail
|
||||
)
|
||||
|
||||
set MODE=-rw
|
||||
|
||||
@ -13,8 +13,10 @@ rem Requires tools\.venv-ble (created automatically on first run).
|
||||
rem ============================================================
|
||||
setlocal
|
||||
set TOOLS=%~dp0
|
||||
set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
|
||||
rem 优先使用随包内嵌的 Python 运行时(tools\python-embed,免安装);
|
||||
rem 没有则回退到自动创建的 .venv-ble(开发机)
|
||||
set PYEXE=%TOOLS%python-embed\python.exe
|
||||
if not exist "%PYEXE%" set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
if not exist "%PYEXE%" (
|
||||
echo [setup] creating venv %TOOLS%.venv-ble ...
|
||||
set PYREAL=
|
||||
@ -24,7 +26,7 @@ if not exist "%PYEXE%" (
|
||||
)
|
||||
if not defined PYREAL set PYREAL=python
|
||||
%PYREAL% -m venv "%TOOLS%.venv-ble" || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet bleak || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet -r "%TOOLS%requirements-ble.txt" || goto :fail
|
||||
)
|
||||
|
||||
"%PYEXE%" "%TOOLS%ble_ota_update.py" %1 %2 %3
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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/
|
||||
END/ABORT -> framed OTA_RSP acks).
|
||||
protocol per docs/ble_protocol.md sections 6.6/6.7 (0xCA frames,
|
||||
OTA_BEGIN/DATA/END/ABORT -> framed OTA_RSP acks).
|
||||
|
||||
Two channels:
|
||||
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
|
||||
serial link to binary frame mode (marker-confirmed
|
||||
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
|
||||
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).
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
ST_OK = 0
|
||||
ST_BAD_FRAME = 1
|
||||
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]
|
||||
|
||||
|
||||
@ -157,7 +168,7 @@ class UartTransport:
|
||||
MARKER_ON = b"[ota] binary mode ON"
|
||||
PROMPT = b"caiic->"
|
||||
|
||||
def __init__(self, port, baud=115200):
|
||||
def __init__(self, port, baud=460800):
|
||||
import serial
|
||||
self.ser = serial.Serial(port, baud, timeout=0.1)
|
||||
self.dec = FrameDecoder()
|
||||
@ -414,6 +425,107 @@ async def ota_session(xfer, chunk, blob, version, target):
|
||||
# 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():
|
||||
from bleak import BleakScanner
|
||||
print("scanning for %s* ..." % NAME_PREFIX)
|
||||
@ -490,7 +602,7 @@ async def run_ble(pkg_path):
|
||||
return 1
|
||||
|
||||
|
||||
async def run_uart(port, pkg_path):
|
||||
async def run_uart(port, pkg_path, lockstep=False):
|
||||
version, banks = parse_combo(pkg_path)
|
||||
print("package: version %s, bank1 %dB / bank2 %dB"
|
||||
% (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)))
|
||||
|
||||
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)
|
||||
except Exception as exc:
|
||||
print("FAIL: %s" % exc)
|
||||
@ -539,12 +661,18 @@ async def run_uart(port, pkg_path):
|
||||
|
||||
|
||||
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
|
||||
if "--uart" in sys.argv[1:]:
|
||||
i = sys.argv[1:].index("--uart")
|
||||
uart = sys.argv[1:][i + 1]
|
||||
args = [a for j, a in enumerate(sys.argv[1:]) if j not in (i, i + 1)]
|
||||
skip = -1
|
||||
if "--uart" in lower:
|
||||
i = lower.index("--uart")
|
||||
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__))
|
||||
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)
|
||||
return 1
|
||||
if uart:
|
||||
return asyncio.run(run_uart(uart, pkg))
|
||||
return asyncio.run(run_uart(uart, pkg, lockstep))
|
||||
return asyncio.run(run_ble(pkg))
|
||||
|
||||
|
||||
|
||||
@ -1,45 +1,29 @@
|
||||
@echo off
|
||||
rem ============================================================
|
||||
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 Connect NS-LINK to SWD (PA4/PA5) and the reset pin first,
|
||||
rem and CLOSE Keil (it holds the probe).
|
||||
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\mothercup_ble_prod.hex
|
||||
if not "%IMAGE:~0,1%"=="\" if not "%IMAGE:~1,1%"==":" set IMAGE=%~dp0%IMAGE%
|
||||
|
||||
if not exist "%IMAGE%" (
|
||||
echo %IMAGE% not found - run tools\make_package.bat first
|
||||
rem 优先使用随包内嵌的 Python 运行时(tools\python-embed,免安装)
|
||||
set PYEXE=%~dp0python-embed\python.exe
|
||||
if not exist "%PYEXE%" (
|
||||
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
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
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 --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
|
||||
%PYEXE% "%~dp0flash_package.py" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
119
tools/flash_package.py
Normal file
@ -0,0 +1,119 @@
|
||||
#!/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__)))
|
||||
TOOLS = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Prefer the copies shipped inside tools/ (portable production package);
|
||||
# fall back to the repo's nations-tec/ SDK layout (developer machine).
|
||||
_local_pyocd = os.path.join(TOOLS, "NSpyocd", "NSpyocd.exe")
|
||||
_local_pack = os.path.join(TOOLS, "N32WB03x_DFP.1.4.0.pack")
|
||||
PYOCD = _local_pyocd if os.path.isfile(_local_pyocd) else \
|
||||
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 = _local_pack if os.path.isfile(_local_pack) else \
|
||||
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())
|
||||
@ -26,8 +26,12 @@ 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...
|
||||
rem 优先随包内嵌的 Python 运行时(merge_image.py 只需标准库)
|
||||
set PYEXE=
|
||||
python --version >nul 2>&1 && set PYEXE=python
|
||||
if exist "%~dp0python-embed\python.exe" set PYEXE="%~dp0python-embed\python.exe"
|
||||
if not defined 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"
|
||||
|
||||
11
tools/make_release.bat
Normal file
@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
rem ============================================================
|
||||
rem make_release.bat - pack tools\ into out\mothercup_tools_<ts>.zip
|
||||
rem Self-contained production package: embedded Python runtime
|
||||
rem (python-embed\) + NSpyocd flasher included - the target PC
|
||||
rem needs nothing installed. Unzip anywhere and run the .bat files.
|
||||
rem ============================================================
|
||||
setlocal
|
||||
set PS=powershell
|
||||
%PS% -noprofile -command "exit 0" >nul 2>&1 || set PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe
|
||||
%PS% -noprofile -executionpolicy bypass -file "%~dp0make_release.ps1"
|
||||
14
tools/make_release.ps1
Normal file
@ -0,0 +1,14 @@
|
||||
# make_release.ps1 - pack tools/ into a self-contained production zip.
|
||||
# The zip holds the embedded Python runtime (python-embed/, bleak+pyserial
|
||||
# preinstalled) and NSpyocd, so the target PC needs NO installation.
|
||||
# Excludes the dev-only venv and caches; keeps out/ (the firmware images).
|
||||
# NOTE: the zip is staged in $TEMP first - writing it into out/ while
|
||||
# reading out/ would make Compress-Archive read its own half-written file.
|
||||
$tools = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$stamp = Get-Date -Format "yyyyMMdd_HHmm"
|
||||
$stage = Join-Path $env:TEMP "mothercup_tools_$stamp.zip"
|
||||
$final = Join-Path $tools "out\mothercup_tools_$stamp.zip"
|
||||
$items = Get-ChildItem $tools | Where-Object { $_.Name -notin @('.venv-ble', '__pycache__') }
|
||||
Compress-Archive -Path ($items | ForEach-Object { $_.FullName }) -DestinationPath $stage -Force -ErrorAction Stop
|
||||
Move-Item $stage $final -Force
|
||||
Write-Output "packed: $final"
|
||||
@ -175,6 +175,28 @@ def load_bin(path, desc):
|
||||
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):
|
||||
"""Extract APP_FW_VERSION_NUM from the APP project's app_version.h.
|
||||
Returns None when not found."""
|
||||
@ -233,6 +255,13 @@ def main():
|
||||
version = read_app_fw_version(root) or 0
|
||||
|
||||
boot_blob = load_bin(os.path.join(root, BOOT_BIN), "bootloader")
|
||||
# The boot region is 16KB (bootsetting starts at 0x01004000) - a fatter
|
||||
# bootloader would silently overlap the bootsetting sector.
|
||||
if len(boot_blob) > BOOTSETTING_ADDR - BOOT_ADDR:
|
||||
raise SystemExit("ERROR: bootloader bin %d bytes exceeds the 16KB "
|
||||
"boot region - trim it before packaging"
|
||||
% len(boot_blob))
|
||||
check_initial_sp(boot_blob, "bootloader")
|
||||
app_path = app_rel if os.path.isabs(app_rel) else os.path.join(root, app_rel)
|
||||
app_blob = load_bin(app_path, "APP1")
|
||||
|
||||
@ -252,6 +281,13 @@ def main():
|
||||
elif 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)
|
||||
|
||||
segments = [
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"file": "caiic_ble_dual_ota.bin",
|
||||
"target_bank": 1,
|
||||
"size": 55408,
|
||||
"crc32": "0xA87539ED",
|
||||
"version": 65559
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"file": "caiic_ble_dual_ota_app2.bin",
|
||||
"target_bank": 2,
|
||||
"size": 55708,
|
||||
"crc32": "0x03825A0D",
|
||||
"version": 65559
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"file": "mothercup_ble_prod_ota.bin",
|
||||
"target_bank": 1,
|
||||
"size": 55408,
|
||||
"crc32": "0xA87539ED",
|
||||
"version": 65559
|
||||
"size": 60168,
|
||||
"crc32": "0xC79D2450",
|
||||
"version": 65579
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"file": "mothercup_ble_prod_ota_app2.bin",
|
||||
"target_bank": 2,
|
||||
"size": 55708,
|
||||
"crc32": "0x03825A0D",
|
||||
"version": 65559
|
||||
"size": 60468,
|
||||
"crc32": "0xC6D610CA",
|
||||
"version": 65579
|
||||
}
|
||||