3. 配置空间枚举原理:总线枚举流程、设备发现、分配总线号、分配BAR空间

PCIe 配置空间枚举是系统软件(通常是 BIOS/UEFI 或操作系统内核)在启动阶段对 PCIe 总线层次结构进行深度遍历的过程。其核心目标包括:发现所有挂载的 PCIe 设备、为每个 PCIe 总线分配唯一的总线号、以及为每个设备的功能(Function)分配合理的 BAR(Base Address Register)地址空间。这一过程是后续 DMA 传输、中断配置和 MMIO 访问的基础。

3.1 总线枚举流程与设备发现

枚举过程从根复合体(Root Complex, RC)开始,通常从总线号 0 开始深度优先遍历。系统软件通过配置读写事务(Configuration Read/Write TLPs)访问每个设备的配置空间,检查 Vendor ID 和 Device ID 寄存器来判断设备是否存在。

关键概念: 配置空间访问通过 Bus:Device.Function (BDF) 三元组进行寻址。例如,BDF = 00:1f.0 表示总线 0、设备 31、功能 0。
PCIe 总线枚举深度优先遍历流程 Root Complex (Bus 0) 读 Vendor ID = 0xFFFF? 设备不存在,跳过 设备存在,读取配置空间 检查 Type 1 Header & 分配总线号 递归枚举子总线 (Secondary Bus)
枚举顺序: 系统软件首先扫描 Bus 0 上的所有 Device(0-31),对于每个 Device 扫描其所有 Function(0-7)。当发现 Type 1 Header(PCIe-PCIe 桥或 Root Port)时,系统会为其分配一个新的总线号,并递归扫描该桥下游的总线。

3.2 分配总线号

每个 PCIe 桥(Bridge)的配置空间中包含三个关键寄存器用于总线号管理:

寄存器名称 偏移地址 位宽 描述
Primary Bus Number 0x18 8 位 桥所在的总线号(上游侧)
Secondary Bus Number 0x19 8 位 桥下游的第一条总线号
Subordinate Bus Number 0x1A 8 位 桥下游可达的最大总线号

分配算法遵循深度优先原则:

  1. 系统从 Bus 0 开始,为每个发现的桥分配一个 Secondary Bus Number(当前最大可用总线号 + 1)。
  2. 递归扫描该桥下游的所有设备,直到没有更多桥为止。
  3. 将桥的 Subordinate Bus Number 设置为下游子树中使用的最大总线号。
  4. 返回上一层桥,继续扫描下一个未处理的桥。
常见问题: 如果桥的 Subordinate Bus Number 设置过小,会导致下游设备无法被正确枚举,配置空间访问将返回全 F(Vendor ID = 0xFFFF),设备不可见。

3.3 分配 BAR 空间

每个 PCIe 设备功能(Function)最多支持 6 个 BAR(Type 0 Header)或 2 个 BAR(Type 1 Header)。BAR 用于向系统声明设备需要的 MMIO 或 I/O 地址空间大小和类型。

BAR 空间分配流程:

  • Step 1 - 读取 BAR 原始值: 系统软件向 BAR 寄存器写入全 1(0xFFFFFFFF),然后读回。设备会返回其需要的地址空间大小(低位为 0 的位数表示空间大小)。
  • Step 2 - 确定 BAR 类型: 根据读回值的 Bit 0 判断:0 表示 MMIO(Memory Space),1 表示 I/O Space。对于 MMIO,Bit 2-1 表示地址宽度(32 位或 64 位)。
  • Step 3 - 分配基地址: 系统根据设备需求大小和系统内存布局,为每个 BAR 分配一个不重叠的基地址,并写回 BAR 寄存器。
  • Step 4 - 使能解码: 设置 Command 寄存器中的 Memory Space Enable 或 I/O Space Enable 位,使设备开始响应分配给它的地址范围。
BAR 空间分配与地址解码流程 设备 BAR 寄存器 写入 0xFFFFFFFF 读回地址空间大小 系统分配不重叠基地址 写回基地址到 BAR 寄存器 使能 Memory/IO 解码 BAR 类型判断 Bit 0 = 0 → MMIO Bit 0 = 1 → I/O Space Bit 2-1 = 10 → 64-bit MMIO

3.4 枚举过程中的关键数据结构

在 Linux 内核中,枚举结果被组织成以下核心数据结构:

数据结构 定义位置 关键字段 作用
pci_bus include/linux/pci.h number, primary, secondary, subordinate 表示一条 PCIe 总线,包含总线号范围和挂载的设备链表
pci_dev include/linux/pci.h bus, devfn, vendor, device, resource[] 表示一个 PCIe 设备功能,包含 BDF 地址和 BAR 资源信息
resource include/linux/ioport.h start, end, flags, parent 描述一段地址空间(MMIO 或 I/O),用于 BAR 分配和冲突检测
驱动开发者视角: 在驱动 probe 函数中,pci_resource_start() 和 pci_resource_len() 用于获取枚举阶段已经分配好的 BAR 基地址和大小。驱动无需重新分配 BAR,但需要调用 pci_enable_device() 来使能设备解码。

3.5 枚举过程中的常见问题与调试

  • 设备未发现: 检查桥的 Subordinate Bus Number 是否覆盖了下游设备的总线号。使用 lspci -vvv 查看桥的总线号范围。
  • BAR 冲突: 两个设备被分配了重叠的 MMIO 地址范围。检查 /proc/iomem 确认地址分配情况。
  • 64 位 BAR 分配失败: 如果系统没有足够的 64 位地址空间(通常位于 4GB 以上),设备可能被分配 32 位地址,导致性能下降或映射失败。
  • 配置空间访问超时: 某些硬件在配置空间读取时可能挂起(尤其是非存在设备),需要驱动实现超时重试机制。
调试命令: lspci -x 查看设备配置空间原始字节;setpci 可以直接读写配置空间寄存器,用于手动验证 BAR 分配结果。