3、PCIe设备枚举基础:枚举的起点(Root Complex)、总线号分配规则、设备号与功能号

PCIe设备枚举是系统软件(通常是BIOS/UEFI或Linux内核)在启动阶段,通过深度优先遍历PCIe拓扑结构,为每一个发现的设备分配唯一标识符(总线号、设备号、功能号)的过程。理解枚举的起点、总线号分配规则以及设备/功能号的语义,是掌握整个PCIe驱动框架的基石。

3.1 枚举的起点:Root Complex(RC)

Root Complex(RC)是PCIe层次结构的根节点,它连接CPU/内存子系统和PCIe交换网络。在枚举过程中,RC扮演着两个关键角色:

  • 拓扑起点:枚举算法从RC内部的虚拟PCI桥(通常称为“Host Bridge”或“PCI Domain 0”)开始,递归扫描其下的所有总线。
  • 配置访问代理:RC负责将CPU发起的MMIO配置访问(通过内存映射的配置空间)转换为PCIe总线上的TLP(事务层包),从而访问下游设备的配置空间。
关键概念:在x86系统中,RC通常集成在芯片组(如Intel的PCH或AMD的FCH)中。在ARM/嵌入式系统中,RC可能是一个独立的IP核,通过AXI总线与CPU连接。无论哪种架构,枚举的起点始终是RC内部的总线0(Bus 0)。

下图展示了RC作为枚举起点的拓扑位置:

枚举起点:Root Complex 拓扑结构 CPU 内存 Root Complex (RC) Host Bridge (Bus 0) 配置空间代理 内部端点 PCIe Switch (Bus 1 - Bus 4) 端点 (EP) (Bus 1, Dev 0, Fun 0) 说明: 枚举从RC内部的Host Bridge(Bus 0)开始。RC负责将CPU的配置读写请求转换为PCIe TLP。 Switch和端点通过RC的配置空间代理进行访问。

3.2 总线号分配规则

PCIe总线号是一个8位字段(范围0-255),用于唯一标识系统中的一条PCIe总线。枚举过程中,总线号的分配遵循以下核心规则:

规则项 详细描述
根总线固定 RC内部的总线0(Bus 0)是固定的枚举起点,不可更改。
深度优先分配 枚举算法采用深度优先遍历:先分配当前桥的次级总线(Secondary Bus),再递归扫描该总线下的所有设备。
桥接寄存器 每个PCIe桥(包括RC内部的虚拟桥和Switch中的透明桥)都有三个关键寄存器:
Primary Bus Number:上游总线号。
Secondary Bus Number:下游直接连接的总线号。
Subordinate Bus Number:下游子树中最大的总线号。
总线号唯一性 在同一PCI Domain(域)内,每个总线号只能分配给一条总线,不能重复。
动态分配 现代系统支持热插拔,当新设备插入时,系统会动态分配一个未被占用的总线号。
实战案例:假设RC(Bus 0)下连接了一个Switch。Switch的上行端口连接到Bus 0,下行端口创建一条新的总线Bus 1。如果Bus 1下又连接了一个多功能端点,该端点内部可能包含一个子桥,从而创建Bus 2。此时,Switch的Subordinate Bus Number会被设置为2。

下图展示了总线号分配的深度优先过程:

总线号深度优先分配示例 Bus 0 (Root) Primary=0, Secondary=1 Bus 1 (Switch) Primary=1, Secondary=2 Bus 2 (Switch) Primary=2, Secondary=3 Bus 3 (EP) Dev 0, Fun 0 Bus 4 (EP) Dev 0, Fun 0 遍历顺序:0 → 1 → 2 → 3 → 4

3.3 设备号与功能号

在一条给定的PCIe总线上,设备通过设备号(Device Number)和功能号(Function Number)进行区分。这两个字段共同构成了配置空间访问地址的一部分。

3.3.1 设备号(Device Number)

  • 范围:5位,取值0-31。
  • 物理含义:对应PCIe链路(Link)上的物理设备。在标准PCI中,设备号通常与AD[31:16]复用线相关;在PCIe中,设备号通过配置路由(Configuration Routing)机制确定。
  • 分配规则:枚举时,系统会扫描总线上的所有32个设备号。如果某个设备号对应的配置空间读取返回成功(即设备存在),则记录该设备。
  • 限制:一条总线上最多只能有32个设备(包括桥和端点)。

3.3.2 功能号(Function Number)

  • 范围:3位,取值0-7。
  • 物理含义:一个物理设备内部可以包含多个独立的功能单元(Function)。每个功能拥有独立的配置空间、BAR空间和中断线。
  • 多功能设备:如果一个设备实现了多个功能,功能0必须存在,且其配置空间中的“多功能设备”位(Header Type寄存器的bit 7)必须置1。
  • 独立枚举:每个功能在枚举时被视为一个独立的逻辑设备,拥有自己的设备ID、厂商ID和驱动程序绑定。
常见误区:设备号不是物理插槽号。在PCIe中,设备号由枚举算法根据拓扑结构动态分配,与物理位置没有固定映射关系。例如,一个x16插槽可能被分配设备号0,而一个x1插槽可能被分配设备号5。

下表总结了BDF(Bus:Device.Function)编码规则:

字段 位宽 取值范围 说明
Bus Number 8 bits 0 - 255 标识系统中的一条PCIe总线。
Device Number 5 bits 0 - 31 标识总线上的一个物理设备。
Function Number 3 bits 0 - 7 标识设备内的一个独立功能。

下图展示了BDF在配置空间访问中的层级关系:

BDF 层级结构:Bus : Device . Function Bus 0 (Root Complex) 8位总线号,范围0-255 Device 0 (PCIe to PCI Bridge) 5位设备号,范围0-31 Function 0 (Primary Function) 3位功能号,范围0-7;每个功能有独立配置空间 完整BDF示例:00:00.0

3.4 枚举流程总结

结合上述概念,一个完整的PCIe设备枚举流程可以概括为以下步骤:

  1. 初始化根总线:系统从RC的Host Bridge开始,将Bus 0作为当前扫描总线。
  2. 扫描设备:遍历当前总线的32个设备号(0-31),对每个设备号的功能0进行配置空间读取。
  3. 识别设备类型:如果读取成功,检查Header Type寄存器:
    • 0x00:标准端点(Endpoint)。
    • 0x01:PCIe桥(Switch或Root Port)。
    • 0x80:多功能设备(Function 0必须存在)。
  4. 桥设备处理:如果发现桥设备,为其分配一个新的总线号(Secondary Bus Number),并递归扫描该新总线。
  5. 更新Subordinate Bus Number:递归返回后,更新当前桥的Subordinate Bus Number为子树中最大的总线号。
  6. 重复直到完成:继续扫描下一个设备号,直到当前总线所有设备处理完毕,然后返回上一级桥。
性能提示:Linux内核在枚举时,会使用pci_scan_bus()和pci_scan_slot()等函数。为了提高效率,内核会缓存已扫描的设备信息,避免重复访问配置空间。对于热插拔事件,内核会动态调用pci_rescan_bus()来重新枚举特定总线。