4. BAR 空间类型:32 位与 64 位 BAR、预取与非预取属性、BAR 空间对齐要求

BAR(Base Address Register)是 PCIe 设备与主机软件之间进行地址空间协商的核心寄存器。每个 PCIe 功能(Function)最多可拥有 6 个 BAR(Type 0 Header)或 2 个 BAR(Type 1 Header)。本章深入剖析 BAR 的类型选择、属性标志以及硬件对齐约束,这些知识直接决定了驱动开发中 ioremap、dma_alloc_coherent 等接口的调用方式。

4.1 32 位与 64 位 BAR

BAR 的宽度决定了设备能够映射的地址空间大小以及其在系统物理地址空间中的位置。

  • 32 位 BAR:占用一个 32 位寄存器(4 字节),可映射的地址范围为 0x00000000 ~ 0xFFFFFFFF(4 GB)。适用于小规模寄存器组或本地内存。
  • 64 位 BAR:占用连续的两个 32 位寄存器(共 8 字节),可映射的地址范围为 0x00000000_00000000 ~ 0xFFFFFFFF_FFFFFFFF(64 TB 可寻址空间)。适用于大容量板载内存(如 GPU 显存、NVMe 控制器缓存)。
关键区别:64 位 BAR 在配置空间中连续占用两个 BAR 槽位(例如 BAR0 和 BAR1),因此使用 64 位 BAR 会减少设备可用的独立地址窗口数量。

4.1.1 硬件检测与驱动适配

驱动通过读取 BAR 寄存器的最低 4 位来判断类型:

/* 读取 BAR 原始值 */
u32 bar_val = pci_read_config_dword(dev, PCI_BASE_ADDRESS_0);

/* 判断是否为 64 位 BAR */
if ((bar_val & PCI_BASE_ADDRESS_MEM_TYPE_MASK) == PCI_BASE_ADDRESS_MEM_TYPE_64) {
    /* 该 BAR 为 64 位,需读取下一个 BAR 寄存器作为高 32 位 */
    u32 bar_high = pci_read_config_dword(dev, PCI_BASE_ADDRESS_1);
    u64 bar_addr = ((u64)bar_high << 32) | (bar_val & PCI_BASE_ADDRESS_MEM_MASK);
    dev_info(&dev->dev, "64-bit BAR detected, address: 0x%016llx\n", bar_addr);
} else {
    /* 32 位 BAR */
    u32 bar_addr = bar_val & PCI_BASE_ADDRESS_MEM_MASK;
    dev_info(&dev->dev, "32-bit BAR detected, address: 0x%08x\n", bar_addr);
}
驱动开发提示:在 Linux 内核中,使用 pci_resource_start() 和 pci_resource_len() 可自动处理 32/64 位差异,无需手动解析 BAR 类型标志。

4.2 预取(Prefetchable)与非预取(Non-Prefetchable)属性

BAR 的 Bit 3 用于标识该内存空间是否支持预取。该属性影响 CPU 缓存策略和驱动映射方式。

属性 Bit 3 值 特性 典型用途 驱动映射方式
非预取 0 读操作可能有副作用(side effect);不允许合并写;必须严格保序 设备控制寄存器、状态寄存器、FIFO 端口 ioremap() → 使用 readl()/writel() 访问
预取 1 读操作无副作用;允许 CPU 缓存和预读;允许写合并 板载 RAM、帧缓冲、大容量数据缓冲区 ioremap_wc() 或 memremap() 带 MEMREMAP_WC 标志
常见错误:将非预取 BAR 映射为 ioremap_wc() 会导致寄存器读写被 CPU 乱序执行或合并,造成设备控制逻辑失效。务必根据 BAR 属性选择正确的映射函数。

4.2.1 预取属性与写合并(Write Combining)

预取 BAR 允许使用 Write Combining(WC)内存类型,这对 DMA 传输性能至关重要:

  • WC 内存:CPU 写入操作被暂存到写合并缓冲区(Write Combine Buffer),在缓存行满或显式刷新时一次性发送到 PCIe 总线,显著减少 TLP 数量。
  • 适用场景:GPU 纹理上传、网络数据包发送、视频帧写入。
  • 驱动实现:
/* 映射预取 BAR 为 Write Combining 内存 */
void __iomem *wc_base = ioremap_wc(pci_resource_start(dev, bar), 
                                   pci_resource_len(dev, bar));

/* 使用 memcpy_toio 进行批量写入,自动利用 WC 特性 */
memcpy_toio(wc_base + offset, data, size);

/* 确保写缓冲区刷新(某些架构需要) */
wmb();  /* 写内存屏障 */

4.3 BAR 空间对齐要求

PCIe 规范要求 BAR 映射的地址必须按 BAR 所请求的大小进行自然对齐。这是硬件设计的基本约束,也是驱动开发中地址分配的核心依据。

4.3.1 对齐规则

BAR 请求大小 对齐要求 示例基地址
4 KB 4 KB 对齐(Bit[11:0] 为 0) 0x00000000_00001000
1 MB 1 MB 对齐(Bit[19:0] 为 0) 0x00000000_01000000
256 MB 256 MB 对齐(Bit[27:0] 为 0) 0x00000000_20000000
4 GB(64 位 BAR) 4 GB 对齐(Bit[31:0] 为 0) 0x00000001_00000000

4.3.2 硬件对齐检测机制

BIOS/UEFI 通过“写全 1 再回读”的方式确定 BAR 大小和对齐:

/* 模拟 BAR 大小检测过程 */
/* 1. 保存原始值 */
u32 original = pci_read_config_dword(dev, bar_offset);

/* 2. 写入全 1 */
pci_write_config_dword(dev, bar_offset, 0xFFFFFFFF);

/* 3. 回读并解析 */
u32 size_raw = pci_read_config_dword(dev, bar_offset);

/* 4. 清除低 4 位标志位,取反加 1 得到大小 */
u32 size = ~(size_raw & PCI_BASE_ADDRESS_MEM_MASK) + 1;

/* 5. 恢复原始值 */
pci_write_config_dword(dev, bar_offset, original);

dev_info(&dev->dev, "BAR size: %u bytes, alignment: %u bytes\n", size, size);
对齐的物理意义:如果 BAR 请求 1 MB 空间,则其基地址的低 20 位(Bit[19:0])必须为 0。这意味着该 BAR 在系统物理地址空间中占据的窗口起始地址一定是 1 MB 的整数倍。

4.4 综合对比:三种 BAR 属性组合

实际设备中,BAR 的类型和属性组合决定了驱动开发中的映射策略和访问方式:

组合场景 BAR 类型 预取属性 典型设备 驱动映射函数 访问方式
控制寄存器 32 位 非预取 网卡 MMIO 寄存器、SATA 控制器 ioremap() readl()/writel()
板载内存 64 位 预取 GPU 显存、NVMe 数据缓冲区 ioremap_wc() memcpy_toio() / memcpy_fromio()
混合功能 32 位 预取 FPGA 数据采集卡(含 RAM) ioremap_wc() 批量读写 + 屏障指令
大容量存储 64 位 非预取 智能网卡控制队列 ioremap() 严格保序的寄存器访问

4.5 BAR 空间结构总览

下图展示了 32 位 BAR 与 64 位 BAR 在配置空间中的布局差异,以及预取/非预取属性对 CPU 访问路径的影响:

BAR 空间类型与属性结构总览 32 位 BAR 结构 占用 1 个配置寄存器(4 字节) 地址范围:0 ~ 4 GB 对齐:按请求大小自然对齐 典型用途:寄存器组、小容量本地内存 64 位 BAR 结构 占用 2 个连续配置寄存器(8 字节) 地址范围:0 ~ 64 TB 对齐:按请求大小自然对齐 典型用途:大容量板载 RAM、GPU 显存 消耗 BAR 槽位 非预取(Non-Prefetchable) Bit 3 = 0 读操作有副作用 禁止 CPU 缓存 严格保序 映射:ioremap() 预取(Prefetchable) Bit 3 = 1 读操作无副作用 允许 CPU 缓存/预读 允许写合并(WC) 映射:ioremap_wc() 驱动开发核心:根据 BAR 类型选择映射函数,根据预取属性决定缓存策略

4.6 驱动开发中的 BAR 处理流程

在 Linux 内核驱动中,处理 BAR 空间的标准化流程如下:

  1. 使能设备:调用 pci_enable_device() 激活设备。
  2. 获取 BAR 资源:使用 pci_resource_start()、pci_resource_len()、pci_resource_flags() 获取地址、大小和属性。
  3. 判断 BAR 类型:通过 pci_resource_flags() 检查 IORESOURCE_MEM_64 标志。
  4. 选择映射方式:根据预取标志 IORESOURCE_PREFETCH 选择 ioremap() 或 ioremap_wc()。
  5. 注册资源:调用 pci_request_region() 声明 BAR 空间所有权。
  6. 执行映射:将物理地址转换为内核虚拟地址,供驱动访问。
/* 完整的 BAR 映射示例 */
static int my_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
    int ret;
    int bar;
    unsigned long flags;
    resource_size_t start, len;
    void __iomem *regs;

    /* 1. 使能设备 */
    ret = pci_enable_device(dev);
    if (ret) return ret;

    /* 2. 遍历所有 BAR */
    for (bar = 0; bar < 6; bar++) {
        flags = pci_resource_flags(dev, bar);
        if (!(flags & IORESOURCE_MEM))
            continue;  /* 跳过 IO 空间 */

        start = pci_resource_start(dev, bar);
        len   = pci_resource_len(dev, bar);

        /* 3. 判断是否为 64 位 BAR */
        if (flags & IORESOURCE_MEM_64)
            dev_info(&dev->dev, "BAR%d: 64-bit, ", bar);
        else
            dev_info(&dev->dev, "BAR%d: 32-bit, ", bar);

        /* 4. 根据预取属性选择映射方式 */
        if (flags & IORESOURCE_PREFETCH) {
            dev_info(&dev->dev, "prefetchable, ");
            regs = ioremap_wc(start, len);
        } else {
            dev_info(&dev->dev, "non-prefetchable, ");
            regs = ioremap(start, len);
        }

        dev_info(&dev->dev, "addr=0x%llx, len=0x%llx\n",
                 (u64)start, (u64)len);

        /* 5. 注册资源 */
        pci_request_region(dev, bar, "my_driver");
    }

    return 0;
}
调试技巧:使用 lspci -vvv -s <BDF> 查看设备 BAR 的当前分配地址、大小和属性标志。驱动加载后,检查 /proc/iomem 确认映射是否正确。

4.7 本章小结

  • 32 位 vs 64 位 BAR:64 位 BAR 占用两个槽位,支持超大地址空间;驱动通过 IORESOURCE_MEM_64 标志区分。
  • 预取 vs 非预取:预取 BAR 允许 CPU 缓存和写合并,映射使用 ioremap_wc();非预取 BAR 必须严格保序,使用 ioremap()。
  • 对齐约束:BAR 基地址必须按请求大小自然对齐,这是硬件分配地址时的硬性规则。
  • 驱动实践:始终使用 pci_resource_* 系列函数获取 BAR 信息,避免手动解析配置空间。