PCI总线驱动开发全解析

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 驱动代码,它不实现任何具体功能,但完整展示了驱动的基本结构。

c

复制代码

// simple_pci_driver.c

#include

#include

#include

#include

// 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。

makefile

复制代码

obj-m += simple_pci_driver.o

KDIR := /lib/modules/$(shell uname -r)/build

all:

make -C $(KDIR) M=$(PWD) modules

clean:

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 来提供用户空间接口。

相关推荐

汽车之家
mobile365体育投注备用

汽车之家

📅 01-03 👁️ 3651
连续致命失误!中国男篮加时负波兰将打生死战
365bet手机娱乐场

连续致命失误!中国男篮加时负波兰将打生死战

📅 11-09 👁️ 7723
5款超实用中英文logo字体搭配指南|AI在线生成
mobile365体育投注备用

5款超实用中英文logo字体搭配指南|AI在线生成

📅 02-12 👁️ 4458