3. PCIe地址空间:配置空间、内存空间、I/O空间的概念与区别,BAR寄存器的原理
3.1 PCIe地址空间体系概述
PCIe总线定义了三种独立的地址空间:配置空间(Configuration Space)、内存空间(Memory Space)和I/O空间(I/O Space)。这三种空间在访问方式、地址范围和用途上存在本质区别,是理解PCIe设备枚举、资源分配和驱动开发的基础。
核心要点:配置空间用于设备发现与初始化,内存空间用于高性能数据交换,I/O空间用于兼容传统x86外设。现代PCIe设备几乎全部使用内存空间映射(MMIO),I/O空间仅在极少数传统设备中出现。
3.2 配置空间(Configuration Space)
配置空间是PCIe设备最核心的地址空间,用于系统软件(如BIOS/UEFI、Linux内核)在启动时发现设备、分配资源、配置中断和电源管理。每个PCIe功能(Function)拥有独立的4KB配置空间(传统PCI只有256字节)。
3.2.1 配置空间结构
配置空间的前256字节(0x00-0xFF)为PCI兼容配置区域,与PCI规范完全兼容;扩展部分(0x100-0xFFF)为PCIe新增的扩展配置空间,包含PCIe能力结构、高级错误报告(AER)、虚拟化(SR-IOV)等扩展功能。
| 偏移地址 | 寄存器名称 | 宽度 | 说明 |
|---|---|---|---|
| 0x00 | Vendor ID / Device ID | 32位 | 厂商和设备标识 |
| 0x04 | Command / Status | 32位 | 控制命令与状态 |
| 0x08 | Revision ID / Class Code | 32位 | 版本号与设备类别 |
| 0x0C | Cache Line / Latency Timer / Header Type | 32位 | 缓存行大小、延迟定时器、头部类型 |
| 0x10 - 0x24 | BAR0 - BAR5 | 32位×6 | 基地址寄存器(核心内容) |
| 0x2C | Subsystem Vendor / Subsystem ID | 32位 | 子系统厂商与设备标识 |
| 0x30 | Expansion ROM Base Address | 32位 | 扩展ROM基地址 |
| 0x3C | Interrupt Line / Interrupt Pin | 32位 | 中断线与中断引脚 |
| 0x100+ | PCIe Extended Capabilities | 可变 | PCIe扩展能力结构 |
调试提示:使用 lspci -xxx -s 01:00.0 可以查看设备完整的配置空间(包括扩展部分)。在驱动开发中,通过 pci_read_config_*() / pci_write_config_*() 系列函数访问配置空间。
3.3 内存空间(Memory Space)与I/O空间(I/O Space)
内存空间和I/O空间是设备与主机进行数据交换的地址空间。设备通过BAR寄存器向系统声明自己需要的内存或I/O地址范围,系统软件(PCI子系统)负责分配实际的物理地址。
3.3.1 内存空间(MMIO)
内存空间映射(Memory-Mapped I/O, MMIO)是PCIe设备最常用的数据交换方式。设备的寄存器或缓冲区被映射到主机的物理地址空间,CPU可以使用普通的mov/ldr/str等内存访问指令直接读写设备。
- 优点:访问延迟低、支持缓存一致性、支持64位寻址、支持预取(Prefetchable)
- 特点:地址空间与系统RAM统一编址,CPU无需特殊I/O指令
- 典型用途:DMA缓冲区、设备控制寄存器、帧缓冲、网络数据包缓冲区
3.3.2 I/O空间
I/O空间是x86架构遗留的地址空间,使用独立的in/out指令访问。PCIe规范仍然支持I/O空间,但现代设备几乎不再使用。
- 缺点:地址空间小(64KB)、访问延迟高、不支持64位寻址
- 现状:Linux内核已逐步弃用I/O空间,新驱动推荐使用MMIO
- 兼容性:部分传统设备(如串口、并口)仍可能使用I/O空间
3.4 BAR寄存器原理
BAR(Base Address Register,基地址寄存器)是PCIe设备配置空间中最关键的寄存器之一。每个PCIe功能最多有6个BAR寄存器(BAR0-BAR5),用于向系统声明设备需要的内存或I/O地址空间大小和类型。
3.4.1 BAR寄存器结构
BAR寄存器的低比特位用于标识地址空间类型和属性,高比特位用于存放基地址。系统软件通过写入全1再读回的方式(BAR sizing)确定设备需要的地址空间大小。
| 比特位 | 含义 | 说明 |
|---|---|---|
| Bit 0 | 地址空间类型 | 0 = 内存空间,1 = I/O空间 |
| Bit 1 | 内存类型(仅内存BAR) | 00 = 32位,10 = 64位 |
| Bit 2 | 预取属性(仅内存BAR) | 0 = 不可预取,1 = 可预取 |
| Bit 3-31 | 基地址(32位BAR) | 对齐到分配的大小 |
| Bit 3-63 | 基地址(64位BAR,占用两个BAR) | 高32位在下一个BAR中 |
3.4.2 BAR Sizing 原理
系统软件通过以下步骤确定BAR对应的地址空间大小:
- 保存原始值:读取BAR当前值并保存
- 写入全1:向BAR写入0xFFFFFFFF(32位)或0xFFFFFFFFFFFFFFFF(64位)
- 读回结果:读取BAR值,低比特位为0的位数决定了地址空间大小
- 恢复原始值:将保存的原始值写回BAR
示例:假设BAR0读回值为0xFFF00000,则低20位为0,表示该BAR需要2^20 = 1MB的地址空间。基地址必须1MB对齐。
3.4.3 64位BAR与32位BAR
当BAR的Bit 1-2为10时,表示该BAR为64位内存BAR,需要占用连续的两个BAR寄存器(如BAR0和BAR1)。64位BAR支持超过4GB的地址空间,适用于需要大容量DMA缓冲区的设备(如GPU、NVMe SSD)。
注意事项:在驱动开发中,应使用 pci_resource_start()、pci_resource_len() 和 pci_resource_flags() 函数获取BAR的基地址、长度和类型,避免直接操作BAR寄存器。Linux内核的PCI子系统在枚举阶段已完成BAR配置。
3.5 三种地址空间对比总结
| 特性 | 配置空间 | 内存空间 | I/O空间 |
|---|---|---|---|
| 访问方式 | 配置事务(CFG Read/Write) | 内存事务(Memory Read/Write) | I/O事务(I/O Read/Write) |
| CPU指令 | 通过MMIO或端口间接访问 | mov/ldr/str 等内存指令 | in/out 专用指令 |
| 地址宽度 | 12位(4KB) | 32位或64位 | 16位(64KB) |
| 主要用途 | 设备发现、资源分配、中断配置 | 数据交换、控制寄存器、DMA | 传统设备兼容 |
| Linux访问API | pci_read_config_* / pci_write_config_* | ioremap / readl / writel | ioport_map / inb / outb |
| 现代设备使用 | 所有设备必须 | 绝大多数设备 | 极少(已弃用) |
3.6 驱动开发中的BAR操作示例
以下代码展示了在Linux内核驱动中如何获取和使用BAR资源:
#include <linux/pci.h>
#include <linux/io.h>
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
unsigned long mmio_start, mmio_len;
void __iomem *mmio_base;
// 启用PCI设备
pci_enable_device(pdev);
// 获取BAR0的资源信息
mmio_start = pci_resource_start(pdev, 0);
mmio_len = pci_resource_len(pdev, 0);
// 检查BAR类型(必须为内存空间)
if (!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM)) {
dev_err(&pdev->dev, "BAR0 is not memory space\n");
return -ENODEV;
}
// 将BAR0映射到内核虚拟地址空间
mmio_base = ioremap(mmio_start, mmio_len);
if (!mmio_base) {
dev_err(&pdev->dev, "ioremap failed\n");
return -ENOMEM;
}
// 通过MMIO读写设备寄存器
u32 val = readl(mmio_base + 0x100); // 读偏移0x100的寄存器
writel(0x1, mmio_base + 0x200); // 写偏移0x200的寄存器
// 保存映射地址供后续使用
pci_set_drvdata(pdev, mmio_base);
return 0;
}
static void my_pci_remove(struct pci_dev *pdev)
{
void __iomem *mmio_base = pci_get_drvdata(pdev);
iounmap(mmio_base); // 解除映射
pci_disable_device(pdev); // 禁用设备
}
调试技巧:使用 lspci -v -s 01:00.0 可以查看设备的BAR分配情况,包括基地址、大小和类型。在驱动加载前后对比 /proc/iomem 和 /proc/ioports 可以验证资源分配是否正确。
3.7 本章小结
- PCIe定义了配置空间(4KB)、内存空间和I/O空间三种独立的地址空间
- 配置空间用于设备发现和初始化,通过
pci_read_config_*系列函数访问 - 内存空间(MMIO)是主流数据交换方式,通过
ioremap映射后使用readl/writel访问 - I/O空间仅用于传统设备兼容,新驱动应避免使用
- BAR寄存器是设备向系统声明地址空间需求的接口,通过BAR Sizing确定空间大小
- 驱动开发中应使用
pci_resource_*系列函数获取BAR信息,避免直接操作配置空间