Linux操作系统下的设备文件主要包括字符设备、块设备和网络设备。字符设备是以字节为单位进行IO操作的设备,一般没有中间缓,IO操作直接影响设备。PCIe设备通常属于字符设备。
PCIe驱动程序提供了对PCIe设备进行操作的接口函数(open、release、read、write、ioctl等),应用程序通过系统调用这些接口函数实现对设备的控制。PCIe驱动程序充当了PCIe设备和应用程序之间的桥梁。
应用程序和驱动程序之间有一个虚拟文件系统(VFS),应用层先通过系统调用进入VFS中,然后调用驱动提供的接口函数控制硬件设备。
PCIe设备驱动的基本框架如下图所示,包括驱动模块的加载与卸载,pci_driver注册和设备文件操作等。
系统上电复位之后,PCIe首先进行链路训练,之后进行枚举扫描。CPU通过枚举扫描来识别系统中有哪些PCIe设备,并为每个设备分配总线号。识别到PCIe设备之后还不能对设备进行操作。当PCIe驱动程序加载后,对驱动中的pci_driver结构体进行注册。
pci_driver结构体里的probe函数,对PCIe设备进行初始化、资源分配等操作,最后注册PCIe字符设备。然后就可以使用file_operations结构体里的操作函数对设备文件进行具体的操作。
驱动模块的加载与卸载
驱动模块加载时会调用pcie_init函数,主要完成设备的初始化工作,具体包括创建设备类、分配设备号并将设备注册到内核。驱动模块卸载时会调用pcie_exit函数,在此函数中注销PCIe驱动、删除pcie_class并释放注册时分配的资源。
pci_driver结构体
pci_driver结构体包含的成员有name变量、id_table指针、probe函数和remove函数等。pci_driver结构体可以定义如下:static struct pci_driver xxx_pcie_driver = { .name = demo_DRV_NAME, .id_table = demo_id_table, .probe = demo_probe, .remove = demo_remove,};
name为设备驱动名称,若驱动模块加载成功,可在/sys/bus/pci/driver/路径下找到对应的驱动名称" demo_DRV_NAME"。
id_table是一个结构体指针,它包含着设备的基本信息,如厂商ID、设备ID,子系统的厂商和设备ID等。
probe函数是一个设备探测函数,当id_table中的ID与PCIE设备的厂商ID和设备ID一致时,会调用probe函数。probe函数实现的功能包括使能PCIE设备、申请IO和内存资源、申请中断以及注册字符设备。
PCIE EP设备支持6个BAR空间,需要使用pci_iomap函数将BAR空间的物理基地址映射为虚拟地址,因为Linux内核只能访问虚拟地址。
PCIe支持MSI中断和传统INTx中断,MSI中断不共享中断源,效率比较高,一般使用MSI中断。使用pci_enable_msi函数使能MSI中断,获得中断号,然后通过request_irq函数来申请中断。
完成内存资源申请及中断申请之后,需要进行字符设备的注册,对设备进行初始化并将设备添加至内核。至此完成probe函数的工作。
remove函数是驱动卸载函数,主要功能为释放内存,禁能PCIe中断,注销字符设备,取消地址映射,最后卸载驱动模块。
file_operations文件操作
驱动注册成功之后,用户就可以通过系统调用来调用驱动里的接口函数。驱动程序的接口函数定义在file_operations结构体中。结构体file_operations示例如下:
static const struct file_operations pcie_fops = { .owner = THIS_MODULE, .open = pcie_fops_open, .release = pcie_fops_release, .read = pcie_fops_read, .write = pcie_fops_write, .unlocked_ioctl = pcie_fops_ioctl, .fasync = pcie_fops_fasync,};
open和release函数分别为打开和关闭设备函数。当用户空间调用open时,内核会调用驱动中的pcie_fops_open函数,open会返回一个描述符,该句柄作为读写操作的输入参数。当调用release时,内核会调用驱动中的pcie_fops_release函数,释放文件资源。
read和write函数为设备读写函数,应用程序通过read和write函数实现对PCIe设备的读写。
fasync为异步通知函数,当应用程序使用fcntl函数改变fasync标记时,内核将会调用驱动中的pcie_fops_fasync函数,该函数通过调用fasync_helper函数初始化一个fasync_struct结构体,该结构体描述了将要发送信号的进程PID。最后在PCIE中断程序中使用kill_fasync函数发送信号给fasync_struct结构体所描述的PID,从而触发应用程序的SIGIO信号处理函数。
PCIe中断驱动设计
在PCIe驱动程序中设计PCIe MSI中断处理函数,将PCIe设备向CPU申请的MSI中断以信号的形式传到用户空间。当PCIe设备产生MSI中断时,驱动程序将PCIe中断传递至应用程序,然后应用程序执行相应的中断处理操作。
PCIE MSI中断是在设备驱动程序初始化时,调用pci_enable_msi进行分配中断号的。使用MSI中断需要先初始化设备MSI结构,分配MSI中断号。接下来通过request_irq来注册中断处理函数,例如向内核注册的中断函数为pcie_interrupt,当PCIe中断产生时,系统会调用pcie_interrupt。在卸载驱动模块时需要先调用free_irq函数释放之前分配的中断号,然后调用pci_disable_msi函数撤销pci_enable_msi的工作,释放此前分配的MSI。
代码示例
PCIe设备初始化:
static int __init pcie_init(void) //PCIe设备初始化{int error;dev_t dev;INIT_LIST_HEAD(&g_private_head); //初始化header of listpcie_class = class_create(THIS_MODULE, "pcie_class"); //创建一个设备类,存放在sysfs目录if (IS_ERR(pcie_class)) {error = PTR_ERR(pcie_class);return error;}error = alloc_chrdev_region(&dev, 0, PCIE_MAX_NUM, PCIE_DRV_NAME);if (error)goto class_destroy;pcie_major = MAJOR(dev);return pci_register_driver(&demo_pcie_driver);class_destroy: class_destroy(pcie_class);}
static int demo_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id){struct demo_card *card;int ret = 0;if (pci_enable_device(pci_dev)) //使能PCIe设备return -EIO;/* 设备DMA标识 */if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) {return -ENODEV;}/* 在内核空间中动态申请内存 */if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL){ printk(KERN_ERR "pci_demo: out of memoryn");return -ENOMEM;}memset(card, 0, sizeof(*card));/* 读取PCI配置信息 */card->iobase = pci_resource_start (pci_dev, 1);card->pci_dev = pci_dev;card->pci_id = pci_id->device;card->irq = pci_dev->irq;card->next = devs;card->magic = DEMO_CARD_MAGIC;pci_set_master(pci_dev); //设定设备工作在总线主设备模式/* 申请I/O和内存资源 */pci_request_regions(pci_dev, demo_DRV_NAME);ret = pci_enable_msi(pdev); //使能msi,然后才能得到中断号pdev->irqif (ret){dev_err("failed:pci_enable_msin");}return 0;}
寄存器读写操作:
// 写操作void write_reg(void __iomem *base, u32 offset, u32 value) {iowrite32(value, base + offset);}// 读操作u32 read_reg(void __iomem *base, u32 offset) {u32 val = ioread32(base + offset);return val;}
PCIe中断处理函数:
static irqreturn_t pcie_interrupt(int irq, void *dev_id){struct pcie_dev *pcie_dev = (struct pcie_dev *)dev_id;dev_info(&pcie_dev->pcie_pdev->dev, "%s occursn", __func__);kill_fasync(&pcie_dev->fasync, SIGIO, POLL_IN); //发送SIGIO信号给fasync_struct描述的PID,触发应用程序的SIGIO信号处理函数return IRQ_HANDLED; //中断处理程序被正确调用且它所对应的设备产生了中断,返回IRQ_HANDLED}
参考内容:
linux PCIE驱动开发
PCIe驱动开发指南
5386