1. PCIe 基础回顾:总线拓扑结构、配置空间、BAR 空间与 MMIO 映射

PCI Express(PCIe)作为现代计算机系统中最核心的高速串行互连总线,是 Linux 驱动开发与性能优化的基石。本章节将系统回顾 PCIe 的物理拓扑结构、配置空间访问机制、基地址寄存器(BAR)原理以及内存映射 I/O(MMIO)的映射流程,为后续 DMA 与中断协同调优打下坚实的硬件抽象基础。

1.1 PCIe 总线拓扑结构

PCIe 采用树形拓扑结构,由根复合体(Root Complex)、交换器(Switch)、端点设备(Endpoint)和桥接器(Bridge)组成。与传统的并行 PCI 不同,PCIe 使用点对点串行链路,每个链路可配置为 x1、x4、x8、x16 等通道宽度。

关键概念: 根复合体(RC)连接 CPU 和内存子系统;交换器(Switch)负责路由数据包,内部包含多个虚拟 PCI 桥;端点(EP)是功能设备,如 NVMe SSD、GPU、网卡。
PCIe 总线拓扑结构 PCIe 总线拓扑结构 根复合体 (RC) CPU 系统内存 交换器 (Switch) 交换器 (Switch) 端点 (EP) NVMe 端点 (EP) GPU 端点 (EP) 网卡 端点 (EP) 其他 说明:RC 是拓扑根节点,Switch 扩展端口数量,Endpoint 是实际功能设备。 每个链路使用点对点串行连接,支持 x1/x4/x8/x16 通道宽度。

1.2 PCIe 配置空间

每个 PCIe 设备(包括桥和端点)都包含一个 4KB 的配置空间,前 256 字节为 PCI 兼容配置空间(Type 0 或 Type 1),剩余部分为 PCIe 扩展配置空间。配置空间通过配置读写总线周期访问,在 x86 平台上通常使用 IO 端口 0xCF8/0xCFC(传统方式)或内存映射配置访问(MMCFG,ECAM)。

偏移地址 寄存器名称 位宽 描述
0x00 Vendor ID 16 bit 厂商识别码(如 Intel 0x8086)
0x02 Device ID 16 bit 设备识别码
0x04 Command 16 bit 控制设备响应(IO、内存、总线主控使能)
0x06 Status 16 bit 设备状态信息
0x08 Revision ID 8 bit 修订版本号
0x09 Class Code 24 bit 设备类别(基类/子类/编程接口)
0x10 - 0x24 BAR0 - BAR5 32 bit 或 64 bit 基地址寄存器,定义设备地址空间
0x3C Interrupt Line 8 bit 中断向量号(由固件分配)
0x3D Interrupt Pin 8 bit 中断引脚(INTA#/INTB#/INTC#/INTD#)
驱动开发提示: Linux 内核通过 pci_read_config_*() / pci_write_config_*() 系列函数访问配置空间。在枚举阶段,内核自动读取 Vendor ID、Device ID、Class Code 来匹配驱动。

1.3 BAR 空间与 MMIO 映射

基地址寄存器(BAR)是 PCIe 设备配置空间中最重要的资源描述机制。每个 BAR 描述设备需要映射的一段地址空间(内存空间或 IO 空间),包括起始地址和大小。驱动通过读取 BAR 获取物理地址,然后将其映射到内核虚拟地址空间,实现 MMIO(Memory-Mapped I/O)访问。

BAR 空间类型

  • Memory Space BAR: 映射到系统物理内存地址空间,支持 32-bit 或 64-bit 地址。64-bit BAR 占用连续的两个 BAR 寄存器(如 BAR0 + BAR1)。
  • I/O Space BAR: 映射到 IO 地址空间(x86 特有),在现代高性能设备中已很少使用。
  • Prefetchable: 可预取内存空间,允许 CPU 推测性读取,通常用于大容量设备内存(如显存)。

MMIO 映射流程

  1. 读取 BAR 寄存器: 驱动调用 pci_resource_start() 获取 BAR 的物理基地址,pci_resource_len() 获取长度。
  2. 请求资源: 使用 pci_request_region() 向内核注册该 BAR 区域,防止冲突。
  3. 建立映射: 调用 ioremap() 或 pcim_iomap() 将物理地址映射到内核虚拟地址空间。
  4. 访问寄存器: 通过 ioread32() / iowrite32() 等函数读写映射后的虚拟地址。
BAR 空间与 MMIO 映射流程 BAR 空间与 MMIO 映射流程 PCIe 设备配置空间 BAR0: 0xF0000000 BAR1: 0xF1000000 ioremap() 映射 物理地址 → 虚拟地址 页表建立 驱动虚拟地址 ioread32/iowrite32 直接访问 关键 API 调用顺序: pci_resource_start() → pci_request_region() → ioremap() → ioread32()/iowrite32() 推荐使用 pcim_iomap() 实现资源自动管理(Managed Device Resource)

1.4 典型驱动初始化代码示例

以下代码展示了一个标准的 PCIe 驱动 probe 函数中,如何获取 BAR 资源并进行 MMIO 映射:

static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
    struct my_device *dev;
    int ret;
    unsigned long mmio_start, mmio_len;
    void __iomem *regs;

    // 1. 启用 PCI 设备
    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. 获取 BAR0 资源
    mmio_start = pci_resource_start(pdev, 0);
    mmio_len   = pci_resource_len(pdev, 0);

    // 4. 请求 BAR0 区域
    ret = pci_request_region(pdev, 0, "my_pci_driver");
    if (ret)
        goto err_disable;

    // 5. 映射 MMIO
    regs = pcim_iomap(pdev, 0, mmio_len);
    if (!regs) {
        ret = -ENOMEM;
        goto err_release;
    }

    // 6. 保存设备私有数据
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        ret = -ENOMEM;
        goto err_release;
    }
    dev->regs = regs;
    pci_set_drvdata(pdev, dev);

    // 7. 读取设备版本寄存器(示例)
    dev_info(&pdev->dev, "Device version: 0x%08x\n", ioread32(regs + 0x00));

    return 0;

err_release:
    pci_release_region(pdev, 0);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
常见陷阱:
  • 未调用 pci_request_region() 直接映射可能导致资源冲突,在热插拔场景下尤其危险。
  • 64-bit BAR 必须成对使用(如 BAR0 + BAR1),读取时需检查 pci_resource_flags() 中的 IORESOURCE_MEM_64 标志。
  • MMIO 映射后必须使用 ioread32/iowrite32 等访问函数,不能直接解引用指针(某些架构需要特殊内存屏障)。

1.5 本章小结