- boot_usart: bare-metal USART1 460800, polled TX, 3KB DMA ring RX, SysTick millis - boot_cli: polled line editor + mini printf + command subset; wire-compatible with the app UART CLI (prompt/devinfo cur bank/ota marker) so ble_ota_update.py --uart works unchanged against the bootloader - share app_ota.c/app_bootset.c into the Boot build via BOOT_FIRMWARE ifdef (single OTA engine source); frame constants moved to shared Boot/src/ota_wire.h - entry: 2s Enter window after the boot banner, or forced when no jumpable image (invalid bootsetting + APP1 vector sanity fail) - brick recovery path - OTA target in boot = inactive bank per bootsetting (APP1 when invalid) - merge_image.py: hard-fail when boot bin exceeds the 16KB region - APP V1.00.28 (shared-source touch only, behavior unchanged) - tested: normal boot / download CLI / OTA APP1<->APP2 round trip / bricked recovery (bootsetting+APP1 sector erased -> forced download -> OTA restore) - docs: dev log sec.52, ble_protocol sec.6.8, AGENTS.md
26 KiB
CAIIC BLE 通信协议 V1.1
适用于 mcm-ddc-ble 固件(N32WB031,Cortex-M0 64MHz,板载 256KB Flash 硅片)。 手机 APP(自研)或通用 BLE 调试工具(如 nRF Connect)通过本文协议与设备通信, 支持三类业务:CLI 命令透传、设备信息查询(温度/风扇转速等)、BLE OTA 固件升级。
1. GATT 服务
广播名:CAIIC-MCM-20260902
使用 SDK rdtss(raw data transfer server)自定义 128-bit UUID 服务:
| 项 | UUID | 属性 |
|---|---|---|
| Service | 00002760-08C2-11E1-9073-0E8AC72E1001 |
Primary Service |
| 下行特征(手机→设备) | 00002760-08C2-11E1-9073-0E8AC72EE001 |
Write Without Response |
| 上行特征(设备→手机) | 00002760-08C2-11E1-9073-0E8AC72EE002 |
Notify(需写 CCCD=0x0001 使能) |
| CLI 读写特征 | 00002760-08C2-11E1-9073-0E8AC72EE003 |
Read + Write(带响应):写原始命令行(≤63B)→ 分包读回输出文本(见 §6.5) |
| 只读参数特征 | 00002760-08C2-11E1-9073-0E8AC72EE004 |
Read:全部信息项 TLV(同 INFO_RSP(all),见 §5) |
| OTA 特征 | 00002760-08C2-11E1-9073-0E8AC72EE005 |
Read + Write(带响应):OTA 传输帧下行(写)→ OTA_RSP 帧读回(见 §6.6) |
MTU:设备在连接建立后主动发起 MTU 交换(请求 247);手机端也可自行发起。 一次 BLE 写入/通知的最大数据量 = att_mtu − 3(默认 MTU 23 时为 20B)。 一个协议帧可以跨多次 BLE 写入传输,一次写入也可以携带多个帧——设备侧按字节流重组。 设备上行每个 notify 固定 ≤20B(对端 MTU 未知时也能工作)。
2. 帧格式(小端)
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0 | SOF = 0xCA | 帧起始 |
| 1 | TYPE | 帧类型(见第 3 节) |
| 2 | SEQ | 序号(模 256,发送方自增;应答帧回显请求帧 SEQ) |
| 3–4 | LEN | payload 长度(0–480),小端 |
| 5.. | PAYLOAD | LEN 字节 |
| 末尾 2B | CRC16 | CRC16-CCITT(poly 0x1021,初值 0xFFFF),覆盖 TYPE..PAYLOAD,小端 |
帧开销 = 7 字节(头 5 + CRC 2)。
- 设备单帧 payload 上限 480B;超出或 CRC 错误的帧被静默丢弃并重新找 SOF。
- 设备→手机的 CLI_RSP 分块大小 = 20 − 7 = 13B(固定按默认 MTU 分包)。
3. 帧类型
| TYPE | 方向 | 名称 | payload |
|---|---|---|---|
| 0x01 | 手机→设备 | CLI_REQ | 一行命令文本(无结束符,≤63B) |
| 0x02 | 设备→手机 | CLI_RSP | 应答文本分块(可多帧) |
| 0x03 | 设备→手机 | CLI_RSP_END | {status u8}:0=ok,1=未知命令/行超长 |
| 0x10 | 手机→设备 | OTA_BEGIN | {total_size u32, image_crc32 u32, version u32} |
| 0x11 | 手机→设备 | OTA_DATA | {offset u32, data…}(offset 必须严格连续) |
| 0x12 | 手机→设备 | OTA_END | {crc32 u32}(与 OTA_BEGIN 中一致) |
| 0x13 | 手机→设备 | OTA_ABORT | 空 payload;中止会话(设备回 OTA_RSP(ok)) |
| 0x1F | 设备→手机 | OTA_RSP | {cmd_echo u8, status u8, offset u32} |
| 0x20 | 手机→设备 | INFO_QUERY | {item_id u8}×n;空 payload 或单个 0xFF = 查询全部 |
| 0x21 | 设备→手机 | INFO_RSP | TLV 序列 {id u8, len u8, value…}×n |
OTA_RSP status:0=ok,1=bad_frame,2=bad_state/offset 乱序(offset 字段=设备期望的下一字节偏移), 3=size_too_big,4=crc_fail,5=flash_fail,6=bank_mismatch(镜像链接地址与目标 bank 不符,见 §6 选包规则)。
4. CLI 透传流程
- 手机:CLI_REQ,payload 如
help、sysinfo、devinfo、led 1 toggle(与 UART CLI 命令集一致)。 - 设备:若干 CLI_RSP(命令输出的文本流分块)+ 最后一帧 CLI_RSP_END 带状态码。
- 无命令执行中时可随时发下一条;命令是同步执行的,设备应答完毕前不要再发 CLI_REQ。
UART CLI 特有的交互功能(行编辑、Tab 补全、历史、Ctrl+Z)不适用于 BLE 通道。
自测示例(默认 MTU,写入下行特征):help 命令帧 =
CA 01 00 04 00 68 65 6C 70 16 EC
5. 设备信息查询流程
手机发 INFO_QUERY,设备回一帧 INFO_RSP,payload 为请求的各信息项的 TLV 序列 (每项 {id u8, len u8, value},数值均小端)。设备不认识的 id 直接跳过、不出现在应答中。
| item_id | 名称 | 类型 | 说明 |
|---|---|---|---|
| 0x01 | FW_VERSION | u32 | 固件版本号,如 0x00010001 = V1.00.01 |
| 0x02 | CHIP_TEMP | i16 | 芯片温度,单位 0.1°C(ADC 内置温度传感器 CH7,出厂 trim 校准) |
| 0x03 | FAN_RPM | u16 | 风扇转速 rpm;0xFFFF = 无此硬件/未接入 |
| 0x04 | VDD_MV | u16 | 电源电压,单位 mV(ADC CH6) |
| 0x05 | UPTIME_S | u32 | 系统运行时间,单位 s |
| 0x06 | FREE_HEAP | u32 | FreeRTOS 堆剩余,单位 B |
| 0x07 | CUR_BANK | u8 | 当前运行 bank:1=APP1,2=APP2(按链接基址判定;OTA 选包依据,见 §6) |
示例:查询温度+风扇 = INFO_QUERY payload 02 03;
应答 INFO_RSP payload 形如 02 02 0A 01 03 02 FF FF(温度 26.6°C,风扇无硬件)。
数据来源约定:温度/电压由设备固件直接采 ADC;风扇转速由应用层注册的
数据源提供(app_fan_get_rpm(),弱符号默认返回 0xFFFF),产品板接上风扇
转速检测电路后重写该函数即可,协议不变。
6. OTA 升级流程(双 bank 直写,无中转区)
Flash 布局(256KB 实装硅片;Boot/bootsetting/APP_DATA 保持 16/8/8KB,余下 224KB 均分双 bank):
| 区域 | 地址 | 大小 |
|---|---|---|
| Bootloader | 0x01000000 | 16KB |
| bootsetting | 0x01004000 | 8KB(用 1 个 4KB 扇区) |
| APP_DATA(预留) | 0x01006000 | 8KB |
| APP1 | 0x01008000 | 112KB |
| APP2 | 0x01024000 | 112KB |
设备当前运行 APP1 则新固件写入 APP2,反之亦然。bank 上限 112KB(0x1C000)。 镜像 = Keil 产物 bin(裸二进制,从 bank 基址开始的镜像)。 flash 擦除单位 = 4KB 扇区。设备侧直写 flash,不做扇区级 RAM 缓存: 数据首次落入某个未擦除的扇区时先擦除该扇区(惰性擦除),收到的数据立即编程写入, 仅保留 4 字节对齐暂存(Qflash 写要求 4 字节对齐),不经过任何 flash 中转区。
- OTA_BEGIN:手机发送 {total_size, image_crc32, version}。
- image_crc32 = IEEE CRC32(poly 0xEDB88320,初值/异或出 0xFFFFFFFF,即 zlib crc32) 对整个镜像 bin 文件的校验值。
- version:u32,如 0x00010001 表示 V1.00.01。
- 设备回 OTA_RSP(0x10, ok, 0) 后开始接收。
- OTA_DATA:从 offset=0 起严格顺序发送,每帧 {offset, data}。
- 单帧 data 建议 ≤ 233B(MTU 247 时一次写入 244B = 帧开销 7 + offset 4 + data 233); MTU 23 时单帧总长度不得超过 20B。
- 设备收到即写入 flash;每一帧都回 OTA_RSP(0x11, ok, 新 offset)—— 逐帧锁步流控(V1.00.19 起;此前为每 4KB 扇区一答)。ack 往返时间天然覆盖 新扇区的惰性擦除窗口(数十 ms 关中断)。
- 主机节奏:逐帧等 ack(锁步)。若应答 status=2(offset 乱序), 从应答中的 offset 处重发即可重新同步。
- OTA_END:{crc32}。设备 flush 尾部(0xFF 补齐到 4 字节对齐后写最后一个扇区),
从 flash 回读整镜像复算 CRC32,与手机端比对:
- 失败 → OTA_RSP(0x12, crc_fail),会话中止,不复位;
- 成功 → 更新 bootsetting(active bank 指向新 bank,记录 size/crc32/version) → OTA_RSP(0x12, ok) → 200ms 后自动复位,bootloader 直接跳转到新固件 (跳转不做校验,校验已在 OTA 过程完成)。
- 任何时刻 BLE 断连,OTA 会话中止,对侧 bank 数据作废(可重新 OTA_BEGIN 重来)。
注意:flash 擦写期间设备关中断数十 ms/扇区(Qflash 算法在 RAM 执行),BLE 链路靠 5s supervision timeout 维持,属正常现象;但请避免在 OTA 期间主动断开。
OTA 载荷必须链接在目标 bank 地址(Cortex-M0 代码位置相关)。发布件为单文件
mothercup_ble_ota.bin(52B 头含双载荷索引与全部 CRC,格式见 §7)。客户端选包
规则:先读信息项 0x07 CUR_BANK(当前运行 bank),OTA 目标 = 对侧 bank,从头里取
对应载荷。固件侧兜底:OTA_END 整镜像 CRC 通过后,还会校验镜像向量表 Reset 地址
落在目标 bank 范围内,不符则回 OTA_RSP(status=6 bank_mismatch) 并中止、不切换
(V1.00.17 起)。
6.6 OTA 传输层:通道无关设计(V1.00.19 起)
OTA 会话(§6 的 BEGIN/DATA/END/ABORT → OTA_RSP)跑在 0xCA 帧上,帧格式与 字节流重组规则和 §2 完全一致,因此同一份协议可以跑在不同物理通道上; 设备侧 OTA 引擎与通道解耦(应答经通道注册的 sink 出口):
| 通道 | 下行(主机→设备) | 上行(设备→主机) |
|---|---|---|
BLE OTA 特征 ...e0005 |
每次"写带响应"携带一帧(≤ att_mtu−3) | 写完读同一特征取回 OTA_RSP 帧;读应答可能先于设备处理完成返回空,读需轮询至非空(擦扇区时数十 ms) |
| UART 串口(460800 8N1,V1.00.26 起;之前为 115200) | CLI 先发文本命令 ota,等到设备回标记行 [ota] binary mode ON 再发帧(握手,防帧被当文本命令吃掉),随后原始帧字节流 |
OTA_RSP 帧直接从 TX 发出;OTA_ABORT 立即退出回 CLI,10s 无帧兜底退出 |
UART 模式切换与容错(V1.00.21 起):
- 进入握手:
ota命令的标记行是 go-ahead;标记之前设备仍在文本 CLI, 提前发二进制帧会被行编辑器吃掉(帧内 0x0D 甚至会触发垃圾命令执行)。 - 退出三路:END 成功 → 复位;OTA_ABORT → 回完 ack 立即回文本 CLI;
10s 空闲兜底(host 失联自救)。host 再同步:发
\r,CLI 态立即回caiic->;二进制态无响应,等兜底超时退出。 - 丢帧恢复(锁步重传):任一帧的 ack 超时(PC 参考实现 2s)就直接
重发同一帧,最多 3 次。重复 DATA 会被设备回
BAD_STATE + 期望 offset——若该 offset 恰好等于本帧发完后的下一 offset,说明设备早已收到、只是 ack 丢了,视为 ack 继续;重复 BEGIN 同理视为会话已建立。设备侧对 100ms 字节间断的半帧自动丢弃重组器状态,保证重发帧能重新对齐; 会话全程只允许一个通道占用(他通道 BEGIN 回 BAD_STATE)。 | BLE 旧帧协议通道(e0001/e0002 notify) | 0xCA 帧写入 e0001 | notify 上行(V1.00.19 修复 CRC 后可用,见开发日志 §42) |
锁步规则:每发一帧必须等到对应 OTA_RSP 再发下一帧(ack 里 offset = 设备期望的下一字节;status≠0 时按其 offset 重发可重新同步)。 CRC16 覆盖范围 = TYPE 到 PAYLOAD 末尾(含 SEQ 与 LEN 两字节,共 4+payLen 字节)——V1.00.19 之前固件漏算最后一个 payload 字节导致全部 帧被丢弃(§42 根因),新旧固件与主机实现必须统一按 4+payLen 计算。
PC 参考实现:tools\ble_ota_update.py(ble_ota.bat;BLE 默认,
--uart COMx 走串口)。
END 应答可靠送达(V1.00.20 起):OTA_END 校验通过后,设备先发出 ok 应答,等主机取走该应答(BLE:e0005 读取消耗;UART:TX 发完)并留 300ms 宽限后才复位进入新 bank,2s 超时兜底。因此主机在正常链路上必定能 收到 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 命令形式提供,
只暴露结构化字段访问,无裸读写命令。UART CLI 与 BLE CLI 读写特征
...c72e0003(写原始命令行 → 分包读回文本应答)命令集完全相同:
| 命令 | 说明 |
|---|---|
appget |
打印 APP_DATA 参数记录与 valid/default 状态(led = LED1 闪烁半周期 ms;pwm = PWM 占空比 %);appget json 输出 JSON 对象 {"led":500,"pwm":0,"valid":1}(V1.00.14 起) |
appset <led|pwm> <val> |
修改参数并立即落盘(led 50appset json {"led":500,"pwm":50} JSON 入参(V1.00.14 起,未知键忽略、任一越界整体不生效) |
bsdump |
打印 bootsetting 记录全字段 + magic/CRC 校验结果 |
bsset <field> <val> |
写 bootsetting 单字段(active 1|2、b1addr/b2addr/b1size/b2size/b1crc/b2crc/b1ver/b2ver),自动重算 CRC、擦除+编程+校验。写错 active/addr 会导致 Boot 跳错,恢复靠 SWD 重烧整片包 |
appsw [1|2] |
切换运行 bank 并复位(V1.00.15 起):无参切对侧 bank;切前校验 bootsetting 记录、目标 bank 记录一致性,并复算目标镜像 CRC32 与记录比对,不匹配拒绝(坏 bank 需先跑 OTA 写入有效镜像) |
reset |
复位单板:应答后延时 200ms 执行 NVIC_SystemReset(),Boot 按 active bank 跳转 |
factory |
恢复出厂设置:APP_DATA 参数区恢复缺省值并落盘后复位(不动 bootsetting) |
uartrst |
复位串口:USART1 按 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 时落盘。
注意:flash 擦写关中断数十 ms/扇区,BLE 应答可能延迟;MTU 协商失败(20B/包)时
长应答会被截断到 19B,建议先协商 MTU 247。
读写特征应答读取(V1.00.13 起分包上报):命令输出捕获上限 1024B;每次读
...e0003 返回下一片(≤ att_mtu−2,单片恒为"短读",不会触发 Read Blob——
blob 偏移不下发应用层,协议栈会切错窗口),读到 0 长度包为应答结束;
写入新命令即重置分包游标。客户端必须循环读至空包。需搭配 V1.00.13+ 固件:
旧固件为单读 ≤512B 静态缓冲,重复读返回同一内容,循环读会重复拼接直至
客户端兜底次数上限。
7. 首次烧录与恢复
-
出厂/首次:SWD 烧录整包(bootloader + bootsetting 缺省配置 + APP1), 由
tools\merge_image.py/make_package.bat生成。 -
OTA 发布文件(单文件发布,V1.00.18 起):
tools\make_package.bat产出mothercup_ble_ota.bin= 52B 头 + bank1 载荷 + bank2 载荷,头已包含原清单 json 的全部信息(version/总大小/总 CRC),无需伴随 json。格式(全小端):偏移 字段 说明 0 magic u32 0xCA10BA114 hdr_len u32 = 52(头部字节数,前向兼容扩展用) 8 version u32 固件版本号( 0x00MMmmpp,写进 OTA_BEGIN 与新 bank 记录)12 total_size u32 整文件字节数(自检) 16 payload_crc u32 CRC32(hdr_len .. 文件尾),即两份载荷整体 20 count u32 = 2 24 b1_off/b1_size/b1_crc u32×3 bank1(APP1 链接,0x01008000)载荷索引 36 b2_off/b2_size/b2_crc u32×3 bank2(APP2 链接,0x01024000)载荷索引 48 hdr_crc u32 CRC32(头 0..47) CRC32 均为 IEEE(zlib 多项式,与固件
caiic_crc32一致)。 升级流程(APP/UART/PC 通用):① 校验 magic + hdr_len + hdr_crc + total_size + payload_crc;② 读设备信息项 0x07 CUR_BANK 得当前运行 bank N; ③ 取对侧 bank(3−N)的载荷(offset/size/crc 取自头部);④ OTA_BEGIN {size, crc32, version} → OTA_DATA 流 → OTA_END{crc32};⑤ 设备自动复位到 新 bank,重连后复核 CUR_BANK 与 FW_VERSION。PC 参考实现:tools\ble_ota_update.py(ble_ota.bat)。 调试用的单 bank 载荷(mothercup_ble_prod_ota.bin/_ota_app2.bin+ json) 仍在 tools/out/ 生成,但不作为发布件。 -
SWD 单 bank 升级(不动运行中的 bank,V1.00.18 实测通过):
NSpyocd load ... "mcm-ddc-ble.bin@0x01008000"(bin 后用@基址指定写入 位置,只擦写涉及扇区)→ CLIbsset修正该 bank 的 size/crc/ver →appsw切入。详见开发日志 §40。 -
OTA 失败变砖恢复:SWD 重烧即可(bootloader 不做串口 DFU)。
-
Keil 调试下载必须用扇区擦除,整片擦除会删掉 bootloader。
8. 手机端开发指南
8.1 连接与初始化步骤
- 扫描:按广播名
CAIIC-MCM-20260902过滤(或服务 UUID00002760-08C2-11E1-9073-0E8AC72E1001)。 - 连接,发现服务,确认两个特征存在:下行
...E0001(Write Without Response)、上行...E0002(Notify)。 - 使能 notify:往上行特征的 CCCD 写
0x0001(Android:setCharacteristicNotification+ 写 descriptor;iOS:setNotifyValue(true))。 - MTU:设备连接后会主动发起 MTU=247 交换;手机端也可以自己
requestMtu(247)。以协商结果为准计算单次写入上限 = mtu − 3。 - 完成以上步骤后即可开始三类业务。建议连接后发一条 INFO_QUERY(查全部)确认链路。
8.2 参考实现(Python,可直接对照移植到 Kotlin/Swift/JS)
def crc16_ccitt(data: bytes) -> int:
"""poly 0x1021, init 0xFFFF, 覆盖 TYPE..PAYLOAD"""
crc = 0xFFFF
for b in data:
crc ^= b << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return crc
def build_frame(ftype: int, seq: int, payload: bytes) -> bytes:
body = bytes([ftype, seq, len(payload) & 0xFF, (len(payload) >> 8) & 0xFF]) + payload
c = crc16_ccitt(body)
return b'\xCA' + body + bytes([c & 0xFF, c >> 8])
def parse_stream(buf: bytearray):
"""把 notify 收到的字节 append 进 buf,循环调用本函数取完整帧。
返回 (ftype, seq, payload) 或 None(数据不足)。坏帧自动丢弃并重新找 0xCA。"""
while True:
try:
i = buf.index(0xCA)
except ValueError:
buf.clear(); return None
del buf[:i]
if len(buf) < 5: return None
plen = buf[3] | (buf[4] << 8)
if plen > 480:
del buf[0]; continue # 长度不可信,丢 SOF 重找
if len(buf) < 7 + plen: return None
body = bytes(buf[1:5 + plen])
crc = buf[5 + plen] | (buf[6 + plen] << 8)
del buf[:7 + plen]
if crc16_ccitt(body) == crc:
return body[0], body[1], body[3:]
# CRC 错:继续循环重找下一个 SOF
镜像 CRC32(OTA_BEGIN / OTA_END 用):标准 zlib/IEEE CRC32,
对 mothercup_ble_ota.bin 中取出的目标 bank 载荷计算
(zlib.crc32(payload) & 0xFFFFFFFF),也可直接读头部的 bN_crc 字段。
8.3 字节级完整示例(均含 SOF 与 CRC16,SEQ 自取)
| 用途 | 字节流(hex) |
|---|---|
| CLI_REQ "help" | CA 01 00 04 00 68 65 6C 70 16 EC |
| CLI_RSP_END(status=0) 应答样例 | CA 03 00 01 00 00 EE C8 |
| INFO_QUERY 查全部 | CA 20 00 00 00 8E B3 |
| INFO_QUERY 温度+风扇 | CA 20 00 02 00 02 03 71 80 |
| INFO_RSP 样例(26.6°C + 无风扇) | CA 21 00 08 00 02 02 0A 01 03 02 FF FF C2 88 |
| OTA_BEGIN(39692B 镜像) | CA 10 00 0C 00 0C 9B 00 00 D8 90 57 F1 01 00 01 00 BB 93 |
| OTA_RSP(BEGIN ok) 应答样例 | CA 1F 00 06 00 10 00 00 00 00 00 51 B9 |
| OTA_DATA(offset=0,4B 数据) | CA 11 01 08 00 00 00 00 00 00 B1 00 20 73 CF |
| OTA_END(crc32=0xF15790D8) | CA 12 02 04 00 D8 90 57 F1 81 D8 |
8.4 时序与异常处理
CLI 透传:
- 命令串行执行:发出 CLI_REQ 后,收到 CLI_RSP_END 之前不要再发下一条。
- CLI_RSP_END.status:0=ok,1=未知命令或行超长(>63B)。
信息查询:
- INFO_RSP 的 TLV 按序解析:{id, len, value},遇到不认识的 id 按 len 跳过。
- CHIP_TEMP 为 i16(0.1°C),注意符号位;0x7FFF = 读取失败。
- FAN_RPM 0xFFFF = 无测速硬件;VDD_MV 0 = 读取失败。
OTA:
- 状态机:OTA_BEGIN(ok) → OTA_DATA×n → OTA_END → 设备自动复位。断连即会话作废,重来即可。
- 流控:设备每写满 4KB 回一帧 OTA_RSP(ok, offset)。推荐节奏:滑动窗口连续发, 每收到一帧 ok 应答就核对 offset 与已发进度一致;不需要逐帧等 ack。
- 乱序/丢帧:收到 status=2(bad_state)时,从应答 offset 处重发后续数据即可恢复,无需重来。
- status=4(crc_fail)/5(flash_fail):会话已中止,从 OTA_BEGIN 重来。
- OTA_END 成功后设备 200ms 内复位;手机端会收到断连,重连后可用 INFO_QUERY 查 FW_VERSION 确认新固件已运行。
- OTA 期间 flash 擦写会关中断数十 ms/扇区,notify 应答可能延迟,属正常;不要主动断开。
通用:
- 设备上行 notify 每包 ≤20B,一帧可能跨多个 notify 到达,手机端必须按字节流重组 (见 8.2 parse_stream),不能假设一次 notify = 一帧。
- 任何时刻收到 CRC 错误或无法解析的字节:丢弃并重新找 0xCA,链路无需重置。