PCI总线驱动开发全解析
深入理解 PCI 总线与驱动:从理论到实践
1. 什么是 PCI 总线?
PCI 是一种高性能的、地址和数据复用的、即插即用的局部总线标准。它被设计用来连接计算机主板上的CPU、内存与各种外设扩展卡。
1.1 PCI 总线的主要特点
- 即插即用:系统启动时自动配置 PCI 设备,分配资源(如内存地址、中断号),无需用户手动设置跳线。
- 高带宽:32位/64位数据宽度,时钟频率通常为33MHz或66MHz。
- 独立于处理器:不依赖于特定型号的 CPU。
- 地址/数据复用:同一组信号线先传输地址,后传输数据,节省引脚。
- 总线枚举:系统可以通过遍历总线来发现所有连接的设备。
2. PCI 设备识别与配置空间
每个 PCI 设备都有一个唯一的标识符,并通过一个称为 配置空间 的寄存器组来进行管理和通信。
2.1 设备识别
一个 PCI 设备由三个关键标识符唯一确定:
- Vendor ID: 16位,由 PCI SIG 分配给设备制造商。
- Device ID: 16位,由制造商分配给特定设备。
- Class Code: 24位,表示设备的类别(如网络控制器、显示控制器等)。
2.2 配置空间
这是一个256字节的标准化结构,包含了设备的所有关键信息。前64字节是标准化的头部,其余为设备相关的配置寄存器。
重要寄存器包括:
- Vendor ID / Device ID: 设备标识。
- BAR: 最多6个 基地址寄存器,用于告知系统该设备需要多少内存或I/O空间,以及系统为其分配的实际基地址。这是驱动与设备通信的基石。
- Interrupt Line: 系统分配给设备使用的中断号。
- Interrupt Pin: 设备使用哪个硬件中断引脚(A, B, C, D)。
当系统启动时,BIOS或操作系统会遍历PCI总线,读取每个设备的配置空间,为其分配资源,并填充BAR寄存器。
3. Linux 内核中的 PCI 驱动框架
在 Linux 中,编写一个 PCI 驱动主要涉及以下步骤:
- 定义 PCI 设备ID表: 告诉内核这个驱动支持哪些设备。
- 注册驱动: 向内核注册一个
pci_driver结构体。 - 实现 Probe 函数: 当内核发现一个设备与驱动匹配时,会调用此函数。在这里进行设备初始化。
- 实现 Remove 函数: 当设备被移除或驱动卸载时调用,进行资源清理。
- 设备操作: 在
probe中,通常会:- 启用 PCI 设备。
- 获取 BAR 映射的地址。
- 申请中断请求线。
- 创建设备节点(如
/dev/xxx)以供用户空间访问。
4. 代码示例:一个简单的 PCI 驱动骨架
下面是一个最简单的 PCI 驱动代码,它不实现任何具体功能,但完整展示了驱动的基本结构。
// simple_pci_driver.c
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/fs.h>
#include <linux/uaccess.h>// 1. 定义该驱动支持的设备ID表
static const struct pci_device_id my_pci_ids[] = {{ PCI_DEVICE(0x1234, 0x1111) }, // 假设支持 Vendor ID 0x1234, Device ID 0x1111 的设备{ 0, } // 必须以全0条目结束
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);// 设备私有数据结构(可选)
struct my_device_priv {void __iomem *bar0; // 用于映射BAR0的虚拟地址int irq_line; // 中断号
};// 3. Probe 函数:设备被发现并匹配时调用
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{int ret;struct my_device_priv *priv;printk(KERN_INFO "My PCI Driver: Device found! Probing...\n");// 3.1 启用PCI设备ret = pci_enable_device(pdev);if (ret) {dev_err(&pdev->dev, "Failed to enable PCI device\n");return ret;}// 3.2 为设备申请DMA掩码(如果设备支持DMA)ret = pci_set_dma_mask(pdev, DMA_BIT_MASK(32));if (ret) {dev_err(&pdev->dev, "No suitable DMA available\n");goto err_disable_device;}// 3.3 分配设备私有数据priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);if (!priv) {ret = -ENOMEM;goto err_disable_device;}pci_set_drvdata(pdev, priv); // 将私有数据与pci_dev关联// 3.4 获取并映射BAR0(假设是内存区域)priv->bar0 = pci_iomap(pdev, 0, 0); // 映射BAR0的全部长度if (!priv->bar0) {dev_err(&pdev->dev, "Cannot map BAR0\n");ret = -ENOMEM;goto err_disable_device;}printk(KERN_INFO "My PCI Driver: BAR0 mapped at virtual address %p\n", priv->bar0);// 3.5 申请中断ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES);if (ret < 0) {dev_err(&pdev->dev, "Failed to allocate IRQ vectors\n");goto err_iounmap;}priv->irq_line = pci_irq_vector(pdev, 0);ret = devm_request_irq(&pdev->dev, priv->irq_line, my_interrupt_handler,IRQF_SHARED, "my_pci_driver", pdev);if (ret) {dev_err(&pdev->dev, "Failed to request IRQ %d\n", priv->irq_line);goto err_free_irq_vectors;}printk(KERN_INFO "My PCI Driver: IRQ %d requested successfully\n", priv->irq_line);// 到这里,设备基本初始化完成// 可以继续:初始化硬件、创建字符设备、sysfs节点等...printk(KERN_INFO "My PCI Driver: Probe successful\n");return 0;// 错误处理:按申请资源的逆序释放资源
err_free_irq_vectors:pci_free_irq_vectors(pdev);
err_iounmap:pci_iounmap(pdev, priv->bar0);
err_disable_device:pci_disable_device(pdev);return ret;
}// 4. Remove 函数:设备移除或驱动卸载时调用
static void my_pci_remove(struct pci_dev *pdev)
{struct my_device_priv *priv = pci_get_drvdata(pdev);printk(KERN_INFO "My PCI Driver: Removing device\n");// 释放资源,顺序与probe相反free_irq(priv->irq_line, pdev);pci_free_irq_vectors(pdev);pci_iounmap(pdev, priv->bar0);pci_disable_device(pdev);
}// 简单的中断处理函数
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{struct pci_dev *pdev = dev_id;printk(KERN_DEBUG "My PCI Driver: Interrupt occurred!\n");// 读取设备状态寄存器,确认中断,处理数据...return IRQ_HANDLED;
}// 2. 定义 pci_driver 结构体
static struct pci_driver my_pci_driver = {.name = "my_simple_pci_driver",.id_table = my_pci_ids, // 设备ID表.probe = my_pci_probe, // 探测函数.remove = my_pci_remove, // 移除函数
};// 模块加载和卸载
module_pci_driver(my_pci_driver); // 这个宏简化了注册和注销操作MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example PCI driver");
代码关键点解释:
pci_device_id: 定义了驱动支持的设备列表。PCI_DEVICE宏用于创建一个pci_device_id条目。pci_driver: 这是驱动的核心结构,向内核注册了驱动名称、ID表、probe和remove函数。my_pci_probe:pci_enable_device: 启用设备,使其可以响应PCI访问。pci_iomap: 将设备的物理BAR地址映射到内核的虚拟地址空间,这样驱动就可以通过指针(如priv->bar0)直接读写设备寄存器。pci_alloc_irq_vectors和devm_request_irq: 申请中断向量并注册中断处理函数。pci_set_drvdata: 将自定义的私有数据结构my_device_priv与pci_dev关联,方便在其他函数中获取。
my_pci_remove: 负责清理所有在probe中分配的资源,防止内存泄漏。my_interrupt_handler: 当设备触发中断时,内核会调用此函数。
5. 编译与测试
-
编译: 需要一个合适的内核
Makefile。obj-m += simple_pci_driver.oKDIR := /lib/modules/$(shell uname -r)/buildall:make -C $(KDIR) M=$(PWD) modulesclean:make -C $(KDIR) M=$(PWD) clean使用
make命令编译。 -
查看PCI设备: 在加载驱动前,可以使用
lspci -v或lspci -xxx命令查看系统中的PCI设备和它们的配置空间。 -
加载驱动: 使用
sudo insmod simple_pci_driver.ko加载模块。使用dmesg查看内核日志,应该能看到 “Probe successful” 等信息。 -
卸载驱动: 使用
sudo rmmod simple_pci_driver卸载模块。
总结
理解 PCI 驱动关键在于理解 总线枚举、配置空间 和 资源分配 的概念。Linux 内核提供了完善的 PCI 核心层(drivers/pci/),驱动开发者只需遵循固定的框架:
- 用ID表声明支持的设备。
- 在
probe中启用设备、映射资源、注册中断。 - 在
remove中妥善清理。
这个骨架代码是理解更复杂PCI驱动(如网卡、显卡驱动)的起点。在实际开发中,你还需要在 probe 之后实现具体的设备功能,例如实现 file_operations 来提供用户空间接口。
