3. PCIe枚举过程:总线号分配、设备扫描、资源分配
好,咱们进入正题。PCIe枚举,说白了就是系统启动时,软件去"认识"整条PCIe树上的所有设备。这个过程分三步走:先分配总线号,再扫描设备,最后给设备分配资源。我做了这么多年驱动,踩过的坑大多都跟枚举顺序有关。
3.1 总线号分配——树怎么长出来的
PCIe拓扑是个树形结构。根节点是Root Complex,下面挂Switch,Switch再挂Endpoint。每个PCIe总线都有一个唯一的总线号,范围0-255。
枚举从总线0开始。Root Complex默认挂在总线0上。然后软件递归地往下走:
- 读设备的Type 1配置空间,检查是否有下游总线
- 如果有,分配一个新的总线号给下游
- 继续扫描下游设备,直到没有更多桥接器
核心规则:每个PCIe-to-PCIe桥(Switch内部)都有三个总线号寄存器:Primary Bus Number、Secondary Bus Number、Subordinate Bus Number。Secondary就是下游总线号,Subordinate是下游子树的最大总线号。
我遇到过一个问题:某款Switch芯片的Subordinate Bus Number默认是0,导致枚举时下游设备全被跳过。后来发现是固件没初始化好。嗯,这种问题排查起来特别费劲。
3.2 设备扫描——谁在树上
总线号分配完了,接下来扫描每个总线上的设备。每个PCIe总线最多挂32个设备(多功能设备算多个Function)。
扫描流程其实很简单:
- 遍历Bus号0-255
- 对每个Bus,遍历Device号0-31
- 对每个Device,遍历Function号0-7
- 读Vendor ID和Device ID,如果全F则说明没设备
// 伪代码:PCIe设备扫描
for (bus = 0; bus <= 255; bus++) {
for (dev = 0; dev < 32; dev++) {
for (func = 0; func < 8; func++) {
vendor = pci_config_read16(bus, dev, func, 0x00);
if (vendor != 0xFFFF) {
// 发现设备,记录信息
add_device_to_list(bus, dev, func);
// 如果是桥接器,递归扫描下游
if (is_pci_bridge(bus, dev, func)) {
scan_downstream_bus(bus, dev, func);
}
}
}
}
}
你想想看,为什么扫描顺序是Bus→Device→Function?因为PCIe配置空间访问机制就是按这个层次寻址的。我刚开始学的时候,总以为先扫Function再扫Device,结果写出来的代码怎么都读不到设备。
小技巧:实际扫描时,如果某个Device的Function 0不存在,那这个Device的其他Function也不用扫了。这是PCIe规范规定的——Function 0必须存在,除非是单Function设备。
3.3 资源分配——给设备分地盘
设备找到了,接下来要分配资源。PCIe设备需要三种资源:MMIO空间、IO空间、中断。其中IO空间在新设备上基本不用了,但老设备还有。
3.3.1 MMIO空间分配
每个PCIe设备都有Base Address Registers(BAR),用来声明自己需要多大的MMIO空间。软件读BAR寄存器,就知道设备要多大内存。
分配策略是这样的:
- 先收集所有设备的BAR大小
- 按地址对齐要求排序(64位BAR要对齐到4KB)
- 从高地址往低地址分配
- 把分配好的基地址写回BAR寄存器
我曾经在一个项目里,NVMe SSD的BAR0声明了8MB空间,但系统只分配了4MB。结果驱动一访问就死机。查了半天,发现是BIOS的MMIO分配算法有bug,把64位BAR当32位处理了。
注意:64位BAR需要两个连续的32位寄存器。分配时一定要保证地址在64位地址空间内,并且对齐到4KB边界。很多SoC的PCIe控制器不支持64位地址,这时候只能把设备映射到32位地址空间。
3.3.2 IO空间分配
IO空间现在基本是历史遗留。x86架构还有IO空间,但ARM、RISC-V基本不用。NVMe设备一般不需要IO空间,但有些老设备会要求。
分配方法和MMIO类似,只是地址范围小得多(0-64KB)。我建议新设计直接禁用IO空间,省得麻烦。
3.3.3 中断分配
PCIe中断有三种:INTx(传统中断)、MSI(消息信号中断)、MSI-X(扩展MSI)。NVMe设备强烈推荐用MSI-X。
分配流程:
- 设备报告支持多少MSI-X向量
- 系统分配中断号(IRQ)给每个向量
- 把中断号写入设备的MSI-X表
- 配置中断控制器(如GIC、IOAPIC)
| 中断类型 | 最大向量数 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| INTx | 4 | 老设备、兼容模式 | 共享中断导致性能抖动 |
| MSI | 32 | 一般设备 | 32个向量不够NVMe用 |
| MSI-X | 2048 | NVMe、高性能网卡 | 表空间分配不足 |
我记得有一次,NVMe SSD支持64个MSI-X向量,但系统只分配了8个。原因是中断控制器驱动里写死了最大向量数。改一下设备树就解决了。这种问题说白了就是软硬件配置不匹配。
3.4 枚举完成后的验证
资源分配完,不代表万事大吉。我习惯做三件事验证:
- 读每个设备的BAR寄存器,确认写入的地址正确
- 做一次简单的MMIO读写测试(比如读Vendor ID)
- 检查中断是否正常触发(写一个测试中断处理函数)
经验之谈:枚举过程中最容易出问题的是总线号分配。如果两个桥接器分配了相同的Secondary Bus Number,下游设备就全冲突了。我建议在枚举时加一个总线号使用位图,每分配一个就标记一下,防止重复。
好了,PCIe枚举的核心内容就这些。说白了就是三步:分总线号、扫设备、分资源。每一步都有坑,但掌握了原理,排查起来就有方向。下一章咱们讲NVMe设备怎么在枚举完成后初始化,那才是真正开始干活的时候。