4. 数据链路层协议(DLLP):DLLP的组成与分类

数据链路层,说白了就是事务层和物理层之间的“翻译官”加“保安”。它不直接处理数据内容,但负责确保数据能安全、有序地到达对端。我个人习惯把DLLP(Data Link Layer Packet)看作是数据链路层的“心跳”和“信使”——它们不是真正的数据包,而是用于管理链路状态的短报文。

4.1 DLLP的组成

一个DLLP只有6个字节,结构非常紧凑。我刚开始接触PCIe时,总觉得这么短的报文能干什么?后来才发现,正是这些短小精悍的报文,撑起了整个链路的可靠性。

字段 长度 说明
DLLP类型 1字节 标识DLLP的种类(如Ack、Nak、FC等)
数据载荷 2字节 根据类型不同,携带序列号或信用量信息
CRC 2字节 16位CRC校验,覆盖前3个字节
保留位 1字节 用于字节对齐

嗯,这里要注意:DLLP的CRC只覆盖前3个字节,而不是整个6字节。为什么?因为保留位是固定的,不需要校验。我在项目中曾经踩过这个坑——自己写DLLP解析时把整个6字节都算了CRC,结果跟对端怎么都对不上。

4.2 DLLP的分类

DLLP主要分三大类,每一类都有自己的“性格”。你想想看,如果把它们比作一个团队:Ack/Nak是“质检员”,InitFC1/InitFC2是“入职培训”,UpdateFC是“库存管理员”。

4.2.1 Ack/Nak DLLP

这是数据链路层的“确认/否认”机制。发送端每发一个TLP,都会给它分配一个序列号(Sequence Number)。接收端收到后,如果CRC校验通过,就回一个Ack DLLP,告诉发送端“我收到了,序列号是N”。如果校验失败,就回一个Nak DLLP,要求重传。

核心要点:Ack/Nak是累积确认的。也就是说,Ack(N)表示“序列号N及之前的所有TLP我都收好了”。这跟TCP的累积确认很像,但PCIe的机制更轻量。

我曾经在调试一个PCIe Switch时,发现链路偶尔会卡死。抓了很长时间的DLLP日志,才发现是接收端的Nak处理逻辑有bug——它把Nak的序列号写错了,导致发送端一直重传同一个包,而其他包被活活饿死。嗯,这种问题在仿真阶段很难复现,只有在实际跑流量时才暴露。

4.2.2 InitFC1/InitFC2 DLLP

这两个是“初始化流控”报文。链路刚建立时,两端都不知道对方的接收缓冲区有多大。InitFC1和InitFC2就是用来交换这个信息的。

  • InitFC1:通告每个VC(Virtual Channel)的初始信用量。包括Posted、Non-Posted、Completion三种类型。
  • InitFC2:在InitFC1之后发送,用于确认双方都完成了初始化。

我记得第一次做PCIe验证时,看到InitFC1和InitFC2的时序图,觉得好复杂。后来自己写了一个简单的状态机,发现其实逻辑很简单:先发InitFC1,等对端的InitFC1,然后发InitFC2,再等对端的InitFC2。搞定。

4.2.3 UpdateFC DLLP

这是“动态流控更新”报文。链路正常工作后,接收端每消耗掉一些缓冲区空间,就会通过UpdateFC告诉发送端:“我又腾出X个空位了,你可以继续发。”

个人经验:UpdateFC的发送频率很关键。发得太频繁,浪费带宽;发得太慢,发送端会因信用量不足而停顿。我一般建议在缓冲区消耗掉一半时触发一次UpdateFC,这样既不会太频繁,也不会让发送端等太久。

数据链路层的流控机制(Credits-based Flow Control)

PCIe的流控机制,说白了就是“先看库存,再发货”。发送端不能想发就发,必须先知道接收端还有多少空闲缓冲区。这个机制叫基于信用量的流控(Credit-based Flow Control)。

4.3 信用量的类型

每个VC都有三组信用量,分别对应三种TLP类型:

信用量类型 对应TLP 单位
PH(Posted Header) Posted请求(如Memory Write) Header
PD(Posted Data) Posted请求的数据载荷 2 DW(8字节)
NPH(Non-Posted Header) Non-Posted请求(如Memory Read) Header
NPD(Non-Posted Data) Non-Posted请求的数据载荷 2 DW
CPLH(Completion Header) 完成报文 Header
CPLD(Completion Data) 完成报文的数据载荷 2 DW

你可能会问:为什么要把Header和Data分开?原因很简单——Header和Data占用的缓冲区资源不同。Header通常只需要一个很小的FIFO,而Data需要更大的存储空间。分开管理可以更精细地控制资源。

4.4 流控的工作流程

流控的流程其实就三步:

  1. 初始化:链路建立后,通过InitFC1/InitFC2交换初始信用量。
  2. 发送:发送端每发一个TLP,就消耗掉对应的信用量。比如发一个Memory Write(Posted),就消耗1个PH和若干PD(取决于数据长度)。
  3. 更新:接收端处理完TLP后,释放缓冲区,并通过UpdateFC通知发送端。

避坑指南:我曾经遇到过一个案例,发送端在收到UpdateFC之前就把信用量用完了,结果链路进入“等待信用量”状态。这时候如果接收端处理速度慢,UpdateFC迟迟不来,整个链路就会卡住。所以,设计时一定要考虑信用量耗尽时的超时处理机制。

重传机制(Replay)

数据链路层的重传机制,是PCIe可靠性的最后一道防线。它基于一个简单的原则:没收到Ack,就重传

4.5 Replay Buffer

每个发送端都有一个Replay Buffer,用于缓存已发送但尚未被确认的TLP。这个Buffer的大小决定了可以“在途”的TLP数量。

我建议Replay Buffer的深度至少能容纳4个最大尺寸的TLP。为什么?因为Ack/Nak的往返延迟(RTT)通常需要几个时钟周期,Buffer太小会导致发送端频繁停顿。

4.6 重传的触发条件

重传在两种情况下触发:

  • 收到Nak:接收端明确告诉你“这个包坏了,重发”。
  • 超时:发送端等Ack等得太久,怀疑包丢了,主动重传。

嗯,这里有个细节:PCIe的重传是选择性重传,而不是回退N步。也就是说,如果接收端丢了序列号5的包,但收到了序列号6和7的包,它只会要求重传序列号5,而不会把6和7也扔掉。这一点比早期的网络协议要高效得多。

4.7 重传的代价

重传不是免费的。每次重传都会占用链路带宽,而且会打乱数据的有序性。我在项目中见过一个极端情况:某个设备的重传率高达30%,导致有效吞吐量直接腰斩。最后发现是接收端的CRC校验逻辑有bug,把正确的包也当成坏包了。

核心观点:重传机制是“最后的手段”,不能依赖它来保证可靠性。真正的可靠性应该来自物理层的信号完整性和事务层的端到端校验。数据链路层的重传,只是兜底。

知识体系总览

下面这张图展示了数据链路层DLLP、流控和重传机制的整体关系。我习惯用这种图来梳理思路,希望对你有帮助。

数据链路层协议核心机制 DLLP分类 • Ack/Nak:确认/否认 • InitFC1:初始信用量通告 • InitFC2:初始化确认 • UpdateFC:动态信用量更新 流控机制 • 基于信用量(Credit) • PH/PD/NPH/NPD/CPLH/CPLD • 初始化流程 • 动态更新机制 重传机制 • Replay Buffer • Nak触发重传 • 超时触发重传 • 选择性重传 InitFC1/2 Ack/Nak 信用量耗尽 触发等待 核心目标:确保TLP可靠、有序传输 DLLP是信使 | 流控是规则 | 重传是兜底 发送端 ←→ 接收端 DLLP双向流动,流控和重传协同工作 DLLP 流控 重传

这张图把DLLP、流控和重传的关系梳理得很清楚。我个人习惯在验证每个模块时,先画一张这样的关系图,确保自己理解了所有交互路径,再动手写testbench。

最后说一句:数据链路层是PCIe协议中最“实在”的一层。它不像事务层那样抽象,也不像物理层那样依赖模拟电路。你只要把DLLP的格式、流控的信用量计算、重传的序列号管理搞明白,就能应对90%的验证场景。剩下的10%,靠的是经验和耐心——就像我当年调那个Nak bug一样,抓了三天日志才找到根因。


公众号:蓝海资料掘金营,微信deep3321