第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表示从头开始枚举,传上一个设备句柄则继续枚举。我第一次写的时候传了个野指针,结果系统直接挂了。

避坑指南:我曾经在读取配置空间时,忘记检查返回值。有些设备在特定状态下会返回0xFF,如果你不校验,后面BAR映射时会一脸懵逼。

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的大小。我刚开始觉得这方法很奇葩,后来发现其实挺巧妙的——硬件设计者用这种方式,让软件能动态探测地址空间。

经验之谈:我在项目中遇到过64位BAR的设备。如果你只读了BAR0,发现值是0xFFFFFFFF,别慌——这可能是64位BAR的高32位在BAR1里。记得检查BAR0的bit 2-1是否为0x04。

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空间。

重要提醒:我曾经在QNX 6.5上遇到一个bug:pci_read_config32()在读取某些能力寄存器时,会返回错误的值。后来发现是QNX的PCI驱动有缓存问题。解决办法是:每次读取前,先调用pci_attach_device()刷新缓存。

4.5 总结一下

PCIe设备枚举,说白了就是三步:读配置空间、映射BAR、拿中断号。每一步都有坑,但只要你按照规范来,基本不会出大问题。

我个人建议:在写驱动前,先用pci -v命令看看设备信息,确认BAR地址和中断号是否正确。这能帮你省下不少调试时间。

下一章,咱们聊聊DMA和描述符环。那才是网络驱动性能的关键所在。