4. PCIe WiFi设备枚举:PCI子系统基础、设备探测流程、BAR空间映射与中断注册
好,我们进入第四章。这一章讲的是WiFi驱动和硬件打交道的第一个关键环节——设备枚举。说白了,就是操作系统怎么发现你插上去的那张WiFi网卡,然后怎么把它的资源(内存地址、中断号)交到驱动手里。
很多刚入行的朋友觉得这步很简单,不就是pci_register_driver吗?其实不然。我见过太多因为BAR空间没配好、中断触发方式选错,导致WiFi吞吐死活上不去的案例。这一章,我们就把它彻底讲透。
4.1 PCI子系统基础:设备是怎么被“看见”的
先问一个问题:你的WiFi网卡插在PCIe槽上,CPU怎么知道它在哪里?
答案是通过PCI配置空间。每个PCIe设备(包括WiFi网卡)都有一组256字节的配置寄存器,里面包含了厂商ID、设备ID、类代码、BAR(基地址寄存器)等信息。Linux内核在启动时,会通过PCI总线扫描,读取这些寄存器,建立起一颗完整的PCI设备树。
核心数据结构:struct pci_dev
内核用struct pci_dev来描述一个PCI设备。驱动开发中,你打交道最多的就是它。
struct pci_dev {
u32 vendor; // 厂商ID,比如0x8086是Intel
u32 device; // 设备ID,WiFi芯片的具体型号
u32 class; // 类代码,网卡通常是0x028000
struct pci_bus *bus; // 所属总线
unsigned int irq; // 中断号
struct resource resource[DEVICE_COUNT_RESOURCE]; // BAR空间资源
// ... 还有很多
};
我个人习惯在驱动入口处先打印一下这些信息,确认设备确实被枚举到了:
static int my_wifi_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
dev_info(&pdev->dev, "Found WiFi device: vendor=0x%04x device=0x%04x\n",
pdev->vendor, pdev->device);
dev_info(&pdev->dev, "IRQ=%d, class=0x%06x\n", pdev->irq, pdev->class);
// ...
}
嗯,这里要注意:class字段是24位的,打印时记得用%06x,不然高位会被截掉。我刚开始写驱动时就犯过这个错,查了半天为什么设备没匹配上。
4.2 设备探测流程:从pci_register_driver到probe
设备探测的流程,可以用下面这张图来概括:
流程看起来不复杂,但每一步都有坑。我们逐个拆解。
4.2.1 pci_device_id:你的设备匹配表
驱动里必须定义一个pci_device_id数组,告诉内核你支持哪些设备:
static const struct pci_device_id my_wifi_ids[] = {
{ PCI_DEVICE(0x8086, 0x24F3) }, // Intel AX200
{ PCI_DEVICE(0x14C3, 0x7961) }, // MediaTek MT7921
{ 0, } // 结束标志
};
MODULE_DEVICE_TABLE(pci, my_wifi_ids);
这里有个细节:MODULE_DEVICE_TABLE宏会把这张表导出到模块的.modinfo段里。当内核发现新设备时,会遍历所有已注册驱动的这张表,找到匹配的就调用probe。
小技巧:调试阶段可以用modinfo命令查看模块支持的设备列表。如果发现设备ID没在列表里,先检查MODULE_DEVICE_TABLE有没有写对。
4.2.2 probe函数的标准骨架
一个合格的WiFi驱动probe函数,至少要做这几件事:
- 使能PCI设备——调用
pci_enable_device() - 设置DMA掩码——
dma_set_mask_and_coherent() - 请求BAR资源——
pci_request_regions() - 映射BAR空间——
pcim_iomap()或ioremap() - 读取MAC地址——从EEPROM或OTP中读取
- 注册中断——
request_irq()或request_threaded_irq() - 初始化WiFi核心——调用
ieee80211_alloc_hw()等
我曾经接手过一个项目,probe函数里把pci_enable_device放在了pci_request_regions后面。结果呢?资源冲突,设备死活起不来。查了两天才发现是顺序问题。所以,顺序很重要。
4.3 BAR空间映射:驱动和硬件的“通信管道”
BAR(Base Address Register)是PCI设备暴露给CPU的寄存器或内存区域。WiFi网卡通常有2~3个BAR:
| BAR编号 | 典型用途 | 映射类型 |
|---|---|---|
| BAR0 | MAC寄存器、控制状态寄存器 | Memory-Mapped I/O (MMIO) |
| BAR1 | DMA描述符环、数据缓冲区 | Memory (可预取) |
| BAR2 | 扩展配置空间(部分芯片) | MMIO |
映射BAR空间的典型代码:
// 先使能设备
ret = pci_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "Cannot enable PCI device\n");
return ret;
}
// 设置DMA掩码,WiFi设备通常需要64位
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(&pdev->dev, "No suitable DMA mask\n");
goto err_disable;
}
}
// 请求BAR0和BAR1的资源
ret = pci_request_regions(pdev, "my_wifi");
if (ret) {
dev_err(&pdev->dev, "Cannot request PCI regions\n");
goto err_disable;
}
// 映射BAR0到CPU虚拟地址空间
void __iomem *bar0 = pcim_iomap(pdev, 0, pci_resource_len(pdev, 0));
if (!bar0) {
dev_err(&pdev->dev, "Cannot iomap BAR0\n");
goto err_release;
}
// 映射BAR1
void __iomem *bar1 = pcim_iomap(pdev, 1, pci_resource_len(pdev, 1));
if (!bar1) {
dev_err(&pdev->dev, "Cannot iomap BAR1\n");
goto err_release;
}
注意:映射完成后,读写BAR空间必须使用ioread32()/iowrite32()等访问函数。不要直接解引用__iomem指针,否则在ARM等非x86架构上会触发段错误。
我个人习惯在probe里打印BAR的物理地址和大小,方便调试:
for (i = 0; i < 6; i++) {
if (pci_resource_len(pdev, i) == 0)
continue;
dev_info(&pdev->dev, "BAR%d: phys=%pap size=%#llx\n",
i, &pci_resource_start(pdev, i),
(u64)pci_resource_len(pdev, i));
}
4.4 中断注册:让硬件能“喊”你
WiFi设备的中断处理,是驱动性能的关键。PCIe设备通常支持MSI-X(Message Signaled Interrupts eXtended),比传统的INTx中断效率高得多。
4.4.1 中断方式对比
| 中断方式 | 特点 | WiFi驱动推荐 |
|---|---|---|
| INTx (传统引脚中断) | 共享中断线,容易冲突,性能差 | 不推荐 |
| MSI | 消息中断,最多32个向量 | 可用,但队列多时不够用 |
| MSI-X | 每个队列独立中断,支持亲和性 | 强烈推荐 |
注册中断的代码示例:
// 先尝试MSI-X,不行再回退到MSI
int nvec = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX | PCI_IRQ_MSI);
if (nvec < 0) {
dev_err(&pdev->dev, "Failed to allocate MSI/MSI-X vectors\n");
goto err_release;
}
// 为每个队列注册中断处理函数
for (i = 0; i < nvec; i++) {
int irq = pci_irq_vector(pdev, i);
ret = request_irq(irq, my_wifi_isr, 0, "my_wifi", &priv->queue[i]);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
goto err_free_irq;
}
}
避坑指南:我曾经在调试一款WiFi6网卡时,发现中断处理函数里读寄存器总是返回0。查了半天,原来是pci_enable_device之后没有调用pci_set_master。没有总线主控能力,设备根本发不了中断。加上这一行就解决了。
4.4.2 中断处理函数的设计原则
WiFi设备的中断处理函数,要尽量短。为什么?因为中断上下文不能睡眠,而且长时间关中断会导致其他设备饿死。
我一般这样设计:
- 上半部(ISR):只做最紧急的事——读中断状态寄存器,清除中断标志,然后唤醒下半部。
- 下半部(Tasklet/Workqueue):处理数据包接收、发送完成、链路状态变化等耗时操作。
static irqreturn_t my_wifi_isr(int irq, void *data)
{
struct wifi_queue *q = data;
u32 cause;
// 读中断原因寄存器
cause = ioread32(q->bar0 + INT_CAUSE_REG);
if (!cause)
return IRQ_NONE; // 不是我们的中断
// 清除中断标志
iowrite32(cause, q->bar0 + INT_CAUSE_REG);
// 调度下半部
tasklet_schedule(&q->tx_tasklet);
return IRQ_HANDLED;
}
嗯,这里有个细节:ioread32和iowrite32是带内存屏障的,能保证读写顺序。如果你用readl/writel,效果一样,但ioread32更符合PCI驱动惯例。
4.5 本章小结
PCIe WiFi设备的枚举,说白了就是三步:
- 匹配——通过
pci_device_id让内核找到你的驱动 - 资源获取——使能设备、映射BAR空间、设置DMA
- 中断注册——让设备能通知CPU有事情要处理
每一步都有对应的内核API,顺序不能乱,参数不能错。我见过太多驱动死在probe阶段,原因无非就是这几个:忘了pci_set_master、BAR映射长度写错、中断向量数分配不够。