通信协议设计
03 — 通信协议设计
版本:v1.0 | 日期:2026-05-30
1. 协议概述
本协议为 PulseQt 上位机与下位机/传感器之间的二进制通信协议,运行于串口(UART)或 TCP 之上。
设计目标:
- 帧边界清晰,支持粘包拆包
- 带完整性校验(CRC16)
- 开销小(最小帧 6 字节)
- 可扩展(帧类型可扩充)
2. 帧格式
┌─────────┬─────────┬────────┬────────┬──────────────┬──────────┐
│ Header │ Length │ Type │ Payload│ CRC16 │ │
│ 2 bytes │ 1 byte │ 1 byte │ N bytes│ 2 bytes │ │
├─────────┼─────────┼────────┼────────┼──────────────┼──────────┤
│ 0xA55A │ 0~255 │ 见下表 │ 变长 │ CRC16-CCITT │ │
└─────────┴─────────┴────────┴────────┴──────────────┴──────────┘
←──────────── CRC 计算范围 ──────────────────→
| 字段 | 大小 | 说明 |
|---|---|---|
| Header | 2 bytes | 帧同步头,固定 0xA5 0x5A |
| Length | 1 byte | Payload 字节数,范围 0~255 |
| Type | 1 byte | 帧类型,见下表 |
| Payload | N bytes | 可变长度负载,格式取决于 Type |
| CRC16 | 2 bytes | 对 Header+Length+Type+Payload 的 CRC16-CCITT 校验值,小端序 |
总帧长 = 6 + N 字节(N ≤ 255),最小 6 字节(空负载心跳帧),最大 261 字节。
3. 帧类型
| Type | 名称 | Payload 格式 | 说明 |
|---|---|---|---|
0x01 | 数据帧 | [ch0][ch1]...[chN] | 通道值,每个通道数据类型在握手阶段约定 |
0x02 | 心跳帧 | 空 | 保活探测,接收方应答 0x03 |
0x03 | 心跳应答帧 | 空 | 对心跳帧的应答 |
0x04 | 握手请求 | {channels:1B, types:N*1B} | 下位机告知通道数和类型 |
0x05 | 握手应答 | {result:1B} | 上位机确认(0x00=OK, 0x01=不支持的配置) |
0xFF | 错误帧 | {code:1B, msg:变长} | 错误码 + 描述文本 |
4. 通道数据类型编码
握手帧中用于约定每个通道的数据类型:
| 编码 | C++ 类型 | 字节数 | 范围 |
|---|---|---|---|
0x01 | uint8_t | 1 | 0 ~ 255 |
0x02 | uint16_t | 2 | 0 ~ 65535 |
0x03 | int16_t | 2 | -32768 ~ 32767 |
0x04 | float | 4 | IEEE 754 单精度 |
默认配置(无握手帧时):3 通道,全部 uint16_t(每个数据帧 payload = 6 字节)。
5. CRC16-CCITT 校验
// CRC16-CCITT (0x1021) — 查表法实现
static const uint16_t crc16_table[256] = { /* 预计算表 */ };
uint16_t crc16_ccitt(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF];
}
return crc;
}
校验失败处理:丢弃该帧,日志记录 WARN 级别,errorCount 计数器 +1。
6. 粘包拆包示例
场景:接收缓冲区中同时到达 2 个完整帧 + 1 个不完整帧。
接收缓冲区(字节流):
┌──────────────┬──────────────┬─────┐
│ Frame 1 (完整) │ Frame 2 (完整) │ Fra │ ← Frame 3 只到了前半部分
└──────────────┴──────────────┴─────┘
解码过程:
1. WAIT_HEADER → 找到 0xA5 0x5A → 进入 WAIT_LENGTH
2. 读 Length=6 → 读 Type=0x01 → 读 6 字节 Payload → 读 2 字节 CRC
3. CRC 校验通过 → 输出 Frame 1 → 回到 WAIT_HEADER
4. 又在当前位置找到 0xA5 0x5A → 重复步骤 2-3 → 输出 Frame 2
5. WAIT_HEADER 找到 0xA5 0x5A → WAIT_LENGTH 读 Length=12
6. WAIT_PAYLOAD 时剩余字节不足 12 → return(等待更多数据)
7. 下次 readyRead 时从 WAIT_PAYLOAD 继续
7. 心跳与重连时序
上位机 下位机
│ │
│ ←──── 数据帧 (100Hz) ──────── │ 正常通信
│ │
│ (空闲 5s) │
│ ──── 心跳帧 (0x02) ────────→ │
│ ←─── 心跳应答 (0x03) ─────── │
│ │
│ ──── 心跳帧 ────────────────→ │ (无应答)
│ (等待 30s) │
│ ──── 判定断线 ──────────────→ │
│ │
│ ──── 重连尝试 1 (1s后) ─────→ │ (失败)
│ ──── 重连尝试 2 (2s后) ─────→ │ (失败)
│ ──── 重连尝试 3 (4s后) ─────→ │ (成功!)
│ ←──── 握手请求 ────────────── │
│ ──── 握手应答 (OK) ────────→ │
│ ←──── 数据帧 ──────────────── │ 恢复通信
8. 模拟数据生成器
开发测试时使用 tools/simulator.py 模拟下位机,通过虚拟串口(socat)发送数据。
# 创建虚拟串口对
socat -d -d pty,raw,echo=0 pty,raw,echo=0
# 输出:/dev/pts/2 和 /dev/pts/3
# 一端运行模拟器
python3 tools/simulator.py /dev/pts/2
# 另一端启动上位机,连接 /dev/pts/3
对于 TCP 测试:
# 用 ncat 模拟 TCP 服务端
ncat -l 9999 -k --exec "python3 tools/simulator.py --tcp"