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(数据端口): 直接映射到目标配置空间寄存器的数据。
3.2 PCI配置空间读取流程
读取PCIe设备配置空间的典型流程分为以下步骤:
- 构造配置地址: 将目标设备的Bus、Device、Function、Offset按CF8h寄存器格式组合成一个32位值,并将位[31]置1。
- 写入CF8h端口: 使用OUT指令将构造好的地址值写入IO端口0xCF8。
- 读取CFCh端口: 使用IN指令从IO端口0xCFC读取32位数据。
- 解析数据: 从读取的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位)
3.6 多设备枚举与配置空间扫描
系统启动时,BIOS/UEFI或操作系统通过CF8/CFC机制扫描PCIe总线上的所有设备。扫描流程如下:
- 遍历总线号: 从Bus 0开始,逐总线扫描。
- 遍历设备号: 对每个总线,遍历Device 0~31。
- 遍历功能号: 对每个设备,遍历Function 0~7。
- 读取Vendor ID: 如果读取的Vendor ID为0xFFFF,表示该位置无设备。
- 记录设备信息: 如果设备存在,记录其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平台支持。