mothercup/docs/ble_protocol.md

252 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CAIIC BLE 通信协议 V1.1
适用于 mcm-ddc-ble 固件(N32WB031KEQ6-2,Cortex-M0 64MHz,512KB 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 使能) |
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 中一致) |
| 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。
## 4. CLI 透传流程
1. 手机:CLI_REQ,payload 如 `help`、`sysinfo`、`devinfo`、`led 1 toggle`(与 UART CLI 命令集一致)。
2. 设备:若干 CLI_RSP(命令输出的文本流分块)+ 最后一帧 CLI_RSP_END 带状态码。
3. 无命令执行中时可随时发下一条;命令是同步执行的,设备应答完毕前不要再发 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 |
示例:查询温度+风扇 = 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 布局(512KB,官方 256KB DFU 布局每区加倍):
| 区域 | 地址 | 大小 |
|---|---|---|
| Bootloader | 0x01000000 | 16KB |
| bootsetting | 0x01004000 | 8KB(用 1 个 4KB 扇区) |
| APP_DATA(预留) | 0x01006000 | 8KB |
| APP1 | 0x01008000 | 224KB |
| APP2 | 0x01040000 | 224KB |
设备当前运行 APP1 则新固件写入 APP2,反之亦然。bank 上限 224KB(0x38000)。
镜像 = Keil 产物 bin(裸二进制,从 bank 基址开始的镜像)。
flash 擦除单位 = 4KB 扇区。设备侧**直写 flash,不做扇区级 RAM 缓存**:
数据首次落入某个未擦除的扇区时先擦除该扇区(惰性擦除),收到的数据立即编程写入,
仅保留 4 字节对齐暂存(Qflash 写要求 4 字节对齐),**不经过任何 flash 中转区**。
1. **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) 后开始接收。
2. **OTA_DATA**:从 offset=0 起严格顺序发送,每帧 {offset, data}。
- 单帧 data 建议 ≤ 233B(MTU 247 时一次写入 244B = 帧开销 7 + offset 4 + data 233);
MTU 23 时单帧总长度不得超过 20B。
- 设备收到即写入 flash;每写满一个 4KB 扇区回一帧 OTA_RSP(0x11, ok, 已写 offset)
——兼作流控与进度显示。
- 手机端节奏:可按扇区等 ack 发送,也可连续发送;若收到 status=2 的应答,
从应答中的 offset 处重发即可重新同步。
3. **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 过程完成)。
4. 任何时刻 BLE 断连,OTA 会话中止,对侧 bank 数据作废(可重新 OTA_BEGIN 重来)。
注意:flash 擦写期间设备关中断数十 ms/扇区(Qflash 算法在 RAM 执行),BLE 链路靠
5s supervision timeout 维持,属正常现象;但请避免在 OTA 期间主动断开。
## 6.5 维护命令:bootsetting 与 APP_DATA 参数区(V1.00.09 起,结构化访问)
bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形式提供,
**只暴露结构化字段访问,无裸读写命令**。UART CLI 与 BLE CLI 读写特征
`...c72e0003`(写原始命令行 → 读回文本应答,≤512B)命令集完全相同:
| 命令 | 说明 |
|---|---|
| `param` | 打印 APP_DATA 参数记录(led1_blink_ms / pwm_duty_pct)与 valid/default 状态 |
| `param set <name> <val>` | 修改参数并落盘(led1_blink_ms 50~10000 即时生效于 LED1 闪烁;pwm_duty_pct 0~100 仅存储备用) |
| `param save` | 当前 RAM 参数强制写盘 |
| `bsdump` | 打印 bootsetting 记录全字段 + magic/CRC 校验结果 |
| `bsset <field> <val>` | 写 bootsetting 单字段(`active 1\|2`、`b1addr/b2addr/b1size/b2size/b1crc/b2crc/b1ver/b2ver`),自动重算 CRC、擦除+编程+校验。**写错 active/addr 会导致 Boot 跳错,恢复靠 SWD 重烧整片包** |
参数记录(APP_DATA 区头,52B):magic `0xCA12DA7A` + layout_ver + 参数字段 +
reserved[8] + CRC32;记录无效时固件以缺省值运行,`param set`/`param save` 时落盘。
注意:flash 擦写关中断数十 ms/扇区,BLE 应答可能延迟;MTU 协商失败(20B/包)时
长应答会被截断到 19B,建议先协商 MTU 247。
## 7. 首次烧录与恢复
- 出厂/首次:SWD 烧录整包(bootloader + bootsetting 缺省配置 + APP1),
由 `tools\merge_image.py` / `make_package.bat` 生成。
- OTA 镜像:每次出整包时 `merge_image.py` 同时输出 `<包名>_ota.bin`
(OTA 载荷,即 APP bin 本体)和 `<包名>_ota.json`(清单:size / crc32 /
version,手机端 OTA_BEGIN 直接取这三个值;version 可用第三个命令行参数
指定,如 `0x00010001`,缺省 0)。
- OTA 失败变砖恢复:SWD 重烧即可(bootloader 不做串口 DFU)。
- Keil 调试下载必须用**扇区擦除**,整片擦除会删掉 bootloader。
## 8. 手机端开发指南
### 8.1 连接与初始化步骤
1. 扫描:按广播名 `CAIIC-MCM-20260902` 过滤(或服务 UUID `00002760-08C2-11E1-9073-0E8AC72E1001`)。
2. 连接,发现服务,确认两个特征存在:下行 `...E0001`(Write Without Response)、上行 `...E0002`(Notify)。
3. **使能 notify**:往上行特征的 CCCD 写 `0x0001`(Android:`setCharacteristicNotification` + 写 descriptor;iOS:`setNotifyValue(true)`)。
4. MTU:设备连接后会主动发起 MTU=247 交换;手机端也可以自己 `requestMtu(247)`。以协商结果为准计算单次写入上限 = mtu − 3。
5. 完成以上步骤后即可开始三类业务。建议连接后发一条 INFO_QUERY(查全部)确认链路。
### 8.2 参考实现(Python,可直接对照移植到 Kotlin/Swift/JS)
```python
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,
`zlib.crc32(open('xxx_ota.bin','rb').read()) & 0xFFFFFFFF`,
也可直接从 `<包名>_ota.json` 清单里读。
### 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,链路无需重置。