第四节:数据链路层——帧结构定义、起始位、数据位、校验位与停止位格式
各位工程师朋友,咱们今天聊聊数据链路层里最基础、也最容易被忽视的东西——帧结构。
说实话,我见过不少刚入行的同事,一上来就盯着应用层协议猛看,觉得帧结构这种“底层活儿”交给硬件就行了。嗯,这种想法其实挺危险的。我在现场调试时,至少有一半的通信故障,追根溯源都是帧结构没搞对。
你想想看,地铁车门系统里,一列车几十个门,每个门控制器都在一条总线上收发数据。要是帧结构定义不清楚,那数据乱套是迟早的事。
4.1 帧结构长什么样?
咱们先看一个典型的帧结构。以我参与过的某条线路为例,车门控制器和门控单元(DCU)之间用的是自定义的串行协议,帧结构大致是这样的:
| 起始位 (1 bit) | 数据位 (8 bits) | 校验位 (1 bit) | 停止位 (1 bit) |
看着简单吧?但每个字段都有讲究。我一个个说。
4.2 起始位——别小看这个“1”
起始位的作用,说白了就是告诉接收方:“嘿,我要发数据了,你准备好!”
在咱们地铁车门协议里,起始位通常是一个逻辑“0”。为什么是0而不是1?我个人习惯用0,因为总线在空闲时是高电平,突然拉低这个跳变,接收端很容易检测到。你想想看,要是用1做起始位,空闲时也是1,那接收端怎么区分是起始位还是空闲状态?
我在项目中遇到过一个问题:某次调试时,车门偶尔会误动作。查了半天,发现是起始位检测阈值设得太宽了,总线上的噪声毛刺被当成了起始位。后来我们把起始位检测加了一个时间窗口——只有低电平持续超过半个位时间才认为是有效起始位。问题就解决了。
4.3 数据位——真正要传的东西
数据位就是帧的“正文”。咱们车门协议里,数据位通常是8位,也就是一个字节。
为什么是8位?其实早期有些协议用7位甚至5位,但8位是最通用的,一个字节刚好能表示0-255,或者一个ASCII字符。我个人建议,除非有特殊需求,否则就用8位数据位,兼容性最好。
这里有个细节:数据位是低位在前还是高位在前?
嗯,这得看协议定义。我参与过的项目里,大部分是低位在前(LSB first)。为什么?因为串口硬件(比如UART)默认就是低位先发。你要是非要用高位在前,就得在软件里做位序反转,增加不必要的开销。
举个例子,你要发送数据0x55(二进制01010101),低位在前的话,线上实际顺序是:
1 0 1 0 1 0 1 0
高位在前的话,顺序反过来:
0 1 0 1 0 1 0 1
你想想看,要是发送端和接收端位序不一致,那数据就全乱了。我曾经在联调时遇到过这个问题——两边工程师各写各的代码,一个用低位在前,一个用高位在前,结果数据对不上,折腾了两天才发现。
4.4 校验位——数据完整性的守门员
校验位的作用,就是检查数据在传输过程中有没有出错。地铁车门系统对安全性要求极高,校验位不是可选项,是必选项。
常见的校验方式有两种:
- 奇校验:数据位中1的个数加上校验位,总共有奇数个1。
- 偶校验:数据位中1的个数加上校验位,总共有偶数个1。
举个例子,数据位是0x55(二进制01010101),里面有4个1,是偶数。如果用偶校验,校验位就是0,保持偶数;如果用奇校验,校验位就是1,凑成奇数。
我个人习惯用偶校验。为什么?没有特别的原因,就是早期跟的一个项目用了偶校验,后来就一直沿用了。其实奇偶校验在检测能力上没区别,都能检测奇数个位错误。但要注意,如果两个位同时出错,奇偶校验是检测不出来的。
我曾经在一条老线路上遇到过这种情况:车门偶尔报通信故障,但重新上电又好了。查了很久,发现是总线上的共模干扰导致数据位同时翻转两位,奇偶校验根本发现不了。后来我们升级了协议,改用CRC校验,才彻底解决。
4.5 停止位——给接收方一点喘息时间
停止位是帧的结尾,通常是一个逻辑“1”。它的作用有两个:
- 告诉接收方“这帧发完了”
- 给接收方留出处理时间
停止位的长度可以是1位、1.5位或2位。地铁车门协议里,我见过的大多用1位停止位。为什么?因为1位就够用了,用2位会降低传输效率。
但有一种情况例外:如果接收端的时钟精度不够,或者波特率比较高,用2位停止位可以给接收端更多时间来做采样。我有个朋友做的一个项目,波特率设到了115200,停止位用1位时偶尔丢帧,改成2位就好了。说白了,就是给接收端多留了点“缓冲”。
4.6 完整的帧传输过程
咱们把整个过程串起来看一遍。假设要发送数据0x55,采用偶校验、1位停止位:
空闲状态:总线为高电平(1)
起始位: 0
数据位: 1 0 1 0 1 0 1 0 (低位在前)
校验位: 0 (因为数据位有4个1,偶数,偶校验保持偶数)
停止位: 1
空闲状态:总线恢复高电平
你想想看,接收端收到这一串信号后,怎么解析?
- 检测到起始位(从1跳变到0)
- 开始采样数据位,按低位在前拼成一个字节
- 读取校验位,计算奇偶是否匹配
- 检测到停止位(回到1),确认帧结束
任何一个步骤出错,接收端就会丢弃这帧数据,或者报错。这就是数据链路层最基本的“帧同步”机制。
4.7 避坑指南——我踩过的几个坑
最后,分享几个我亲身踩过的坑,希望能帮你少走弯路:
- 坑一:起始位检测太灵敏。 我曾经在一个项目里,接收端把总线上的噪声当成了起始位,结果收到一堆乱码。后来加了“低电平持续时间检测”,才解决。
- 坑二:位序搞反。 前面说过,发送端和接收端位序不一致,数据全乱。这个坑我踩过两次,第一次是跟第三方设备对接,第二次是跟同事联调。从此以后,我写协议文档时一定会用粗体标注位序。
- 坑三:停止位不够长。 有次调试,波特率设得比较高,停止位用1位,结果接收端老是丢帧。后来发现是接收端的时钟有偏差,采样点没对准。改成2位停止位就好了。
- 坑四:校验方式不统一。 跟不同厂商的设备对接时,一定要确认校验方式。我遇到过一家厂商用奇校验,另一家用偶校验,结果两边都报校验错误,互相甩锅。
好了,关于帧结构就聊这么多。下一节咱们聊聊更高级的——多字节帧和超时处理。到时候见。