3. BAR空间基础:BAR的作用、宽度与地址空间类型

基地址寄存器(Base Address Register,BAR)是PCI/PCIe设备配置空间中最关键的寄存器组之一。驱动程序通过读取BAR来获知设备在系统中的物理地址映射,从而实现对设备寄存器和内存的访问。本章深入剖析BAR的核心作用、宽度差异以及地址空间类型,为后续的映射与枚举打下基础。

3.1 BAR的核心作用

每个PCIe Function最多可拥有6个BAR(Type 0配置头),或2个BAR(Type 1配置头,用于Bridge)。BAR的本质是设备向系统报告其所需地址空间大小的“声明寄存器”,同时也是系统软件(BIOS/OS)分配实际物理地址的“写入目标”。

关键理解: BAR不是存储数据的普通寄存器,而是一种“可编程地址解码器”。设备通过BAR告诉系统:“我需要一块连续的地址空间,大小是X,类型是Y”。系统软件在枚举时写入基地址,设备随后对该地址范围内的访问进行解码响应。

BAR在驱动开发中的具体作用包括:

  • 资源发现: 驱动读取BAR的初始值(全F或特定值),通过回写测试确定设备需要的地址空间大小。
  • 地址分配: 系统固件或操作系统将分配好的物理基地址写入BAR,设备据此响应总线事务。
  • 类型识别: 通过BAR的低位比特位,驱动可判断该BAR映射的是内存空间还是I/O空间,以及是32位还是64位地址。
  • 驱动映射: 驱动从BAR中读出已分配的物理地址,调用ioremap()或MmMapIoSpace()等接口建立虚拟地址到物理地址的映射,从而访问设备寄存器。

3.2 BAR的宽度:32位与64位

PCIe设备支持两种宽度的BAR:32位BAR和64位BAR。宽度决定了设备能够请求的地址空间范围。

特性 32位BAR 64位BAR
地址范围 0x00000000 ~ 0xFFFFFFFF(4GB) 0x00000000_00000000 ~ 0xFFFFFFFF_FFFFFFFF(16EB)
占用BAR槽位 1个(如BAR0) 2个连续槽位(如BAR0 + BAR1)
适用场景 小容量寄存器组、传统设备 大容量显存、高带宽缓冲区、FPGA大地址空间
驱动处理 读取单个BAR即可获得完整地址 需将高低两个BAR组合为一个64位地址
驱动开发提示: 当检测到BAR的低4位为 0b0100(即bit[2:1] = 10)时,表示该BAR为64位内存空间。此时必须将下一个BAR(BARn+1)视为高32位部分,驱动应一次性读取两个BAR并拼接为64位地址。

下图展示了32位BAR与64位BAR在配置空间中的占用方式:

32位BAR与64位BAR在配置空间中的占用对比 BAR0 (32-bit) — 4GB地址空间 BAR1 (未使用或独立32位) 32位BAR布局: 每个BAR独立占用一个配置空间DWORD,地址范围上限4GB。 BAR0 (64-bit) — 低32位 BAR1 (64-bit) — 高32位 BAR2 (未使用或独立32位) 64位BAR布局: BAR0 + BAR1 组合为一个64位地址,范围可达16EB。 BAR0的bit[2:1] = 10 标识64位模式。

3.3 BAR的地址空间类型:内存空间与I/O空间

PCIe规范定义了两种地址空间类型,BAR通过其最低位(bit 0)来标识:

  • bit 0 = 0: 映射到内存空间(Memory Space)
  • bit 0 = 1: 映射到I/O空间(I/O Space)

两种空间类型在驱动开发中的差异显著,下表总结了关键区别:

对比维度 内存空间(Memory Space) I/O空间(I/O Space)
访问指令 普通内存读写(MOV、STR、LDR) 专用I/O指令(IN、OUT,x86架构)
地址范围 系统物理地址空间的一部分 独立的64KB I/O地址空间(x86)
性能 高,支持CPU缓存、预取、合并写 低,每次访问需通过I/O控制器,不可缓存
驱动映射方式 ioremap() / MmMapIoSpace() ioport_map() / 直接使用IN/OUT指令
现代设备趋势 主流,几乎所有高性能设备使用 逐渐淘汰,仅用于兼容旧设备或控制寄存器
PCIe规范支持 完全支持,推荐使用 可选支持,部分Root Complex可能不支持
重要提醒: 在x86以外的架构(如ARM、RISC-V)中,I/O空间可能不存在或通过内存映射方式模拟。因此,新开发的PCIe驱动应优先使用内存空间BAR,以保证跨平台兼容性。如果设备同时提供两种类型的BAR,驱动应优先选择内存空间BAR进行映射。

3.4 BAR的编码格式与类型识别

BAR寄存器的低4位(bit[3:0])包含了类型和编码信息,驱动通过读取这些位即可判断BAR的属性。下图展示了BAR的位布局:

BAR寄存器位布局与类型编码 31 : 4 — 基地址(可编程) bit[3:2] 预取/类型 bit[1] 64位标识 bit[0] 空间类型 bit[3] 保留 编码规则: • bit[0]=0 → 内存空间;bit[0]=1 → I/O空间 • bit[2:1]=00 → 32位内存;bit[2:1]=10 → 64位内存;bit[2:1]=01 → 保留

驱动在枚举过程中,通过以下伪代码逻辑识别BAR类型:

/* 伪代码:BAR类型识别 */
uint32_t bar_value = pci_config_readl(dev, offset);

if (bar_value & 0x1) {
    /* bit[0] = 1 → I/O空间BAR */
    bar_type = PCI_BAR_TYPE_IO;
    bar_size = 计算I/O空间大小();
} else {
    /* bit[0] = 0 → 内存空间BAR */
    if ((bar_value & 0x6) == 0x4) {
        /* bit[2:1] = 10 → 64位内存BAR */
        bar_type = PCI_BAR_TYPE_MEM64;
        /* 需要读取下一个BAR作为高32位 */
        bar_addr_hi = pci_config_readl(dev, offset + 4);
        bar_addr = ((uint64_t)bar_addr_hi << 32) | (bar_value & ~0xF);
    } else {
        /* bit[2:1] = 00 → 32位内存BAR */
        bar_type = PCI_BAR_TYPE_MEM32;
        bar_addr = bar_value & ~0xF;
    }
}

3.5 BAR空间大小探测方法

驱动开发中一个经典操作是“探测BAR大小”。其原理是:向BAR写入全1值,然后回读,根据保留为0的位确定地址范围。具体步骤如下:

  1. 保存原始值: 读取BAR当前值并保存。
  2. 写入全1: 向BAR写入 0xFFFFFFFF(32位)或 0xFFFFFFFF_FFFFFFFF(64位)。
  3. 回读并取反: 读取BAR值,对低有效位取反加1,得到地址空间大小。
  4. 恢复原始值: 将保存的原始值写回BAR。
示例: 假设BAR0原始值为 0xFE000000(已分配地址)。写入 0xFFFFFFFF 后回读得到 0xFF000000。取反低24位得到 0x00FFFFFF,加1得到 0x01000000,即该BAR大小为16MB。

下表总结了不同大小BAR的探测结果特征:

BAR大小 写入全1后回读值 地址位宽 典型应用
4KB 0xFFFFF000 12位 小型控制寄存器组
1MB 0xFFF00000 20位 中等大小缓冲区
16MB 0xFF000000 24位 帧缓冲、DMA区域
256MB 0xF0000000 28位 大容量显存
4GB(64位BAR) 0x00000000_FFFFFFFF 32位 超大地址空间设备

3.6 本章小结

  • BAR是PCIe设备向系统声明地址空间需求的机制,驱动通过BAR获取设备的物理地址映射。
  • 32位BAR支持最大4GB地址空间,64位BAR支持16EB,但需占用两个连续BAR槽位。
  • 内存空间BAR(bit[0]=0)是现代PCIe设备的主流选择,性能优于I/O空间BAR。
  • 驱动通过BAR的低4位编码识别类型,并通过“写全1回读”方法探测BAR大小。
  • 理解BAR的宽度和类型是后续实现ioremap映射和DMA缓冲区分配的前提。