第3章 TLP包格式详解:Memory Read/Write TLP、Completion TLP、Message TLP的格式与字段解析

好,咱们今天来啃一块硬骨头——TLP包的格式。说实话,很多工程师做了好几年PCIe,一说到TLP包头里每个bit是干嘛的,还是有点含糊。我个人觉得,搞懂TLP格式是PCIe通信的“内功”,内功练好了,后面调链路、抓bug才会顺手。

咱们这节课重点讲三类最常用的TLP:Memory读写、Completion完成包、还有Message消息包。我会把每个字段掰开揉碎了讲,再配上我实际项目中踩过的坑。

3.1 TLP包的整体结构

先看个全景。一个完整的TLP包长这样:

+----------------+----------------+----------------+----------------+
|  起始(1B)      |  序列号(2B)    |  包头(12B/16B) |  数据负载(0-4KB)|
+----------------+----------------+----------------+----------------+
|  ECRC(4B)      |  LCRC(4B)      |  结束(1B)      |                 |
+----------------+----------------+----------------+----------------+

嗯,这里要注意:起始、序列号、LCRC、结束这些是数据链路层加的,事务层只负责构造包头+数据+ECRC。我刚开始学的时候老是把这两层搞混,后来画了个分层图才理清楚。

核心要点:事务层只关心“我要发什么数据”,链路层关心“怎么可靠地发过去”。调试时抓到的包,通常是完整的数据链路层帧。

3.2 Memory Read TLP格式

Memory Read,说白了就是“读内存”。RC(根复合体)或者EP(端点)都可以发起。格式如下:

Byte 0: Fmt(2b) | Type(5b) | R(1b)
Byte 1: TC(3b) | R(5b) | Attr(2b) | R(1b) | TH(1b) | R(1b) | TD(1b) | EP(1b) | Attr(1b) | R(1b)
Byte 2-3: Length(10b) | R(6b)
Byte 4-7: Requester ID(16b) | Tag(8b) | Last DW BE(4b) | First DW BE(4b)
Byte 8-11: Address[63:32] (仅64位寻址时存在)
Byte 12-15: Address[31:2] | R(2b)

字段太多记不住?我教你个笨办法:先记住前4个关键字段——Fmt/Type、Length、Requester ID、Address。剩下的用到再查。

说说我踩过的坑。有一次调试FPGA与CPU通信,CPU发了个Memory Read请求,FPGA死活不响应。抓包一看,Tag字段全是0。原来PCIe规范要求Tag不能重复,我那个老代码里Tag没做递增,CPU以为所有请求都是同一个,直接丢弃了。从那以后,我习惯在代码里加个Tag计数器,每发一个请求就加1。

避坑指南:我曾经在Memory Read请求中忘记设置First DW BE和Last DW BE,结果读回来的数据全是乱的。这两个字段决定了你要读哪些字节,千万别默认填0。

3.3 Memory Write TLP格式

Memory Write和Read的包头结构基本一样,区别在于:

  • Fmt字段:Read是00或01,Write是10或11
  • Write必须带数据负载,Read不带
  • Write不需要Completion,属于Posted事务

格式对比:

字段 Memory Read (32位) Memory Write (32位)
Fmt 00 (无数据) 10 (有数据)
Type 00000 00000
Length 要读取的DW数 要写入的DW数
数据负载 有 (跟在包头后)

你想想看,Write不需要回应,所以性能比Read好很多。我做过一个高速数据采集项目,所有控制寄存器都用Write操作,只有读数据时才用Read。这样CPU不用等回应,吞吐量直接翻倍。

注意:Memory Write是Posted事务,意味着发送端不保证接收端已经处理完。如果你需要确认写入完成,要么读回验证,要么用Message中断通知。

3.4 Completion TLP格式

Completion是PCIe里最容易被忽视的包。很多人以为发完Read请求就完事了,其实Read请求必须对应一个Completion,否则发起方会一直等,直到超时。

Completion的包头:

Byte 0: Fmt(2b) | Type(5b) | R(1b)
Byte 1: TC(3b) | R(5b) | Attr(2b) | R(1b) | TH(1b) | R(1b) | TD(1b) | EP(1b) | Attr(1b) | R(1b)
Byte 2-3: Length(10b) | R(6b)
Byte 4-7: Completer ID(16b) | Completion Status(3b) | BCM(1b) | Byte Count(12b)
Byte 8-11: Requester ID(16b) | Tag(8b) | R(1b) | Lower Address(7b)
Byte 12-15: 数据负载 (可选)

这里有个关键字段——Completion Status

  • 000:成功完成(SC)
  • 001:不支持的请求(UR)
  • 010:配置请求重试(CRS)
  • 100:完成者异常(CA)

我记得有一次调试,FPGA老是返回UR状态。查了半天,原来是Completer ID没配对。FPGA的Bus/Device/Function号必须和配置空间里的一致,否则CPU不认。

经验之谈:Completion里的Tag必须和对应的Request Tag一致。我习惯在FPGA里维护一个“未完成请求表”,收到Completion后查表匹配,超时未匹配的就报错。

3.5 Message TLP格式

Message TLP是PCIe里最灵活的包,它不访问内存,而是传递“消息”。比如中断、错误通知、电源管理事件等。

Message的包头很特殊:

Byte 0: Fmt(2b) | Type(5b) | R(1b)
Byte 1: TC(3b) | R(5b) | Attr(2b) | R(1b) | TH(1b) | R(1b) | TD(1b) | EP(1b) | Attr(1b) | R(1b)
Byte 2-3: Length(10b) | R(6b)
Byte 4-7: Requester ID(16b) | Tag(8b) | Message Code(8b)
Byte 8-11: Message Data (可选)

注意看,Type字段是10xxx,和Memory/IO/Config都不一样。Message Code决定了消息类型:

Message Code 含义 说明
0x10 INTx中断 传统中断信号
0x20-0x2F PME电源管理事件 唤醒、状态变化等
0x30 错误消息 ERR_COR、ERR_NONFATAL等
0x40-0x4F 厂商自定义消息 可以自己定义用途

我项目中常用的是厂商自定义消息。比如FPGA处理完一批数据后,发一个Message给CPU,CPU收到后就知道可以来取数据了。这比用中断线灵活多了。

小技巧:Message TLP是Posted事务,不需要Completion。但如果你需要确认对方收到了,可以用Vendor Defined Message带个序列号,对方回一个带相同序列号的Message确认。

3.6 三种TLP的对比总结

最后做个对比,方便你记忆:

特性 Memory Read Memory Write Completion Message
事务类型 Non-Posted Posted Non-Posted Posted
是否需要回应 是(Completion)
数据负载 有(返回数据) 可选
典型用途 读寄存器/内存 写寄存器/内存 返回读结果 中断/错误/自定义

嗯,到这里TLP格式就讲完了。说实话,这些字段光靠背是记不住的,最好的办法是抓包看。我每次调PCIe都会开逻辑分析仪,对着波形看每个字段,看多了自然就熟了。

下一章咱们讲TLP的路由机制,看看包是怎么在PCIe拓扑里找到目的地的。到时候你会明白,为什么Requester ID和Completer ID这么重要。