3. PCIe 地址空间:内存空间、I/O 空间、配置空间、BDF 编码规则
PCIe 总线继承了传统 PCI 的地址空间模型,并在此基础上进行了扩展。理解这三种地址空间(Memory、I/O、Configuration)以及设备标识符 BDF(Bus/Device/Function)是进行设备枚举和驱动开发的基础。本章将深入剖析这些核心概念。
3.1 PCIe 三大地址空间概述
PCIe 规范定义了三种独立的地址空间,用于不同的访问目的。驱动程序通过操作这些空间来控制设备。
| 地址空间 | 访问目标 | 主要用途 | 地址宽度 |
|---|---|---|---|
| Memory Space | 设备内部寄存器、数据缓冲区、MMIO | 高性能数据交换、控制寄存器访问 | 32-bit 或 64-bit |
| I/O Space | 传统 I/O 端口(x86 架构特有) | 兼容旧设备、简单控制寄存器 | 32-bit |
| Configuration Space | PCIe 配置寄存器(Capability 结构) | 设备枚举、资源分配、错误报告、电源管理 | 8-bit(通过 ECAM 机制访问) |
关键点:在现代 PCIe 驱动开发中,Memory Space 是主要操作对象。I/O Space 在 x86 平台外已基本废弃,而 Configuration Space 是操作系统和固件进行设备发现与初始化的核心战场。
3.2 配置空间(Configuration Space)详解
每个 PCIe 设备(Function)都拥有最多 4KB 的配置空间。前 256 字节(0x00-0xFF)是兼容 PCI 的配置头(Configuration Header),后 256 字节(0x100-0xFFF)用于存放 PCIe 扩展能力(PCIe Extended Capabilities)。
3.2.1 配置空间中的关键寄存器
- Vendor ID (0x00): 设备制造商编号(如 0x8086 为 Intel)。
- Device ID (0x02): 设备型号编号。
- Command Register (0x04): 控制设备行为(如 I/O Space Enable、Memory Space Enable、Bus Master Enable)。
- Status Register (0x06): 反映设备状态(如 Interrupt Status、Capabilities List)。
- Class Code (0x09-0x0B): 设备功能分类(如 0x010000 为 Mass Storage Controller)。
- Base Address Registers (BARs) (0x10-0x24): 设备请求的 Memory/I/O 空间基地址和大小。
- Capabilities Pointer (0x34): 指向第一个 Capability 结构的偏移。
驱动开发实战:在 Linux 内核中,通过
pci_read_config_byte/word/dword() 和 pci_write_config_byte/word/dword() 系列函数访问配置空间。例如,读取 Vendor ID 的代码片段如下:
#include <linux/pci.h>
struct pci_dev *dev = ...; // 获取设备结构体
u16 vendor_id;
pci_read_config_word(dev, PCI_VENDOR_ID, &vendor_id);
printk(KERN_INFO "Vendor ID: 0x%04x\n", vendor_id);
3.3 内存空间与 I/O 空间
设备通过 BAR 寄存器向系统请求地址空间。驱动程序需要将 BAR 中记录的物理地址映射到内核虚拟地址空间才能访问。
3.3.1 Base Address Registers (BARs)
每个 Function 最多有 6 个 BAR(BAR0-BAR5)。BAR 的类型决定了它请求的是 Memory 空间还是 I/O 空间。
| BAR 类型 | Bit 0 值 | 地址宽度 | 典型用途 |
|---|---|---|---|
| Memory BAR | 0 | 32-bit 或 64-bit (由 Bit 2-1 决定) | 设备寄存器、DMA 缓冲区、帧缓冲 |
| I/O BAR | 1 | 32-bit | 传统 I/O 端口(x86 平台) |
3.3.2 内存空间映射(MMIO)
Memory BAR 请求的物理地址空间,在驱动中必须通过 ioremap() 映射为内核虚拟地址才能安全访问。
// 假设已经获取了 pci_dev 结构体 dev
// 获取 BAR0 的物理地址和大小
resource_size_t phys_addr = pci_resource_start(dev, 0);
resource_size_t size = pci_resource_len(dev, 0);
// 将物理地址映射到内核虚拟地址空间
void __iomem *virt_addr = ioremap(phys_addr, size);
if (!virt_addr) {
dev_err(&dev->dev, "ioremap failed\n");
return -ENOMEM;
}
// 通过映射后的虚拟地址读写设备寄存器
u32 reg_val = ioread32(virt_addr + REG_OFFSET);
iowrite32(0x1, virt_addr + CONTROL_REG);
// 使用完毕后取消映射
iounmap(virt_addr);
常见错误:直接对
phys_addr 进行指针解引用会导致内核崩溃。必须使用 ioremap() 和 ioread32()/iowrite32() 系列函数进行访问。
3.4 BDF 编码规则
BDF(Bus:Device.Function)是 PCIe 总线上唯一标识一个 Function 的地址。它由三个字段组成:
- Bus Number (8-bit): 总线编号,范围 0-255。Root Complex 下的总线通常为 0。
- Device Number (5-bit): 设备编号,范围 0-31。每个 Bus 最多挂载 32 个 Device。
- Function Number (3-bit): 功能编号,范围 0-7。每个 Device 最多支持 8 个 Function。
3.4.1 BDF 在配置空间访问中的应用
在 x86 平台上,访问配置空间有两种传统方法:
- I/O 端口方法(CF8/CFC): 向 I/O 端口 0xCF8 写入 BDF 和寄存器偏移,然后从 0xCFC 读写数据。
- ECAM(Enhanced Configuration Access Mechanism): 将配置空间映射到一段连续的 Memory 区域,通过 MMIO 直接访问。这是 PCIe 的推荐方法。
ECAM 的地址映射公式如下:
// ECAM 地址计算
// 基地址 + (Bus * 32 * 8 * 4096) + (Device * 8 * 4096) + (Function * 4096) + RegisterOffset
// 简化公式:
// ECAM_Address = ECAM_Base + (BDF << 12) + RegisterOffset
// 其中 BDF = (Bus << 8) | (Device << 3) | Function
u32 bdf = (bus << 8) | (device << 3) | function;
void __iomem *ecam_base = ...; // 系统提供的 ECAM 基地址
void __iomem *reg_addr = ecam_base + (bdf << 12) + reg_offset;
u32 value = ioread32(reg_addr);
内核视角:Linux 内核的 PCI 子系统已经封装了 BDF 和 ECAM 的细节。驱动开发者通常不需要手动计算 ECAM 地址,而是使用
pci_get_domain_bus_and_slot() 等高层 API 来定位设备。
struct pci_dev *dev;
// 查找 BDF 为 0000:00:1f.0 的设备
dev = pci_get_domain_bus_and_slot(0, 0, PCI_DEVFN(0x1f, 0));
if (dev) {
// 操作设备
pci_dev_put(dev); // 释放引用
}
3.5 本章小结
- PCIe 定义了 Memory、I/O 和 Configuration 三种地址空间,驱动主要操作 Memory 和 Configuration 空间。
- 配置空间是设备枚举和初始化的核心,包含 Vendor ID、BARs、Capabilities 等关键信息。
- BAR 寄存器决定了设备请求的地址空间类型和大小,驱动必须通过
ioremap()映射后才能访问。 - BDF 是设备的唯一硬件地址,在 ECAM 机制下直接用于计算配置空间的 MMIO 偏移。