第2章 PCIe配置空间
配置空间是PCIe设备的"身份证"和"控制台"。面试考这个点,说白了就是看你懂不懂设备枚举和资源分配。我记得自己刚入行时,花了整整一周才把BAR空间的映射逻辑搞清楚——踩过的坑今天给你们一一拆开。
2.1 配置空间结构:256字节 vs 4KB
PCIe向前兼容PCI的配置空间规范。传统PCI只给256字节,PCIe把扩展空间拉到了4KB。嗯,这里要注意:操作系统和固件默认只访问前256字节(即配置空间头),剩下的扩展空间(Capability结构)由驱动主动读取。
| 范围 | 大小 | 用途 |
|---|---|---|
| 0x00–0x3F | 64字节 | 配置空间头(Type 0 / Type 1) |
| 0x40–0xFF | 192字节 | PCI兼容Capability结构 |
| 0x100–0xFFF | 3.75KB | PCIe扩展Capability空间 |
我在项目中遇到过一个坑:某个FPGA的PCIe核,扩展空间里定义的ACS (Access Control Services) Capability偏移写错了,导致Linux内核枚举时直接宕机。查了三天才发现是厂商的RTL bug。所以,一定要用lspci -vvv仔细看每个Capability的偏移和长度。
2.2 Vendor ID / Device ID:设备唯一的"指纹"
这两个寄存器位于配置空间头的0x00–0x03字节。Vendor ID由PCI-SIG分配(比如Intel是0x8086,AMD是0x1022),Device ID由厂商自己定义。它们决定了系统用什么驱动来绑定设备。
一次面试常见的问题:"如果两块卡Vendor ID相同但Device ID不同,驱动怎么区分?"答案很简单:通过PCIe的Class Code和Subsystem ID做二次匹配。我建议在驱动里写一个匹配表:
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x7011), .driver_data = DMA_CH_A },
{ PCI_DEVICE(0x10EE, 0x7012), .driver_data = DMA_CH_B },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
注意:Vendor ID 0xFFFF是无效值,配置空间没读回来时会出现。我刚开始调试时常看到这个值,第一反应是"设备死了"——后来发现只是PCIe链路没训练好,复位一下就好。
2.3 BAR空间配置:基地址寄存器的奥秘
BAR (Base Address Register) 是PCIe设备向系统申请内存/IO地址空间的"申请书"。每个Endpoint最多支持6个BAR(Type 0头),每个BAR可以是32位或64位,可以映射到Memory空间或IO空间。
你想想看:系统怎么知道你的BAR需要多大空间?答案是——写全1再读回。这是枚举时的经典操作:先往BAR写0xFFFFFFFF,再读回去,低位置的"1"个数就指明了空间大小。比如你读回来是0xFFFF0000,说明低16位是"1",需要1MB空间。
BAR空间大小判断公式:
size = ~(bar_value & ~0xF) + 1
注意:IO空间的BAR低2位是01,Memory空间的BAR低4位要看类型(0=32位,2=64位)。
我个人的习惯:在驱动probe函数里用pci_resource_start()和pci_resource_len()获取分配好的物理地址和长度,千万别自己去读BAR寄存器算——Linux内核已经帮你做完了。
2.4 配置空间枚举流程
整个流程其实不复杂。系统上电后,Root Complex先发送Configuration Read请求,扫描Bus 0上的设备。发现设备后,读Vendor ID,如果不是0xFFFF,就给设备分配Bus Number、分配BAR空间,然后递归扫描下级总线。
我曾经在调试一个多层级PCIe Switch时,发现某个下游设备的BAR空间和上游Switch的桥窗口重叠了。那次排查用到了lspci -t看拓扑,再用setpci手动读写配置空间验证。嗯,硬件手册里写的"Bridge Memory Base/Limit"寄存器真不能忽略。
2.5 类型0头 vs 类型1头
Endpoint用Type 0配置头,Switch/Root Port用Type 1头。两者的区别主要在BAR数量和桥窗口寄存器。Type 0最多6个BAR,Type 1只有0和1两个BAR,但多了Memory Base/Limit、IO Base/Limit用于地址转发。
我回忆:有一次在调试一个multi-function设备时,Function 0是Type 0,Function 1是Type 1的桥?不可能! 一个物理设备的所有Function必须同类型。别被绕晕。
| 字段 | Type 0 (Endpoint) | Type 1 (Bridge) |
|---|---|---|
| BAR数量 | 6个 | 2个 |
| Subordinate Bus Number | 无 | 有 |
| Memory Base/Limit | 无 | 有 |
| Expansion ROM BAR | 有 | 有 |
重要警告:配置空间头的前16字节必须符合标准。如果你在BAR里写入了非对齐的地址(比如给32位BAR写了0x1_0000_0000),硬件会直接忽略低32位以外的位,导致地址映射错误。我曾经见过一个驱动把64位BAR的低32位写对了,高32位没写,结果访问时直接产生Unsupported Request。
2.6 实战检查清单
面试时如果让我现场查配置空间,我一般按这个顺序来:
- 读Vendor ID + Device ID → 确认设备存在,没有PCIe链路异常
- 读Class Code → 确认设备类型(比如DMA控制器一般是0x0880)
- 读Capabilities Pointer → 找到第一个Capability(通常是Power Management)
- 读BAR0-BAR5 → 确认系统分配了合理地址,并且地址范围与期待一致
- 读MSI/MSI-X Capability → 确认中断配置正常
我个人习惯用setpci写一个shell脚本来批量dump:
#!/bin/bash
BUS="01:00.0"
echo "=== Config Space Dump ==="
setpci -s $BUS 0x00.l # Vendor/Device
setpci -s $BUS 0x08.l # Class/Revision
setpci -s $BUS 0x10.l # BAR0
setpci -s $BUS 0x14.l # BAR1
setpci -s $BUS 0x34.b # Capabilities Pointer
嗯,最后再啰嗦一句:配置空间的所有读写都是通过PCIe的Config TLP完成的,驱动里调用的pci_read_config_*函数最终会走到硬件层的I/O端口或MMIO方式。理解了这个层级,你才能自己写一个裸机下的配置空间访问代码。