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 封装、数据对齐检查等关键环节:

Memory Read/Write TLP 传输流程 Memory Read/Write TLP 传输流程 Requester (主机/驱动) TLP 封装/解析 地址 + 长度 + 对齐检查 Completer (设备/EP) 地址/长度 TLP 报文 Memory Read 流程(Non-Posted) ① Read Request ② 地址/对齐检查 ③ 读取数据 ④ Completion ⑤ 解析 Completion ⑥ 数据返回驱动 Memory Write 流程(Posted) ① Write Request + 数据 ② 地址/对齐检查 ③ 写入目标地址 ④ 无 Completion ⑤ 写完成确认 ⑥ 驱动继续执行

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 要求。