1. PCIe 基础概念

PCI Express(PCIe)作为现代计算机系统中主流的高速串行总线标准,是 Linux 内核驱动开发者必须掌握的核心知识。本章从总线拓扑、配置空间、地址映射三个维度,系统梳理 PCIe 的底层硬件模型,为后续 DMA 引擎编程打下坚实的硬件抽象基础。

1.1 PCIe 总线拓扑结构

PCIe 采用树形分层拓扑,由根复合体(Root Complex)、交换器(Switch)、端点设备(Endpoint)和桥接器(Bridge)构成。与传统的并行 PCI 不同,PCIe 使用点对点串行链路,每个设备拥有独立的虚拟通道。

关键拓扑角色:
  • 根复合体(RC): CPU 与 PCIe 域之间的桥梁,通常集成在 SoC 或芯片组中,负责生成配置请求、管理中断和地址转换。
  • 交换器(Switch): 由上游端口(Upstream Port)和多个下游端口(Downstream Port)组成,实现数据包的路由与转发,等效于 PCI 桥的扩展。
  • 端点设备(EP): 功能设备,如网卡、NVMe SSD、GPU、FPGA 加速卡,是驱动开发的主要目标。
  • 桥接器(Bridge): 用于连接 PCIe 与其它总线(如 PCI、PCI-X),实现协议转换。
PCIe 树形拓扑结构图 PCIe 树形拓扑结构 CPU / 内存控制器 根复合体 (Root Complex) 交换器 (Switch) 端点 EP 1 (NVMe) 端点 EP 2 (网卡) 端点 EP 3 (GPU) 说明: 每个端点通过独立串行链路与 Switch 或 RC 直连,链路宽度(x1/x4/x8/x16)和速率(Gen1~Gen5)决定带宽。
驱动开发提示: 在 Linux 内核中,通过 lspci -t 可以查看 PCIe 总线树;lspci -vvv 可查看链路状态(LnkSta)和带宽信息。驱动中通过 pci_dev 结构体的 bus->number 和 devfn 字段定位设备拓扑位置。

1.2 PCIe 配置空间

每个 PCIe 设备(包括 RC、Switch、EP)都拥有 4KB 配置空间(传统 PCI 只有 256 字节)。前 256 字节为配置空间头部(Type 0 用于端点,Type 1 用于桥/交换器),后 3840 字节为 PCIe 扩展能力结构(Extended Capabilities)。

PCIe 配置空间头部关键字段(Type 0)
偏移 字段 宽度 说明
0x00Vendor ID16 bit厂商编号(如 0x8086 为 Intel)
0x02Device ID16 bit设备型号编号
0x04Command16 bit控制 I/O、内存、总线主控等使能
0x06Status16 bit中断状态、能力列表等
0x08Revision ID8 bit版本修订号
0x09Class Code24 bit设备分类(基类/子类/编程接口)
0x10~0x24BAR0~BAR532 bit each基地址寄存器,定义设备地址空间
0x34Capabilities Pointer8 bit指向能力链表首项
0x3CInterrupt Line8 bit中断号分配(由固件填写)
0x3DInterrupt Pin8 bit中断引脚(INTA#~INTD#)
常见误区: 传统 PCI 驱动常直接使用 pci_read_config_byte/word/dword 访问配置空间,但 PCIe 扩展能力(如 AER、VC、DPC)位于 0x100~0xFFF 区域,必须通过 pcie_capability_read_word 等专用接口访问,否则可能读取到无效数据。
PCIe 配置空间布局 PCIe 配置空间布局(4KB) 配置空间头部 (256B) — Type 0 / Type 1 PCIe 扩展能力结构 (3840B) Power Management MSI/MSI-X AER ... 扩展能力通过链表串联,每个能力项首字节为能力 ID,次字节为指向下一项的偏移(0 表示结束)。

1.3 BAR 空间与地址映射

基地址寄存器(BAR) 是 PCIe 设备向系统声明自身地址空间的桥梁。每个端点设备最多支持 6 个 BAR(BAR0~BAR5),每个 BAR 可以映射为 MMIO(内存映射 I/O) 或 I/O 空间。BAR 的宽度可以是 32 位或 64 位(占用连续两个 BAR 槽位)。

BAR 空间关键属性:
  • MMIO(Memory Space): 设备寄存器映射到 CPU 的物理地址空间,CPU 通过 ioread32/iowrite32 或直接内存访问(如 memcpy_toio)操作。支持预取(Prefetchable)和非预取两种类型。
  • I/O Space: 使用 x86 特有的 I/O 端口指令(in/out)访问,地址空间独立于内存,现代设备已逐渐淘汰。
  • 64 位 BAR: 当 BAR 的 bit 2~1 为 10 时,表示该 BAR 为 64 位,需要与下一个 BAR 合并为一个 64 位地址。
BAR 寄存器编码格式
Bit 位置 含义 说明
bit 0空间类型0 = MMIO, 1 = I/O
bit 2~1MMIO 类型00 = 32 位, 10 = 64 位, 01/11 保留
bit 3Prefetchable1 = 预取使能(允许合并写、缓存)
bit 31~4基地址对齐到 16 字节边界,实际对齐由 BAR 大小决定
BAR 地址映射与 MMIO 访问路径 BAR 地址映射与 MMIO 访问路径 CPU 核心 内存控制器 系统物理地址空间 DDR 内存区域 ... PCIe MMIO 窗口 PCIe 端点设备 BAR0 寄存器 ioremap 驱动 MMIO 访问流程: 1. BIOS/内核枚举设备,将 BAR 地址写入系统物理地址空间 2. 驱动调用 pci_ioremap_bar() 将 BAR 映射到内核虚拟地址空间

1.4 MMIO 与 I/O 空间对比

MMIO 与 I/O 空间核心差异
对比维度 MMIO(Memory-Mapped I/O) I/O 空间(I/O Port)
地址空间共享 CPU 物理地址空间(0~4GB 或更高)独立 64KB 地址空间(x86)
访问指令普通内存加载/存储(mov, ldr)专用 I/O 指令(in, out)
Linux 访问接口ioread32/iowrite32、memcpy_toioinb/outb、inw/outw、inl/outl
预取/缓存支持 Prefetchable 和 Non-Prefetchable不支持,每次访问必须直达设备
性能高,可利用 CPU 缓存一致性(需谨慎)低,每次访问开销大
现代设备支持主流,几乎所有 PCIe 设备使用 MMIO已淘汰,仅部分传统设备保留
驱动最佳实践: 在 Linux 内核中,推荐使用 pci_iomap() 统一处理 MMIO 和 I/O 空间映射。该函数根据 BAR 类型自动选择 ioremap 或 ioport_map,返回的虚拟地址可直接用于 ioread32/iowrite32 等访问宏。
// 典型 PCIe 驱动 BAR 映射示例
#include <linux/pci.h>
#include <linux/io.h>

static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_dev *dev;
    int ret;
    int bar = 0;  // 使用 BAR0

    // 1. 启用 PCIe 设备
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    // 2. 设置 DMA 掩码(后续章节详述)
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (ret)
        goto err_disable;

    // 3. 请求 BAR 资源
    ret = pci_request_region(pdev, bar, "my_pcie_bar");
    if (ret)
        goto err_disable;

    // 4. 映射 BAR 到内核虚拟地址
    dev->regs = pci_iomap(pdev, bar, pci_resource_len(pdev, bar));
    if (!dev->regs) {
        ret = -ENOMEM;
        goto err_release;
    }

    // 5. 读取设备 ID 寄存器验证映射
    u32 id = ioread32(dev->regs + 0x00);
    dev_info(&pdev->dev, "Device ID register: 0x%08x\n", id);

    return 0;

err_release:
    pci_release_region(pdev, bar);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
注意事项:
  • BAR 映射前必须调用 pci_enable_device(),否则 MMIO 访问可能触发 Master Abort。
  • 64 位 BAR 在 32 位系统上可能映射失败,驱动应检查 pci_iomap 返回值。
  • 对于 Prefetchable MMIO,驱动应使用 memremap() 而非 ioremap(),以利用缓存一致性优化。

1.5 本章小结