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 流控的工作流程
流控的流程其实就三步:
- 初始化:链路建立后,通过InitFC1/InitFC2交换初始信用量。
- 发送:发送端每发一个TLP,就消耗掉对应的信用量。比如发一个Memory Write(Posted),就消耗1个PH和若干PD(取决于数据长度)。
- 更新:接收端处理完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、流控和重传的关系梳理得很清楚。我个人习惯在验证每个模块时,先画一张这样的关系图,确保自己理解了所有交互路径,再动手写testbench。
最后说一句:数据链路层是PCIe协议中最“实在”的一层。它不像事务层那样抽象,也不像物理层那样依赖模拟电路。你只要把DLLP的格式、流控的信用量计算、重传的序列号管理搞明白,就能应对90%的验证场景。剩下的10%,靠的是经验和耐心——就像我当年调那个Nak bug一样,抓了三天日志才找到根因。
公众号:蓝海资料掘金营,微信deep3321