4、PCIe 事务层(TLP):格式、类型与路由规则

事务层,说白了就是 PCIe 通信的“翻译官”。

它把上层软件要发的数据,打包成统一的 TLP 格式,再交给数据链路层去传。反过来,收到的 TLP 也在这儿拆包解析。没有它,你的显卡、NVMe 硬盘根本就不知道对方在说什么。

这一节咱们就把它扒开,看看 TLP 到底长什么样,有哪些常用类型,以及它在网络中是怎么找到路的。

4.1 TLP 的格式:一个包的骨架

一个完整的 TLP,由四个部分组成。不过有的部分可以省略,但规矩得先讲清楚。

字段 长度 说明
前导 (Start) 1 DW 帧开始标记。告诉物理层:注意,后面是事务包。
TLP 报头 (Header) 3 DW 或 4 DW 核心信息:谁发的,发给谁,操作类型是什么。
数据负载 (Payload) 0~1024 DW 真正的读写数据。读请求一般不带 Payload,写请求和数据完成才带。
摘要 (Digest / TLP Digest) 1 DW(可选) 端到端 CRC(ECRC),用来校验包是否被篡改,高级功能。

我个人习惯是:先看报头的格式字段。只要能把报头看懂了,后面的 Payload 和摘要就好办了。

至于前面的 Start 和后面的结束标记,那是物理层和数据链路层的事,事务层只管处理和转发。

最小 TLP 包:仅报头不带 Payload,比如内存读请求,只占 3 个 DW。节省带宽,适合非对齐的小请求。

4.2 TLP 的主要类型:MRd、MWr、Cpl

TLP 类型其实就三大类:读、写、完成。咱们平时接触最多的就是内存读写和配置读写。

类型缩写 全称 方向 是否带数据
MRd Memory Read 请求者 → 完成者 否(报头仅 3 DW)
MRdLk Memory Read Lock 同上(带锁,很少用) 否
MWr Memory Write 请求者 → 完成者 是(报头 3 DW + Payload)
IORd IO Read(仅 Legacy 需要) 请求者 → 完成者 否
IOWr IO Write 请求者 → 完成者 是
CfgRd0/CfgRd1 Configuration Read RC → EP 否
CfgWr0/CfgWr1 Configuration Write RC → EP 是(仅 4 字节)
Cpl Completion(无数据完成) 完成者 → 请求者 否(3 DW,带回写状态和标签)
CplD Completion with Data 完成者 → 请求者 是(带读取数据)
Msg Message(注意:都是广播/组播) 任何设备 可带 Payload

有一个常见的坑:写请求不需要完成包。你发送一个 MWr,目标收到后直接写进去,不再回复 Cpl。读请求则需要接收方准备数据后,用 CplD 送回来。

记住简化规则:
- 读(MRd/IORd/CfgRd) → 必有 CplD 回复
- 写(MWr/IOWr/CfgWr) → 无回复
- 消息(Msg) → 看消息类型,有些需确认,有些不需

我曾经有一次调试 NVMe 驱动,发现读回来的数据总是“移位”了几个 DWORD,仔细一看,原来是 TLP 报头里 First/BE Byte Enable 字段填错了,导致数据在 128B 边界上出现了不对齐。

如果你觉得报头字段看得头晕,干脆先看下面这个 TLP 最小报头的表达。

// 一个 32 位地址的 MRd TLP 报头(3 DW,共 12 字节)
DW0: [R Fmt=001 Type=00000] [TC=000] [Attr=000] [TH=0][TD=0][EP=0][Attr2=0][AT=00][Length=000000001]
DW1: [Requester ID: BDF=00.00.0] [Tag=0x01] [Last DW BE=1111] [1st DW BE=1111]
DW2: [Address = 0x1000_0000]

// 一个 64 位地址的 MWr 报头(4 DW,共 16 字节)
DW0: [R Fmt=011 Type=00000] [TC=000] [Attr=000] ... [Length=000000100]
DW1: [Requester ID: BDF=00.01.0] [Tag=0x02] ...
DW2: [Address Low = 0x0000_8000]
DW3: [Address High = 0x0000_0000]

你看到“Fmt”字段了吧?它等于 001 表示 3DW 不带数据;011 表示 4DW 带数据。只要死记住这个映射表,解析就快了。

4.3 路由规则:包是怎么送到目的地的?

PCIe 不像以太网,没有复杂的 MAC 地址协商。它是用三种路由方式把 TLP 送给对应的设备。

4.3.1 地址路由 (Address Routing)

用得最多。内存读写(MRd/MWr)直接用目标地址路由。接收方(比如某个下游桥)看地址范围,如果在自己映射的地址区内,就接收;否则继续往下扔。

我的建议:在调试地址路由时,抓 TLP 报头里 Address 字段,再对照设备配置空间的 BAR(Base Address Register)。不匹配就说明地址空间映射没配好,或者 BAR 在 BIOS 里没被正确分配。

4.3.2 ID 路由 (ID Routing)

配置读写(CfgRd/Wr)和消息(Msg)里都会用到。ID 就是 BDF(Bus:Device.Function),每个端点都有唯一编号。

上游桥(RC)发配置请求时,用这个 BDF 找到目标设备;如果目标设备不是直属子设备,桥再向下游桥转发。读回来时,完成包也会带相同的 Requester ID,以便请求方能识别对应哪个请求。

要当心什么?
别用 ID 路由来访问未映射的 Bus 号,交换机可能会直接丢包(Unsupported Request)。我见过一次升级固件,结果 BIOS 枚举总线时漏配了 Bus 号,导致下游 NVMe 控制器根本收不到配置写,改不了 BAR。

4.3.3 隐式路由 (Implicit Routing)

只用于消息(Msg)。靠消息类型里的 Route Field 决定目标:

  • Route to Root Complex:就是发给 RC(比如:中断消息 MSI/MSI-X)
  • Route by Address:地址路由变体(传统消息用)
  • Broadcast:发给所有设备
  • Local:发给设备自己(比如自环测试)

因为消息不需要知道具体地址或 ID,桥收到后按隐式规则转发。在调试 PM 电源管理时,经常看到 PM_TurnOff 和 PM_Active_State_Nak 这类广播消息,隐式路由大大简化了固件操作。

4.4 路由决策的流程

现在我把三种路由串起来,画一个简单的路由决策流程图。

TLP 路由决策流程 收到 TLP TLP 类型 == 消息 (Msg)? 隐式路由 (依据 Route Field) 配置/完成/消息? ID 路由 (通过 BDF 匹配) 地址路由 (地址匹配 BAR) 转发到目标端口 / 返回完成包

4.5 避坑指南:路由规则中的常见错误

  • 地址路由的地址跨度不对:BAR 的大小必须是 2 的幂,且对齐到自身大小。我曾在 FPGA 上配了一个 8MB 的 BAR,但给的是 0x4000_0000 起始,结果 8MB 超出 2MB 对齐边界,导致地址重叠,交换机转发错乱。
  • ID 路由中 Tag 耗尽:同一 Requester ID 只能用 256 个 Tag(0-255)。如果你在短时间内发出 256 个未完成的读请求,所有后续请求都会被阻塞——直到有完成包释放 Tag。
  • 隐式路由消息被丢弃:如果消息类型是广播类(如 PM_Active_State_Nak),而某个中间桥不能处理该消息,可能直接把它吞掉,导致下游设备收不到电源状态切换通知。你最好在桥的配置空间查 Device Capabilities 里的消息支持位。

好了,事务层的格式、类型和路由说完了。你拿到一份 TLP 抓包后,先看报头格式,再根据类型匹配路由方式,就能很快定位错误是否出在事务层——这可比大海捞针搜信号快多了。