4. TLP包格式深度解析:Memory/IO/Configuration/Message请求与Completion

好,咱们今天来啃一块硬骨头——TLP包格式。说实话,我刚接触PCIe那会儿,看到TLP的格式定义,头都大了。各种字段、各种类型,感觉像在看天书。但后来我发现,只要抓住几个核心点,这东西其实没那么复杂。

TLP,全称Transaction Layer Packet,是PCIe事务层的核心。说白了,它就是设备之间沟通的“语言”。你想想看,CPU要读内存、写外设,总得有个统一的格式吧?TLP就是干这个的。

4.1 TLP包的基本结构

每个TLP包,都包含三个部分:

  • TLP前缀(可选):用于扩展功能,比如数据完整性校验
  • TLP头:描述这个包是干什么的,去哪里,多大
  • 数据负载(可选):真正要传输的数据

嗯,这里要注意,不是所有TLP都有数据负载。比如Memory读请求,它只有头,没有数据。数据是在返回的Completion包里带回来的。

4.2 Memory请求与Completion

Memory请求,这是最常用的类型。我在项目中调试DMA驱动时,天天跟它打交道。

4.2.1 Memory Read请求

格式长这样:

+--------+--------+--------+--------+
| Fmt[1:0] | Type[4:0] | R | TC[2:0] |
+--------+--------+--------+--------+
| R | Attr[1:0] | AT[1:0] | Length[9:0] |
+--------+--------+--------+--------+
| Requester ID[15:0] | Tag[7:0] | Last DW BE[3:0] | 1st DW BE[3:0] |
+--------+--------+--------+--------+
| Address[63:32] |
+--------+--------+--------+--------+
| Address[31:2] | 00 |
+--------+--------+--------+--------+

关键字段我解释一下:

  • Fmt:格式字段,2'b00表示3DW头无数据,2'b01表示4DW头无数据
  • Type:类型字段,Memory Read是5'b00000
  • Length:要读多少个DW(双字),单位是DW
  • Requester ID:谁发起的请求,由Bus/Device/Function组成
  • Tag:标签,用于匹配请求和完成包
  • Address:目标地址,64位地址用4DW头,32位用3DW头

重要:Length字段是1-based的。也就是说,Length=1表示读1个DW,Length=2表示读2个DW。我曾经见过新手在这里栽跟头,写Length=0,结果设备没反应。

4.2.2 Memory Write请求

写请求和读请求类似,但多了数据负载:

+--------+--------+--------+--------+
| Fmt[1:0] | Type[4:0] | R | TC[2:0] |
+--------+--------+--------+--------+
| R | Attr[1:0] | AT[1:0] | Length[9:0] |
+--------+--------+--------+--------+
| Requester ID[15:0] | Tag[7:0] | Last DW BE[3:0] | 1st DW BE[3:0] |
+--------+--------+--------+--------+
| Address[63:32] |
+--------+--------+--------+--------+
| Address[31:2] | 00 |
+--------+--------+--------+--------+
| Data (可变长度) |
+--------+--------+--------+--------+

写请求的Fmt字段是2'b10(3DW头有数据)或2'b11(4DW头有数据)。

个人经验:我建议你在设计RC驱动时,尽量使用64位地址(4DW头)。虽然32位地址够用,但有些EP设备只支持64位寻址。用4DW头可以避免兼容性问题。

4.2.3 Completion包

Completion包,就是读请求的“回复”。格式如下:

+--------+--------+--------+--------+
| Fmt[1:0] | Type[4:0] | R | TC[2:0] |
+--------+--------+--------+--------+
| R | Attr[1:0] | AT[1:0] | Length[9:0] |
+--------+--------+--------+--------+
| Completer ID[15:0] | Completion Status[2:0] | BCM | R | Byte Count[11:0] |
+--------+--------+--------+--------+
| Requester ID[15:0] | Tag[7:0] | R | Lower Address[6:0] |
+--------+--------+--------+--------+
| Data (可选) |
+--------+--------+--------+--------+

Completion包的关键字段:

  • Completer ID:谁完成的这个请求
  • Completion Status:完成状态,00表示成功,01表示不支持的请求,10表示异常
  • Byte Count:实际返回的字节数
  • Lower Address:返回数据的起始地址低7位

避坑指南:我曾经在调试一个FPGA的EP时,发现Completion包的Byte Count字段总是算错。后来查了规范才知道,Byte Count是返回数据的总字节数,不是剩余字节数。这个坑,我踩过,你别再踩了。

4.3 IO请求

IO请求,现在用得少了。但在一些老设备或者特殊场景下,还是会遇到。

IO请求的格式和Memory请求类似,但Type字段不同:

  • IO Read:Type = 5'b00010
  • IO Write:Type = 5'b00011

IO请求只支持32位地址,所以头是3DW的。而且IO请求的地址空间只有64KB,不像Memory可以到64位地址空间。

说实话,我在实际项目中很少用IO请求。现在的设备基本都用Memory Mapped IO了。但如果你要兼容老设备,还是得了解。

4.4 Configuration请求

Configuration请求,这是枚举设备时用的。Type 0用于访问Endpoint,Type 1用于访问Switch。

4.4.1 Configuration Read/Write Type 0

+--------+--------+--------+--------+
| Fmt[1:0] | Type[4:0] | R | TC[2:0] |
+--------+--------+--------+--------+
| R | Attr[1:0] | AT[1:0] | Length[9:0] |
+--------+--------+--------+--------+
| Requester ID[15:0] | Tag[7:0] | Last DW BE[3:0] | 1st DW BE[3:0] |
+--------+--------+--------+--------+
| Bus Number[7:0] | Device Number[4:0] | Function Number[2:0] | Register Number[11:0] |
+--------+--------+--------+--------+

Configuration请求的Type字段:

  • Type 0 Read:5'b00100
  • Type 0 Write:5'b00101
  • Type 1 Read:5'b00110
  • Type 1 Write:5'b00111

关键区别:Type 0的地址是Bus/Device/Function + Register,而Type 1除了这些,还包含目标Bus Number,用于穿越Switch。

核心要点:Configuration请求的Length字段固定为1,因为一次只能访问一个DW(4字节)。而且Configuration请求的Completion包,如果状态是“不支持的请求”,说明这个设备不存在或者不支持这个寄存器。

4.5 Message请求

Message请求,这是PCIe里比较灵活的一种类型。它没有地址,只有头和数据(可选)。

Message的Type字段是5'b01110(带数据)或5'b01111(不带数据)。Message Code字段在头的第2个DW里,用来区分不同的消息。

常见的Message类型:

Message Code 含义 说明
0x00 INTx中断 传统中断信号
0x10 PME_Turn_Off 电源管理关闭通知
0x11 PME_TO_Ack 电源管理关闭确认
0x20 Unlock 解锁消息
0x30 Vendor_Defined 厂商自定义消息

Message请求不需要Completion包。它是“发完就忘”的类型。你想想看,中断信号需要回复吗?不需要。所以Message很适合做中断、电源管理这类事情。

我的习惯:在设计EP时,我一般用MSI中断而不是INTx。MSI本质上就是一个Memory Write TLP,比INTx的Message效率高。而且MSI可以支持多个中断向量,INTx只能支持4个。

4.6 总结与对比

好了,咱们把四种请求类型和Completion包都过了一遍。我整理了一个对比表,方便你查阅:

类型 头大小 数据负载 需要Completion 典型用途
Memory Read 3/4 DW 读内存、读寄存器
Memory Write 3/4 DW 写内存、写寄存器
IO Read 3 DW 读IO端口
IO Write 3 DW 写IO端口
Config Read 3 DW 读配置空间
Config Write 3 DW 写配置空间
Message 4 DW 可选 中断、电源管理
Completion 3 DW 可选 N/A 读请求的回复

最后说一句,TLP包格式虽然看起来复杂,但只要你理解了每个字段的作用,再结合实际的抓包工具(比如PCIe Analyzer)看看,很快就能上手。我记得我第一次用分析仪抓TLP包时,看到那些0和1变成有意义的读写操作,那种感觉,真的很爽。