3. Memory Read/Write TLP:格式、地址与数据对齐
Memory Read/Write TLP 是 PCIe 设备驱动开发中最核心的报文类型,负责主机与设备之间的数据搬运。本章深入解析 Memory Read Request/Completion 与 Memory Write TLP 的逐字段格式、地址映射规则以及数据对齐要求,为后续 DMA 传输实现奠定协议基础。
3.1 Memory Read Request TLP 格式
Memory Read Request TLP 由发起者(Requester)发送给完成者(Completer),请求从指定地址读取一定长度的数据。其格式遵循 PCIe Base Spec 定义的通用 TLP 头结构,包含以下关键字段:
| 字段 | 位宽 | 描述 |
|---|---|---|
| Fmt[1:0] | 2 bits | 格式编码:Memory Read Request 为 00(3DW 无数据)或 01(4DW 无数据,带地址扩展) |
| Type[4:0] | 5 bits | 类型编码:Memory Read Request 为 00000 |
| TC[2:0] | 3 bits | Traffic Class,用于 QoS 优先级区分 |
| TD | 1 bit | TLP Digest 存在标志,通常为 0 |
| EP | 1 bit | 毒包(Poisoned)标志,指示数据损坏 |
| Attr[2:0] | 3 bits | 属性:顺序模型、No Snoop、Relaxed Ordering |
| Length[9:0] | 10 bits | 请求读取的 DW 数量(1~1024),实际数据长度 = Length × 4 Bytes |
| Requester ID[15:0] | 16 bits | 发起者 Bus/Device/Function 编号 |
| Tag[7:0] | 8 bits | 标签,用于匹配 Request 与 Completion |
| Address[31:2] | 30 bits | 32 位地址的高 30 位,低 2 位隐含为 0(DW 对齐) |
| Address[63:32] | 32 bits | 仅 4DW 头存在,用于 64 位地址空间 |
关键理解: Memory Read Request 本身不携带数据负载,仅携带地址和长度信息。实际读取的数据通过 Completion TLP 返回给 Requester。Length 字段以 DW(4 字节)为单位,且必须满足对齐约束。
3.2 Memory Read Completion TLP 格式
Completer 收到 Memory Read Request 后,返回一个或多个 Completion TLP。Completion 包含请求的数据以及完成状态。其格式如下:
| 字段 | 位宽 | 描述 |
|---|---|---|
| Fmt[1:0] | 2 bits | 10(3DW 带数据)或 11(4DW 带数据) |
| Type[4:0] | 5 bits | Completion 类型:01010(Completion with Data) |
| Length[9:0] | 10 bits | 本次 Completion 携带的 DW 数量 |
| Requester ID[15:0] | 16 bits | 复制自 Request 的 Requester ID |
| Tag[7:0] | 8 bits | 复制自 Request 的 Tag,用于匹配 |
| Completer ID[15:0] | 16 bits | 完成者的 Bus/Device/Function 编号 |
| Completion Status[2:0] | 3 bits | 完成状态:000 成功,001 UR,010 CA,100 CRS |
| BCM | 1 bit | Byte Count Modified,用于多 Completion 场景 |
| Byte Count[11:0] | 12 bits | 剩余未传输的字节数(用于拆分传输) |
| Data Payload | 可变 | 读取到的实际数据,长度由 Length 字段决定 |
驱动开发注意: Completion 的 Tag 必须与 Request 的 Tag 严格匹配。驱动中通常使用 Tag 池管理并发请求,避免 Tag 冲突导致数据错乱。Completion 可能拆分多个 TLP 返回,驱动需根据 Byte Count 和 Length 字段重组数据。
3.3 Memory Write TLP 格式
Memory Write TLP 由发起者直接向目标地址写入数据,无需 Completion 响应(Posted 事务)。其格式如下:
| 字段 | 位宽 | 描述 |
|---|---|---|
| Fmt[1:0] | 2 bits | 10(3DW 带数据)或 11(4DW 带数据) |
| Type[4:0] | 5 bits | Memory Write 类型:00000(与 Read 同 Type,通过 Fmt 区分) |
| Length[9:0] | 10 bits | 写入数据的 DW 数量 |
| Requester ID[15:0] | 16 bits | 写入发起者 ID |
| Tag[7:0] | 8 bits | 标签(Write 为 Posted,Tag 通常置 0) |
| Address[31:2] | 30 bits | 目标地址高 30 位 |
| Address[63:32] | 32 bits | 64 位地址扩展(仅 4DW 头) |
| Data Payload | 可变 | 待写入的数据,长度 = Length × 4 Bytes |
Posted vs Non-Posted: Memory Write 是 Posted 事务,发送后无需等待 Completion,总线利用率高。Memory Read 是 Non-Posted 事务,必须等待 Completion 返回。驱动设计时,写操作通常比读操作具有更低的延迟开销。
3.4 地址与数据对齐要求
PCIe 协议对 Memory TLP 的地址和数据对齐有严格约束,违反对齐会导致 UR(Unsupported Request)或数据错误。以下是核心规则:
- 地址 DW 对齐: Memory Read/Write 的起始地址必须对齐到 4 字节边界(Address[1:0] = 00)。低 2 位在 TLP 头中隐含为 0。
- 长度 DW 对齐: Length 字段以 DW 为单位,数据总长度必须是 4 的倍数。例如 Length=3 表示 12 字节。
- Max Payload Size (MPS): 单个 TLP 的数据负载不能超过设备/系统协商的 MPS(通常为 128/256/512/1024 字节)。超长请求必须拆分为多个 TLP。
- Read Completion 边界: Completion 返回的数据必须与 Request 的地址和长度严格对应,且不能跨 4KB 边界(除非设备支持 4KB 边界跨越)。
- Write 合并: 多个连续的 Memory Write 可能被 RC 或 Switch 合并为一个更大的 TLP,但合并后的地址和长度仍需满足对齐。
驱动实践: 在分配 DMA 缓冲区时,建议使用
dma_alloc_coherent() 或 __get_free_pages() 确保缓冲区起始地址至少 4 字节对齐。对于非对齐访问,需使用 Read-Modify-Write 策略,但会显著降低性能。
3.5 Memory TLP 传输流程 SVG 图
下图展示了 Memory Read Request/Completion 与 Memory Write 的完整传输流程,包含地址映射、TLP 封装、数据对齐检查等关键环节:
3.6 数据对齐检查代码示例
以下代码展示驱动中如何检查 Memory TLP 的地址和数据对齐,确保符合 PCIe 协议要求:
/* 检查 Memory TLP 地址和数据对齐 */
static int check_tlp_alignment(u64 addr, size_t size)
{
/* 1. 地址必须 4 字节对齐 */
if (addr & 0x3) {
pr_err("TLP address 0x%llx not DW aligned\n", addr);
return -EINVAL;
}
/* 2. 数据长度必须 4 字节对齐 */
if (size & 0x3) {
pr_err("TLP size %zu not DW aligned\n", size);
return -EINVAL;
}
/* 3. 长度不能超过 Max Payload Size (MPS) */
u16 mps = pcie_get_mps(dev);
if (size > mps) {
pr_err("TLP size %zu exceeds MPS %u\n", size, mps);
return -EINVAL;
}
/* 4. 地址 + 长度不能跨 4KB 边界(除非设备支持) */
u64 end = addr + size;
if ((addr & 0xFFF) + size > 0x1000) {
pr_warn("TLP crosses 4KB boundary, may cause UR\n");
return -EPERM;
}
/* 5. 对于 64 位地址,检查高 32 位是否有效 */
if (addr & 0xFFFFFFFF00000000ULL) {
if (!pci_is_pcie(dev) || !pcie_has_flr(dev)) {
pr_err("64-bit address not supported by device\n");
return -EOPNOTSUPP;
}
}
return 0;
}
/* 拆分大请求为多个 TLP */
static int split_and_send_tlp(struct pci_dev *dev, u64 addr, void *data, size_t size)
{
u16 mps = pcie_get_mps(dev);
size_t offset = 0;
int ret;
while (offset < size) {
size_t chunk = min_t(size_t, size - offset, mps);
/* 确保每个 chunk 对齐 */
if ((addr + offset) & 0x3 || chunk & 0x3) {
pr_err("Split chunk misaligned at offset %zu\n", offset);
return -EINVAL;
}
ret = pcie_send_memory_write(dev, addr + offset, data + offset, chunk);
if (ret)
return ret;
offset += chunk;
}
return 0;
}
3.7 常见对齐错误与避坑指南
| 错误场景 | 协议后果 | 驱动修复方法 |
|---|---|---|
| 地址未 4 字节对齐 | Completer 返回 UR,请求被丢弃 | 使用 ALIGN() 宏对齐缓冲区地址 |
| 数据长度非 4 的倍数 | TLP 被硬件截断或填充,数据错误 | 填充到 4 字节边界,或使用 Read-Modify-Write |
| 单 TLP 超过 MPS | 硬件拆分或丢弃,性能下降 | 驱动层手动拆分,确保每片 ≤ MPS |
| 跨 4KB 边界 | 部分 RC 返回 UR,设备不可用 | 分配 4KB 对齐缓冲区,或逐页发送 |
| 64 位地址未使能 | 高 32 位被忽略,地址空间错误 | 检查 PCI_EXP_DEVCTL2 的 64 位地址使能位 |
调试建议: 使用 PCIe 分析仪或
lspci -vvv 检查设备的 MPS、MRRS(Max Read Request Size)和地址空间能力。驱动开发初期,建议在 TLP 发送前后打印地址和长度,配合 pci_err() 定位对齐问题。
3.8 本章小结
- Memory Read Request 使用 Non-Posted 事务,必须等待 Completion 返回数据;Memory Write 使用 Posted 事务,无需等待。
- TLP 头中的 Address 字段低 2 位隐含为 0,Length 字段以 DW 为单位,数据负载必须是 4 的倍数。
- 地址对齐、长度对齐、MPS 限制和 4KB 边界是驱动开发中最常见的协议约束,违反会导致 UR 或数据损坏。
- 驱动中应实现对齐检查函数,并在 DMA 缓冲区分配时确保起始地址和大小满足 PCIe 要求。