3. x86平台PCIe配置空间访问:CF8/CFC IO端口机制

x86平台访问PCIe配置空间的核心机制是CF8/CFC IO端口对。这一机制源于PCI规范,并在PCIe时代被保留和扩展。通过两个32位IO端口——配置地址端口(CF8h)和配置数据端口(CFCh)——CPU可以发起对PCIe设备配置空间的读写操作。

3.1 CF8/CFC端口工作原理

CF8h(配置地址寄存器)和CFCh(配置数据寄存器)是x86平台访问PCIe配置空间的“桥梁”。软件通过向CF8h写入目标设备的总线号(Bus)、设备号(Device)、功能号(Function)和寄存器偏移(Offset),然后对CFCh进行读写,即可完成配置空间的访问。

关键寄存器格式:
  • CF8h(地址端口): 位[31] = 1(使能位),位[23:16] = 总线号,位[15:11] = 设备号,位[10:8] = 功能号,位[7:2] = 寄存器偏移(DWORD对齐)。
  • CFCh(数据端口): 直接映射到目标配置空间寄存器的数据。
CF8/CFC 配置空间访问流程 CF8/CFC 配置空间访问流程 CPU / 软件 CF8h (地址端口) CFCh (数据端口) PCIe 总线 目标设备配置空间 说明: 1. 软件先向CF8h写入目标设备的Bus/Dev/Func/Offset。 2. 然后对CFCh进行读/写操作,数据通过PCIe总线到达目标设备配置空间。

3.2 PCI配置空间读取流程

读取PCIe设备配置空间的典型流程分为以下步骤:

  1. 构造配置地址: 将目标设备的Bus、Device、Function、Offset按CF8h寄存器格式组合成一个32位值,并将位[31]置1。
  2. 写入CF8h端口: 使用OUT指令将构造好的地址值写入IO端口0xCF8。
  3. 读取CFCh端口: 使用IN指令从IO端口0xCFC读取32位数据。
  4. 解析数据: 从读取的32位数据中提取所需字段(如Vendor ID、Device ID、BAR等)。
提示: 配置空间访问必须按DWORD(4字节)对齐。如果需要读取单个字节或字,需读取整个DWORD后通过移位和掩码提取。

3.3 配置空间读取代码示例

以下代码展示了在x86平台下通过CF8/CFC端口读取PCIe设备Vendor ID和Device ID的典型实现:

// x86平台:通过CF8/CFC读取PCIe配置空间
#include <stdint.h>
#include <x86intrin.h>

// 读取PCIe配置空间DWORD
uint32_t pci_config_read(uint8_t bus, uint8_t dev, uint8_t func, uint8_t offset) {
    // 构造配置地址
    uint32_t address = (1 << 31) |          // 使能位
                       ((uint32_t)bus << 16) |
                       ((uint32_t)dev << 11) |
                       ((uint32_t)func << 8) |
                       (offset & 0xFC);       // DWORD对齐
    
    // 写入CF8h地址端口
    __outdword(0xCF8, address);
    
    // 从CFCh数据端口读取
    return __indword(0xCFC);
}

// 读取Vendor ID和Device ID
void read_pci_ids(uint8_t bus, uint8_t dev, uint8_t func) {
    uint32_t id_reg = pci_config_read(bus, dev, func, 0x00);
    uint16_t vendor_id = id_reg & 0xFFFF;
    uint16_t device_id = (id_reg >> 16) & 0xFFFF;
    
    printf("Bus:%02x Dev:%02x Func:%02x - Vendor:0x%04x Device:0x%04x\n",
           bus, dev, func, vendor_id, device_id);
}

3.4 CF8/CFC与MMCONFIG对比

特性 CF8/CFC (IO端口) MMCONFIG (内存映射)
访问方式 IN/OUT指令 内存读写指令
地址空间 IO空间 内存空间
性能 较低(每次访问需两次IO操作) 较高(直接内存访问)
扩展性 有限(需轮询所有Bus/Dev/Func) 好(支持ECAM直接索引)
兼容性 所有x86平台 需BIOS/UEFI启用

3.5 配置空间布局与关键寄存器

PCIe配置空间大小为4KB(PCI兼容区域为256字节)。前64字节为PCI兼容头部,包含关键寄存器:

  • 0x00: Vendor ID (16位) + Device ID (16位)
  • 0x04: Command (16位) + Status (16位)
  • 0x08: Revision ID (8位) + Class Code (24位)
  • 0x10~0x24: BAR0~BAR5 (每个32位)
  • 0x3C: Interrupt Line (8位) + Interrupt Pin (8位)
PCIe配置空间布局 PCIe配置空间布局(前64字节) Vendor ID (16位) Device ID (16位) 0x00 Command (16位) Status (16位) 0x04 Revision ID Class Code (24位) 0x08 Cache Line Size / Latency Timer / Header Type / BIST 0x0C BAR0 ~ BAR5 (每个32位, 共24字节) 0x10~0x24 CardBus CIS / Subsystem ID / Expansion ROM / Reserved 0x28~0x38 Interrupt Line (8位) Interrupt Pin (8位) 0x3C 注:每个DWORD寄存器必须按4字节对齐访问,BAR寄存器用于设备地址映射。

3.6 多设备枚举与配置空间扫描

系统启动时,BIOS/UEFI或操作系统通过CF8/CFC机制扫描PCIe总线上的所有设备。扫描流程如下:

  1. 遍历总线号: 从Bus 0开始,逐总线扫描。
  2. 遍历设备号: 对每个总线,遍历Device 0~31。
  3. 遍历功能号: 对每个设备,遍历Function 0~7。
  4. 读取Vendor ID: 如果读取的Vendor ID为0xFFFF,表示该位置无设备。
  5. 记录设备信息: 如果设备存在,记录其Bus/Dev/Func及Class Code等信息。
注意: 在x86平台下,CF8/CFC机制对配置空间的访问是同步阻塞的。在多核系统中,需要确保对CF8/CFC的访问是原子操作,避免多个CPU核心同时访问导致数据错乱。通常使用自旋锁或互斥锁保护CF8/CFC的访问序列。

3.7 配置空间访问的原子性保证

由于CF8h和CFCh是两个独立的IO端口,配置空间访问需要两次IO操作(先写地址,后读/写数据)。这带来了原子性问题:

  • 问题: 如果两个CPU核心同时执行配置空间访问,一个核心写入CF8h后可能被另一个核心的CF8h写入覆盖,导致后续CFCh操作指向错误的设备。
  • 解决方案: 在操作系统层面,使用IO端口锁(如Linux中的pci_lock)确保CF8/CFC访问序列的原子性。
// 原子性访问示例(伪代码)
spin_lock(&pci_lock);

// 写入地址
__outdword(0xCF8, address);
// 读取数据
uint32_t data = __indword(0xCFC);

spin_unlock(&pci_lock);

3.8 x86平台配置空间访问的局限性

尽管CF8/CFC机制在x86平台广泛使用,但它存在以下局限性:

局限性 说明
性能瓶颈 每次配置空间访问需要两次IO操作(写地址+读/写数据),IO操作本身比内存访问慢。
扩展性差 CF8/CFC只能访问PCIe配置空间的前256字节(PCI兼容区域),无法直接访问PCIe扩展配置空间(256~4095字节)。
原子性问题 多核环境下需要额外的锁机制保证访问序列的原子性。
不支持ECAM CF8/CFC无法利用PCIe的Enhanced Configuration Access Mechanism(ECAM),后者通过内存映射提供更高效的访问。
总结: CF8/CFC机制是x86平台访问PCIe配置空间的基础方法,适用于系统启动早期和兼容性场景。在现代x86系统中,MMCONFIG(内存映射配置空间)已成为主流访问方式,但CF8/CFC作为回退机制仍然被所有x86平台支持。