4. PCIe驱动框架:pci_driver结构体、probe/remove回调、pci_device_id匹配表
PCIe设备驱动的核心骨架由Linux内核提供的pci_driver结构体定义。理解这个结构体及其回调函数,是编写任何PCIe端点驱动的基础。本章将深入剖析驱动注册、设备匹配、初始化和卸载的完整生命周期。
4.1 pci_driver结构体:驱动的“身份证”
在Linux内核中,每个PCIe驱动都必须实例化一个struct pci_driver,并将其注册到PCI核心子系统。该结构体是驱动与内核PCI总线层之间的契约。
| 成员 | 类型 | 说明 |
|---|---|---|
| name | const char * |
驱动名称,用于sysfs和调试 |
| id_table | const struct pci_device_id * |
设备匹配表,驱动支持的设备列表 |
| probe | int (*probe)(struct pci_dev *, const struct pci_device_id *) |
设备发现时的初始化回调 |
| remove | void (*remove)(struct pci_dev *) |
设备移除或驱动卸载时的清理回调 |
| suspend / resume | 电源管理回调 | 系统休眠/唤醒时的处理 |
| shutdown | void (*shutdown)(struct pci_dev *) |
系统关机时的处理 |
| driver | struct device_driver |
内嵌的通用设备驱动结构体 |
关键点: probe和remove是驱动开发中最常实现的两个回调。probe负责硬件初始化、资源分配、DMA和中断设置;remove负责反向释放所有资源。
4.2 pci_device_id匹配表:驱动与设备的“握手协议”
当PCIe设备插入系统或驱动加载时,PCI核心会遍历所有已注册驱动的id_table,与设备的Vendor ID、Device ID、Subsystem ID等进行匹配。匹配成功则调用驱动的probe函数。
/* 典型的pci_device_id匹配表 */
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x7011) }, /* 仅匹配VID/PID */
{ PCI_DEVICE_SUB(0x10EE, 0x7012, 0x10EE, 0x0001) }, /* 匹配VID/PID/SubVID/SubPID */
{ PCI_DEVICE_CLASS(0x020000, 0xFFFFFF) }, /* 匹配网络设备类 */
{ 0, } /* 结束标志 */
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
| 宏 | 匹配字段 | 适用场景 |
|---|---|---|
PCI_DEVICE(vid, pid) |
Vendor ID + Device ID | 最常用,精确匹配单设备 |
PCI_DEVICE_SUB(vid, pid, svid, spid) |
+ Subsystem Vendor/Device ID | 区分同一芯片的不同板卡版本 |
PCI_DEVICE_CLASS(class, mask) |
Class Code | 匹配一类设备(如所有存储控制器) |
PCI_VDEVICE(vendor, pid) |
厂商缩写 + Device ID | 使用内核预定义的厂商常量 |
工程建议: 对于自研FPGA或ASIC PCIe设备,务必在驱动中精确匹配VID/PID,避免误绑定到其他厂商设备。使用
lspci -n命令可查看实际设备的VID/PID。
4.3 probe回调:设备初始化的“主战场”
probe函数是驱动开发的核心。当PCI核心发现匹配的设备时,probe被调用,驱动需要在此完成以下关键步骤:
- 启用设备:调用
pci_enable_device(),激活PCIe配置空间中的I/O和Memory空间。 - 设置DMA掩码:调用
dma_set_mask()和dma_set_coherent_mask(),告知设备支持的DMA地址宽度(32位或64位)。 - 请求BAR资源:调用
pci_request_regions(),声明对BAR空间的独占访问。 - 映射BAR空间:调用
pcim_iomap()或ioremap(),将物理地址映射到内核虚拟地址空间。 - 设置DMA和中断:分配DMA缓冲区、注册中断处理函数(MSI-X或INTx)。
- 初始化硬件:通过MMIO读写配置设备内部寄存器。
- 创建字符/网络设备:注册设备节点,暴露给用户空间。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
/* 1. 分配私有数据结构 */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/* 2. 启用PCIe设备 */
ret = pci_enable_device(pdev);
if (ret)
return ret;
/* 3. 设置64位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;
}
/* 4. 请求并映射BAR0 */
ret = pci_request_regions(pdev, "my_pci_driver");
if (ret)
goto err_disable;
dev->bar0 = pcim_iomap(pdev, 0, pci_resource_len(pdev, 0));
if (!dev->bar0) {
ret = -ENOMEM;
goto err_release;
}
/* 5. 设置MSI-X中断 */
ret = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX);
if (ret < 0)
goto err_release;
/* 6. 注册中断处理 */
ret = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, 0),
my_interrupt_handler, 0, "my_pci", dev);
if (ret)
goto err_free_vectors;
/* 7. 保存设备指针 */
pci_set_drvdata(pdev, dev);
dev->pdev = pdev;
dev_info(&pdev->dev, "device probed successfully\n");
return 0;
err_free_vectors:
pci_free_irq_vectors(pdev);
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
常见错误: 忘记调用
pci_enable_device()就进行MMIO访问,会导致总线错误。另外,DMA掩码设置失败后必须回退到32位,否则64位设备在32位系统上无法工作。
4.4 remove回调:优雅的“反向操作”
remove函数是probe的逆过程。当设备热拔出、驱动卸载或系统关机时,内核调用remove。必须确保所有资源被正确释放,避免内存泄漏或内核崩溃。
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_dev *dev = pci_get_drvdata(pdev);
/* 1. 停止所有硬件操作(停止DMA、屏蔽中断) */
writel(0, dev->bar0 + REG_DMA_CTRL);
writel(0, dev->bar0 + REG_INT_MASK);
/* 2. 注销中断(devm_*管理的资源自动释放,但显式释放更安全) */
pci_free_irq_vectors(pdev);
/* 3. 释放BAR映射和区域(devm_*自动处理,但pci_release_regions需要显式) */
pci_release_regions(pdev);
/* 4. 禁用PCIe设备 */
pci_disable_device(pdev);
/* 5. 私有数据由devm_kzalloc自动释放 */
dev_info(&pdev->dev, "device removed\n");
}
最佳实践: 优先使用
devm_系列API(如devm_kzalloc、devm_request_irq、pcim_iomap),这些资源会在设备注销时自动释放,减少remove回调中的遗漏风险。
4.5 驱动注册与注销:模块入口/出口
驱动模块的入口和出口函数负责向PCI核心注册/注销pci_driver实例。
static struct pci_driver my_pci_driver = {
.name = "my_pci_driver",
.id_table = my_pci_ids,
.probe = my_pci_probe,
.remove = my_pci_remove,
};
static int __init my_pci_init(void)
{
return pci_register_driver(&my_pci_driver);
}
static void __exit my_pci_exit(void)
{
pci_unregister_driver(&my_pci_driver);
}
module_init(my_pci_init);
module_exit(my_pci_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("PCIe Driver Expert");
4.6 驱动生命周期流程图
下图展示了从模块加载到设备移除的完整生命周期,以及各阶段调用的关键函数。
4.7 关键数据结构关系图
下图展示了pci_driver、pci_dev、pci_device_id以及内核PCI核心之间的协作关系。
4.8 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| probe未被调用 | id_table中的VID/PID与实际设备不匹配 | 使用lspci -n确认设备ID,检查MODULE_DEVICE_TABLE宏 |
| probe返回-ENODEV | pci_enable_device失败或DMA掩码设置不当 | 检查设备是否被BIOS禁用,尝试回退到32位DMA掩码 |
| remove后系统崩溃 | DMA仍在进行时释放了缓冲区 | 在remove中先停止DMA引擎,等待所有传输完成,再释放资源 |
| 中断未触发 | MSI-X配置错误或中断号注册失败 | 检查pci_alloc_irq_vectors返回值,确认MSI-X使能寄存器 |
| BAR映射失败 | BAR空间被其他驱动占用或未使能 | 确保pci_request_regions在映射前调用,检查BIOS设置 |
调试技巧: 在probe和remove函数中添加
dev_dbg()或dev_info()打印,配合dmesg观察驱动加载/卸载流程。使用cat /sys/bus/pci/devices/<BDF>/driver确认驱动绑定状态。
4.9 本章小结
本章详细剖析了PCIe驱动框架的核心组件:
- pci_driver结构体是驱动的入口,定义了名称、匹配表、probe和remove回调。
- pci_device_id匹配表决定了驱动支持哪些设备,精确匹配是避免冲突的关键。
- probe回调是设备初始化的主战场,必须按顺序完成使能、DMA设置、资源映射、中断注册。
- remove回调是probe的逆操作,确保所有资源被安全释放。
- 优先使用devm_系列API简化资源管理,降低内存泄漏风险。