第4章 PCIe数据链路层:DLLP与核心机制

数据链路层,说白了就是PCIe通信的“承上启下”层。上面接着事务层,下面压着物理层。它的两大使命:一是保证TLP端到端可靠传输,二是管理链路本身的控制信息。今天我们从DLLP格式、ACK/NAK、流控制初始化,再到链路训练概览,把这一层彻底讲透。

本章核心脉络:DLLP是数据链路层的“信使” → ACK/NAK保证TLP不丢 → 流控制初始化协商缓冲区水位 → 链路训练把物理通道对齐。

4.1 DLLP格式:麻雀虽小五脏俱全

DLLP(Data Link Layer Packet)只有6个字节。比TLP小得多,它不携带用户数据,只运载控制信息。结构固定:1字节类型 + 2字节数据 + 1字节序列号(仅部分DLLP使用) + 2字节CRC。

字段长度说明
Type8 bit标识DLLP种类:ACK、NAK、InitFC1/2、UpdateFC、PM等
Data16 bit携带具体参数(如FC credit值)
Seq# / Reserved8 bitACK/NAK时放接收到的TLP SeqNo,其他DLLP可保留
CRC16 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核实现细节略有差别。

避坑提醒:ACK/NAK并不是逐包应答,而是“累计确认”。收到ACK(N)表示“N及以前的所有TLP都OK”。所以NAK的场景很少见,一旦出现往往意味着链路质量严重下降。

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→流控制初始化→正常事务传输。

个人建议:调试时抓DLLP最方便的方法是看UpdateFC。如果UpdateFC一直为0,说明对端已经停止接受该类TLP,可能是应用程序层背压了。

4.4 链路训练概述:从物理混沌到有序

链路训练(Link Training)是物理层和数据链路层的联合动作,但数据链路层提供上层状态指示。训练状态机(LTSSM)有十几个状态,核心路径是:

  1. Detect:检测对端是否在位(通过电气特性)
  2. Polling:发送TS1/TS2有序集合,协商链路宽度和速度
  3. Configuration:交换链路配置参数(如极性反转、去重)
  4. L0:正常操作状态,开始流控制初始化和TLP传输

链路训练过程中,数据链路层处于“挂起”状态,直到L0才激活。我调试过一个奇葩问题:板卡插槽接触不良,导致Polling阶段反复失败,LTSSM在Detect和Polling之间死循环。用逻辑分析仪看,TS1序列始终收不到正确的应答。这种情况用软件清空链路配置寄存器再重新触发训练,往往能恢复。

数据链路层核心功能结构 TLP输入 添加Seq# + LCRC 发送缓冲区 接收缓冲区+LCRC检查 ACK/NAK DLLP生成 InitFC/UpdateFC DLLP 链路训练状态同步 关键观察点:ACK/NAK和流控制两类DLLP并行工作,但必须在链路训练到达L0后才允许发送

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取样更准。

不要混淆:ACK/NAK DLLP只针对TLP,不针对DLLP本身。DLLP的CRC由物理层保证(16位CRC足够应对)。数据链路层不重传DLLP,但物理层的Replay机制可以重新发送有序集合。

好了,数据链路层的四大核心我们已经拆完了。DLLP格式是底层语法,ACK/NAK是可靠性语义,流控制是资源管理语义,链路训练是状态机驱动。下一层我们遇到事务层时,你会发现很多“上层协议”都是建立在这套机制之上的。嗯,这一章的内容已经足够支撑你调试常见的数据链路层问题了——至少我自己的项目里,80%的链路异常都能用这里讲的知识点定位。