4. PCIe驱动模型:Linux内核PCI子系统架构、pci_driver结构体、probe/remove回调机制
4.1 Linux内核PCI子系统整体架构
Linux内核PCI子系统采用分层设计,从顶层设备驱动到底层硬件访问,共分为三个核心层次:PCI核心层、PCI总线驱动层和PCI设备驱动层。理解这一分层结构是编写正确PCIe驱动的先决条件。
关键概念: PCI子系统通过
pci_bus、pci_dev、pci_driver三个核心数据结构管理所有PCI/PCIe设备。驱动开发者主要与pci_driver交互,而枚举、配置空间访问、DMA映射等底层操作由核心层封装。
4.2 pci_driver结构体深度解析
pci_driver是PCI设备驱动的核心载体,定义在include/linux/pci.h中。驱动通过填充该结构体并向内核注册,实现与PCI子系统的绑定。
| 成员名称 | 类型 | 功能描述 |
|---|---|---|
| name | const char * | 驱动名称,用于sysfs标识和错误日志 |
| id_table | const struct pci_device_id * | 设备ID匹配表,定义驱动支持的Vendor/Device/Class |
| probe | int (*probe)(struct pci_dev *, const struct pci_device_id *) | 设备匹配成功时调用,完成硬件初始化和数据结构创建 |
| remove | void (*remove)(struct pci_dev *) | 设备移除或驱动卸载时调用,释放资源 |
| suspend | int (*suspend)(struct pci_dev *, pm_message_t) | 电源管理:挂起设备 |
| resume | int (*resume)(struct pci_dev *) | 电源管理:恢复设备 |
| shutdown | void (*shutdown)(struct pci_dev *) | 系统关机时调用 |
| driver | struct device_driver | 内嵌通用设备驱动结构体 |
开发提示:
id_table是驱动与硬件匹配的关键。使用PCI_DEVICE(vendor, device)宏精确匹配单设备,或使用PCI_DEVICE_CLASS(class, class_mask)匹配设备类别。匹配表必须以空条目{0, }结尾。
4.3 probe/remove回调机制与生命周期
probe()和remove()是PCI驱动中最重要的两个回调函数,它们定义了设备从发现到移除的完整生命周期。
4.3.1 probe()回调执行流程
当PCI核心层检测到设备且id_table匹配成功后,按以下顺序调用probe():
- 使能设备:调用
pci_enable_device(),激活I/O和内存空间 - 请求资源:调用
pci_request_regions(),声明BAR空间所有权 - 设置DMA掩码:调用
dma_set_mask()和dma_set_coherent_mask() - 读取配置信息:通过
pci_read_config_*()获取设备能力寄存器 - 映射BAR空间:使用
pci_iomap()或ioremap()将物理地址映射到内核虚拟地址 - 注册中断:调用
request_irq()或pci_request_irq()注册中断处理函数 - 初始化私有数据结构:分配并填充驱动自定义的私有数据
- 注册子设备/接口:如misc设备、net_device、cdev等
4.3.2 remove()回调执行流程
remove()是probe()的逆操作,在设备热拔插、驱动卸载或系统关机时被调用:
- 停止所有I/O操作:确保没有正在进行的DMA或中断处理
- 注销子设备:移除通过probe注册的misc/net/char设备
- 释放中断:调用
free_irq()或pci_free_irq() - 取消映射BAR:调用
pci_iounmap()释放虚拟地址映射 - 释放资源:调用
pci_release_regions() - 禁用设备:调用
pci_disable_device() - 释放私有数据:kfree驱动分配的私有结构体
常见错误: remove()中遗漏
pci_release_regions()会导致下次加载驱动时资源被占用,pci_request_regions()返回-EBUSY。务必保证probe和remove的资源操作完全对称。
4.4 完整驱动模板与代码示例
以下是一个标准的PCIe驱动框架,展示了pci_driver结构体的完整实现:
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/fs.h>
#define DRIVER_NAME "pcie_sample"
#define VENDOR_ID 0x10EE /* 示例厂商ID */
#define DEVICE_ID 0x7011 /* 示例设备ID */
/* 设备私有数据结构 */
struct pcie_dev_priv {
struct pci_dev *pdev;
void __iomem *bar0_addr;
unsigned long bar0_phys;
int irq_num;
};
/* ID匹配表 */
static const struct pci_device_id pcie_id_table[] = {
{ PCI_DEVICE(VENDOR_ID, DEVICE_ID) },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, pcie_id_table);
/* probe回调 */
static int pcie_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct pcie_dev_priv *priv;
int ret;
/* 1. 分配私有数据 */
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 2. 使能PCI设备 */
ret = pci_enable_device(pdev);
if (ret)
return ret;
/* 3. 请求BAR资源 */
ret = pci_request_regions(pdev, DRIVER_NAME);
if (ret)
goto err_disable;
/* 4. 设置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_release;
}
/* 5. 映射BAR0 */
priv->bar0_addr = pci_iomap(pdev, 0, 0);
if (!priv->bar0_addr) {
ret = -EIO;
goto err_release;
}
priv->bar0_phys = pci_resource_start(pdev, 0);
/* 6. 注册中断(MSI优先) */
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (ret < 0)
goto err_unmap;
priv->irq_num = pci_irq_vector(pdev, 0);
priv->pdev = pdev;
pci_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "PCIe device probed: BAR0=0x%lx\n",
priv->bar0_phys);
return 0;
err_unmap:
pci_iounmap(pdev, priv->bar0_addr);
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
/* remove回调 */
static void pcie_remove(struct pci_dev *pdev)
{
struct pcie_dev_priv *priv = pci_get_drvdata(pdev);
/* 逆序释放probe中获取的资源 */
pci_free_irq_vectors(pdev);
pci_iounmap(pdev, priv->bar0_addr);
pci_release_regions(pdev);
pci_disable_device(pdev);
dev_info(&pdev->dev, "PCIe device removed\n");
}
/* pci_driver结构体定义 */
static struct pci_driver pcie_driver = {
.name = DRIVER_NAME,
.id_table = pcie_id_table,
.probe = pcie_probe,
.remove = pcie_remove,
};
/* 模块入口/出口 */
module_pci_driver(pcie_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("PCIe Driver Expert");
MODULE_DESCRIPTION("PCIe Sample Driver for JTAG Debug");
4.5 设备匹配与加载时序
PCIe驱动加载和设备匹配遵循以下时序:
| 步骤 | 执行者 | 动作描述 |
|---|---|---|
| 1 | 内核启动/模块加载 | 执行module_pci_driver(),调用pci_register_driver() |
| 2 | PCI核心层 | 遍历已枚举的pci_dev链表,与id_table逐条匹配 |
| 3 | PCI核心层 | 匹配成功则调用驱动probe(),传入pci_dev和匹配的pci_device_id |
| 4 | 驱动probe() | 执行硬件初始化,返回0表示成功,负值表示失败 |
| 5 | PCI核心层 | probe成功后将驱动与设备绑定,设备进入操作状态 |
| 6 | 驱动卸载 | 调用pci_unregister_driver(),触发所有绑定设备的remove() |
调试要点: 使用
lspci -vvv查看设备当前驱动绑定状态;/sys/bus/pci/devices/目录下每个设备都有driver符号链接指向当前绑定的驱动。若probe失败,检查dmesg中的错误码和pci_enable_device返回值。
4.6 常见probe失败原因与排查
- -ENODEV:设备未使能或配置空间访问失败,检查
pci_enable_device()是否被调用 - -EBUSY:BAR资源已被其他驱动占用,使用
lspci -vv查看Region状态 - -EIO:BAR映射失败,检查BAR地址是否有效,或尝试使用
ioremap()替代pci_iomap() - -ENOMEM:内存分配失败,检查GFP标志或减少分配大小
- -EINVAL:中断注册参数错误,确认
irq_handler_t函数签名正确
4.7 本章小结
PCIe驱动模型的核心是pci_driver结构体及其probe/remove回调机制。驱动开发者需要掌握:
- PCI子系统三层架构:设备驱动层 → PCI核心层 → 总线驱动层
pci_driver各成员的正确填充方式,特别是id_table的匹配规则probe()中严格的资源获取顺序和错误处理路径remove()中与probe完全对称的资源释放顺序- 通过
module_pci_driver()宏简化模块注册
下一阶段准备: 在JTAG调试链路中,probe()的成功执行是后续通过JTAG访问PCIe配置空间和BAR内存的前提。确保probe()中正确映射BAR空间,是JTAG调试链路分析的基础。