3. UniPro数据链路层(L2)详解
数据链路层,也就是L2层,是UniPro协议栈里我最喜欢跟人聊的一层。为什么?因为它干的全是「脏活累活」——把物理层传上来的比特流,整理成规规矩矩的数据帧,还要管流量、管重传、管链路死活。说白了,L2层就是整个UniPro系统的交通警察兼快递员。
我个人习惯把L2层拆成四个核心模块来看:帧结构、流控、重传、链路管理。咱们一个一个聊。
3.1 帧结构(PA_Data)——快递包裹长什么样
UniPro的L2层数据单元叫PA_Data。嗯,这个名字有点拗口,你把它想象成一个快递包裹就行。
一个完整的PA_Data帧长这样:
+--------+--------+--------+--------+--------+--------+--------+--------+
| SOF | Seq# | DevID | CPort | Data | ECC | CRC | EOF |
| (1B) | (2B) | (1B) | (1B) | (0-..) | (1B) | (2B) | (1B) |
+--------+--------+--------+--------+--------+--------+--------+--------+
每个字段都有讲究:
- SOF/EOF:帧起始和结束标记。各1字节,固定值0x7E。我在项目中遇到过有人把SOF和EOF写反了,结果链路死活对不上,排查了两天。
- Seq#:序列号,2字节。这是重传机制的基础,后面细说。
- DevID:设备ID,1字节。标识数据从哪个设备来、到哪个设备去。
- CPort:通信端口号,1字节。相当于快递单上的「收件人姓名」。
- Data:有效载荷,长度可变。0到最大传输单元(MTU)之间。
- ECC:纠错码,1字节。能纠正单比特错误,检测双比特错误。
- CRC:循环冗余校验,2字节。端到端的数据完整性保证。
关键点:ECC和CRC是两套独立的校验机制。ECC保护帧头,CRC保护整个帧。别搞混了,我见过有人只配了CRC没配ECC,结果帧头出错时整个帧被丢弃,效率极低。
3.2 流控机制(FC)——别把对方撑死
流控,说白了就是「你发慢点,我快吃不消了」。UniPro用的是基于信用(Credit)的流控方式。
怎么理解?我给你打个比方:
你手里有10张「信用券」,每发一帧就消耗一张。接收方处理完一帧,就还你一张。你手里的券用完了,就得等。就这么简单。
具体实现上,UniPro定义了两种流控消息:
| 消息类型 | 代码 | 作用 |
|---|---|---|
| FC_CREDIT | 0x01 | 发送方通知接收方「我还有X个信用可用」 |
| FC_ACK | 0x02 | 接收方确认收到数据,并返还信用 |
实战技巧:信用值的初始配置很关键。设得太小,链路利用率低;设得太大,缓冲区压力大。我一般建议初始信用设为8-16之间,具体看你的缓冲区深度和延迟要求。
我曾经在一个项目中,接收方的缓冲区只有4个帧深,但发送方初始信用设了32。结果呢?发送方一口气发了32帧,接收方缓冲区瞬间爆了,丢帧、重传、链路抖动……那叫一个惨。后来我把初始信用改成8,问题就解决了。
3.3 重传机制(ARQ)——丢了就补
UniPro用的是选择性重传ARQ(Automatic Repeat reQuest)。不是停等协议,也不是回退N步,而是「丢了哪个补哪个」。
具体流程:
- 发送方给每个帧分配一个序列号(Seq#)。
- 接收方收到帧后,检查CRC。如果正确,发ACK确认;如果错误,发NAK,并告诉发送方「我需要重传序列号为X的帧」。
- 发送方收到NAK后,只重传那个特定的帧。
- 发送方维护一个重传定时器。如果超时还没收到ACK,也触发重传。
这里有个坑:重传定时器的值怎么设?
设得太短,容易误触发重传,浪费带宽。设得太长,延迟太大。我个人的经验是:定时器值 = 2 × 往返延迟 + 处理时间余量。具体数值需要你在实际系统中测量,没有万能公式。
注意:重传次数有限制。UniPro默认最大重传次数是3次。超过这个次数,链路会被判定为故障,触发链路关闭流程。我曾经遇到过射频干扰导致连续丢帧,重传3次后链路直接断了,后来我们把最大重传次数改成了5次,同时加强了物理层的抗干扰设计。
3.4 链路启动与关闭流程
链路启动,说白了就是「握手」。UniPro的链路启动分三个阶段:
- 唤醒阶段:物理层从休眠状态唤醒,MPhy开始发送同步序列。
- 配置阶段:L2层交换配置信息,包括信用值、MTU大小、重传参数等。
- 就绪阶段:双方确认配置无误,链路进入正常工作状态。
链路关闭则相反:
- 停止阶段:发送方停止发送新数据,等待所有未确认帧处理完毕。
- 关闭阶段:交换关闭消息,释放资源。
- 休眠阶段:物理层进入低功耗模式。
下面这张图展示了完整的链路状态机:
嗯,这里要注意:链路启动过程中,配置阶段最容易出问题。我遇到过好几次,双方配置的MTU大小不一致,结果链路虽然起来了,但传大包就断。后来我在配置阶段加了一个「MTU协商」步骤,强制双方取较小的那个值,问题就解决了。
避坑指南:我曾经在链路关闭流程中忘记等待未确认帧处理完毕,直接发了关闭消息。结果接收方那边还有几个帧没确认,数据丢了,上层应用报错。后来我强制在停止阶段加了一个超时等待,确保所有帧都处理完再关闭。
好了,L2层的内容就聊到这儿。帧结构、流控、重传、链路管理,这四个模块环环相扣,缺一不可。你想想看,如果流控没做好,重传再多也没用;如果重传机制太激进,链路稳定性反而下降。做系统集成,就是要找到这些模块之间的平衡点。