输入关键词开始搜索

通信协议设计

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 计算范围 ──────────────────→
字段大小说明
Header2 bytes帧同步头,固定 0xA5 0x5A
Length1 bytePayload 字节数,范围 0~255
Type1 byte帧类型,见下表
PayloadN bytes可变长度负载,格式取决于 Type
CRC162 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++ 类型字节数范围
0x01uint8_t10 ~ 255
0x02uint16_t20 ~ 65535
0x03int16_t2-32768 ~ 32767
0x04float4IEEE 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"