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作为枚举起点的拓扑位置:
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。
下图展示了总线号分配的深度优先过程:
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在配置空间访问中的层级关系:
3.4 枚举流程总结
结合上述概念,一个完整的PCIe设备枚举流程可以概括为以下步骤:
- 初始化根总线:系统从RC的Host Bridge开始,将Bus 0作为当前扫描总线。
- 扫描设备:遍历当前总线的32个设备号(0-31),对每个设备号的功能0进行配置空间读取。
- 识别设备类型:如果读取成功,检查Header Type寄存器:
- 0x00:标准端点(Endpoint)。
- 0x01:PCIe桥(Switch或Root Port)。
- 0x80:多功能设备(Function 0必须存在)。
- 桥设备处理:如果发现桥设备,为其分配一个新的总线号(Secondary Bus Number),并递归扫描该新总线。
- 更新Subordinate Bus Number:递归返回后,更新当前桥的Subordinate Bus Number为子树中最大的总线号。
- 重复直到完成:继续扫描下一个设备号,直到当前总线所有设备处理完毕,然后返回上一级桥。
性能提示:Linux内核在枚举时,会使用
pci_scan_bus()和pci_scan_slot()等函数。为了提高效率,内核会缓存已扫描的设备信息,避免重复访问配置空间。对于热插拔事件,内核会动态调用pci_rescan_bus()来重新枚举特定总线。