3.1 串口协议帧与多机通信


3.1 串口协议帧与多机通信

本节摘要:Serial.println 只是调试拐杖,设备与设备之间的串口必须是一条带纪律的数据通道。本节给出一个"帧头、长度、命令、载荷、校验"五段式协议帧设计法,比较文本协议与二进制协议的取舍,并以两块开发板之间的命令应答为例给出完整可跑的双端实现——读完你就能为自己的项目定制通信协议,而不是套用别人的 println。

第 2 章末尾的实录已经证明:串口的可靠性一半在物理层、一半在协议层。物理层的排查方法上一章给过了,本章 3.1 从协议层接手:字节送达之后,怎么让两台设备对"这一串字节是什么意思"达成毫无歧义的共识。这是串口从调试工具变成通信骨干的分水岭。

从 println 到协议帧,缺的是什么

println 通道有三个先天缺陷。无边界的长度:接收方读到换行才知道一条结束,数据里出现换行内容就乱套。无完整性保障:传输中任何一位翻转,接收方收到的都是"看起来合法的错误内容"。无身份标识:一条消息是心跳、是数据还是命令,接收方只能靠猜。

协议帧逐条补齐:帧头定边界,长度字段定范围,命令字段定身份,载荷放数据,校验和兜底完整性。一个经典五段式如下:

| 帧头 0xAA 0x55 | 长度 1 字节 | 命令 1 字节 | 载荷 N 字节 | 校验 1 字节 | 固定两字节 不含校验 消息身份 变长数据 长度+命令+载荷累加和低8位

帧头选 0xAA 0x55 这类位型对称的字节对,是为了在字节流里重同步时容易与随机数据区分。校验用累加和虽不如 CRC 稳壮,但对单字节翻转足够,且计算量近乎为零;要求高的链路可升级 CRC16,第 6 章的 Modbus 对话会见到它。

文本协议还是二进制协议

这是协议设计的第一个岔路口。文本协议(每行一条 JSON 或键值对)人眼可读、调试方便、字段可扩展,代价是体积大、解析要占内存——一条"温度 25.3 度"的 JSON 快 30 字节,而二进制帧只要 8 字节。二进制协议紧凑高效、解析 O(1),代价是调试要靠工具、加字段要双方同步升级。

工程上的成熟做法是看链路两端是谁:跟人打交道(日志、配置、低频上报)用文本;跟设备打交道(高频采样、带宽紧张、MCU 对 MCU)用二进制。别用道德判断代替工程判断——"JSON 简单所以先进"和"二进制专业所以硬核"都是外行话,合适与否只看约束。

图:一条命令的应答时序

图:一条命令的应答时序

注意超时与重发不是可选项:串口是全双工的,但没有超时的协议等于要求链路永远不出错——而 2.3 节刚刚证明过它一定会出错。超时时间取"正常应答时间的 5 到 10 倍",重发上限两到三次,超限上报链路层故障,让上层决定降级还是告警。

双机通信的完整实现

下面是主机端的帧组包与应答解析,配合第 2 章的中断接收缓冲使用:

// 主机端:组包发送命令帧,带超时等待应答 #include "RingBuffer.h" // 第2章的中断接收缓冲 bool sendCommand(uint8_t cmd, const uint8_t *payload, uint8_t len, uint8_t *reply, uint16_t *replyLen) { uint8_t frame[64]; uint8_t idx = 0, sum = 0; frame[idx++] = 0xAA; frame[idx++] = 0x55; // 帧头 frame[idx++] = len + 2; // 长度 = 命令 + 载荷 + 校验 frame[idx++] = cmd; for (uint8_t i = 0; i < len; i++) { frame[idx] = payload[i]; sum += frame[idx++]; } frame[idx++] = (sum + cmd + len + 2) & 0xFF; // 校验 Serial1.write(frame, idx); // 发出 uint32_t deadline = millis() + 100; // 超时窗口 while (millis() < deadline) { if (tryParseFrame(reply, replyLen)) return true; // 从接收缓冲解帧 } return false; // 超时:上层决定重发或降级 }

输入是命令号与载荷,输出是应答载荷与成功标志。解帧函数 tryParseFrame 在字节流里找帧头、按长度字段收全帧、验校验,任何一步失败都退回重新同步——这正是帧头存在的意义:让接收方在任意污损后都能找回节奏。

案例:一次"偶发无应答"的协议层定位

背景:双机方案里,主机每秒发一次读数命令,从机偶发不回。直觉指向 2.3 节的老朋友"接收丢字节",但已确认双方都用了中断缓冲,主循环也没有长阻塞。

操作:给从机加一行统计——每收到一帧就记录校验是否通过。跑一晚发现:从机侧共收到主机 86400 条命令,其中 3 条校验错误、被静默丢弃;而主机侧只重发了一次。真相是:3 条坏帧只重发了 1 次,剩下 2 次主机超时后没有重发就放过了。

结果与解读:物理层确实偶有误码(长线、干扰),这在预期内;真正的问题是协议状态机没有把"校验失败"与"无应答"区别对待——两者都该触发重发。修正后无应答率降为零。这个案例的教训:丢帧不可怕,丢帧后的策略缺失才可怕。协议的可靠性来自"检测 + 重试"的闭环,而不是指望链路完美。

变式:多机共线的场景(一主多从的 RS485)只需两处扩展:命令帧加一字节目标地址,从机只应答与自己匹配的帧;总线方向由主机控制,任何时刻只有一个从机有权说话。RS485 芯片负责把 TTL 转成差分信号,通信距离从几米拉到上千米——协议不变,物理层升级而已。

⚠️ 常见坑:用 float 直接填进二进制帧是可移植性陷阱——不同平台的字节序与浮点格式未必一致,跨平台协议请固定用整数字段乘以缩放系数(如温度乘 100 存为 int16),解码端再除回来。

💡 关键直觉:设计协议时先想"字节流在半路被撕开怎么办"。边界、校验、超时、重同步,四个机制都在回答这一个问题;把这一个问题想透,协议设计就成了一半。

本节要点回顾

  • 五段式协议帧:帧头、长度、命令、载荷、校验,各管一件事,缺一段就少一道防线;
  • 文本与二进制按链路两端选:对人用文本,对设备用二进制,不谈主义只看约束;
  • 超时与重发是协议的闭环,重发上限与链路故障上报缺一不可;
  • 校验失败与无应答要同策:都触发重试,可靠性来自闭环而非链路完美;
  • 跨平台用定长整数字段,别让 float 的字节序差异埋雷。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U