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。
枚举顺序: 系统软件首先扫描 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 位 | 桥下游可达的最大总线号 |
分配算法遵循深度优先原则:
- 系统从 Bus 0 开始,为每个发现的桥分配一个 Secondary Bus Number(当前最大可用总线号 + 1)。
- 递归扫描该桥下游的所有设备,直到没有更多桥为止。
- 将桥的 Subordinate Bus Number 设置为下游子树中使用的最大总线号。
- 返回上一层桥,继续扫描下一个未处理的桥。
常见问题: 如果桥的 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 位,使设备开始响应分配给它的地址范围。
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 分配结果。