PCIe事务层:TLP类型、地址空间、事务顺序规则
事务层是PCIe协议里离软件最近的一层,说白了就是负责把软件发起的读写请求打包成TLP(事务层包),然后往下扔给数据链路层。反过来,从对端收到的TLP也要在这里拆包、检查、再送给软件。
我刚接触PCIe那会儿,觉得事务层就是“发发包,收收包”,没什么难度。直到有一次调一个NVMe SSD的驱动,发现读回来的数据总是错位。抓了三天包,最后发现是TLP的Traffic Class和Attr字段配错了,导致顺序规则没走对,数据被乱序提交了。
TLP类型
TLP的类型其实就三种大方向,按传输方式分:Posted(只管发)、Non-Posted(要等回复)、Completion(回复)。
- Memory Write (MWr) — Posted。写内存,不需要回复。性能最好。
- Memory Read (MRd) — Non-Posted。读内存,必须等Completion。
- IO Read/Write (IORd, IOWr) — Non-Posted。访问IO空间,现在很少用了。
- Configuration Read/Write (CfgRd, CfgWr) — Non-Posted。枚举阶段用的,访问配置空间。
- Message (Msg) — Posted。中断、错误通知、电源管理等。
- Completion (Cpl, CplD) — 对Non-Posted请求的回复,带数据或不带数据。
| TLP类型 | 缩写 | Posted/Non-Posted | 需要Completion |
|---|---|---|---|
| Memory Write | MWr | Posted | 否 |
| Memory Read | MRd | Non-Posted | 是 |
| IO Write | IOWr | Non-Posted | 是 |
| IO Read | IORd | Non-Posted | 是 |
| Config Write | CfgWr | Non-Posted | 是 |
| Config Read | CfgRd | Non-Posted | 是 |
| Message | Msg | Posted | 否 |
| Completion (带数据) | CplD | — | — |
| Completion (不带数据) | Cpl | — | — |
地址空间
PCIe定义了四种地址空间,每个TLP的Fmt和Type字段决定了它访问哪个空间。
- Memory空间:普通读写,支持64位地址、原子操作。平时大部分流量都是这个。
- IO空间:32位,兼容老设备。我现在基本没见过新设备用IO空间了。
- Configuration空间:每个设备固定4KB,前256字节是标准配置头。枚举时全靠它。
- Message空间:没有地址,靠
Message Code区分。比如INTx中断、PME、错误转发等。
事务顺序规则
PCIe允许多个TLP在链路上乱序传输,但乱序必须符合一套严格的规则,不然数据一致性就崩了。规则的核心是:Posted请求、Non-Posted请求、Completion这三者之间的通过顺序。
规范里用一张表(PCIe Base Spec 6.0, Table 2-36)列出了所有可能的两两依赖。我把它简化成几个记忆点:
- Posted请求之间可以互相绕过。两个MWr谁先完成无所谓,反正都是写。
- Non-Posted请求之间不能互相绕过。MRd必须按发起顺序到达,否则Completion会乱套。
- Posted可以绕过Non-Posted。MWr可以超过前面的MRd先完成,但MRd不能超过MWr。
- Completion不能绕过Posted。也就是说,MWr先发出去了,哪怕后面有Completion已经完成,Completion也得等前面的MWr先过。
- 同一个Request的多个Completion必须按顺序返回。比如读32字节,分两次Completion返回,第一个必须是低地址,第二个是高地址。
为什么要有这些规则?你想想看,如果先发的写被后发的读超过去了,那读回来的数据可能是旧值。生产者-消费者模型里,必须保证写命令的可见性先于读返回。
我在一次PCle switch验证项目中,遇到过由于违反顺序规则导致的死锁。两个Endpoint都往Host写数据,同时Host发读请求给Endpoint A,Endpoint A的Completion被Endpoint B的写包堵住了,而Endpoint B又在等Host的读返回。最后所有队列都被占满,链路冻住了。抓包看到一长串MWr排在最前面,后面全是Pending的Completion。
Attr字段里的No Snoop和Relaxed Ordering位。这两个位允许设备临时绕过某些顺序规则,但容易埋坑。我一般会先强制关掉这两个选项,排除干扰。
知识结构一览
这张图很直观:最上面是TLP大类别,下面挂具体类型和地址空间,右边是我总结的4条顺序规则。实际调试时,我经常把这张图贴在白板上,看到异常包就快速定位它属于哪一类,然后按规则检查链路顺序。
讲到这里,事务层的大框架基本清晰了。你掌握了TLP类型、地址空间映射和顺序规则,再配合抓包工具看实际流,就能理解80%的事务层行为。剩下20%是原子操作、TLP Digest、端到端CRC这些边角料,后面章节慢慢聊。