3. 枚举流程总览:从Root Complex到Endpoint的深度优先遍历、Bus号分配、设备号分配

PCIe枚举是系统软件(通常是BIOS/UEFI或操作系统内核)在复位后,通过配置事务发现并初始化整个PCIe拓扑结构的过程。其核心算法是深度优先遍历(DFS),从Root Complex(RC)出发,逐级向下扫描,为每个发现的设备分配唯一的Bus号和Device号,并配置其配置空间中的相关寄存器。

核心要点:枚举的本质是“遍历-发现-配置”的循环。遍历顺序决定了Bus号的分配顺序,而设备号则由设备在总线上的物理位置(即AD线)决定。

3.1 枚举的起点:Root Complex与Bus 0

枚举的起点是Root Complex内部的Root Port。系统固件或内核首先读取RC的配置空间,确认其作为Host Bridge的身份。随后,枚举器将Bus 0作为根总线,开始扫描Bus 0上的所有设备。

  • Bus 0:由Root Complex内部逻辑生成,是枚举的起始总线。
  • Device 0:通常对应Root Complex自身或集成的Root Port。
  • Function 0:每个设备至少包含Function 0,用于标识设备是否存在。
工程提示:在嵌入式系统中,如果RC是FPGA实现的,Bus 0的配置空间基地址通常由硬件设计决定(如0xE000_0000)。驱动开发者在调试初期,应首先确认Bus 0的配置空间能否被正确访问。

3.2 深度优先遍历(DFS)算法

PCIe枚举采用深度优先遍历策略,而非广度优先。这意味着枚举器会沿着一条路径一直向下探索,直到遇到Endpoint或无法继续下行的Switch Downstream Port,然后回溯到上一个分支点继续扫描。

DFS的典型步骤如下:

  1. 扫描当前总线:对当前Bus上的所有Device(0~31)进行读操作(读取Vendor ID寄存器)。
  2. 判断设备类型:如果读到有效的Vendor ID(非0xFFFF),则设备存在。进一步读取Class Code和Header Type寄存器,判断是Endpoint、Switch Upstream Port还是Switch Downstream Port。
  3. 分配Bus号(仅对Bridge):如果设备是PCIe-to-PCIe Bridge(包括Switch的Upstream/Downstream Port),则为其分配一个新的、未被使用的Bus号作为Secondary Bus,并配置其Primary Bus、Secondary Bus和Subordinate Bus寄存器。
  4. 递归下行:将当前总线切换为刚分配的Secondary Bus,重复步骤1~3。
  5. 回溯:当当前总线上的所有Device扫描完毕,且没有发现新的Bridge时,回溯到上一级总线,继续扫描下一个Device。
避坑指南:DFS算法要求枚举器必须正确处理“Bridge下无设备”的情况。如果为一个Bridge分配了Bus号,但扫描其Secondary Bus时发现没有任何设备,必须将该Bridge的Subordinate Bus寄存器设置为与Secondary Bus相同的值,并立即回溯,避免无限递归。

3.3 Bus号分配机制

Bus号是PCIe拓扑中用于路由配置事务的关键标识。每个PCIe-to-PCIe Bridge(包括Switch)都包含三个关键的Bus号寄存器:

寄存器名称 偏移地址 描述
Primary Bus Number 0x18 记录Bridge上游(靠近RC)的总线号。
Secondary Bus Number 0x19 记录Bridge下游(远离RC)的第一级总线号。由枚举器分配。
Subordinate Bus Number 0x1A 记录Bridge下游子树中最大的总线号。由枚举器在遍历完成后更新。

Bus号分配遵循以下规则:

  • 唯一性:整个PCIe域中,每个Bus号必须唯一。
  • 递增性:枚举器通常从Bus 0开始,每发现一个Bridge,就分配下一个可用的Bus号(如Bus 1、Bus 2...)。
  • 范围界定:Subordinate Bus寄存器定义了Bridge可以转发的配置事务的目标Bus范围。只有目标Bus号在[Secondary Bus, Subordinate Bus]范围内的配置事务,才会被Bridge转发到下游。
PCIe枚举DFS流程与Bus号分配示例 Root Complex (Bus 0) Root Port 0 (Dev 0) Root Port 1 (Dev 1) Switch Upstream (Bus 1) Downstream 0 (Bus 2) Endpoint (Bus 2, Dev 0) Downstream 1 (Bus 3) Endpoint (Bus 3, Dev 0) Endpoint (Bus 0, Dev 1) DFS遍历顺序:Bus 0 → Root Port 0 → Bus 1 → Downstream 0 → Bus 2 → Endpoint → 回溯 → Downstream 1 → Bus 3 → Endpoint → 回溯 → Root Port 1 → Endpoint Bus号分配:Bus 0(RC)→ Bus 1(Switch Upstream)→ Bus 2(Downstream 0)→ Bus 3(Downstream 1)

3.4 设备号(Device Number)分配

与Bus号由软件动态分配不同,Device Number(设备号)是由设备在总线上的物理连接位置决定的,枚举器只能“发现”而不能“分配”。

设备号的确定基于以下机制:

  • AD线映射:在PCIe中,配置事务通过ID路由或ECAM(Enhanced Configuration Access Mechanism)访问设备。在传统的PCI配置机制中,Device Number对应32根AD线中的一根(AD[31:11])。在ECAM中,Device Number直接作为内存映射地址的一部分。
  • 硬件固定:当一个Endpoint或Bridge连接到Switch的Downstream Port时,其Device Number由该Downstream Port的硬件设计决定。例如,Switch的Downstream Port 0通常对应Device 0,Downstream Port 1对应Device 1,以此类推。
  • 扫描发现:枚举器通过遍历Device 0到Device 31来发现设备。如果某个Device位置读回全F(0xFFFF),则表示该位置没有设备。
关键区别:Bus号是软件分配的拓扑标识,用于路由事务;Device号是硬件决定的物理位置标识,用于在总线上寻址。驱动开发者必须理解,不能随意更改Device号。

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

在Linux内核中,枚举结果被组织成一系列数据结构,最核心的是pci_dev和pci_bus。

// Linux内核中pci_bus结构体(简化)
struct pci_bus {
    struct list_head node;      // 用于链接到全局总线列表
    struct pci_bus *parent;     // 父总线(上游总线)
    struct list_head children;  // 子总线列表
    struct list_head devices;   // 该总线上的设备列表
    unsigned char number;       // 本总线的Bus号
    unsigned char primary;      // 对于Bridge,这是Primary Bus号
    unsigned char secondary;    // 对于Bridge,这是Secondary Bus号
    unsigned char subordinate;  // 对于Bridge,这是Subordinate Bus号
    // ... 其他字段
};

// Linux内核中pci_dev结构体(简化)
struct pci_dev {
    struct list_head bus_list;  // 用于链接到所在总线的设备列表
    struct pci_bus *bus;        // 指向设备所在的总线
    unsigned int devfn;         // 编码了Device Number和Function Number
    unsigned short vendor;      // Vendor ID
    unsigned short device;      // Device ID
    unsigned int class;         // Class Code
    // ... 其他字段
};

3.6 枚举流程总结

下表总结了枚举流程中每个步骤的核心操作与输出:

步骤 操作 输入 输出/配置
1. 初始化 读取RC配置空间,确认Host Bridge身份 RC硬件寄存器 Bus 0基地址
2. 扫描总线 遍历Device 0~31,读取Vendor ID 当前Bus号 设备存在性列表
3. 识别Bridge 读取Header Type和Class Code 设备配置空间 设备类型(Bridge/Endpoint)
4. 分配Bus号 为Bridge分配新的Bus号,写入Secondary Bus寄存器 当前最大Bus号 Bridge的Secondary Bus寄存器
5. 递归下行 切换到Secondary Bus,重复步骤2~4 新分配的Bus号 子树拓扑
6. 更新Subordinate 遍历完成后,将子树最大Bus号写入Bridge的Subordinate寄存器 子树最大Bus号 Bridge的Subordinate Bus寄存器
7. 回溯 返回上一级总线,继续扫描下一个Device 父总线号 完成所有分支扫描
调试技巧:在嵌入式系统开发中,如果发现某个Endpoint无法被枚举到,可以首先检查其上游Bridge的Secondary Bus和Subordinate Bus寄存器是否配置正确。一个常见的错误是Subordinate Bus值小于Secondary Bus,导致配置事务无法被正确路由。