4. PCI设备探测与初始化:probe函数详解、资源申请与Master使能

PCI设备驱动的核心入口是probe回调函数。当PCI核心枚举总线、发现设备并与驱动ID表匹配成功后,便会调用驱动的probe函数。本节将深入剖析probe函数的典型实现模式,涵盖资源申请(IO端口、内存映射、中断)、pci_enable_device与pci_set_master的调用时机与作用,以及常见初始化陷阱。

4.1 probe函数的标准骨架

一个典型的PCI驱动probe函数遵循“启用设备 → 获取资源 → 注册子系统”的三段式结构。下面给出一个标准实现模板:

static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_dev *dev;
    int ret;

    // 1. 分配私有数据结构
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;
    pci_set_drvdata(pdev, dev);

    // 2. 启用PCI设备
    ret = pci_enable_device(pdev);
    if (ret)
        goto err_free;

    // 3. 请求MMIO区域
    ret = pcim_iomap_regions(pdev, BIT(0), "my_driver");
    if (ret)
        goto err_disable;
    dev->regs = pcim_iomap_table(pdev)[0];

    // 4. 设置DMA掩码
    ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
    if (ret) {
        ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
        if (ret)
            goto err_disable;
    }

    // 5. 设置总线主控
    pci_set_master(pdev);

    // 6. 申请中断
    ret = request_irq(pdev->irq, my_isr, IRQF_SHARED, "my_driver", dev);
    if (ret)
        goto err_disable;

    // 7. 注册设备到内核框架(如misc、net、char等)
    // ...

    dev_info(&pdev->dev, "device probed successfully\n");
    return 0;

err_disable:
    pci_disable_device(pdev);
err_free:
    kfree(dev);
    return ret;
}
关键点:probe函数必须保证资源申请顺序与错误回滚路径一一对应。推荐使用pcim_*系列托管API,可自动释放资源,减少内存泄漏风险。

4.2 资源申请:IO端口、MMIO与中断

PCI设备通过基地址寄存器(BAR)暴露资源。每个BAR可映射为IO端口空间或内存映射IO(MMIO)空间。驱动必须正确识别BAR类型并申请对应资源。

资源类型 BAR标志位 申请API 访问方式
IO端口 bit 0 = 1 pci_request_region(pdev, bar, name) inb/outb 系列
MMIO 32-bit bit 0 = 0, bit 2:1 = 00 pcim_iomap_regions(pdev, mask, name) ioread32/iowrite32
MMIO 64-bit bit 0 = 0, bit 2:1 = 10 pcim_iomap_regions(pdev, mask, name) ioread64/iowrite64
最佳实践:优先使用pcim_iomap_regions代替手动pci_request_region + ioremap组合。托管API在驱动卸载或probe失败时自动释放映射,显著降低错误处理复杂度。

4.2.1 中断资源申请

PCI中断分为INTx(传统中断线)和MSI/MSI-X(消息 signaled 中断)。现代驱动应优先使用MSI-X,以获得更好的中断亲和性和性能。

// MSI-X 申请示例
int nr_vectors = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX);
if (nr_vectors < 0) {
    // 回退到MSI
    nr_vectors = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
}
if (nr_vectors < 0) {
    // 回退到INTx
    nr_vectors = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_INTX);
}

for (i = 0; i < nr_vectors; i++) {
    ret = request_irq(pci_irq_vector(pdev, i), my_isr, 0,
                      "my_driver", &dev->irq_ctx[i]);
}
注意:使用MSI/MSI-X时,request_irq的flags参数必须为0(不能加IRQF_SHARED)。INTx中断必须设置IRQF_SHARED,因为INTx线可能被多个设备共享。

4.3 pci_enable_device 与 pci_set_master

这两个函数是PCI初始化的基石,但它们的职责和调用时机常被混淆。下面通过对比表格厘清差异:

函数 作用 调用时机 副作用
pci_enable_device 唤醒设备、分配IO/内存资源、设置电源状态D0 probe最开头,资源申请之前 增加设备引用计数;必须配对pci_disable_device
pci_set_master 设置PCI命令寄存器中的Bus Master位,允许设备发起DMA 资源映射完成、DMA掩码设置之后 设备可主动读写主存;必须配对pci_clear_master

4.3.1 pci_enable_device 的深层行为

调用pci_enable_device时,PCI核心执行以下操作:

  1. 检查设备是否已被启用(引用计数防止重复调用)。
  2. 将设备电源状态从D3hot恢复至D0(如果之前被挂起)。
  3. 分配所有BAR对应的IO/内存地址空间(由BIOS或firmware分配)。
  4. 设置PCI命令寄存器:启用IO空间(bit 0)和内存空间(bit 1),但不设置Bus Master位。
常见错误:在pci_enable_device之前调用pci_request_region会导致资源冲突,因为设备尚未被激活,BAR内容可能无效。正确的顺序是:pci_enable_device → pci_request_region → pcim_iomap。

4.3.2 pci_set_master 与 DMA 的关系

pci_set_master设置命令寄存器的Bus Master Enable (BME)位(bit 2)。只有设置此位后,设备才能作为总线主控发起DMA传输。未设置BME时,设备只能作为目标响应CPU的MMIO访问。

// pci_set_master 的内核实现(简化)
void pci_set_master(struct pci_dev *dev)
{
    u16 cmd;
    pci_read_config_word(dev, PCI_COMMAND, &cmd);
    cmd |= PCI_COMMAND_MASTER;   // 设置 bit 2
    pci_write_config_word(dev, PCI_COMMAND, cmd);
    dev->is_busmaster = 1;
}
调试技巧:如果设备无法发起DMA,首先检查/sys/bus/pci/devices/<BDF>/config中命令寄存器的bit 2是否为1。也可使用lspci -vvv查看“Capabilities: [xxx] Power Management version 3”之后的“Status:”行确认Master是否使能。

4.4 PCI初始化流程SVG架构图

下图展示了从probe入口到设备完全初始化的完整流程,包含资源申请、中断注册和Master使能的时序关系:

PCI设备探测与初始化流程 PCI设备探测与初始化流程 probe() 入口 pci_enable_device() 唤醒设备 → 分配BAR资源 → 设置D0 资源申请 pci_request_region() / pcim_iomap_regions() dma_set_mask() 尝试64-bit → 回退32-bit pci_set_master() 设置Bus Master位 → 允许DMA 中断申请 pci_alloc_irq_vectors() + request_irq() 注册子系统 misc_register / netdev / cdev probe 成功 初始化顺序约束 ① pci_enable_device 必须最先调用 ② 资源申请在设备启用之后 ③ pci_set_master 在DMA掩码之后 错误回滚路径 每个步骤失败时 回滚已申请的资源

4.5 常见初始化陷阱与排查方法

  • 陷阱1:忘记调用pci_enable_device — 直接访问BAR返回全F(0xFFFFFFFF),因为设备未激活。排查:检查/sys/bus/pci/devices/<BDF>/enable是否为1。
  • 陷阱2:pci_set_master调用过早 — 在DMA掩码设置之前使能Master,可能导致设备使用错误的地址宽度。排查:确认dma_set_mask返回0后再调用pci_set_master。
  • 陷阱3:中断申请时flags错误 — INTx使用IRQF_SHARED,MSI/MSI-X不能使用。排查:cat /proc/interrupts查看中断触发类型。
  • 陷阱4:资源泄漏 — probe失败时未释放已申请的BAR或中断。排查:使用pcim_*托管API自动管理生命周期。
内核调试利器:在probe函数中添加dev_dbg(&pdev->dev, "bar0 start=%pap len=%d\n", &pdev->resource[0].start, pci_resource_len(pdev, 0));,配合dynamic_debug或ignore_loglevel启动参数,可实时观察资源分配情况。

4.6 资源申请与释放对照表

为确保初始化与清理的对称性,下表列出每项资源的申请与释放API配对:

资源 申请API 释放API 托管替代
设备使能 pci_enable_device pci_disable_device —
IO/内存区域 pci_request_region pci_release_region pcim_iomap_regions
MMIO映射 ioremap iounmap pcim_iomap
中断向量 pci_alloc_irq_vectors pci_free_irq_vectors —
中断处理 request_irq free_irq devm_request_irq
总线主控 pci_set_master pci_clear_master —
总结:一个健壮的PCI probe函数应遵循“启用 → 资源 → 掩码 → Master → 中断 → 注册”的固定顺序,并确保每个步骤都有对应的错误回滚路径。使用托管API(pcim_*、devm_*)可大幅简化清理逻辑,是生产级驱动的推荐做法。