PCIe事务层:TLP类型、地址空间、事务顺序规则

事务层是PCIe协议里离软件最近的一层,说白了就是负责把软件发起的读写请求打包成TLP(事务层包),然后往下扔给数据链路层。反过来,从对端收到的TLP也要在这里拆包、检查、再送给软件。

我刚接触PCIe那会儿,觉得事务层就是“发发包,收收包”,没什么难度。直到有一次调一个NVMe SSD的驱动,发现读回来的数据总是错位。抓了三天包,最后发现是TLP的Traffic Class和Attr字段配错了,导致顺序规则没走对,数据被乱序提交了。

TLP类型

TLP的类型其实就三种大方向,按传输方式分:Posted(只管发)、Non-Posted(要等回复)、Completion(回复)。

核心分类速记:所有TLP都可以归入这三类,再按地址空间细分。
  • 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 WriteMWrPosted否
Memory ReadMRdNon-Posted是
IO WriteIOWrNon-Posted是
IO ReadIORdNon-Posted是
Config WriteCfgWrNon-Posted是
Config ReadCfgRdNon-Posted是
MessageMsgPosted否
Completion (带数据)CplD——
Completion (不带数据)Cpl——
我的经验:抓包时看到没有对应请求的Completion,基本都是TLP头里的Requester ID或Tag配错了。先查这两个字段,能省下一半时间。

地址空间

PCIe定义了四种地址空间,每个TLP的Fmt和Type字段决定了它访问哪个空间。

  • Memory空间:普通读写,支持64位地址、原子操作。平时大部分流量都是这个。
  • IO空间:32位,兼容老设备。我现在基本没见过新设备用IO空间了。
  • Configuration空间:每个设备固定4KB,前256字节是标准配置头。枚举时全靠它。
  • Message空间:没有地址,靠Message Code区分。比如INTx中断、PME、错误转发等。
注意:IO空间虽然在规范里,但很多RC(Root Complex)默认不开放IO访问。你在新平台上用IO读,可能会直接收到Unsupported Request。我踩过这个坑,当时还以为是硬件坏了。

事务顺序规则

PCIe允许多个TLP在链路上乱序传输,但乱序必须符合一套严格的规则,不然数据一致性就崩了。规则的核心是:Posted请求、Non-Posted请求、Completion这三者之间的通过顺序。

规范里用一张表(PCIe Base Spec 6.0, Table 2-36)列出了所有可能的两两依赖。我把它简化成几个记忆点:

  1. Posted请求之间可以互相绕过。两个MWr谁先完成无所谓,反正都是写。
  2. Non-Posted请求之间不能互相绕过。MRd必须按发起顺序到达,否则Completion会乱套。
  3. Posted可以绕过Non-Posted。MWr可以超过前面的MRd先完成,但MRd不能超过MWr。
  4. Completion不能绕过Posted。也就是说,MWr先发出去了,哪怕后面有Completion已经完成,Completion也得等前面的MWr先过。
  5. 同一个Request的多个Completion必须按顺序返回。比如读32字节,分两次Completion返回,第一个必须是低地址,第二个是高地址。
一句话记忆:Posted最优先,Non-Posted其次,Completion最后。但Completion不能超过Posted,Non-Posted不能超过Posted。

为什么要有这些规则?你想想看,如果先发的写被后发的读超过去了,那读回来的数据可能是旧值。生产者-消费者模型里,必须保证写命令的可见性先于读返回。

我在一次PCle switch验证项目中,遇到过由于违反顺序规则导致的死锁。两个Endpoint都往Host写数据,同时Host发读请求给Endpoint A,Endpoint A的Completion被Endpoint B的写包堵住了,而Endpoint B又在等Host的读返回。最后所有队列都被占满,链路冻住了。抓包看到一长串MWr排在最前面,后面全是Pending的Completion。

调试技巧:怀疑顺序问题时,先看TLP头的Attr字段里的No Snoop和Relaxed Ordering位。这两个位允许设备临时绕过某些顺序规则,但容易埋坑。我一般会先强制关掉这两个选项,排除干扰。

知识结构一览

PCIe事务层核心结构 PCIe事务层:TLP分类与顺序关系 TLP (事务层包) Posted (MWr, Msg) Non-Posted (MRd, IO, Cfg) Completion (Cpl, CplD) 地址空间:Mem / IO / Cfg / Msg 顺序规则(简化) 1. Posted可绕过Non-Posted 2. Posted可绕过Completion 3. Non-Posted不能绕过Posted 4. Completion不能绕过Posted 参考

这张图很直观:最上面是TLP大类别,下面挂具体类型和地址空间,右边是我总结的4条顺序规则。实际调试时,我经常把这张图贴在白板上,看到异常包就快速定位它属于哪一类,然后按规则检查链路顺序。

特别提醒:顺序规则在不同VC(Virtual Channel)之间是独立的。不同VC的包完全允许互相乱序。但同一个VC内必须遵守上述规则。如果你看到设备违反了规则,多半是Attr字段配错了,或者设备实现有bug。

讲到这里,事务层的大框架基本清晰了。你掌握了TLP类型、地址空间映射和顺序规则,再配合抓包工具看实际流,就能理解80%的事务层行为。剩下20%是原子操作、TLP Digest、端到端CRC这些边角料,后面章节慢慢聊。