配置空间访问机制:IO端口访问、内存映射访问与CFG cycles
说到配置空间怎么读,其实绕不开三种路子。我刚开始接触PCIe时,一看手册里又是IO端口又是内存映射,头都大了。后来调了几块板子才明白,每种方式都有自己的脾气。这一章咱们就把它捋清楚。
在PCI/PCIe体系里,配置空间就是设备的“身份证”和“控制面板”。CPU想读它,有三种手段:IO端口访问、内存映射访问、以及配置周期(CFG cycles)。说白了,就是往哪写、怎么写的区别。
IO端口访问:老江湖的做法
这是最原始的方式。x86体系里专门留了两个IO端口:0xCF8和0xCFC。CF8存地址,CFC存数据。你要读哪个设备的哪个寄存器,先往CF8写一个32位的“配置地址”,然后从CFC读回来。
配置地址的结构是这样的:
- Bit 31:启用位,必须为1
- Bits 30-24:保留
- Bits 23-16:总线号
- Bits 15-11:设备号
- Bits 10-8:功能号
- Bits 7-2:寄存器偏移(单位字节,必须4字节对齐)
- Bits 1-0:固定为0
代码也很直接。比如读总线0、设备2、功能0、偏移0x10(Bar0寄存器):
// 构造配置地址
uint32_t addr = (1 << 31) | (0 << 16) | (2 << 11) | (0 << 8) | (0x10 & 0xFC);
outl(0xCF8, addr);
uint32_t data = inl(0xCFC);
// data就是Bus0 Dev2 Func0 的 Bar0 值
我在一个老旧的工控主板上调试网卡时用过这招。结果发现IO端口访问在最新平台虽然还支持,但性能和灵活性都不如后面的。不过,它有个好处——兼容性好,几乎所有x86平台都认。
注意:IO端口访问一次只能操作一个DWORD(4字节)。而且它只能访问配置空间的前256字节。扩展配置空间(256-4095)必须用内存映射方式。
内存映射访问:现代主流玩法
现在的主板几乎都用内存映射(MMIO)。就是把配置空间直接映射到物理内存地址空间里。你只需像读写普通内存一样读写那块区域,芯片组会自动帮你完成PCI配置周期。
映射方式分两种:一是通过MCFG表(ACPI的表)告诉OS配置空间的基地址;二是通过ECAM(Enhanced Configuration Access Mechanism)特性。每个PCIe设备的功能共享4KB地址空间:前256字节是兼容的配置空间,后3840字节是扩展配置空间。
MMIO的好处很明显:速度快、支持任意字节数读写(不限于DWORD)、能访问整个4KB配置空间。我调试高速网卡时,为了读扩展配置空间里的VC(虚通道)寄存器,只能用MMIO。
对应代码通常长这样(假设已知MMIO基地址):
// 计算某设备配置空间地址
uint64_t mmio_base = 0xE000000000; // 根据MCFG表获得
int bus = 0, dev = 2, func = 0;
int offset = 0x10; // 要读的寄存器偏移
volatile uint32_t *cfg_addr = (uint32_t *)(mmio_base +
(bus << 20) | (dev << 15) | (func << 12) | offset);
uint32_t data = *cfg_addr;
CFG cycles:芯片组在背后做的事
不管是IO端口访问还是内存映射,最终都会触发一个PCI配置周期(CFG cycles)。这就是芯片组截获CPU的访问请求,在PCIe总线上发起具体的配置读写事务。
CFG cycles分为两种:Type 0和Type 1。
| 类型 | 目标 | ID字段含义 |
|---|---|---|
| Type 0 | 直接连接在本总线上的设备 | 只包含设备号、功能号,不包含总线号 |
| Type 1 | 连接在下游总线上的设备 | 包含完整的总线号、设备号、功能号 |
我记得以前做PCIe switch兼容性测试时,发现Type 0和Type 1的转换逻辑错误会导致二级总线下设备无法枚举。嗯,这坑我踩过。
简单理解:IO端口方式和MM方式都是“用户界面”,而CFG cycles才是“底层执行者”。你不管用哪种方式发起请求,最后总线上走的都是Type 0或Type 1配置事务。
三种方式对比
这里我列个表,方便你选型时快速判断:
| 特性 | IO端口访问 | 内存映射访问 | CFG cycles(底层) |
|---|---|---|---|
| 访问粒度 | 仅DWORD | 任意字节(但建议对齐) | DWORD(硬件层) |
| 可访问空间 | 前256字节 | 整个4KB | 取决于上层方式 |
| 速度 | 慢(每次IO操作~几百ns) | 快(类似内存访问) | 与上层方式一致 |
| 兼容性 | 所有x86平台 | 需要OS或BIOS支持MCFG | 由芯片组保证 |
| 典型用途 | 早期系统、调试驱动 | 现代OS、高性能驱动 | 内部机制,不直接使用 |
避坑指南
- IO端口访问别忘置位bit31。 我见过一个同事在CF8里忘了写1,死活读不回数据,排查了两天。
- 内存映射基地址必须从MCFG表中拿。 别自己乱猜。不同BIOS给出的基地址可能不一样,写死了换块主板就崩。
- 如果系统不支持ECAM,内存映射可能失效。 这时得退回IO端口方式。不过现在PCIe系统基本都支持了。
- 跨总线访问时小心桥的配置。 我曾经因为忘了配置PCIe桥的下游总线号,导致Type 1事务传递不过去。
一张图看懂三种访问方式的关系
下面这张图总结了整章的核心逻辑。CPU通过两种“入口”(IO端口或内存地址)发起请求,芯片组统一转换成PCIe总线上的CFG cycles,最终到达目标设备功能。
说白了,你调驱动时最常接触的就是IO端口和MMIO两种。CFG cycles是硬件帮你兜底的事,别被它吓着。但理解它对你排查总线枚举、桥配置类问题极有帮助。
好了,关于三种访问方式就说到这。下次你在看PCIe驱动代码时,再看到outl + inl或者直接读内存地址,心里就该有数了——原来走的是不同的路,但终点都是同一个配置空间。