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),实现协议转换。
驱动开发提示: 在 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)。
| 偏移 | 字段 | 宽度 | 说明 |
|---|---|---|---|
| 0x00 | Vendor ID | 16 bit | 厂商编号(如 0x8086 为 Intel) |
| 0x02 | Device ID | 16 bit | 设备型号编号 |
| 0x04 | Command | 16 bit | 控制 I/O、内存、总线主控等使能 |
| 0x06 | Status | 16 bit | 中断状态、能力列表等 |
| 0x08 | Revision ID | 8 bit | 版本修订号 |
| 0x09 | Class Code | 24 bit | 设备分类(基类/子类/编程接口) |
| 0x10~0x24 | BAR0~BAR5 | 32 bit each | 基地址寄存器,定义设备地址空间 |
| 0x34 | Capabilities Pointer | 8 bit | 指向能力链表首项 |
| 0x3C | Interrupt Line | 8 bit | 中断号分配(由固件填写) |
| 0x3D | Interrupt Pin | 8 bit | 中断引脚(INTA#~INTD#) |
常见误区: 传统 PCI 驱动常直接使用
pci_read_config_byte/word/dword 访问配置空间,但 PCIe 扩展能力(如 AER、VC、DPC)位于 0x100~0xFFF 区域,必须通过 pcie_capability_read_word 等专用接口访问,否则可能读取到无效数据。
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 位地址。
| Bit 位置 | 含义 | 说明 |
|---|---|---|
| bit 0 | 空间类型 | 0 = MMIO, 1 = I/O |
| bit 2~1 | MMIO 类型 | 00 = 32 位, 10 = 64 位, 01/11 保留 |
| bit 3 | Prefetchable | 1 = 预取使能(允许合并写、缓存) |
| bit 31~4 | 基地址 | 对齐到 16 字节边界,实际对齐由 BAR 大小决定 |
1.4 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_toio | inb/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(),以利用缓存一致性优化。