一、PCI基础

1.PCI总线为高性能局部总线,主要解决外部设备之间以及外部设备与主机之间告诉数据传输。在数字图形、图像等处理,以及高速实时数据采集与处理等对数据传输率要求高的应用中,采用PCI总线进行数据传输。

2.PCI规范能够实现32位并行数据传输,工作频率为33MHZ或66MHZ,最大吞吐率为266MB/s。PCI-E是目前PCI系列最具代表性的一种,与并行PCI总线区别在于它采用串行的传输模式,最大支持32个串行连接。每个传输方向上吞吐率为250MB/s,方向上总传输速率达8GB/s。

3.串行通信比并行通信速度快成本低。比如PCIe64位总线,133MHZ,吞吐率1GB/s,内存于台式机和服务器,比PCI宽,但能插入PCI卡。

4.PCI总线特点

        (1)具有隐含的中央仲裁系统。

        (2)具有与处理器和存储器子系统完全并行的操控能力。

        (3)提供地址和数据的奇偶校验,完全的多总线主控能力。

二、PCI数据结构

1.连接cpu以及PCI系统对应的数据结构类型pci_host_bridge:

2.描述PCI总线对应的数据结构pci_bus:

3.用于指向PCI读写操作函数集的结构体pci_ops:

4.专门用于描述总线的物理插槽结构体pci_slot:

 

5.PCI设备结构体pci_dev:

6.PCI驱动程序结构体pci_driver:

7.PCI卡标识结构体pci_device_i:

 8.结构体之间的关系 

三、示例代码 

/*
下面的代码用于实现一个PCI,设备驱动程序。其主要功能是识别特定的PCI设备,
对这些设备进行初始化、配置和管理,处理设备终端,并在设备移除时进行相关清理工作
*/
#include<linux/module.h>
#include<linux/pci.h>  //包含了与 PCI 设备驱动相关的定义和函数,用于处理 PCI 设备的识别、配置和管理
#include<linux/types.h>

//用户自定义的结构体类型,作用于中断服务函数里面
struct pci_Card{
    //这是一个表示端口读写变量的成员,它通常存储的是 PCI 设备所使用的 I/O 端口的基地址。通过这个地址,驱动程序可以对设备进行读写操作,实现与设备的数据交互
    resource_size_t io;   
    long range,flags;     //IO地址范围,标志位
    void __iomem *ioaddr; //IO映射地址
    //表示 PCI 设备使用的中断号。当设备需要向 CPU 发出中断请求时,会使用这个中断号,驱动程序可以通过注册中断服务函数来处理这些中断
    int irq;            
};

//驱动程序支持的设备列表,如果有匹配的设备,那么改驱动程序就会被执行
static struct pci_device_id ids[] = 
{
    { 
        PCI_DEVICE(PCI_VENDOR_ID_INTEL, /* vendor id, 厂家ID */
        0x100f)                         /* device id, 设备ID,可以通过lspci或者到相应的目录里面看 */
    },
    { 
        PCI_DEVICE(PCI_VENDOR_ID_INTEL, 
        PCI_DEVICE_ID_INTEL_80332_0) 
    },
    {0,} /*最后一组为0,表示结束*/
};

/* 进行注册,PCI总线,ids为上面定义的设置 */
//MODULE_DEVICE_TABLE:是一个宏,用于将设备 ID 表注册到内核中,告知内核该驱动程序支持哪些 PCI 设备
MODULE_DEVICE_TABLE(pci, ids);

/* 打印配置空间里面的一些数据信息 */
void skel_get_configs(struct pci_dev *dev) 
{
    uint8_t revisionId;
    uint16_t vendorId, deviceId;
    uint32_t classId;
    /*
    从参数dev里面也是可以打印vendorID等信息的,
    这个结构体里面包含这个成员变量,用下面的API
    获取得到的结果也是一致的
    */
    pci_read_config_word(dev, PCI_VENDOR_ID, &vendorId);
    printk("vendorID = %x", vendorId);
    pci_read_config_word(dev, PCI_DEVICE_ID, &deviceId);
    printk("deviceID = %x", deviceId);
    pci_read_config_byte(dev, PCI_REVISION_ID, &revisionId);
    printk("revisionID = %x",revisionId);
    pci_read_config_dword(dev, PCI_CLASS_REVISION, &classId);
    printk("classID = %x",classId);
}

//设备中断服务函数的实现
static irqreturn_t pci_Mcard_interrupt(int irq,void *dev_id){
    //是设备的私有数据指针,这里将其转换为 struct pci_Card 类型的指针
    struct pci_Card *pci_Mcard = (struct pci_Card *)dev_id;
    //中断函数里面打印中断号
    printk("irq = %d, pci_Mcard_irq = %d\n", irq, pci_Mcard->irq);
    //表示中断已被处理
    return IRQ_HANDLED;
}

/*
probe 函数是一个 PCI 驱动的探测函数,当系统检测到与驱动匹配的 PCI 设备时,会调用该函数进行设备的初始化和资源分配。函数主要完成以下几个任务:
使能 PCI 设备。
为存储设备信息的结构体分配内存。
获取设备的中断号并检查其有效性。
获取设备的 I/O 内存相关信息。
请求使用设备的资源区域。
将设备的物理地址映射到内核虚拟地址空间。
申请中断并设置中断服务子函数。
将设备信息与驱动关联起来。
获取设备的配置信息。
*/
/* 有匹配的设备,这个函数会执行 */
static int probe(struct pci_dev *dev, const struct pci_device_id *id) 
{
    // 初始化返回值变量,用于记录函数执行过程中的状态
    int retval = 0;
    // 定义一个指向自定义结构体 pci_Card 的指针,用于存储 PCI 设备相关信息
    struct pci_Card *pci_Mcard;
    // 打印信息,表明进入了 probe 函数
    printk("probe func\n"); 

    /* 设备使能 */
    // 调用 pci_enable_device 函数使能 PCI 设备
    /*
    功能:该函数用于使能指定的 PCI 设备。在对 PCI 设备进行操作之前,通常需要先使能它,这样设备才能正常工作。
    使能操作会设置设备的配置寄存器,开启设备的电源、时钟等必要条件,同时还会请求设备所需的基本资源(如 I/O 端口、内存空间等)。
    */
    if(pci_enable_device(dev)) {
        // 如果使能失败,打印错误信息
        printk (KERN_ERR "IO Error.\n");
        // 返回错误码 -EIO 表示输入输出错误
        return -EIO;
    }

    // 为 pci_Mcard 结构体分配内存,使用 GFP_KERNEL 标志表示在正常内核内存分配模式下分配
    pci_Mcard = kmalloc(sizeof(struct pci_Card), GFP_KERNEL);
    // 检查内存分配是否成功
    if(!pci_Mcard) {
        // 如果分配失败,打印错误信息,__func__ 是一个预定义宏,代表当前函数名
        printk("In %s,kmalloc err!", __func__);
        // 返回错误码 -ENOMEM 表示内存不足
        return -ENOMEM;
    }

    /* 设备中断号 */
    // 将 PCI 设备的中断号赋值给 pci_Mcard 结构体的 irq 成员
    pci_Mcard->irq = dev->irq;
    // 检查中断号是否有效
    if(pci_Mcard->irq < 0) {
        // 如果中断号无效,打印错误信息
        printk("IRQ is %d, it's invalid!\n", pci_Mcard->irq);
        // 跳转到 out_pci_Mcard 标签处进行错误处理
        goto out_pci_Mcard;
    }

    /*获取 io 内存相关信息*/
    // 获取 PCI 设备第 0 个 BAR(Base Address Register)的起始地址,并赋值给 pci_Mcard 结构体的 io 成员
    /*获取指定 PCI 设备的某个 BAR(Base Address Register,基地址寄存器)所对应的资源起始地址。
    每个 PCI 设备通常有多个 BAR,用于标识设备的不同资源区域(如 I/O 端口范围、内存映射区域等)。*/
    pci_Mcard->io = pci_resource_start(dev, 0);
    // 计算第 0 个 BAR 的地址范围,并赋值给 pci_Mcard 结构体的 range 成员
    pci_Mcard->range = pci_resource_end(dev, 0) - pci_Mcard->io + 1;
    // 获取第 0 个 BAR 的资源标志,并赋值给 pci_Mcard 结构体的 flags 成员
    pci_Mcard->flags = pci_resource_flags(dev, 0);
    // 打印第 0 个 BAR 的起始地址、范围和标志信息
    printk("start %llx %lx %lx\n", pci_Mcard->io, pci_Mcard->range, pci_Mcard->flags);
    // 根据 flags 标志判断第 0 个 BAR 是内存类型还是端口类型,并打印相应信息
    printk("PCI base addr 0 is io%s.\n", (pci_Mcard->flags & IORESOURCE_MEM)? "mem" : "port");

    /*防止地址访问冲突,所以这里先申请*/
    // 调用 pci_request_regions 函数请求使用 PCI 设备的资源区域,名称为 "pci_module"
    /*
    功能:该函数用于请求使用指定 PCI 设备的所有资源区域(如 I/O 端口、内存区域等)。在使用这些资源之前,需要先向系统请求使用权限,以避免资源冲突。
    参数:
    dev:指向 struct pci_dev 结构体的指针,表示要请求资源的 PCI 设备。
    res_name:一个字符串,用于标识这些资源的名称,通常用于调试和日志记录。
    */
    retval = pci_request_regions(dev, "pci_module");
    // 检查资源请求是否成功
    if(retval) {
        // 如果请求失败,打印错误信息
        printk("PCI request regions err!\n");
        // 跳转到 out_pci_Mcard 标签处进行错误处理
        goto out_pci_Mcard;
    }

    /*再进行映射*/
    // 调用 pci_ioremap_bar 函数将第 0 个 BAR 的物理地址映射到内核虚拟地址空间,并赋值给 pci_Mcard 结构体的 ioaddr 成员
    /*
    功能:该函数用于将指定 PCI 设备的某个 BAR 所对应的物理地址映射到内核的虚拟地址空间。
    在 Linux 内核中,不能直接访问物理地址,需要通过虚拟地址来间接访问。
    pci_ioremap_bar 函数会为指定的物理地址分配一块虚拟地址空间,并建立映射关系,
    这样驱动程序就可以通过操作虚拟地址来访问 PCI 设备的硬件寄存器或内存区域。
    参数:
    dev:指向 struct pci_dev 结构体的指针,表示要操作的 PCI 设备。
    bar:指定要映射的 BAR 编号。
    */
    pci_Mcard->ioaddr = pci_ioremap_bar(dev, 0);
    // 检查映射是否成功
    if(!pci_Mcard->ioaddr) {
        // 如果映射失败,打印错误信息
        printk("ioremap err!\n");
        // 设置返回值为 -ENOMEM 表示内存不足
        retval = -ENOMEM;
        // 跳转到 out_regions 标签处进行错误处理
        goto out_regions;
    }

    /*申请中断 IRQ 并设定中断服务子函数*/
    // 调用 request_irq 函数申请中断,指定中断号、中断服务子函数 pci_Mcard_interrupt、中断标志 IRQF_SHARED(表示允许共享中断)、中断名称 "pci_module" 和上下文数据 pci_Mcard
    /*
    功能:该函数用于向系统请求一个中断号,并注册一个中断处理程序。当指定的中断号对应的中断事件发生时,系统会调用注册的中断处理程序来处理该事件。
    参数:
    irq:要请求的中断号。
    handler:中断处理程序的函数指针,其原型为 irqreturn_t (*irq_handler_t)(int, void *)。
    flags:中断标志,用于指定中断的触发方式(如上升沿触发、下降沿触发等)、是否允许共享中断等属性。常见的标志有 IRQF_SHARED(允许共享中断)、IRQF_TRIGGER_RISING(上升沿触发)等。
    name:一个字符串,用于标识该中断的名称,通常用于调试和日志记录。
    dev:传递给中断处理程序的上下文数据指针,在中断处理程序中可以通过这个指针访问相关的数据。
    */
    retval = request_irq(pci_Mcard->irq, pci_Mcard_interrupt, IRQF_SHARED, "pci_module", pci_Mcard);
    // 检查中断申请是否成功
    if(retval) {
        // 如果申请失败,打印错误信息
        printk (KERN_ERR "Can't get assigned IRQ %d.\n", pci_Mcard->irq);
        // 跳转到 out_iounmap 标签处进行错误处理
        goto out_iounmap;
    }

    // 将 pci_Mcard 结构体指针作为驱动数据与 PCI 设备关联起来
    pci_set_drvdata(dev, pci_Mcard);
    // 调用 skel_get_configs 函数获取 PCI 设备的配置信息
    skel_get_configs(dev);
    // 函数执行成功,返回 0
    return 0;

out_iounmap:
    // 解除之前通过 pci_ioremap_bar 建立的映射
    iounmap(pci_Mcard->ioaddr);
out_regions:
    // 释放之前通过 pci_request_regions 申请的资源区域
    pci_release_regions(dev);
out_pci_Mcard:
    // 释放之前为 pci_Mcard 结构体分配的内存
    kfree(pci_Mcard);
    // 返回错误码
    return retval;
}

//移除PCI设备
static void remove(struct pci_dev *dev){
    //从指定的 PCI 设备对象(由 dev 指针代表)中获取之前为该设备关联的私有数据
    struct pci_Card *pci_Mcard = pci_get_drvdata(dev);
    //释放之前为PCI设备注册的中断处理程序
    free_irq(pci_Mcard->irq,pci_Mcard);
    // 解除之前映射的PCI设备的I/O地址空间
    iounmap(pci_Mcard->ioaddr);
    // 释放PCI设备占用的资源区域
    pci_release_regions(dev);
    // 释放之前为pci_Mcard结构体分配的内存
    kfree(pci_Mcard);   
    // 禁用PCI设备
    pci_disable_device(dev);   
    // 打印信息,表示PCI设备移除成功
    printk("remove pci device ok\n");
}

/* 结构体成员变量填充 */
//struct pci_driver pci_driver:定义了一个 PCI 驱动结构体,包含驱动的名称、支持的设备列表、探测函数和移除函数。
static struct pci_driver pci_driver = 
{
    .name = "pci_module",
    .id_table = ids,
    .probe = probe,
    .remove = remove,
};

/* 模块入口函数 */
static int __init pci_module_init(void) 
{
    printk("PCI module entry function\n");
    /* 而pci_register_driver()就是把某个设备下的driver程序放入struct kset drivers结构体中,
    而这个过程又分成如下子过程:
    首先会调用int _pci_register_driver()函数,在这个函数里面,
    首先会把pci_driver存储的信息复制到device_driver中,
    因为内核最终要注册的是一个device的driver信息,
    而不单单指pci设备的driver信息。在复制完成之后,
    内核会调用driver_register()函数注册device_driver的信息,*/
    return pci_register_driver(&pci_driver);
}

/* 模块退出函数 */
static void __exit pci_module_exit(void) 
{
    printk("PCI module exit function.\n");
    // PCI驱动被卸载时,需要调用pci_unregister_driver
    pci_unregister_driver(&pci_driver);
}

MODULE_LICENSE("GPL");
module_init(pci_module_init);
module_exit(pci_module_exit);

/*
触发驱动程序调用的硬件操作
1. 设备插入或开机检测
PCI 设备插入:如果你的系统支持热插拔 PCI 设备,当你将支持的 PCI 设备插入到系统的 PCI 插槽中时,内核会检测到新设备的插入,并尝试为其寻找合适的驱动程序。如果 pci_driver 模块已经加载,并且设备的厂商 ID 和设备 ID 与驱动程序支持的列表匹配,内核会调用 probe 函数对设备进行初始化和配置。
开机检测:在系统启动过程中,内核会扫描所有已连接的 PCI 设备,并为它们加载相应的驱动程序。如果 pci_driver 模块已经被添加到内核模块自动加载列表中(可以通过修改 /etc/modules 文件实现),内核会在启动时自动加载该模块,并对匹配的 PCI 设备进行初始化。
2. 设备中断触发
当 PCI 设备产生中断时,会触发驱动程序中的中断服务函数 pci_Mcard_interrupt。设备产生中断的原因可能有很多,例如设备完成了一次数据传输、检测到了某种异常情况等。一旦中断发生,内核会根据之前注册的中断号和中断服务函数,调用 pci_Mcard_interrupt 函数来处理中断。
*/

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐