第三章 PCIe协议基础(二):TLP、DLLP与Ordered Sets

调试PCIe,说来说去就三种东西在链路上跑:事务层包(TLP)、数据链路层包(DLLP)和有序集(Ordered Sets)。三者各有各的活,但最容易出问题的还是TLP。

我记得刚做PCIe验证那会儿,拿着逻辑分析仪抓了一堆信号,满屏的0x1C、0x2A,根本看不明白。后来才搞懂——包结构看清了,协议就懂了一半。

3.1 TLP——真正干活的包

TLP是PCIe事务层的核心。说白了,CPU要写数据、读数据、发消息,都得靠TLP来传。

3.1.1 TLP格式

一个标准TLP长这样:

+--------+--------+--------+--------+--------+--------+
| 起始   | 序列号 | 头标   | 数据   | ECRC   | 结束   |
| (STP)  | (Seq#) | (Header)|(Payload)|        | (END)  |
+--------+--------+--------+--------+--------+--------+
     ↑ DLLP层加的     ↑ TLP核心部分      ↑ 可选项

真正我们在调试中解析的是TLP头标和数据负载。头标长度可以是3DW或4DW(1DW=4字节)。头标决定了包的类型、地址、长度、标签等信息。

TLP头标关键字段(3DW为例):

  • Fmt[1:0]:格式,指示有无数据、头标长度(00=3DW无数据,01=4DW无数据,10=3DW有数据,11=4DW有数据)
  • Type[4:0]:事务类型(存储器读/写、IO、配置、消息等)
  • TC[2:0]:流量类别,用于QoS
  • TD:TLP Digest存在位(指示末尾是否有ECRC)
  • EP:毒包标志(Poisoned,数据损坏)
  • Attr[2:0]:属性(排序、监听等)
  • Length[9:0]:数据负载长度,单位是DW
  • Requester ID:请求者总线/设备/功能号
  • Tag:标签,用于匹配请求和完成

我个人习惯:调试时先看Fmt和Type,确定这是读还是写;再看Length,判断数据量;最后查Requester ID+Tag,找到是谁发的。

3.1.2 TLP类型

类型 作用 典型场景
MRd / MRdLk 存储器读 CPU读内存、DMA读
MWr 存储器写 CPU写内存、DMA写
IORd / IOWr IO读写 旧式设备配置,极少用
CfgRd0/1 / CfgWr0/1 配置读写 枚举、BAR分配
Cpl / CplD 完成(无数据/有数据) 读请求的响应
Msg / MsgD 消息(无数据/有数据) 中断、电源管理、错误报告

举个例子:你看到一个Fmt=2'b10、Type=5'b00000——那就是MRd(存储器读)。如果后面跟着CplD回来,就是读完成了。

我曾经遇到过一次奇怪的问题:设备收不到配置写。抓包发现CfgWr的Length字段填了2,但配置空间只允许单DW访问。改了Length为1就通了。嗯,细节决定成败。

3.1.3 完成包与拆分事务

PCIe的读请求是拆分事务(Split Transaction)的:

  • 主设备发MRd,带上Tag
  • 目标设备收到后,准备好数据,用CplD发回来,Tag必须匹配
  • CplD里还有Byte Count、Lower Address,告诉主设备给了多少字节、从哪个地址开始

我最头疼的一次问题就是Tag耗尽——设备发了一大堆读请求,但完成的太慢,Tag用光了,后续请求直接卡死。后来加了请求队列深度限制才解决。

3.2 DLLP——数据链路层的“信令包”

TLP走的是事务层,而DLLP只在链路的两个端点之间交换。它不携带用户数据,而是做管理。

DLLP只有6字节:

+------+------+------+------+------+------+
| 类型 | 数据 | 数据 | 数据 | CRC  | CRC  |
|(1B)  |(2B)  |(2B)  |(2B)  |(1B)  |(1B)  |
+------+------+------+------+------+------+

常见DLLP类型:

类型 功能
Ack / Nak 确认TLP接收成功/失败(用于重传)
InitFC1 / InitFC2 初始化流量控制信用值
UpdateFC 更新流量控制信用(告诉对端我可用的缓冲区空间)
PM_EnterL1 / PM_ActiveState 电源管理状态转换
Vendor_Defined 厂商自定义

避坑指南:我曾经遇到过链路训练成功,但就是发不了数据。抓DLLP发现对端一直没发UpdateFC,我方累计信用为零。后来发现是RC侧FC初始化没完成,卡在InitFC2握手阶段。检查FC协议时一定要看DLLP的信用值是否==0x7FF(最大值)。

流量控制(Flow Control)是我觉得最容易被忽略的点。每个VC(虚拟通道)有6种缓冲区类型(Posted、Non-Posted、Completion等),对端通过UpdateFC告诉你它能接收多少。发TLP之前一定要申请信用,否则包发不出去。

3.3 Ordered Sets——链路上的“口令”

Ordered Sets跑在物理层,但调试时抓到的包经常能看到它们穿插在TLP/DLLP之间。它们不参与数据交换,而是用于链路控制、训练、复位、电管理。

Ordered Sets分两大类:

  • 训练序列(TS1/TS2):链路训练时互发,协商速率、宽度、极性
  • SKP Ordered Set:用于时钟补偿,消除两端时钟差异
  • FTS(Fast Training Sequence):退出L0s低功耗状态时的快速重训练
  • EIEOS(Electrical Idle Exit Ordered Set):退出电气空闲的信号

看一个典型的Training序列:

TS1 Order Set: 
  - Symbol 0: COM (0xBC)
  - Symbol 1: Link Number
  - Symbol 2: Lane Number
  - Symbol 3-15: 全0或训练控制

有一次链路一直没升到Gen3,抓TS1发现对端始终报0x7F(未训练),而我方已经准备好了。后来查到是LTSSM状态机跳转条件里的接收检测时间没满足。说白了,就是一根线虚焊导致电气空闲检测失败。

快速判断链路状态:看链路上多不多TS1/TS2。如果一直有TS1,说明在训练;如果没有Ordered Sets、只有IDLE,说明已经稳定。

3.4 三者关系流程图

PCIe协议层交互示意图 事务层 数据链路层 物理层 TLP(事务层包) TLP头标 + 数据 完成(Cpl/CplD) DLLP(链路层包) Ack/Nak 确认 FC信用更新 序列号 + LCRC Ordered Sets TS1 / TS2 训练 SKP 时钟补偿 EIEOS / FTS TLP封装成DLLP添加序列号和CRC,再经物理层加帧定界符发送 Ordered Sets不封装TLP/DLLP,独立在物理层传输

从图里可以看得很清楚:TLP在事务层产生,下行交给数据链路层加序列号、LCRC,再通过物理层加帧定界符发出去。DLLP直接由数据链路层生成,Ordered Sets全程在物理层。调试时看到什么包就知道问题出在哪一层。

3.5 实战小贴士

抓包调试时必须能快速分类,我自己的判断顺序是这样:

  1. 看到0xFB或0xBC开头 → Ordererd Sets(物理层问题)
  2. 看到6字节、类型码(0x00~0x0F)→ DLLP(链路协议问题)
  3. 看到STP (0x1C) 开头、头标有Fmt/Type/Length → TLP(事务内容问题)

有一次客户报“链路断了,重新枚举就好了”。我让他们抓原始位流,发现丢失TLP后重传机制正常工作,问题其实是RC侧的Ack超时设置太短。改了一下DLLP的ACK延迟时间阈值就正常了。

调试时先扔开业务负载,只观察DLLP的Ack/Nak和FC信用。如果链路两端的FC信用都正常但TLP就是发不动,大概率是DLLP层出了BUG。