4、串口通信协议设计:帧头、帧尾、数据长度、校验和(CRC/累加和)的设计思路
好,咱们接着聊。上一章我们把串口调通了,数据能收能发了。但问题来了——你发一串字节过去,接收端怎么知道这一包数据从哪开始、到哪结束?中间有没有被干扰?数据对不对?
这就引出了我们今天的主角:通信协议。说白了,就是给数据定个规矩,让收发双方能“对上话”。
4.1 为什么需要协议?
我刚开始做嵌入式那会儿,也犯过傻。直接把传感器数据往串口里扔,结果接收端那边显示出来的全是乱码。后来才明白——没有协议的串口,就像没有交通规则的路口,数据包撞车是迟早的事。
一个靠谱的协议,至少得解决三个问题:
- 定界:哪几个字节是一包?
- 完整性:这包数据有没有被截断或合并?
- 正确性:传输过程中有没有发生比特翻转?
嗯,这三个问题,分别对应帧头帧尾、数据长度、校验和。咱们一个一个说。
4.2 帧头与帧尾:给数据包画个边界
帧头,就是一包数据的起始标志。我习惯用 0xAA 或 0x55 这种有规律的字节。为什么?因为它们在示波器上看波形特别明显,调试时一眼就能认出来。
帧尾呢,就是结束标志。常见的有 0x0D 0x0A(回车换行),或者 0xCC 这种。
0xAA,帧尾用 0x55。否则接收端容易误判边界。
举个例子,一个最简单的协议帧结构:
帧头(1字节) | 数据长度(1字节) | 数据(N字节) | 校验和(1字节) | 帧尾(1字节)
0xAA | 0x05 | [5字节数据] | 0x?? | 0x55
接收端怎么工作?它不断读串口,一旦读到 0xAA,就认为新包开始了。然后读长度字段,知道后面要收多少字节。最后读到 0x55,一包结束。
0xAA 或 0x55,就会造成误判。这叫“帧头帧尾冲突”。解决办法是——用转义字符,或者干脆把帧头帧尾设成不可能出现在数据中的值。
4.3 数据长度:别多收也别少收
数据长度字段,告诉接收端“这包数据里有多少个有效字节”。
我个人习惯把长度字段放在帧头后面、数据前面。这样接收端一读到帧头,马上就知道要等多久才能收完。
长度字段本身占几个字节?看情况:
- 如果一包数据不超过255字节,1字节就够了。
- 如果数据量比较大(比如图像数据),建议用2字节,甚至4字节。
我曾经在一个项目里吃过亏——图像数据包长度超过255字节,但我只用了1字节存长度。结果数据一长,长度字段溢出,接收端直接崩溃。从那以后,我但凡涉及图像传输,长度字段至少用2字节。
4.4 校验和:数据到底对不对?
串口传输虽然稳定,但也不是100%可靠。电磁干扰、线缆接触不良,都可能导致某一位翻转。校验和就是用来检测这种错误的。
常用的校验方式有两种:累加和 和 CRC。
4.4.1 累加和:简单粗暴
累加和的做法很简单:把帧头之后、校验和之前的所有字节加起来,取低8位(或16位)作为校验值。
uint8_t calc_checksum(uint8_t *data, uint16_t len) {
uint8_t sum = 0;
for (uint16_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
接收端收到数据后,用同样的算法算一遍,跟收到的校验和对比。一致就认为数据正确。
优点:实现简单,计算快,适合资源受限的MCU。
缺点:检测能力有限。比如两个字节同时翻转,累加和可能不变,错误就漏掉了。
4.4.2 CRC:更可靠的校验
CRC(循环冗余校验)本质上是一个多项式除法。它比累加和复杂,但检测能力也强得多——能检测出几乎所有常见的错误模式。
常用的CRC有:
- CRC-8:1字节校验,适合小数据包
- CRC-16:2字节校验,工业上最常用
- CRC-32:4字节校验,用于文件传输、网络协议
对于串口图像传输,我个人推荐 CRC-16。它占用的带宽不大,但检测能力已经非常强了。
下面是一个CRC-16的查表法实现,速度很快:
uint16_t crc16(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
4.5 完整协议帧设计示例
好了,我们把上面这些元素拼起来,看看一个完整的图像数据协议帧长什么样:
帧头(2字节) | 数据长度(2字节) | 包序号(1字节) | 图像数据(N字节) | CRC-16(2字节) | 帧尾(2字节)
0xAA 0x55 | 0x00 0x64 | 0x01 | [100字节] | 0x?? 0x?? | 0x0D 0x0A
解释一下:
- 帧头:
0xAA 0x55,两个字节,降低误判概率 - 数据长度:2字节,大端模式,这里表示100字节
- 包序号:1字节,用于检测丢包或乱序
- 图像数据:实际要传输的像素数据
- CRC-16:对帧头之后、CRC之前的所有字节计算
- 帧尾:
0x0D 0x0A,回车换行,方便用串口助手查看
4.6 协议设计的一些小建议
最后,分享几个我在实际项目中总结出来的经验:
- 协议版本号:在帧头后面加一个版本字段。万一以后协议升级,接收端能自动识别。
- 超时处理:接收端如果收到帧头后,在规定时间内没收到帧尾,就丢弃这包数据。防止死等。
- 调试友好:帧尾用
0x0D 0x0A,这样你用串口助手看原始数据时,每包数据自动换行,一目了然。 - 不要过度设计:如果只是传几个温度值,用累加和就够了。别一上来就上CRC-32,浪费CPU和带宽。
嗯,协议设计这块,说白了就是“够用就好”。你想想看,一个简单的传感器节点,你给它配个CRC-32,MCU算半天,电池也耗得快,何必呢?
下一章,我们聊聊怎么用Python写个上位机,把串口收到的图像数据解析出来,显示在屏幕上。到时候你就知道,今天设计的这个协议到底好不好用了。