第4章:PCIe设备枚举:PCI配置空间访问、BAR空间映射、中断号获取
好,咱们今天聊聊PCIe设备枚举。这玩意儿,说白了就是让系统知道“你插了个啥设备”。在QNX下做网络驱动,这一步绕不开。我个人习惯是把枚举看作“设备的三步认证”——先读身份证(配置空间),再分配地址(BAR映射),最后绑定通知方式(中断号)。
4.1 PCI配置空间访问:设备的“身份证”
每个PCIe设备都有256字节的配置空间。前64字节是标准头,后面是设备相关的能力结构。我刚开始做驱动时,总觉得直接读配置空间就行,结果发现QNX的访问方式跟Linux不太一样。
在QNX下,我们通过pci_attach_device()来获取设备句柄。嗯,这里要注意:这个函数会返回一个pci_dev_t类型的句柄,后续所有操作都靠它。
#include <hw/pci.h>
pci_dev_t dev;
uint16_t vendor_id, device_id;
// 枚举所有PCI设备
dev = pci_attach_device(0, PCI_SHARE, 0, NULL, NULL);
while (dev != NULL) {
// 读取Vendor ID和Device ID
pci_read_config16(dev, PCI_CFG_VENDOR_ID, &vendor_id);
pci_read_config16(dev, PCI_CFG_DEVICE_ID, &device_id);
// 匹配我们的网卡
if (vendor_id == 0x8086 && device_id == 0x1539) {
// 找到目标设备
break;
}
dev = pci_attach_device(0, PCI_SHARE, 0, NULL, dev);
}
我曾经踩过一个坑:pci_attach_device()的最后一个参数,传NULL表示从头开始枚举,传上一个设备句柄则继续枚举。我第一次写的时候传了个野指针,结果系统直接挂了。
4.2 BAR空间映射:给设备分配“门牌号”
BAR(Base Address Register)是设备与系统通信的窗口。每个PCIe设备最多有6个BAR,但网卡通常只用2-3个。我个人习惯先读BAR的原始值,再判断它是内存空间还是I/O空间。
为什么需要判断?因为内存空间和I/O空间的访问方式完全不同。内存空间可以用mmap()直接映射,I/O空间则要用pci_io_*系列函数。
uint32_t bar0_raw;
uint64_t bar0_addr;
uint32_t bar0_size;
// 读取BAR0原始值
pci_read_config32(dev, PCI_CFG_BAR0, &bar0_raw);
// 判断类型
if (bar0_raw & 0x01) {
// I/O空间
bar0_addr = bar0_raw & ~0x03;
} else {
// 内存空间
if ((bar0_raw & 0x06) == 0x04) {
// 64位BAR,需要读BAR1
uint32_t bar1_raw;
pci_read_config32(dev, PCI_CFG_BAR1, &bar1_raw);
bar0_addr = ((uint64_t)bar1_raw << 32) | (bar0_raw & ~0x0F);
} else {
// 32位BAR
bar0_addr = bar0_raw & ~0x0F;
}
}
// 获取BAR大小(通过写全1再读回)
pci_write_config32(dev, PCI_CFG_BAR0, 0xFFFFFFFF);
pci_read_config32(dev, PCI_CFG_BAR0, &bar0_size);
bar0_size = ~(bar0_size & ~0x0F) + 1;
// 恢复原始值
pci_write_config32(dev, PCI_CFG_BAR0, bar0_raw);
你想想看,为什么BAR大小要这么算?因为PCIe规范规定:写全1到BAR,读回来的值取反加1,就是BAR的大小。我刚开始觉得这方法很奇葩,后来发现其实挺巧妙的——硬件设计者用这种方式,让软件能动态探测地址空间。
4.3 中断号获取:设备怎么“喊”你
PCIe设备支持三种中断方式:INTx(传统中断)、MSI(消息信号中断)、MSI-X(扩展消息信号中断)。在QNX下,我建议优先使用MSI-X,因为它支持每个队列独立中断,对多核系统特别友好。
获取中断号的核心是读取配置空间中的Interrupt Line字段。但要注意:这个字段在设备枚举时可能还没被系统分配,所以最好在驱动初始化时再读。
uint8_t irq_line;
uint16_t irq_pin;
// 读取中断线
pci_read_config8(dev, PCI_CFG_INTERRUPT_LINE, &irq_line);
pci_read_config8(dev, PCI_CFG_INTERRUPT_PIN, &irq_pin);
// 检查是否支持MSI
uint16_t status;
pci_read_config16(dev, PCI_CFG_STATUS, &status);
if (status & PCI_STATUS_CAPABILITIES) {
uint8_t cap_ptr;
pci_read_config8(dev, PCI_CFG_CAP_PTR, &cap_ptr);
// 遍历能力链表,找到MSI能力
while (cap_ptr != 0) {
uint8_t cap_id;
pci_read_config8(dev, cap_ptr, &cap_id);
if (cap_id == PCI_CAP_ID_MSI) {
// 配置MSI
uint16_t msi_ctrl;
pci_read_config16(dev, cap_ptr + 2, &msi_ctrl);
msi_ctrl |= 0x01; // 使能MSI
pci_write_config16(dev, cap_ptr + 2, msi_ctrl);
break;
}
pci_read_config8(dev, cap_ptr + 1, &cap_ptr);
}
}
嗯,这里有个细节:MSI的配置需要操作系统支持。在QNX下,你还需要调用InterruptAttach()或InterruptAttachEvent()来绑定中断处理函数。我个人习惯用InterruptAttachEvent(),因为它支持事件驱动,性能更好。
- 配置空间访问:用
pci_read_config*系列函数,注意字节对齐 - BAR映射:先判断类型(内存/I/O),再计算大小,最后用
mmap()或pci_io_*映射 - 中断获取:优先MSI-X,其次MSI,最后INTx。中断号在
Interrupt Line字段
4.4 实战中的那些坑
说几个我实际项目中遇到的坑,你以后碰到了能少走弯路。
第一个坑:BAR映射失败。 有一次我映射BAR0,mmap()总是返回MAP_FAILED。查了半天,发现是设备还没使能。记住:在映射BAR之前,一定要设置配置空间的Command寄存器,使能内存空间和I/O空间。
uint16_t cmd;
pci_read_config16(dev, PCI_CFG_COMMAND, &cmd);
cmd |= PCI_COMMAND_MEM_ENABLE | PCI_COMMAND_IO_ENABLE;
pci_write_config16(dev, PCI_CFG_COMMAND, cmd);
第二个坑:中断号冲突。 我曾经在一个多网卡系统上,两个设备的中断号居然一样。后来发现是BIOS没分配好。解决办法是:在驱动里不要硬编码中断号,而是动态读取Interrupt Line,并且支持中断共享。
第三个坑:64位地址的BAR。 有些高端网卡使用64位BAR,地址空间在4GB以上。如果你的系统是32位的,mmap()可能映射不了。解决办法是:检查系统是否支持PAE(物理地址扩展),或者干脆用I/O空间。
pci_read_config32()在读取某些能力寄存器时,会返回错误的值。后来发现是QNX的PCI驱动有缓存问题。解决办法是:每次读取前,先调用pci_attach_device()刷新缓存。
4.5 总结一下
PCIe设备枚举,说白了就是三步:读配置空间、映射BAR、拿中断号。每一步都有坑,但只要你按照规范来,基本不会出大问题。
我个人建议:在写驱动前,先用pci -v命令看看设备信息,确认BAR地址和中断号是否正确。这能帮你省下不少调试时间。
下一章,咱们聊聊DMA和描述符环。那才是网络驱动性能的关键所在。