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变成有意义的读写操作,那种感觉,真的很爽。