4. 第一个PCIe驱动:模块初始化、probe/remove回调、设备ID表编写

编写第一个PCIe驱动是理解Linux设备模型与PCIe子系统协同工作的关键一步。一个完整的PCIe驱动核心由三部分组成:模块初始化/退出函数、probe/remove回调、以及设备ID表。本节将逐一拆解这些组件,并展示它们如何协同工作,使内核能够在设备插入时自动加载并初始化驱动。

4.1 驱动骨架:模块初始化与退出

每个PCIe驱动都必须向内核注册一个pci_driver结构体。模块初始化函数负责完成注册,退出函数负责注销。这是驱动的“入口”和“出口”。

关键点: 使用 module_pci_driver() 宏可以简化代码,它自动生成 module_init() 和 module_exit() 函数。但对于需要额外初始化(如分配全局资源、注册sysfs接口)的驱动,仍需手动编写。
#include <linux/module.h>
#include <linux/pci.h>

static int __init mypci_init(void)
{
    int ret;
    pr_info("mypci: module init\n");
    ret = pci_register_driver(&mypci_driver);
    if (ret)
        pr_err("mypci: failed to register driver: %d\n", ret);
    return ret;
}

static void __exit mypci_exit(void)
{
    pr_info("mypci: module exit\n");
    pci_unregister_driver(&mypci_driver);
}

module_init(mypci_init);
module_exit(mypci_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Sample PCIe Driver");

4.2 设备ID表:驱动与设备的“握手协议”

设备ID表是驱动声明“我能支持哪些设备”的清单。内核PCI子系统在枚举总线时,会遍历所有已注册驱动的ID表,匹配成功的设备才会调用该驱动的probe回调。

ID表使用struct pci_device_id结构体数组定义,并以空结构体结尾。常用匹配字段如下:

字段 含义 示例值
vendor 厂商ID(PCI SIG分配) 0x10EE (Xilinx)
device 设备ID(厂商自定义) 0x7021
subvendor 子系统厂商ID(可选) PCI_ANY_ID
subdevice 子系统设备ID(可选) PCI_ANY_ID
class 设备类别(可选) 0x000000
class_mask 类别掩码 0x000000
driver_data 私有数据指针,传递给probe (kernel_ulong_t)&my_data
/* 设备ID表 */
#define MY_VENDOR_ID  0x10EE
#define MY_DEVICE_ID  0x7021

static const struct pci_device_id mypci_ids[] = {
    { PCI_DEVICE(MY_VENDOR_ID, MY_DEVICE_ID) },
    /* 也可以匹配多个设备 */
    { PCI_DEVICE(0x8086, 0x1234) },
    /* 通配符匹配(谨慎使用) */
    // { PCI_DEVICE(PCI_ANY_ID, PCI_ANY_ID) },
    { 0, }
};

MODULE_DEVICE_TABLE(pci, mypci_ids);
最佳实践: 始终使用 PCI_DEVICE() 或 PCI_DEVICE_SUB() 宏来填充ID表,避免手动赋值导致字节序错误。宏 MODULE_DEVICE_TABLE 用于将ID表导出到模块信息中,使 modprobe 等工具能够识别驱动支持的设备。

4.3 probe回调:驱动的“初始化舞台”

当内核发现一个设备的vendor/device ID与驱动的ID表匹配时,会调用probe函数。这是驱动初始化硬件、分配资源、注册接口的场所。一个典型的probe流程如下:

  1. 启用设备:调用 pci_enable_device() 使能PCI配置空间访问。
  2. 设置DMA掩码:调用 dma_set_mask() 和 dma_set_coherent_mask()。
  3. 请求MMIO/I/O区域:使用 pci_request_regions() 或 pci_iomap()。
  4. 设置总线主控:调用 pci_set_master() 使能DMA。
  5. 分配私有数据结构:使用 devm_kzalloc() 等托管分配函数。
  6. 注册中断:调用 request_irq() 或 pci_alloc_irq_vectors()。
  7. 注册设备接口:如字符设备、net_device、misc_device等。
static int mypci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct mydev_priv *priv;
    int ret;

    pr_info("mypci: probe device %04x:%04x\n", pdev->vendor, pdev->device);

    /* 1. 启用PCI设备 */
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    /* 2. 设置DMA掩码(64位优先) */
    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;
    }

    /* 3. 请求MMIO区域 */
    ret = pci_request_regions(pdev, "mypci");
    if (ret)
        goto err_disable;

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

    /* 5. 分配私有数据 */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv) {
        ret = -ENOMEM;
        goto err_release;
    }
    pci_set_drvdata(pdev, priv);

    /* 6. 映射BAR0 */
    priv->bar0 = pci_iomap(pdev, 0, 0);
    if (!priv->bar0) {
        ret = -EIO;
        goto err_release;
    }

    /* 7. 注册中断(MSI-X优先) */
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY);
    if (ret < 0)
        goto err_iounmap;

    priv->irq = pci_irq_vector(pdev, 0);
    ret = request_irq(priv->irq, mypci_irq_handler, 0, "mypci", priv);
    if (ret)
        goto err_free_vectors;

    pr_info("mypci: probe success, irq=%d\n", priv->irq);
    return 0;

err_free_vectors:
    pci_free_irq_vectors(pdev);
err_iounmap:
    pci_iounmap(pdev, priv->bar0);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

4.4 remove回调:优雅的“退场”

remove函数是probe的逆操作,负责释放所有已分配的资源。顺序必须与probe相反,以避免悬空指针或资源泄漏。

static void mypci_remove(struct pci_dev *pdev)
{
    struct mydev_priv *priv = pci_get_drvdata(pdev);

    pr_info("mypci: remove device\n");

    /* 逆序释放:先中断,再映射,再区域,最后设备 */
    free_irq(priv->irq, priv);
    pci_free_irq_vectors(pdev);
    pci_iounmap(pdev, priv->bar0);
    pci_release_regions(pdev);
    pci_disable_device(pdev);
    /* devm_kzalloc 自动释放,无需手动kfree */
}
常见陷阱: 在 remove 中忘记调用 pci_disable_device() 会导致设备无法被后续驱动重新启用。如果使用了 devm_ 系列托管API,部分资源(如 devm_kzalloc、devm_request_irq)会在设备移除时自动释放,但 pci_request_regions 和 pci_iomap 仍需手动释放。

4.5 完整驱动结构图

下图展示了PCIe驱动从模块加载到设备移除的完整生命周期,以及各组件之间的调用关系。

PCIe驱动生命周期与组件关系 module_init() pci_register_driver() 设备ID表匹配 probe() 回调 设备插入/匹配成功时调用 pci_enable_device() dma_set_mask() pci_request_regions() pci_set_master() devm_kzalloc() pci_iomap() request_irq() remove() 回调 设备拔出/模块卸载时调用 free_irq() pci_iounmap() pci_release_regions() 注意 remove顺序必须与probe相反

4.6 完整驱动示例

将上述组件组合在一起,就构成了一个完整的PCIe驱动框架。以下代码展示了pci_driver结构体的定义与注册:

static struct pci_driver mypci_driver = {
    .name     = "mypci",
    .id_table = mypci_ids,
    .probe    = mypci_probe,
    .remove   = mypci_remove,
    /* 可选回调 */
    // .shutdown = mypci_shutdown,
    // .suspend  = mypci_suspend,
    // .resume   = mypci_resume,
};

module_pci_driver(mypci_driver);  /* 如果使用简单初始化,可替代module_init/exit */
调试检查清单:
  • lspci -nn 确认设备vendor/device ID与ID表一致。
  • modprobe mypci 后检查 dmesg 是否有probe成功日志。
  • cat /sys/bus/pci/drivers/mypci 查看驱动是否绑定到设备。
  • 如果probe失败,检查 pci_enable_device 返回值,常见错误为 -EIO(设备未就绪)或 -ENOMEM。

4.7 常见错误与调试技巧

错误现象 可能原因 解决方案
probe未被调用 ID表不匹配或驱动未注册 检查 lspci -n 输出,确认ID表包含正确的vendor/device
pci_enable_device失败 设备被BIOS禁用或电源管理状态异常 尝试 setpci -s <BDF> COMMAND=0x07 手动启用
dma_set_mask失败 设备不支持请求的DMA位宽 降级到32位掩码,或检查设备DMA能力寄存器
pci_request_regions冲突 其他驱动已占用相同BAR区域 使用 lspci -vvv 查看BAR占用情况,卸载冲突驱动
中断不触发 MSI/MSI-X配置错误或INTx未使能 检查 /proc/interrupts,尝试 pci_alloc_irq_vectors 回退到INTx