第4章 PCIe数据链路层:DLLP与核心机制
数据链路层,说白了就是PCIe通信的“承上启下”层。上面接着事务层,下面压着物理层。它的两大使命:一是保证TLP端到端可靠传输,二是管理链路本身的控制信息。今天我们从DLLP格式、ACK/NAK、流控制初始化,再到链路训练概览,把这一层彻底讲透。
4.1 DLLP格式:麻雀虽小五脏俱全
DLLP(Data Link Layer Packet)只有6个字节。比TLP小得多,它不携带用户数据,只运载控制信息。结构固定:1字节类型 + 2字节数据 + 1字节序列号(仅部分DLLP使用) + 2字节CRC。
| 字段 | 长度 | 说明 |
|---|---|---|
| Type | 8 bit | 标识DLLP种类:ACK、NAK、InitFC1/2、UpdateFC、PM等 |
| Data | 16 bit | 携带具体参数(如FC credit值) |
| Seq# / Reserved | 8 bit | ACK/NAK时放接收到的TLP SeqNo,其他DLLP可保留 |
| CRC | 16 bit | 覆盖Type+Data+Seq#,多项式x¹⁶ + x¹² + x⁵ + 1 |
我个人习惯把DLLP类比为“握手包”。不像TLP那样需要事务层应答,DLLP在数据链路层内部就闭环了。实际项目中抓过PCIe总线的人都懂,如果你看到连续发出同一序列号的NAK,多半是物理层误码率太高或者对端接收缓冲区异常。
4.2 ACK/NAK机制:可靠传输的基石
发送端每发一个TLP,都要在数据链路层给它加上一个递增的序列号(Seq#)和LCRC。接收端收到后检查LCRC,正确就回ACK(带上那个Seq#),错误就回NAK。发送端维护一个重传缓冲区,收到NAK或超时未收到ACK,就重发那个Seq#之后的所有未确认TLP。
// 发送端行为伪代码(简化)
if (接收到ACK with Seq = expected_ack) {
清除缓冲区中Seq <= expected_ack的TLP;
} else if (接收到NAK) {
重传缓冲区中从NAK Seq开始的所有TLP;
} else if (超时未收到ACK) {
重传整个缓冲区(保守策略);
}
我曾经在某个Gen3 EP验证中发现,重传缓冲区设计太小,导致连续两个TLP出错时,缓冲区被冲掉,丢包了。后来改成“至少保留未确认最大Seq+1”的深度才解决。嗯,这里要注意:NAK携带的Seq#是出错的那个Seq,不是期望的Seq,不同IP核实现细节略有差别。
4.3 流控制初始化:先商量好“水位线”
链路刚起来的时候,双方都不知道对方能收多少数据。流控制(Flow Control)就是预先交换“信用量”(Credit)。每个TLP类型(Posted、Non-Posted、Completion)各有独立的信用计数器。初始化过程通过InitFC1和InitFC2两个DLLP完成。
- InitFC1:请求端发送,通知对端自己期望的初始信用值(一般写0,表示“你告诉我你能给多少”)
- InitFC2:对端回复,把自己分配的各类型信用最大值发回来
- 之后双方进入UpdateFC阶段,用完一个单位就发UpdateFC汇报剩余信用
我记得第一次做RC(Root Complex)时,流控制初始化一直卡在InitFC1交换后没收到InitFC2,查了半天发现是EP侧链路训练还没完成。协议规定流控制初始化必须在链路进入L0状态后才启动。顺序很重要:链路训练→L0→流控制初始化→正常事务传输。
4.4 链路训练概述:从物理混沌到有序
链路训练(Link Training)是物理层和数据链路层的联合动作,但数据链路层提供上层状态指示。训练状态机(LTSSM)有十几个状态,核心路径是:
- Detect:检测对端是否在位(通过电气特性)
- Polling:发送TS1/TS2有序集合,协商链路宽度和速度
- Configuration:交换链路配置参数(如极性反转、去重)
- L0:正常操作状态,开始流控制初始化和TLP传输
链路训练过程中,数据链路层处于“挂起”状态,直到L0才激活。我调试过一个奇葩问题:板卡插槽接触不良,导致Polling阶段反复失败,LTSSM在Detect和Polling之间死循环。用逻辑分析仪看,TS1序列始终收不到正确的应答。这种情况用软件清空链路配置寄存器再重新触发训练,往往能恢复。
4.5 三种DLLP的实战区分
在协议分析仪上抓包时,DLLP类型字段(Type)一眼就能看出在干什么。下面总结常见场景:
| DLLP类型 | 使用时机 | 数据字段含义 |
|---|---|---|
| ACK | 收到正确TLP | 已接收完成的Seq#(累计确认) |
| NAK | 收到CRC错误TLP | 出错TLP的Seq#(要求重传从此开始) |
| InitFC1 | 链路进入L0后发送 | 各VC的请求信用值(通常为0) |
| InitFC2 | 收到InitFC1后回复 | 实际分配的初始信用额度 |
| UpdateFC | 每个TLP成功处理后 | 当前剩余信用量 |
你想想看,如果UpdateFC停止更新,发送端很快会用完信用,事务层就会背压。这是PCIe拥塞控制的底层机制。我在做NVMe SSD验证时,利用这种机制反推主机侧的处理延迟,比直接用Timer取样更准。
好了,数据链路层的四大核心我们已经拆完了。DLLP格式是底层语法,ACK/NAK是可靠性语义,流控制是资源管理语义,链路训练是状态机驱动。下一层我们遇到事务层时,你会发现很多“上层协议”都是建立在这套机制之上的。嗯,这一章的内容已经足够支撑你调试常见的数据链路层问题了——至少我自己的项目里,80%的链路异常都能用这里讲的知识点定位。