第四节:数据链路层——帧结构定义、起始位、数据位、校验位与停止位格式

各位工程师朋友,咱们今天聊聊数据链路层里最基础、也最容易被忽视的东西——帧结构。

说实话,我见过不少刚入行的同事,一上来就盯着应用层协议猛看,觉得帧结构这种“底层活儿”交给硬件就行了。嗯,这种想法其实挺危险的。我在现场调试时,至少有一半的通信故障,追根溯源都是帧结构没搞对。

你想想看,地铁车门系统里,一列车几十个门,每个门控制器都在一条总线上收发数据。要是帧结构定义不清楚,那数据乱套是迟早的事。

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校验,才彻底解决。

重要提醒:奇偶校验只能防“粗心”,不能防“恶意”。在安全关键系统中,建议使用CRC或更高级的校验方式。

4.5 停止位——给接收方一点喘息时间

停止位是帧的结尾,通常是一个逻辑“1”。它的作用有两个:

  • 告诉接收方“这帧发完了”
  • 给接收方留出处理时间

停止位的长度可以是1位、1.5位或2位。地铁车门协议里,我见过的大多用1位停止位。为什么?因为1位就够用了,用2位会降低传输效率。

但有一种情况例外:如果接收端的时钟精度不够,或者波特率比较高,用2位停止位可以给接收端更多时间来做采样。我有个朋友做的一个项目,波特率设到了115200,停止位用1位时偶尔丢帧,改成2位就好了。说白了,就是给接收端多留了点“缓冲”。

建议:如果系统时钟精度在±2%以内,1位停止位足够了。如果时钟精度差,或者总线负载重,可以考虑用2位。

4.6 完整的帧传输过程

咱们把整个过程串起来看一遍。假设要发送数据0x55,采用偶校验、1位停止位:

空闲状态:总线为高电平(1)
起始位:   0
数据位:   1 0 1 0 1 0 1 0 (低位在前)
校验位:   0 (因为数据位有4个1,偶数,偶校验保持偶数)
停止位:   1
空闲状态:总线恢复高电平

你想想看,接收端收到这一串信号后,怎么解析?

  1. 检测到起始位(从1跳变到0)
  2. 开始采样数据位,按低位在前拼成一个字节
  3. 读取校验位,计算奇偶是否匹配
  4. 检测到停止位(回到1),确认帧结束

任何一个步骤出错,接收端就会丢弃这帧数据,或者报错。这就是数据链路层最基本的“帧同步”机制。

4.7 避坑指南——我踩过的几个坑

最后,分享几个我亲身踩过的坑,希望能帮你少走弯路:

  • 坑一:起始位检测太灵敏。 我曾经在一个项目里,接收端把总线上的噪声当成了起始位,结果收到一堆乱码。后来加了“低电平持续时间检测”,才解决。
  • 坑二:位序搞反。 前面说过,发送端和接收端位序不一致,数据全乱。这个坑我踩过两次,第一次是跟第三方设备对接,第二次是跟同事联调。从此以后,我写协议文档时一定会用粗体标注位序。
  • 坑三:停止位不够长。 有次调试,波特率设得比较高,停止位用1位,结果接收端老是丢帧。后来发现是接收端的时钟有偏差,采样点没对准。改成2位停止位就好了。
  • 坑四:校验方式不统一。 跟不同厂商的设备对接时,一定要确认校验方式。我遇到过一家厂商用奇校验,另一家用偶校验,结果两边都报校验错误,互相甩锅。

好了,关于帧结构就聊这么多。下一节咱们聊聊更高级的——多字节帧和超时处理。到时候见。