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流程如下:
- 启用设备:调用
pci_enable_device()使能PCI配置空间访问。 - 设置DMA掩码:调用
dma_set_mask()和dma_set_coherent_mask()。 - 请求MMIO/I/O区域:使用
pci_request_regions()或pci_iomap()。 - 设置总线主控:调用
pci_set_master()使能DMA。 - 分配私有数据结构:使用
devm_kzalloc()等托管分配函数。 - 注册中断:调用
request_irq()或pci_alloc_irq_vectors()。 - 注册设备接口:如字符设备、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驱动从模块加载到设备移除的完整生命周期,以及各组件之间的调用关系。
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 |