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 路由决策的流程
现在我把三种路由串起来,画一个简单的路由决策流程图。
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 抓包后,先看报头格式,再根据类型匹配路由方式,就能很快定位错误是否出在事务层——这可比大海捞针搜信号快多了。