4. 设备枚举原理

PCIe设备枚举是驱动开发的基础环节,其核心在于理解总线号、设备号、功能号(BDF)的编码规则,以及配置空间的读写机制。本章将深入剖析这两部分内容,为后续的BAR空间映射和驱动初始化打下坚实基础。

4.1 BDF编码规则

在PCIe拓扑中,每个端点设备(Endpoint)或桥设备(Bridge)都由一个唯一的BDF(Bus:Device.Function)三元组标识。该标识用于配置空间访问和中断路由。

  • 总线号(Bus Number):8位,范围0-255。根复合体(Root Complex)通常位于总线0,通过桥设备扩展出次级总线。
  • 设备号(Device Number):5位,范围0-31。挂载在同一总线上的物理设备编号。
  • 功能号(Function Number):3位,范围0-7。一个物理设备最多可支持8个独立功能(如多功能网卡)。
关键点: BDF组合形成一个16位的标识符,在配置读写操作中作为目标地址的一部分。例如,BDF = 0x00:1F.0 表示总线0、设备31、功能0。

4.1.1 BDF编码结构图

BDF编码结构 BDF 编码结构 (16位配置空间地址) 总线号 (Bus) 8位 [15:8] 设备号 (Device) 5位 [7:3] 功能号 (Func) 3位 [2:0] 示例:BDF = 0x00:1F.0 → 总线0,设备31,功能0 配置空间地址 = (Bus << 8) | (Device << 3) | Function

4.2 配置读写机制

PCIe配置空间是设备初始化与资源分配的核心。系统软件通过两种方式访问配置空间:I/O端口方式(传统方式)和内存映射方式(MMIO方式)。

4.2.1 两种访问方式对比

特性 I/O端口方式 (CF8/CFC) 内存映射方式 (MMIO)
访问端口/地址 0xCF8 (地址端口) / 0xCFC (数据端口) 通过MCFG表映射到物理内存
地址编码 BDF + 寄存器偏移 (8位) 物理地址 = 基址 + (BDF << 12) + 偏移
性能 慢 (需要两次I/O操作) 快 (直接内存访问)
适用范围 PCI/PCI-X 传统设备 PCIe 设备 (必需)
并发性 不支持多线程并发访问 支持 (每个设备独立地址空间)

4.2.2 I/O端口方式 (CF8/CFC)

传统PCI配置访问使用两个I/O端口:

  • 0xCF8:配置地址端口(CONFIG_ADDRESS),32位寄存器,用于指定目标BDF和寄存器偏移。
  • 0xCFC:配置数据端口(CONFIG_DATA),32位寄存器,用于读写配置数据。

访问流程如下:

  1. 将目标BDF和寄存器偏移编码写入0xCF8端口。
  2. 从0xCFC端口读取或写入32位数据。
注意: 0xCF8端口的位31必须置1(Enable位),表示配置空间访问有效。否则写入被忽略。
配置地址寄存器格式
CF8配置地址寄存器格式 0xCF8 配置地址寄存器 (32位) E [31] 保留 [30:24] 总线号 [23:16] 设备号 [15:11] 功能号 [10:8] 寄存器偏移 [7:0] E=1 启用配置空间访问;寄存器偏移必须按4字节对齐 示例:访问设备0x00:1F.0的寄存器0x10 → 写入0x80001F10到0xCF8
I/O端口方式代码示例
// 通过I/O端口读取配置空间DWORD
uint32_t pci_config_read_dword(uint8_t bus, uint8_t dev, uint8_t func, uint8_t offset) {
    uint32_t address;
    uint32_t data;

    // 构造配置地址
    address = (1 << 31) |               // Enable位
              ((uint32_t)bus << 16) |   // 总线号
              ((uint32_t)dev << 11) |   // 设备号
              ((uint32_t)func << 8) |   // 功能号
              (offset & 0xFC);          // 寄存器偏移(低2位清零)

    // 写入地址端口
    outl(0xCF8, address);
    // 读取数据端口
    data = inl(0xCFC);

    return data;
}

// 通过I/O端口写入配置空间DWORD
void pci_config_write_dword(uint8_t bus, uint8_t dev, uint8_t func, 
                            uint8_t offset, uint32_t value) {
    uint32_t address;

    address = (1 << 31) |
              ((uint32_t)bus << 16) |
              ((uint32_t)dev << 11) |
              ((uint32_t)func << 8) |
              (offset & 0xFC);

    outl(0xCF8, address);
    outl(0xCFC, value);
}

4.2.3 内存映射方式 (MMIO)

PCIe规范要求所有操作系统必须支持内存映射配置访问(ECAM,Enhanced Configuration Access Mechanism)。系统通过ACPI的MCFG表获取ECAM基地址,将配置空间映射到物理内存。

ECAM地址计算公式:

物理地址 = ECAM基地址 + (bus << 20) | (device << 15) | (function << 12) | offset
  • 每个设备占用4KB配置空间(4096字节)。
  • 每个总线最多支持32个设备 × 8个功能 = 256个功能。
  • ECAM基地址通常位于高物理内存区域(如0xE0000000以上)。
ECAM地址映射结构图
ECAM内存映射结构 ECAM 内存映射结构 ECAM 基地址 (来自MCFG表) 总线0 (1MB空间: 0x00000 - 0xFFFFF) 设备0 功能0 (4KB) 设备0 功能1 (4KB) 设备1 功能0 (4KB) ... 总线1 (1MB空间: 0x100000 - 0x1FFFFF) 每个总线占用1MB空间,每个功能占用4KB空间
MMIO方式代码示例
// 通过MMIO读取配置空间DWORD
uint32_t pci_mmio_read_dword(void *ecam_base, uint8_t bus, 
                              uint8_t dev, uint8_t func, uint8_t offset) {
    uint32_t *reg_addr;
    
    // 计算寄存器地址
    reg_addr = (uint32_t *)((uint8_t *)ecam_base + 
                            ((uint32_t)bus << 20) |
                            ((uint32_t)dev << 15) |
                            ((uint32_t)func << 12) |
                            (offset & 0xFC));
    
    return *reg_addr;
}

// 通过MMIO写入配置空间DWORD
void pci_mmio_write_dword(void *ecam_base, uint8_t bus, 
                           uint8_t dev, uint8_t func, 
                           uint8_t offset, uint32_t value) {
    uint32_t *reg_addr;
    
    reg_addr = (uint32_t *)((uint8_t *)ecam_base + 
                            ((uint32_t)bus << 20) |
                            ((uint32_t)dev << 15) |
                            ((uint32_t)func << 12) |
                            (offset & 0xFC));
    
    *reg_addr = value;
}

4.3 设备枚举流程

系统启动时,BIOS/UEFI或操作系统通过深度优先遍历PCIe树,完成设备枚举。核心步骤如下:

  1. 扫描总线0:从总线0的设备0开始,读取每个设备的Vendor ID和Device ID。
  2. 检测桥设备:如果设备是PCI-to-PCI桥(Class Code = 0x060400),则读取其次级总线号和 subordinate 总线号。
  3. 递归扫描:对每个桥设备的次级总线重复步骤1-2,直到所有总线被扫描完毕。
  4. 分配BDF:为每个发现的设备分配唯一的BDF。
常见问题: 如果设备Vendor ID读取为0xFFFFFFFF,表示该设备不存在或未连接。这是判断设备是否存在的标准方法。

4.3.1 设备枚举流程图

PCIe设备枚举流程 PCIe 设备枚举流程 开始枚举 扫描当前总线 读取 Vendor ID ID有效? 是 否 记录设备信息 检查是否为桥 是桥? 是 递归 否 下一个设备

4.4 本章小结

概念 关键点
BDF编码 总线号(8位) + 设备号(5位) + 功能号(3位) = 16位标识符
I/O端口方式 使用0xCF8/0xCFC端口,适用于传统PCI设备
MMIO方式 通过ECAM基地址直接内存访问,性能更优
设备枚举 深度优先遍历PCIe树,读取Vendor ID判断设备存在性
实战建议: 在Linux内核中,设备枚举由 pci_scan_bus() 和 pci_scan_slot() 函数完成。驱动开发者通常不需要手动枚举,但理解BDF和配置访问机制对调试BAR映射和中断分配至关重要。