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核心执行以下操作:
- 检查设备是否已被启用(引用计数防止重复调用)。
- 将设备电源状态从D3hot恢复至D0(如果之前被挂起)。
- 分配所有BAR对应的IO/内存地址空间(由BIOS或firmware分配)。
- 设置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使能的时序关系:
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_*)可大幅简化清理逻辑,是生产级驱动的推荐做法。