3. BAR空间原理

BAR(Base Address Register,基地址寄存器)是PCIe设备配置空间中最关键的寄存器组,它决定了主机CPU如何访问设备内部的寄存器和内存缓冲区。理解BAR的工作原理是进行MMIO编程的基础。

3.1 BAR寄存器的作用

每个PCIe设备在配置空间中拥有最多6个BAR寄存器(Function 0通常有6个,其他Function可能较少)。每个BAR描述了一段连续的地址空间,用于以下目的:

  • 设备资源声明:设备通过BAR向系统声明它需要多大的地址空间(内存或I/O)。
  • 基地址分配:系统固件(BIOS/UEFI)或操作系统在枚举过程中,将物理地址写入BAR,确定设备资源在系统地址空间中的位置。
  • 访问接口:CPU通过BAR中保存的基地址加上内部偏移,直接读写设备内部的寄存器或内存缓冲区。
关键概念:BAR本身只是一个32位或64位的寄存器,但它背后代表的是设备内部一块完整的地址空间。写入BAR的值决定了这块空间在系统物理地址空间中的映射位置。

3.2 BAR地址映射机制

BAR地址映射是PCIe枚举过程中的核心步骤,分为两个阶段:大小探测和基地址分配。

3.2.1 大小探测(Size Detection)

系统软件通过向BAR写入全1(0xFFFFFFFF),然后回读,来确定设备需要多大的地址空间。设备硬件会将不关心的位(即地址空间大小对应的低位)强制返回0,从而让软件计算出所需空间大小。

// 伪代码:探测BAR0所需内存大小
uint32_t bar_original = pci_read_config_dword(dev, 0x10); // 保存原始值
pci_write_config_dword(dev, 0x10, 0xFFFFFFFF);           // 写入全1
uint32_t bar_probe = pci_read_config_dword(dev, 0x10);   // 回读
uint32_t bar_size = ~(bar_probe & 0xFFFFFFF0) + 1;       // 计算大小
pci_write_config_dword(dev, 0x10, bar_original);         // 恢复原始值
计算示例:如果回读值为0xFFFF0000,则取反低28位(0x0000FFFF)加1得到0x00010000,即64KB。设备需要64KB的地址空间。

3.2.2 基地址分配(Base Address Assignment)

系统软件根据探测到的大小,在系统物理地址空间中分配一块连续的、未使用的区域,并将起始地址写入BAR。写入时,软件必须保证地址与BAR大小对齐(即地址必须是大小的整数倍)。

对齐要求:BAR的基地址必须与其大小自然对齐。例如,一个需要1MB空间的BAR,其基地址必须是1MB的整数倍(即低20位为0)。如果写入未对齐的地址,设备行为是未定义的。

3.2.3 映射关系示意图

下图展示了从CPU虚拟地址到PCIe设备内部寄存器的完整映射路径:

PCIe BAR地址映射路径 CPU 虚拟地址 0x7f00_0000 MMU 系统物理地址 0x9000_0000 RC BAR 寄存器 0x9000_0000 设备 CPU访问 0x9000_1000 → RC将地址映射到BAR0 → 设备内部偏移 0x1000 的寄存器 地址映射三要素 要素 描述 来源/设置 基地址 设备地址空间的起始物理地址 系统软件写入BAR 大小 设备占用的地址空间长度 设备硬件固定(通过探测获得)

3.3 内存空间与IO空间的区别

PCIe规范支持两种地址空间类型:内存空间(Memory Space)和IO空间(I/O Space)。BAR寄存器的第0位(即最低位)用于区分类型:0表示内存空间,1表示IO空间。

对比维度 内存空间(Memory Space) IO空间(I/O Space)
BAR位0值 0 1
地址宽度 32位或64位(支持64位BAR) 32位(x86实际使用16位地址)
访问指令 普通内存访问指令(MOV/STR/LDR) 专用IO指令(IN/OUT on x86)
访问粒度 字节、字、双字、四字 通常为字节或字
缓存行为 可配置为Uncacheable(强烈建议) 总是非缓存、非合并
地址空间大小 整个系统物理地址空间(可达TB级) 64KB(x86传统限制)
性能 高(可利用CPU缓存一致性协议) 低(每次访问需通过IO控制器)
现代设备使用 绝大多数PCIe设备(NVMe、GPU、网卡) 极少使用(仅遗留设备)

3.3.1 内存空间详解

内存空间是PCIe设备最常用的地址空间类型。当BAR被配置为内存空间时,设备内部的寄存器被映射到系统的物理地址空间中,CPU可以直接使用内存访问指令进行读写。

  • Prefetchable vs Non-Prefetchable:BAR的第3位表示是否可预取(Prefetchable)。可预取空间允许CPU推测性读取和合并写入,适用于不含读副作用的缓冲区(如帧缓冲)。不可预取空间用于包含状态寄存器的区域,每次读取必须精确执行。
  • 64位BAR:当BAR的第2:1位为10时,表示该BAR是64位的,占用连续的两个BAR寄存器(如BAR0和BAR1),提供更大的地址空间。
实践建议:在Linux驱动开发中,几乎总是使用内存空间BAR。使用ioremap()将物理地址映射到内核虚拟地址空间,然后通过readl()/writel()等函数访问。对于可预取空间,可以使用memremap()并指定MEMREMAP_WB标志。

3.3.2 IO空间详解

IO空间是x86架构遗留的地址空间概念,通过专用的IN/OUT指令访问。在PCIe时代,IO空间的使用已经大幅减少:

  • 地址限制:x86 IO空间只有64KB(0x0000-0xFFFF),且大部分已被系统板载设备占用。
  • 性能瓶颈:每次IO访问都需要通过IO控制器,无法利用CPU缓存,延迟远高于内存访问。
  • 非x86架构:ARM、RISC-V等架构通常不支持IO空间,所有外设访问都通过内存映射实现。
避坑指南:新设计的PCIe设备应避免使用IO空间BAR。如果必须支持IO空间(例如兼容遗留软件),建议同时提供内存空间BAR作为主要访问方式。Linux内核在PCIe枚举时,如果设备同时支持两种空间,默认优先使用内存空间。

3.3.3 空间类型选择决策流程

下图帮助开发者在设计或配置PCIe设备时选择合适的地址空间类型:

PCIe BAR空间类型选择决策流程 设备需要地址空间 需要IO空间? 是 使用IO空间 否 需要64位地址? 是 64位内存BAR 否 32位内存BAR

3.4 BAR寄存器格式详解

BAR寄存器的位定义因类型而异,理解这些位是正确配置BAR的前提。

3.4.1 32位内存空间BAR格式

位 31:4   — 基地址(Base Address),必须与大小对齐
位 3      — Prefetchable(可预取):1=可预取,0=不可预取
位 2:1    — 类型:00=32位,10=64位(占用两个BAR)
位 0      — 空间类型:0=内存空间

3.4.2 IO空间BAR格式

位 31:2   — 基地址(Base Address),必须与大小对齐
位 1      — 保留(必须为0)
位 0      — 空间类型:1=IO空间
快速识别:读取BAR寄存器后,检查位0即可判断类型。如果位0=0,再检查位2:1:若为00则是32位内存BAR,若为10则是64位内存BAR(需要读取下一个BAR寄存器作为高32位)。

3.5 Linux内核中的BAR操作

在Linux驱动开发中,通过PCI核心API操作BAR,无需直接读写配置空间:

#include <linux/pci.h>

struct pci_dev *dev = pci_get_device(0x10EE, 0x1234, NULL);
if (!dev) {
    pr_err("Device not found\n");
    return -ENODEV;
}

// 获取BAR0的物理地址和大小
resource_size_t bar0_start = pci_resource_start(dev, 0);
resource_size_t bar0_end   = pci_resource_end(dev, 0);
resource_size_t bar0_len   = pci_resource_len(dev, 0);
unsigned long bar0_flags   = pci_resource_flags(dev, 0);

// 检查BAR类型
if (bar0_flags & IORESOURCE_MEM) {
    pr_info("BAR0 is Memory Space\n");
    if (bar0_flags & IORESOURCE_PREFETCH)
        pr_info("BAR0 is Prefetchable\n");
} else if (bar0_flags & IORESOURCE_IO) {
    pr_info("BAR0 is I/O Space\n");
}

// 将BAR0映射到内核虚拟地址空间
void __iomem *bar0_virt = pci_iomap(dev, 0, bar0_len);
if (!bar0_virt) {
    pr_err("pci_iomap failed\n");
    return -ENOMEM;
}

// 通过MMIO访问设备寄存器
u32 val = ioread32(bar0_virt + 0x1000);  // 读取偏移0x1000的寄存器
iowrite32(0x1, bar0_virt + 0x1004);      // 写入偏移0x1004的寄存器

// 解除映射
pci_iounmap(dev, bar0_virt);
关键API说明:
  • pci_resource_start():返回BAR的物理基地址(系统分配后的值)。
  • pci_resource_len():返回BAR的大小(通过探测获得)。
  • pci_resource_flags():返回BAR的类型标志(IORESOURCE_MEM/IORESOURCE_IO等)。
  • pci_iomap():将BAR物理地址映射到内核虚拟地址空间,返回void __iomem *指针。

3.6 本章小结

知识点 核心要点
BAR寄存器作用 声明设备所需地址空间大小,保存系统分配的基地址,提供CPU访问设备的入口
地址映射机制 系统通过写入全1探测大小,然后分配对齐的基地址写入BAR,建立物理地址到设备内部的映射
内存空间 vs IO空间 内存空间是现代PCIe设备的主流选择,支持64位地址、高性能、非x86架构兼容;IO空间仅用于遗留设备
BAR格式 位0决定空间类型,位2:1决定32/64位,位3决定可预取性;IO空间格式更简单
Linux操作 使用pci_resource_*系列函数获取BAR信息,pci_iomap建立MMIO映射,ioread/iowrite访问寄存器