4. BAR 空间初始化流程:BIOS/UEFI 枚举过程、操作系统对 BAR 的分配、64 位 BAR 处理

BAR(Base Address Register)空间的初始化是 PCIe 设备在系统中可被软件访问的前提。整个流程横跨固件(BIOS/UEFI)枚举阶段与操作系统内核驱动加载阶段,涉及地址空间分配、资源冲突解决以及 64 位地址支持等关键环节。本章将深入剖析 BAR 初始化的完整生命周期。

4.1 BIOS/UEFI 枚举与资源分配

系统上电后,BIOS 或 UEFI 固件首先执行 PCIe 总线枚举,其核心目标是发现所有 PCIe 设备、分配总线号,并为每个设备的 BAR 分配初始的物理地址空间。

4.1.1 枚举过程的核心步骤

  1. 总线号分配:从根复合体(Root Complex)开始,固件通过配置空间中的 Primary/Secondary/Subordinate Bus Number 寄存器,递归遍历所有 PCIe 桥,为每个桥下的总线分配唯一的总线号。
  2. 设备发现:在每个总线上扫描设备号(Device Number)和功能号(Function Number),读取 Vendor ID 和 Device ID 寄存器,确认设备是否存在。
  3. BAR 大小探测:固件向每个 BAR 寄存器写入全 1(0xFFFFFFFF),然后回读。回读值中低比特位为 0 的部分即表示该 BAR 所需地址空间的大小。例如,回读值为 0xFFFF0000,表示该 BAR 需要 64KB 空间。
  4. 资源需求汇总:固件收集所有设备 BAR 的地址空间需求(包括内存空间和 I/O 空间),以及中断引脚需求。
  5. 地址空间分配:固件根据汇总的需求,在系统物理地址空间中为每个 BAR 分配不重叠的基地址,并将分配结果写入 BAR 寄存器。
关键概念:BAR 大小探测是固件与操作系统通用的标准方法。写入全 1 后,BAR 寄存器中可编程的位(即可被分配的地址位)会返回 0,从而确定地址范围。不可编程的位(如硬件固定的地址位)则保持原值。

4.1.2 固件分配策略对比

特性 BIOS (Legacy) UEFI
地址分配基础 基于 PCI 总线拓扑的静态分配 基于 ACPI 和 UEFI 协议的动态分配
资源报告 通过中断 1Ah 功能调用 通过 GetMemoryMap() 和 PCI 协议
64 位支持 有限,通常限制在 4GB 以下 原生支持 64 位地址分配
热插拔支持 不支持 支持,通过 UEFI 事件机制
配置空间访问 通过 I/O 端口 CF8h/CFCh 通过内存映射配置空间 (MMCFG)

4.1.3 固件枚举与 BAR 分配流程图

BIOS/UEFI 枚举与 BAR 分配流程 1. 系统上电,固件启动 2. 遍历 Root Complex,分配总线号 3. 扫描设备,读取 Vendor/Device ID 4. 向 BAR 写入全 1,探测大小 5. 汇总所有设备资源需求 6. 分配物理地址空间,写入 BAR 7. 配置桥接寄存器,完成枚举 说明: 固件分配的地址空间可能被操作系统重新调整。操作系统会基于自身的内存管理策略, 在系统物理地址映射中为 PCIe BAR 分配新的地址。

4.2 操作系统对 BAR 的分配

当操作系统接管系统控制权后,它通常会重新进行 PCIe 总线枚举和资源分配。这是因为操作系统拥有更高级的内存管理单元(MMU)和更灵活的地址空间布局策略。

4.2.1 操作系统重新分配的原因

  • 地址空间优化:操作系统可以根据当前系统物理内存布局、IOMMU 映射以及热插拔预留空间,更合理地分配 BAR 地址。
  • 兼容性修复:某些固件分配的地址可能存在冲突或不符合操作系统要求,操作系统会进行修正。
  • 动态资源管理:支持 PCIe 热插拔时,操作系统必须动态分配新插入设备的 BAR 空间。

4.2.2 Linux 内核中的 BAR 分配流程

在 Linux 内核中,PCIe 子系统负责 BAR 的重新分配。主要流程如下:

  1. pci_scan_bus():内核扫描 PCIe 总线,创建 pci_dev 结构体,并读取每个设备的配置空间。
  2. pci_read_bases():内核读取每个 BAR 的当前值(固件分配的值),并再次执行大小探测,确认 BAR 的实际需求。
  3. pci_assign_unassigned_resources():对于固件未分配或分配失败的 BAR,内核会调用此函数进行重新分配。
  4. pci_bus_assign_resources():内核遍历总线树,为每个需要分配资源的设备调用 pci_assign_resource()。
  5. pci_assign_resource():核心分配函数,通过 __pci_assign_resource() 在系统资源树中查找可用区域,并调用 pci_update_resource() 将新地址写入 BAR 寄存器。
内核关键函数调用链:
pci_scan_bus()
  └─ pci_scan_slot()
       └─ pci_scan_device()
            └─ pci_read_bases()        // 读取并探测 BAR
pci_assign_unassigned_resources()
  └─ pci_bus_assign_resources()
       └─ pci_assign_resource()
            └─ __pci_assign_resource() // 核心分配算法
                 └─ pci_update_resource() // 写入 BAR 寄存器

4.2.3 操作系统分配与固件分配的对比

对比维度 固件分配 操作系统分配
分配时机 POST 阶段,操作系统启动前 内核 PCI 子系统初始化时
地址空间范围 通常限制在 32 位地址空间 支持 64 位地址空间(如果硬件支持)
分配策略 基于拓扑的静态分配 基于系统资源树的动态分配
热插拔支持 不支持 原生支持,通过 ACPI 事件
IOMMU 感知 无 可以感知 IOMMU 域和 DMA 映射
用户干预 通过 BIOS 设置选项 通过内核参数(如 pci=assign-buses)

4.3 64 位 BAR 处理

随着高性能设备(如 GPU、NVMe SSD、网络控制器)对内存映射 I/O 空间需求的增长,32 位地址空间(最大 4GB)已无法满足需求。PCIe 规范引入了 64 位 BAR,允许设备请求超过 4GB 的地址空间。

4.3.1 64 位 BAR 的硬件实现

64 位 BAR 在物理上占用两个连续的 32 位配置空间寄存器。第一个寄存器(低 32 位)和第二个寄存器(高 32 位)共同组成一个 64 位基地址。硬件通过 BAR 寄存器中的第 0 位(Memory Space Indicator)和第 1-2 位(Type 字段)来标识这是一个 64 位 BAR:

  • Type 字段 = 10b:表示这是一个 64 位内存空间 BAR。
  • 低 32 位寄存器:包含基地址的低 32 位,以及可编程的地址位。
  • 高 32 位寄存器:包含基地址的高 32 位,全部 32 位均可编程。
重要注意事项:当处理 64 位 BAR 时,软件必须成对读取和写入两个寄存器。在大小探测阶段,需要先向低 32 位写入全 1,再向高 32 位写入全 1,然后依次回读。分配地址时,也必须同时设置低 32 位和高 32 位,以确保地址的原子性。

4.3.2 64 位 BAR 的探测与分配算法

以下代码展示了 Linux 内核中处理 64 位 BAR 探测的核心逻辑:

/* drivers/pci/probe.c - pci_read_bases() 简化逻辑 */
static void pci_read_bases(struct pci_dev *dev, unsigned int howmany)
{
    unsigned int pos, reg;
    u32 l, sz;
    u64 base, size64;
    struct resource *res;

    for (pos = 0; pos < howmany; pos++) {
        res = &dev->resource[pos];
        reg = PCI_BASE_ADDRESS_0 + (pos << 2);
        pci_read_config_dword(dev, reg, &l);
        pci_write_config_dword(dev, reg, ~0);
        pci_read_config_dword(dev, reg, &sz);
        pci_write_config_dword(dev, reg, l);

        if (sz == 0 || sz == 0xffffffff)
            continue;

        if ((l & PCI_BASE_ADDRESS_SPACE_IO) == 0) {
            /* 内存空间 BAR */
            if ((l & PCI_BASE_ADDRESS_MEM_TYPE_MASK)
                == PCI_BASE_ADDRESS_MEM_TYPE_64) {
                /* 64 位 BAR:需要读取高 32 位 */
                u32 l2, sz2;
                pci_read_config_dword(dev, reg + 4, &l2);
                pci_write_config_dword(dev, reg + 4, ~0);
                pci_read_config_dword(dev, reg + 4, &sz2);
                pci_write_config_dword(dev, reg + 4, l2);

                size64 = ((u64)sz2 << 32) | sz;
                base = ((u64)l2 << 32) | l;
                /* 处理 64 位资源 */
                res->flags |= IORESOURCE_MEM_64;
                res->start = base & ~(size64 - 1);
                res->end = res->start + size64 - 1;
                pos++; /* 跳过下一个寄存器 */
            } else {
                /* 32 位 BAR */
                size64 = sz;
                base = l;
                res->start = base & ~(size64 - 1);
                res->end = res->start + size64 - 1;
            }
        }
    }
}

4.3.3 64 位 BAR 处理流程示意图

64 位 BAR 探测与分配流程 读取 BAR 低 32 位寄存器 检查 Type 字段是否为 10b 是 64 位 BAR:读取高 32 位寄存器 组合成 64 位地址,计算大小 在 64 位地址空间中分配资源 同时写入低 32 位和高 32 位寄存器 关键点: 64 位 BAR 占用两个连续的配置空间寄存器。软件必须保证对这两个寄存器的读写操作是原子的, 否则可能导致地址不完整,引发系统错误。

4.4 驱动兼容性设计要点

在编写 PCIe 设备驱动时,必须考虑 BAR 初始化的各种情况,以确保驱动在不同固件和操作系统环境下都能正常工作。

4.4.1 驱动开发最佳实践

  • 不要假设 BAR 地址固定:始终通过 pci_resource_start() 和 pci_resource_end() 等内核 API 获取 BAR 地址,不要硬编码地址值。
  • 检查 BAR 类型:使用 pci_resource_flags() 检查 IORESOURCE_MEM_64 标志,以确定 BAR 是否为 64 位,并据此处理地址。
  • 处理分配失败:如果 pci_request_region() 或 pci_ioremap_bar() 失败,驱动应优雅地降级或返回错误,而不是直接崩溃。
  • 支持热插拔:在 probe() 和 remove() 回调中正确处理 BAR 资源的申请和释放,确保设备热插拔时资源管理正确。

4.4.2 常见兼容性问题与解决方案

问题 原因 解决方案
BAR 地址为 0 固件或操作系统未分配地址 检查 dmesg 日志,使用 pci=assign-buses 内核参数强制重新分配
64 位 BAR 分配在 32 位空间 系统不支持 64 位地址映射 检查硬件平台支持,或使用 iommu=soft 绕过硬件限制
BAR 大小探测失败 设备配置空间损坏或硬件故障 使用 lspci -vvv 检查 BAR 寄存器值,确认设备状态
资源冲突 多个设备 BAR 地址重叠 使用 pci=realloc 内核参数让操作系统重新分配所有资源
驱动代码示例:安全获取 BAR 资源
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct resource *res;
    unsigned long flags;
    phys_addr_t start, end;
    void __iomem *bar_addr;

    // 获取 BAR 0 的资源
    res = &pdev->resource[0];
    if (!res->start) {
        dev_err(&pdev->dev, "BAR 0 not assigned\n");
        return -ENODEV;
    }

    // 检查是否为 64 位 BAR
    flags = pci_resource_flags(pdev, 0);
    if (flags & IORESOURCE_MEM_64) {
        dev_info(&pdev->dev, "BAR 0 is 64-bit\n");
    }

    // 请求并映射 BAR
    if (pci_request_region(pdev, 0, "my_driver")) {
        dev_err(&pdev->dev, "Failed to request BAR 0\n");
        return -EBUSY;
    }

    bar_addr = pci_ioremap_bar(pdev, 0);
    if (!bar_addr) {
        dev_err(&pdev->dev, "Failed to ioremap BAR 0\n");
        pci_release_region(pdev, 0);
        return -ENOMEM;
    }

    // ... 使用 bar_addr 进行寄存器访问 ...

    return 0;
}

4.5 本章小结

BAR 空间初始化是 PCIe 设备驱动开发的基础。理解 BIOS/UEFI 枚举过程、操作系统重新分配机制以及 64 位 BAR 的特殊处理,对于编写健壮、兼容性强的驱动至关重要。驱动开发者应始终使用内核提供的标准 API 来操作 BAR 资源,避免对地址空间的硬编码假设,并妥善处理资源分配失败等异常情况。