第三章 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 三者关系流程图
从图里可以看得很清楚:TLP在事务层产生,下行交给数据链路层加序列号、LCRC,再通过物理层加帧定界符发出去。DLLP直接由数据链路层生成,Ordered Sets全程在物理层。调试时看到什么包就知道问题出在哪一层。
3.5 实战小贴士
抓包调试时必须能快速分类,我自己的判断顺序是这样:
- 看到0xFB或0xBC开头 → Ordererd Sets(物理层问题)
- 看到6字节、类型码(0x00~0x0F)→ DLLP(链路协议问题)
- 看到STP (0x1C) 开头、头标有Fmt/Type/Length → TLP(事务内容问题)
有一次客户报“链路断了,重新枚举就好了”。我让他们抓原始位流,发现丢失TLP后重传机制正常工作,问题其实是RC侧的Ack超时设置太短。改了一下DLLP的ACK延迟时间阈值就正常了。
调试时先扔开业务负载,只观察DLLP的Ack/Nak和FC信用。如果链路两端的FC信用都正常但TLP就是发不动,大概率是DLLP层出了BUG。