尊敬的用户:五一期间(5月1日-5月5日),本站所有审核统一于节后处理,如不便之处敬请谅解! icspec全体员工祝您节日愉快!
PCIE(RIFFA)驱动解读(三)
来源:AriesOpenFPGA 发布时间:2023-01-08 分享至微信

解读RIFFA驱动

  • 基于Linux

感谢西安电子科技大学锤王投稿。

Part4PCI设备驱动框架

14.1 pci_dev结构体

对X86来说,上电时由BIOS枚举PCI设备,此时PCI设备处于未激活状态,仅能访问其配置空间,内存和I/O空间还没被映射。BIOS从Host主桥/Root Complex开始探测扫描第一条总线上的设备并记录(桥也记录),若探测到是桥则进一步探测次级总线,如此探测下去,直到探测完所有设备,此时就会在内存中构成一个PCI设备链表。PCI设备的探测一般是通过读取配置空间中的Vendor ID,如果返回的是全1,代表PCI/PCIe插槽上没设备,反之有设备。

每一个PCI设备都会被分配一个pci_dev结构体空间,这个pci_dev记录了这个PCI设备(精确到设备的function,即每个function都拥有一个pci_dev)的重要信息,是PCI设备驱动程序中一个非常重要的数据结构!

需要知道的一点是,pci_dev不是自己初始化的,而是由BIOS将PCI设备参数传入内核后构建的。

pci_dev结构体在/linux/pci.h中定义,成员太多就不放图了,下面介绍几个有用的成员。

struct pci_bus *bus:总线指针,指向这个PCI设备所在的PCI总线的pci_bus结构。

unsigned int devfn:devfn是这个PCI设备的设备功能号,[7:3]是Device Number,[2:0]是Function Number。至此,可由bus号、Device Number和Function Number进行ID路由,唯一地表示此设备了。

unsigned short vendor:Vendor ID,PCI厂商ID(例如Intel)。

unsigned short device:Device ID,PCI设备ID。

unsigned short subsystem_vendor:PCI设备子系统厂商ID(例如Xilinx)。

unsigned short subsystem_device:PCI设备子系统设备ID。

unsigned int irq:中断号,PCI设备通过这个中断号进行中断。

struct pci_driver *driver:类似于字符设备驱动中的struct file_operations结构体,负责定义本PCI设备的名称、绑定probe函数、绑定remove、设置设备ID表。这是一个非常重要的结构体,不过这个结构体不是由BIOS写入的,而是在驱动里完成然后保存到pci_dev中的。

struct device dev:通用设备接口,用于保存驱动中的私有信息。

24.2 PCI设备驱动的ID号设置

看到本节的标题你可能会有疑问了,Vendor ID和Device ID这些ID号不是PCI设备的配置空间中写死的吗,怎么还需要在驱动中设置这些ID号呢?其实这里要设置的这些ID号不是要写到PCI设备中的,而是为驱动程序写的,是这个PCI驱动程序的PCI ID,在加载驱动时内核会对比驱动中的ID和PCI链表中存在的设备的ID是否有一样的。

Linux中提供了为驱动使用的PCI ID号的数据结构struct pci_device_id,这个结构体在/linux/mod_devicetable.h中定义,如下所示:

vendor和device就是PCI ID号了,对这个结构体的初始化其实也不用我们一个一个成员地填写,Linux提供了一个宏PCI_DEVICE,定义在/linux/pci.h中,如下所示:

可以看到,这个宏传入了两个参数,vend就是vendor ID、dev就是device ID;而它们下面的subvendor和subdevice被一个宏赋值了,宏PCI_ANY_ID定义如下:

这个宏是32位全1,反正子系统ID无所谓了,就用全1吧。

接下来看看RIFFA中如何对struct pci_device_id结构体初始化的,如下图所示:

上图中的宏DEFINE_PCI_DEVICE_TABLE定义如下:

这个宏是struct pci_device_id的结构体数组,这意味着Linux内核不仅可以为一个PCI设备驱动添加一组vendor ID和device ID,而是维护了一张pci device table,这个表会记录你在驱动中所设置的所有ID号,像RIFFA驱动就设置了两组ID。

另外,第1637行调用了一个宏MODULE_DEVICE_TABLE,执行这个宏的意思把上面设置好的struct pci_device_id的结构体数组导出到用户空间中,形参pci是指导出的ID号属于PCI设备,它的作用将在下一小节说明。

34.3 probe函数和它的执行条件

probe函数是PCI设备驱动中一个非常重要的系统调用函数。probe这个词的含义是"探查;探测",正如它的词义一样,probe函数是用来探测PCI总线上的设备并对其进行相应初始化操作的。

probe函数是系统调用但不是像open、close一样由用户去调用的,当用户加载驱动模块时,Linux会在用户空间将驱动程序中设置的ID号与PCI BUS上挂载的设备的ID号作对比,如果有设备的固有ID与这个驱动中设置的ID对应上了就会把这个设备的pci_dev结构体传入驱动并调用probe函数初始化这个设备。

但有一个问题,加载驱动模块是在用户空间下进行的,那用户空间怎么能知道这个驱动里设置的ID是啥呢?其实就是上一小节中的宏MODULE_DEVICE_TABLE,是这个宏把驱动中的ID号导出到用户空间中的。

44.4 pci_driver结构体

在4.1小节中介绍过pci_dev结构体中有一个pci_driver结构体类型的指针变量*driver,这个*driver指针不是由BIOS传入的,而是由驱动编写者自己填充并传入给*driver保存的,本节就介绍一下这个重要的结构体pci_driver。

pci_driver定义在/linux/pci.h中,如下所示:

上图中可以看到,pci_driver和字符设备驱动中的file_operations结构体类似,成员中都有大量的函数指针;pci_driver的作用就是定义了这个PCI驱动的名称、定义probe函数和remove函数(remove函数与probe函数是对应的,一个加载一个卸载)、还有保存4.2小节中介绍的pci device table,里面保存着PCI驱动ID号列表。

而file_operations结构体中也是这些东西,只不过是字符设备名、init函数和exit函数、设备号罢了。

当然,需要知道的一点是,如果仅有字符设备驱动的相关配置,驱动可以正常工作;只有PCI设备驱动的相关配置,驱动也能正常工作,他们本身就是两种驱动框架,是独立的,但后面第八章中我们也会看到RIFFA驱动是如何把这两种驱动框架捏在一起用的。

下面看一下RIFFA驱动中对pci_driver结构体的初始化:

像上面说的一样,RIFFA初始化了PCI驱动名、填充了ID号列表、绑定了probe/remove函数。

54.5 PCI驱动在init函数中要做的事

init函数在第三章讲了,主要是注册字符设备驱动和设置自动创建设备节点,那么PCI设备驱动什么时候注册呢?也是在init函数中注册,因为首先需要把4.4节中讲的pci_driver结构体对象注册进内核。

Linux中注册PCI设备驱动使用pci_register_driver函数,函数原型如下所示:

可见唯一的输入参数就是pci_driver的结构体指针变量,那么就只需要在init函数中调用这个API并把4.4节初始化的pci_driver结构体首地址放进去就可以了。

64.6 PCI驱动初始化流程

PCI驱动初始化流程仅指RC的初始化和相关必要设置,和DMA、内存管理什么的无关,实际的RIFFA驱动中会有其他的初始化和设置在其中穿插。下面先看一张PCI初始化流程图,然后再一个一个地讲解。

这些初始化都是在probe函数中完成的。从初始化流程来看,还是很符合《PCI Express体系结构导读》里面所讲述的内容的。

  1. 首先在驱动可以访问PCI设备的任何资源之前需要激活PCI设备,调用pci_enable_device函数激活PCI设备,其原型如下所示:

参数为pci_dev结构体指针变量,就是probe与设备匹配后传进来的那个。

(2)Linux内核中普遍使用C语言的面向对象编程思想(OOP),用结构体模拟Cpp中的类,驱动程序的编写也推荐这种面向对象的编程思想,最好为一个实体设备构建一个专属的结构体,以记录它的信息和特征,RIFFA中也是如此。pci_dev结构体传进来了这个PCI设备的许多信息,我们可以选择保存这些信息,可以保存中断号,当然也可以保存ID号什么的,这个很随意,目的是为了符合Linux内核的OOP思想,让驱动程序看起来更专业!毕竟谁知道哪天你的驱动程序会不会被合并到Linux的Master Branch里呢,不专业的驱动会让社区审核者很狂躁!

在结构体创建后需要定义结构体变量,在驱动中的常用做法是动态分配内存,因为当报错或者驱动没用了的时候可以把内存空间释放掉。常用的小空间内存分配接口就是kmalloc函数,这个函数与C标准库中的malloc用法一致,只不过他的作用是给内核态的虚拟地址空间分配物理地址连续的物理内存并将其首地址映射成虚拟地址后交给内核中的驱动程序,具体关于kmalloc和vmalloc的区别将在Linux内存管理中讲述。

(3)对于PCI驱动程序来说只能访问到FPGA内部的BAR空间,而BAR空间需要映射到内存中才能被驱动访问到,当驱动向这片内存空间读写时,RC就会对对应BAR空间的相应位置读写,这种映射方式称为MMIO(X86中还有64KB大小的I/O端口映射方式,不过目前PCIe开发中用不到了,因为64KB太小了!) MMIO的好处就是不管你访问什么外设,都只用访存指令就能完成。

说了这么多,还是得先在内存中分配BAR空间的映射吧!Linux内核提供了几个访问BAR寄存器的API(注意是BAR寄存器,配置空间里的那个)

pci_resource_start、pci_resource_end、pci_resource_len、pci_resource_flags,

pci_resource_start函数能获取BAR寄存器里代表BAR空间在内存中的首地址(物理地址,是BIOS写给配置空间的,这片内存没人敢动,等你来分配)。

pci_resource_end函数能获取BAR空间在内存中的结尾物理地址。

用结尾地址减首地址加1就是BAR空间的长度了,当然Linux也提供了直接获取BAR空间长度的API,即pci_resource_len。

pci_resource_flags函数是获取BAR空间的一些信息,比如可预取,不可预取。

然后调用pci_request_regions函数请求初始化BAR寄存器中记录的那片空间。

(4)BAR空间的物理首地址和长度都有了,但驱动在内核空间没法操作物理地址,故需要将BAR空间的物理地址映射为内核虚拟地址供驱动使用。实现物理地址和虚拟地址映射的API就是大名鼎鼎的ioremap了,当然Linux也给PCI驱动专门提供了一个地址映射的API,就是pci_ioremap_bar,不过原理都完全一样,用谁都行,RIFFA驱动中用的是传统的ioremap。

(5)中断对于PCIe设备通常是必不可少的,Linux对中断提供了一套注册API以及处理中断的思路,本节先只讲述中断的注册,处理中断放在第六章讲述。

中断事件想要触发对应的中断处理,需要把中断号、中断回调函数入口地址、中断类型等信息对应起来才能正确执行。Linux提供了专门的API接口用于注册中断request_irq。

request_irq函数在/linux/interrupt.h中定义,定义如下:

参数irq就是中断号了,注意中断号是非负数;handler就是中断回调函数的函数指针,中断处理在这个回调函数中进行;flags是中断类型,中断类型定义在/linux/interrupt中,如下所示:

PCI设备属于IRQF_SHARED(共享中断);*name是驱动名;*dev是往中断回调函数里传入的私有数据,其类型是无类型指针,传什么指针都行,这个数据没有限制,主要是为了在共享中断中区分不同设备的中断,推荐传入这个设备的结构体指针,因为这里面通常有最多的设备信息。

(6)最后一步,保存驱动的私有信息。使用pci_set_drvdata函数将想要保存的数据类型保存到pci_dev结构体中的device类型中;用pci_get_drvdata函数可以把保存的数据类型再给读出来。

可以像上面这么理解,但这个数据类型实际上不是被保存了,而是被device结构体中的无类型指针driver_data给指向了,如下所示。


[ 新闻来源:AriesOpenFPGA,更多精彩资讯请下载icspec App。如对本稿件有异议,请联系微信客服specltkj]
存入云盘 收藏
举报
全部评论

暂无评论哦,快来评论一下吧!