3. TLP包格式详解:TLP头部格式、请求类型与Completion包
好,咱们今天来聊聊TLP包。这东西是PCIe通信的“最小单元”,说白了,所有数据在PCIe链路上跑,都得装进TLP这个信封里。我刚开始接触PCIe时,总觉得TLP头部那几十个字节看着头疼,后来踩了几个坑才明白——这玩意儿的设计其实非常精巧。
3.1 TLP头部格式:32位与64位寻址
TLP头部,我习惯叫它“信封的封面”。它告诉接收方:这包数据从哪来、要到哪去、是什么类型。头部长度通常是3个或4个双字(DW,每个DW=4字节)。
为什么会有3DW和4DW的区别? 关键就在寻址方式上。
- 32位寻址(3DW头部):地址字段只占2个字节?不对,其实是占一个DW(4字节),但只用了低32位。头部总共3个DW,也就是12字节。适合小地址空间,比如一些简单的IO设备。
- 64位寻址(4DW头部):地址字段占两个DW(8字节),头部变成4个DW,共16字节。现代大内存系统基本都用这个。我做过一个项目,设备需要访问64位地址空间,结果有人用了32位头部,数据死活写不对位置——嗯,这就是典型的“地址截断”问题。
来看个具体的头部结构(以Memory写请求为例):
// 32位寻址的Memory写TLP头部(3DW)
DW0: [Fmt=10][Type=00000][R][TC][R][Attr][R][TH][R][TD][EP][Attr2][AT][Length]
DW1: [Requester ID][Tag][Last DW BE][First DW BE]
DW2: [Address[31:2]][00] // 32位地址,低2位为0
// 64位寻址的Memory写TLP头部(4DW)
DW0: [Fmt=11][Type=00000]... // 注意Fmt字段不同
DW1: [Requester ID][Tag][Last DW BE][First DW BE]
DW2: [Address[63:32]] // 高32位地址
DW3: [Address[31:2]][00] // 低32位地址
你想想看,Fmt字段就是用来区分3DW还是4DW的。Fmt=10表示3DW,Fmt=11表示4DW。这个细节,我在一次调试中差点忽略,结果抓了半天的波形才发现是Fmt配错了。
3.2 请求类型:Memory、IO、Configuration、Message
TLP按用途分四大类。我按使用频率排个序:
3.2.1 Memory请求(最常用)
说白了就是读写内存地址。分两种:
- Memory Read (MRd):从目标地址读数据。需要对方回复一个Completion(后面细说)。
- Memory Write (MWr):往目标地址写数据。不需要回复,发完就完事——这叫“Posted”事务。
我个人习惯把Memory请求看作“快递”:写是寄出去不用回执,读是寄出去要对方回信。
3.2.2 IO请求(基本淘汰)
这是PCI时代的遗留物。IO地址空间很小(64KB),而且访问速度慢。我在新项目中几乎没见过它。如果你还在用IO请求,建议尽快迁移到Memory映射。
3.2.3 Configuration请求(初始化专用)
用于枚举和配置PCIe设备。分两种:
- Configuration Read (CfgRd):读设备的配置空间。
- Configuration Write (CfgWr):写设备的配置空间。
系统启动时,BIOS或OS会通过Configuration请求扫描总线上的设备。我记得有一次,设备在枚举阶段就挂了,抓包发现是Configuration请求的Tag字段被重复使用了——嗯,这是个容易踩的坑。
3.2.4 Message请求(中断与信号)
Message请求不访问地址,而是传递“事件”。最常见的是:
- INTx中断消息:传统中断信号的消息化。
- MSI/MSI-X中断:通过Memory写实现的中断,效率更高。
- 错误消息:比如UR(不支持的请求)、CA(完成者异常)等。
- PME(电源管理事件):设备唤醒主机。
Message请求也是Posted事务,不需要回复。我曾经调试一个MSI中断不生效的问题,最后发现是Message的Target Address写错了——它必须指向中断控制器的一个特定地址。
3.3 Completion包:读请求的“回信”
Completion包,我管它叫“回执”。只有Non-Posted请求(比如Memory Read、Configuration Read)才需要它。
Completion包的结构:
// Completion头部(3DW)
DW0: [Fmt=10][Type=01010][R][TC][R][Attr][R][TH][R][TD][EP][Attr2][AT][Length]
DW1: [Requester ID][Tag][R][Completion Status][BCM][Byte Count]
DW2: [Requester ID] // 注意:这里重复了Requester ID?不,是Completer ID
[Lower Address]
关键字段:
- Completion Status:表示请求是否成功。常见值:00=成功(SC),01=不支持的请求(UR),10=完成者异常(CA),11=完成者超时(CRS)。
- Byte Count:实际返回的数据字节数。注意,它可能比请求的少——这叫“部分完成”。
- Tag:必须与原始请求的Tag一致。这是匹配请求和完成的关键。
3.4 实战中的TLP分析技巧
最后分享几个我常用的技巧:
- 抓包时先看Fmt和Type:这两个字段决定了TLP的“骨架”。Fmt=10/11是Memory,Fmt=00是Configuration,Fmt=01是IO。
- 注意Tag的管理:每个Non-Posted请求都需要一个唯一的Tag。如果Tag用完了,新的请求就得排队等。我见过一个设计,Tag池只有16个,结果高负载时性能直接腰斩。
- Completion的延迟:从发出读请求到收到Completion,这个时间叫“读延迟”。如果延迟太大,可以考虑用“读合并”或“预取”来优化。
- Message请求的陷阱:Message请求虽然不需要回复,但接收方必须保证它被处理了。比如MSI中断,如果丢失了,设备可能永远等不到响应。
好了,TLP包格式就聊到这儿。下一章咱们会深入TLP的流控机制——说白了,就是怎么让这些包在链路上跑得又快又不丢。到时候见。