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 驱动主要涉及以下步骤:

  1. 定义 PCI 设备ID表: 告诉内核这个驱动支持哪些设备。
  2. 注册驱动 : 向内核注册一个 pci_driver 结构体。
  3. 实现 Probe 函数: 当内核发现一个设备与驱动匹配时,会调用此函数。在这里进行设备初始化。
  4. 实现 Remove 函数: 当设备被移除或驱动卸载时调用,进行资源清理。
  5. 设备操作 : 在 probe 中,通常会:
    • 启用 PCI 设备。
    • 获取 BAR 映射的地址。
    • 申请中断请求线。
    • 创建设备节点(如 /dev/xxx)以供用户空间访问。

4. 代码示例:一个简单的 PCI 驱动骨架

下面是一个最简单的 PCI 驱动代码,它不实现任何具体功能,但完整展示了驱动的基本结构。

c 复制代码
// 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");

代码关键点解释:

  1. pci_device_id : 定义了驱动支持的设备列表。PCI_DEVICE 宏用于创建一个 pci_device_id 条目。
  2. pci_driver: 这是驱动的核心结构,向内核注册了驱动名称、ID表、probe和remove函数。
  3. my_pci_probe
    • pci_enable_device: 启用设备,使其可以响应PCI访问。
    • pci_iomap: 将设备的物理BAR地址映射到内核的虚拟地址空间,这样驱动就可以通过指针(如 priv->bar0)直接读写设备寄存器。
    • pci_alloc_irq_vectorsdevm_request_irq: 申请中断向量并注册中断处理函数。
    • pci_set_drvdata: 将自定义的私有数据结构 my_device_privpci_dev 关联,方便在其他函数中获取。
  4. my_pci_remove : 负责清理所有在 probe 中分配的资源,防止内存泄漏。
  5. my_interrupt_handler: 当设备触发中断时,内核会调用此函数。

5. 编译与测试

  1. 编译 : 需要一个合适的内核 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 命令编译。

  2. 查看PCI设备 : 在加载驱动前,可以使用 lspci -vlspci -xxx 命令查看系统中的PCI设备和它们的配置空间。

  3. 加载驱动 : 使用 sudo insmod simple_pci_driver.ko 加载模块。使用 dmesg 查看内核日志,应该能看到 "Probe successful" 等信息。

  4. 卸载驱动 : 使用 sudo rmmod simple_pci_driver 卸载模块。

总结

理解 PCI 驱动关键在于理解 总线枚举配置空间资源分配 的概念。Linux 内核提供了完善的 PCI 核心层(drivers/pci/),驱动开发者只需遵循固定的框架:

  1. 用ID表声明支持的设备。
  2. probe 中启用设备、映射资源、注册中断。
  3. remove 中妥善清理。

这个骨架代码是理解更复杂PCI驱动(如网卡、显卡驱动)的起点。在实际开发中,你还需要在 probe 之后实现具体的设备功能,例如实现 file_operations 来提供用户空间接口。

相关推荐
贝塔实验室20 小时前
Altium Designer 6.3 PCB LAYOUT教程(四)
驱动开发·嵌入式硬件·硬件架构·硬件工程·信息与通信·基带工程·pcb工艺
小狗爱吃黄桃罐头1 天前
正点原子【第四期】Linux之驱动开发学习笔记-10.1 Linux 内核定时器实验
linux·驱动开发·学习
钢门狂鸭2 天前
go开发规范指引
开发语言·驱动开发·golang
被遗忘的旋律.2 天前
Linux驱动开发笔记(十九)——IIC(AP3216C驱动+MPU6050驱动)
linux·驱动开发·笔记
Shang180989357263 天前
T41LQ 一款高性能、低功耗的系统级芯片(SoC) 适用于各种AIoT应用智能安防、智能家居方案优选T41L
人工智能·驱动开发·嵌入式硬件·fpga开发·信息与通信·信号处理·t41lq
抠脚学代码3 天前
Linux开发-->驱动开发-->字符设备驱动框架
linux·数据结构·驱动开发
木木木丫3 天前
嵌入式项目:韦东山驱动开发第六篇 项目总结——显示系统(framebuffer编程)
c语言·c++·驱动开发·dsp开发
DeeplyMind3 天前
第10章:中断处理-6:Implementing a Handler
linux·驱动开发
workflower4 天前
FDD(Feature Driven Development)特征驱动开发
大数据·数据库·驱动开发·需求分析·个人开发