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 枚举过程的核心步骤
- 总线号分配:从根复合体(Root Complex)开始,固件通过配置空间中的 Primary/Secondary/Subordinate Bus Number 寄存器,递归遍历所有 PCIe 桥,为每个桥下的总线分配唯一的总线号。
- 设备发现:在每个总线上扫描设备号(Device Number)和功能号(Function Number),读取 Vendor ID 和 Device ID 寄存器,确认设备是否存在。
- BAR 大小探测:固件向每个 BAR 寄存器写入全 1(0xFFFFFFFF),然后回读。回读值中低比特位为 0 的部分即表示该 BAR 所需地址空间的大小。例如,回读值为 0xFFFF0000,表示该 BAR 需要 64KB 空间。
- 资源需求汇总:固件收集所有设备 BAR 的地址空间需求(包括内存空间和 I/O 空间),以及中断引脚需求。
- 地址空间分配:固件根据汇总的需求,在系统物理地址空间中为每个 BAR 分配不重叠的基地址,并将分配结果写入 BAR 寄存器。
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 分配流程图
4.2 操作系统对 BAR 的分配
当操作系统接管系统控制权后,它通常会重新进行 PCIe 总线枚举和资源分配。这是因为操作系统拥有更高级的内存管理单元(MMU)和更灵活的地址空间布局策略。
4.2.1 操作系统重新分配的原因
- 地址空间优化:操作系统可以根据当前系统物理内存布局、IOMMU 映射以及热插拔预留空间,更合理地分配 BAR 地址。
- 兼容性修复:某些固件分配的地址可能存在冲突或不符合操作系统要求,操作系统会进行修正。
- 动态资源管理:支持 PCIe 热插拔时,操作系统必须动态分配新插入设备的 BAR 空间。
4.2.2 Linux 内核中的 BAR 分配流程
在 Linux 内核中,PCIe 子系统负责 BAR 的重新分配。主要流程如下:
- pci_scan_bus():内核扫描 PCIe 总线,创建 pci_dev 结构体,并读取每个设备的配置空间。
- pci_read_bases():内核读取每个 BAR 的当前值(固件分配的值),并再次执行大小探测,确认 BAR 的实际需求。
- pci_assign_unassigned_resources():对于固件未分配或分配失败的 BAR,内核会调用此函数进行重新分配。
- pci_bus_assign_resources():内核遍历总线树,为每个需要分配资源的设备调用 pci_assign_resource()。
- 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 位均可编程。
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 处理流程示意图
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 内核参数让操作系统重新分配所有资源 |
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 资源,避免对地址空间的硬编码假设,并妥善处理资源分配失败等异常情况。