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步,而是「丢了哪个补哪个」。

具体流程:

  1. 发送方给每个帧分配一个序列号(Seq#)。
  2. 接收方收到帧后,检查CRC。如果正确,发ACK确认;如果错误,发NAK,并告诉发送方「我需要重传序列号为X的帧」。
  3. 发送方收到NAK后,只重传那个特定的帧。
  4. 发送方维护一个重传定时器。如果超时还没收到ACK,也触发重传。

这里有个坑:重传定时器的值怎么设?

设得太短,容易误触发重传,浪费带宽。设得太长,延迟太大。我个人的经验是:定时器值 = 2 × 往返延迟 + 处理时间余量。具体数值需要你在实际系统中测量,没有万能公式。

注意:重传次数有限制。UniPro默认最大重传次数是3次。超过这个次数,链路会被判定为故障,触发链路关闭流程。我曾经遇到过射频干扰导致连续丢帧,重传3次后链路直接断了,后来我们把最大重传次数改成了5次,同时加强了物理层的抗干扰设计。

3.4 链路启动与关闭流程

链路启动,说白了就是「握手」。UniPro的链路启动分三个阶段:

  • 唤醒阶段:物理层从休眠状态唤醒,MPhy开始发送同步序列。
  • 配置阶段:L2层交换配置信息,包括信用值、MTU大小、重传参数等。
  • 就绪阶段:双方确认配置无误,链路进入正常工作状态。

链路关闭则相反:

  • 停止阶段:发送方停止发送新数据,等待所有未确认帧处理完毕。
  • 关闭阶段:交换关闭消息,释放资源。
  • 休眠阶段:物理层进入低功耗模式。

下面这张图展示了完整的链路状态机:

休眠 唤醒 配置 就绪 停止 关闭 唤醒信号 同步完成 配置确认 关闭请求 数据排空 进入休眠 UniPro L2链路状态机 启动流程:休眠 → 唤醒 → 配置 → 就绪 关闭流程:就绪 → 停止 → 关闭 → 休眠

嗯,这里要注意:链路启动过程中,配置阶段最容易出问题。我遇到过好几次,双方配置的MTU大小不一致,结果链路虽然起来了,但传大包就断。后来我在配置阶段加了一个「MTU协商」步骤,强制双方取较小的那个值,问题就解决了。

避坑指南:我曾经在链路关闭流程中忘记等待未确认帧处理完毕,直接发了关闭消息。结果接收方那边还有几个帧没确认,数据丢了,上层应用报错。后来我强制在停止阶段加了一个超时等待,确保所有帧都处理完再关闭。

好了,L2层的内容就聊到这儿。帧结构、流控、重传、链路管理,这四个模块环环相扣,缺一不可。你想想看,如果流控没做好,重传再多也没用;如果重传机制太激进,链路稳定性反而下降。做系统集成,就是要找到这些模块之间的平衡点。


专注资料整理