PCIe事务层TLP详解
嗯,到这里我们终于要碰PCIe协议里最核心的「皮肉」——事务层TLP(Transaction Layer Packet)。
我个人觉得,搞懂TLP,才算是真正看懂PCIe在干什么。物理层是骨架,数据链路层是肌肉,那事务层就是神经系统——各种命令、数据、状态都在TLP里跑。
1. TLP是什么?
说白了,TLP就是PCIe设备之间传递「意图」的报文。你读一下内存,写一个寄存器,发一个中断,全都是靠TLP来完成的。
每个TLP由三部分组成:
- Header:包头,至少12/16字节,定义操作类型、地址、长度、标签等。
- Data Payload:数据载荷,0~4096字节,读请求不带数据,写请求和完成包才带。
- Digest(可选):ECRC,端到端CRC,我一般在调试时不使能,因为会增加开销。
关键记住:所有TLP都是从发起端(Requester)发给完成端(Completer),中间可能经过Switch路由。事务层不关心物理走几条lane,只关心报文封装。
2. TLP头格式——硬功夫
我记得刚入行那会,总是记不住Fmt和Type字段的值。后来自己画了个速查表,每次调逻辑分析仪时就盯着那几位看。
TLP Header的前两个字节是Fmt[1:0]和Type[4:0],它们决定TLP的种类和长度:
| Fmt[1:0] | 含义 | Type示例 |
|---|---|---|
| 00 | 3DW Header,无数据 | Memory Read、Completion无数据 |
| 01 | 4DW Header,无数据 | 地址宽度64位读请求 |
| 10 | 3DW Header,有数据 | Memory Write |
| 11 | 4DW Header,有数据 | 64位Memory Write |
你想想看,每次抓包第一件事就是看Fmt和Type,连起来就是0x41、0x42这些值。我的习惯是把常见组合贴在显示器边缘:40 Memory Read 32b,41 Memory Read 64b,60 Memory Write 32b……
避坑指南:我曾经遇到过一个bug,上游设备发送的Memory Write Fmt=10(3DW有数据),下游期望是4DW导致地址错位。后来发现是PCIe Gen3升级时地址空间没对齐。所以建议只要访问64位地址空间,统一用4DW Header(Fmt=11),省心。
3. TLP类型与路由
PCIe把TLP分为四大类,每类对应不同的路由方式。
| 大类 | 子类型 | 路由方式 | 常用场景 |
|---|---|---|---|
| Memory | Read / Write | 地址路由 | DMA、寄存器访问 |
| IO | Read / Write | 地址路由 | Legacy设备(很少用了) |
| Configuration | Type0 / Type1 | ID路由(Bus:Dev:Fnc) | 枚举、配置空间读写 |
| Message | PME、INTx、Err等 | 广播/点对点 | 电源管理、中断、错误报告 |
这里尤其要注意路由方式。地址路由很简单——看目标地址落在哪个设备的内存窗口里。ID路由则是靠Bus Number、Device Number、Function Number,也就是我们常说的BDF。举个例子,配置读写TLP的包头里会带着BDF,Switch根据这个决定转发给哪个下游端口。
4. 事务层处理流程——请求与完成
PCIe里有两个基本的协议握手:Non-Posted(非报告式)和Posted(报告式)。
- Posted:发出去就不管了,不需要完成包。典型:Memory Write、Message。好处是延迟低,坏处是万一写错了没人通知你。我在写驱动时一定会在关键寄存器操作后用Memory Read回读确认,就因为Posted写不可靠。
- Non-Posted:发送请求后必须等待一个Completion回来。典型:Memory Read、IO Read、Configuration Read。Completion里会带回读取的数据,或者错误状态。
为什么会这样设计?你想想,PCIe是分层的,读写请求都在事务层打包,然后丢给数据链路层。如果等ACK再发下一个,延迟太大。所以Posted操作允许流水线,而Non-Posted必须等返回结果才能继续。
5. TLP的传输与边界
这里需要强调一个关键概念:TLP边界(Transaction Layer Packet Boundary)。物理层以Symbol为单位传输,但事务层以DW(4字节)为单位。所以任何TLP的长度必须是DW对齐的。
我曾经掉进过一个坑:某加速卡驱动构造了一个长度为5字节的写数据包(不够DW对齐),结果数据链路层自动补了3个零,导致对方设备读到错误数据。从那以后我每次都严格按payload_len % 4 == 0来检查。
特别注意:PCIe强制TLP Data Payload不能超过Max_Payload_Size(MPS),这是通过设备能力协商的。如果上层驱动发了大于MPS的包,数据链路层就会切包(拆成多个TLP),这会带来性能下降。我调优时第一步就是检查MPS是否对齐到256B或512B。
6. 一张图看懂TLP封装
7. 实际调参的几点经验
看了这么多概念,最后分享几个我调PCIe性能时常用的原则:
- 尽量用Memory Read/Write,少用IO和Configuration。IO和Cfg事务是Non-Posted且延迟大,平时枚举用就好,运行时千万别频繁发。
- Read Completion Boundary (RCB)必须对齐RCB(默认为128B或64B)。我曾在NVMe SSD上碰到过因为RCB不匹配导致完成包被拆分,吞吐掉了一半。
- 利用Relaxed Ordering:如果写操作的顺序不重要,可以设置RO位,让Switch自由重排,能提升带宽利用率。前提是你的应用可以接受乱序。
- ECRC尽量关闭:除非你的链路非常长或者有强纠错要求,否则ECRC的额外开销不值得。现在数据链路层的LCRC+重传机制已经足够可靠。
我的小工具:我习惯在仿真阶段用脚本自动检查所有TLP的Header字段——写一个小解析器,把0x41、0x42这些值直接翻译成“64位Memory Read”、“32位Memory Write”。你有空也可以写一个,排查问题快不少。
好了,事务层的东西就这些。记住:TLP是PCIe协议的精髓,你把它拆明白了,后面的调优都是水到渠成的事。