4、TLP生成与解析:存储器读/写TLP格式详解、完成(Completion)TLP处理、Max Payload Size与Max Read Request Size对性能的影响
好,咱们进入第四讲。这一讲的内容,说白了就是PCIe DMA引擎的“血肉”——TLP的生成与解析。你光有DMA的骨架(状态机、描述符)不行,得知道怎么把数据打包成TLP发出去,怎么把收到的TLP拆开。我个人习惯把这一讲称为“协议落地”,因为这里全是硬碰硬的细节。
4.1 存储器写TLP(MWr)格式详解
先看写操作。DMA最常见的操作就是把数据从本地搬到远端,这对应的是存储器写TLP。
一个标准的MWr TLP长什么样?我直接给你看核心字段:
+--------+--------+--------+--------+
| Byte 0 | Byte 1 | Byte 2 | Byte 3 |
+--------+--------+--------+--------+
| Fmt=10 | Type=00000 | TC | ... |
+--------+--------+--------+--------+
| Length (DW count) |
+-----------------------------------+
| Requester ID | Tag |
+-----------------------------------+
| Last DW BE | First DW BE |
+-----------------------------------+
| Address (64-bit or 32-bit) |
+-----------------------------------+
| Data (1 to Max Payload Size DW) |
+-----------------------------------+
这里有几个关键点,我重点说一下:
- Fmt与Type:对于64位地址的MWr,Fmt=10(3DW头+数据),Type=00000。32位地址则是Fmt=01(2DW头+数据)。
- Length:以DW(4字节)为单位。注意,Length=0是无效的,最小是1个DW。
- First/Last DW BE:字节使能。这个坑我踩过——如果你要写一个非对齐的地址,比如从地址偏移1开始写4字节,First DW BE就得是0x0E(只使能Byte1-3),Last DW BE是0x00。很多新手直接填0xFF,结果数据写错位置。
- Address:64位地址需要两个DW。注意地址必须按DW对齐(低2位为0),除非你用了字节使能。
核心要点:MWr TLP的Data Payload大小不能超过Max Payload Size(MPS)。如果你要传的数据超过MPS,必须拆成多个TLP。
4.2 存储器读TLP(MRd)与完成(Cpl/CplD)处理
读操作比写复杂,因为它涉及“请求-完成”的握手。你发一个MRd出去,对方得回一个Completion回来。
4.2.1 MRd TLP格式
MRd的头部和MWr很像,但少了Data字段:
+--------+--------+--------+--------+
| Fmt=00 (32-bit) or 10 (64-bit) |
| Type=00000 |
+-----------------------------------+
| Length (DW count to read) |
+-----------------------------------+
| Requester ID | Tag |
+-----------------------------------+
| First DW BE | Last DW BE |
+-----------------------------------+
| Address |
+-----------------------------------+
注意这里的Length是你请求读取的DW数量。这个值不能超过Max Read Request Size(MRRS)。
避坑指南:我曾经遇到一个案例,MRd的Length设成了128DW,但MRRS只有64DW。结果RC(Root Complex)直接返回UR(Unsupported Request),整个DMA卡死。记住:MRd的Length ≤ MRRS,这是硬性规定。
4.2.2 Completion TLP(Cpl/CplD)处理
对方收到你的MRd后,会返回一个Completion。有两种:
- Cpl:不带数据的完成,通常用于错误响应(比如UR、CA)。
- CplD:带数据的完成,这才是我们想要的。
CplD的格式如下:
+--------+--------+--------+--------+
| Fmt=10 | Type=01010 (CplD) |
+-----------------------------------+
| Length (实际返回的DW数) |
+-----------------------------------+
| Completer ID | Byte Count |
+-----------------------------------+
| Requester ID | Tag |
+-----------------------------------+
| Lower Address |
+-----------------------------------+
| Data (实际数据) |
+-----------------------------------+
这里有个容易搞混的地方:
- Byte Count:表示从起始地址开始,还有多少字节没传完。这个字段用于“拆分完成”的场景。
- Lower Address:返回数据的起始地址的低7位(对于DW对齐的地址,低2位为0)。
举个例子:你请求读地址0x1000开始的16字节。如果对方MPS=64字节,它可能一次返回全部16字节。但如果对方MPS=8字节,它就得拆成两个CplD:第一个返回0x1000-0x1007,Byte Count=8;第二个返回0x1008-0x100F,Byte Count=0。
我的经验:处理CplD时,一定要检查Tag和Requester ID是否匹配。我曾经在一个多通道DMA设计中,因为Tag分配逻辑有bug,导致A通道的读请求收到了B通道的完成数据——数据全乱了。从那以后,我强制要求每个DMA通道使用独立的Tag池。
4.3 Max Payload Size(MPS)与Max Read Request Size(MRRS)
这两个参数,直接决定了你的DMA性能。我见过太多人忽略它们,结果DMA跑起来像乌龟。
4.3.1 它们是什么?
| 参数 | 含义 | 典型值 | 谁设置 |
|---|---|---|---|
| MPS | 单个TLP能携带的最大数据量(写TLP的Payload上限) | 128B / 256B / 512B / 1024B | 由系统固件(BIOS/UEFI)协商决定 |
| MRRS | 单个读请求能请求的最大数据量(MRd的Length上限) | 128B / 256B / 512B / 1024B / 2048B / 4096B | 由系统固件协商,但设备可以主动降低 |
4.3.2 对性能的影响
你想想看,如果MPS=128字节,你要写1MB数据,就得生成8192个TLP。每个TLP都有头部开销(约20字节),加上链路层的ACK/NAK,效率能高才怪。
反过来,如果MPS=1024字节,同样的数据只需要1024个TLP,头部开销减少8倍。这就是为什么高性能DMA引擎都倾向于使用大MPS。
但注意,MPS不是越大越好。我记得在一个FPGA项目中,我们把MPS设为1024字节,结果发现PCIe核内部的FIFO深度不够,导致频繁的背压(Backpressure)。后来降到512字节,反而吞吐量更高了。
性能公式(粗略估算):
有效带宽 ≈ (Payload大小) / (Payload大小 + 开销) × 链路速率
其中开销包括:TLP头(16-20字节)、序列号(4字节)、LCRC(4字节)、ACK/NAK等。
当Payload=128B时,开销占比约15%;当Payload=1024B时,开销占比不到3%。
4.3.3 如何选择MPS和MRRS?
我个人建议遵循以下原则:
- MPS:尽量使用系统协商的最大值。但如果你发现DMA引擎内部缓冲区紧张,可以主动降低。比如你的DMA描述符缓冲区只有2KB,MPS设为1024B可能只够缓存2个TLP,容易断流。
- MRRS:这个可以比MPS大。比如MPS=256B,MRRS=1024B。这样你发一个读请求,对方可以拆成4个CplD回来。但注意,MRRS太大可能导致RC端的内部缓冲区溢出,反而增加延迟。
- 匹配原则:MRRS最好是MPS的整数倍。比如MPS=128B,MRRS=512B(4倍)。这样拆分完成时逻辑简单。
我曾经踩过的坑:在一个NVMe SSD的DMA驱动中,我把MRRS设成了4096B,但MPS只有128B。结果每次读请求,对方返回32个CplD,每个只有128B。这导致完成队列(Completion Queue)瞬间被填满,CPU频繁中断,性能反而下降了30%。后来我把MRRS降到512B(4个CplD),性能才恢复正常。
4.4 实战:TLP生成与解析的代码片段
最后,我贴一段简化的TLP生成代码(Verilog风格),帮你理解实际实现:
// 生成一个64位地址的MWr TLP
// 假设:MPS=256B,地址对齐,数据已准备好
always_ff @(posedge clk) begin
if (tx_start) begin
// TLP头部
tlp_data[0] <= {4'b0010, 5'b00000, 3'b000, 4'b0000}; // Fmt=10, Type=00000
tlp_data[1] <= {8'b0000_0000, 8'd64}; // Length=64 DW (256B)
tlp_data[2] <= {requester_id, tag};
tlp_data[3] <= {last_be, first_be};
tlp_data[4] <= addr[63:32];
tlp_data[5] <= addr[31:0];
// 数据部分从tlp_data[6]开始
// 注意:数据必须按DW对齐,且总长度不超过MPS
end
end
// 解析CplD(简化版)
always_ff @(posedge clk) begin
if (rx_valid && (rx_type == CPLD)) begin
// 检查Tag是否匹配
if (rx_tag == expected_tag) begin
// 提取Byte Count和Lower Address
byte_count <= rx_data[47:32]; // Byte Count字段
lower_addr <= rx_data[31:25]; // Lower Address(低7位)
// 提取数据
for (int i=0; i<rx_length; i++) begin
data_buffer[i] <= rx_data[6+i];
end
// 更新完成状态
cpl_done <= (byte_count == 0) ? 1'b1 : 1'b0;
end
end
end
嗯,代码只是示意,实际工程中还要处理背压、重传、超时等。但核心逻辑就是这些。
小结
这一讲我们深入了TLP的细节。写TLP相对简单,读TLP涉及请求-完成握手,需要小心处理Tag匹配和拆分完成。MPS和MRRS是性能的关键,选对了事半功倍,选错了事倍功半。下一讲我们会把这些TLP操作集成到DMA引擎中,看看状态机怎么和TLP层交互。
记住:PCIe协议是死的,但你的设计是活的。多在实践中摸索,才能找到最适合你系统的参数。