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、网卡。
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 映射流程
- 读取 BAR 寄存器: 驱动调用
pci_resource_start()获取 BAR 的物理基地址,pci_resource_len()获取长度。 - 请求资源: 使用
pci_request_region()向内核注册该 BAR 区域,防止冲突。 - 建立映射: 调用
ioremap()或pcim_iomap()将物理地址映射到内核虚拟地址空间。 - 访问寄存器: 通过
ioread32()/iowrite32()等函数读写映射后的虚拟地址。
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等访问函数,不能直接解引用指针(某些架构需要特殊内存屏障)。