《电子技术应用》
您所在的位置:首页 > 测试测量 > 设计应用 > 基于Linux系统的PCI设备驱动程序的开发
基于Linux系统的PCI设备驱动程序的开发
崔 莉, 赵大政
摘要: 以一个具体的PCI设备的驱动开发过程为基础,总结了与PCI设备驱动开发的相关问题,详细阐述了基本开发步骤、具体实现、驱动程序内核块的加载以及用户进程和驱动程序的协同工作问题。
Abstract:
Key words :

  摘  要: 以一个具体的PCI设备的驱动开发过程为基础,总结了与PCI设备驱动开发的相关问题,详细阐述了基本开发步骤、具体实现、驱动程序内核块的加载以及用户进程和驱动程序的协同工作问题。
  关键词: PCI设备  设备驱动  中断

1 Linux 系统下设备驱动的概念
  在Linux操作系统下,系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口[2]。设备驱动程序为应用程序屏蔽了硬件的细节。这样在应用程序看来,硬件设备只是一个文件,即特殊的设备文件。因此应用程序通过特定的设备驱动程序可以像操作普通文件一样对具体的硬件设备进行操作。应用程序和设备驱动的关系如图1所示。

 


  在Linux操作系统下有二种主要的设备文件类型,一种是字符设备,另一种是块设备。每种设备文件和实际的硬件相联系。字符设备和块设备的主要区别是:字符设备在写请求的同时,实际的硬件I/O操作便发生了。块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求读/写时,它首先察看缓冲区的内容。如果缓冲区的数据能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备主要是针对磁盘等慢速设备设计的,以免等待时耗费过多的CPU时间。
  就特定的PCI卡,用户可根据用途来确定是将其作为块设备,还是字符设备来处理。本文根据实际工作中所用到的PCI采集卡的特点,简要阐述了其驱动程序开发过程中需要注意的问题和基本步骤。
2  Linux系统PCI设备驱动程序的开发
2.1 基本需求分析
 
  根据设备不同的用途,可以区分不同的PCI设备类型。基于这一设备类型,又可以分析出其他一些基本需求。从本文所使用的数据采集卡可知,其主要用途是用于采集和控制。根据工控过程的特点,需要PCI采集卡在每采样一个数据点时,就以中断的方式交给内核缓冲,再由用户程序适时取出使用。因此,将PCI采集卡作为一个字符设备来处理,并选择触发模式为内触发,数据传送模式为中断传送。本文就是基于这样的需求,按如下所述的步骤和具体实现过程,开发了其驱动程序。
2.2 基本步骤
  (1)PCI设备文件的建立
  既然PCI设备被操作系统当作特殊的文件来看,就要有个文件名。因此,建立一设备文件的名字来代表硬件设备是开发驱动的第一步。按照习惯,设备文件都放在系统目录/dev下。一般在开发过程中,由于要经常查看设备文件的状态,而在这一目录下已经有很多设备文件,查看起来特别不方便,因此,可以自己在某个地方建立一个文件夹,将该设备文件放在该文件夹下。使用mknod命令可以建立设备特殊文件(注意:只有root账号的超级用户才能使用些命令),其格式示例为:
  $mknod  /subfolders/mydev/PCIdrv   c  254  0
  也就是用主设备号254(一般在Linux操作系统下设备文件的主设备号不会超过254,所以选用254,以确保该设备号是惟一的)和辅助设备号0在目录/subfolders/mydev下建立特殊设备文件PCIdrv。
  (2)file_operation数据结构
  设备驱动程序就是一组能完成特定任务的、在内核态下运行的子函数的集合。每个设备驱动程序都有一个被称作file_operation的数据结构来管理、组织这些子函数。该结构包含了指向驱动程序内部这些子函数的指针。当系统引导时,内核会调用每个驱动程序的初始化函数。它有二个任务要完成:(1)将设备驱动程序使用的主设备号通知内核;(2)初始化函数将file_operation指针传送给内核。基本结构为:
  struct file_operations PCI_fops={NULL,PCIread,
  PCIwrite,NULL,NULL,NULL,NULL,
  PCIopen,PCIrelease,NULL,NULL};
  结构中的每一个非NULL成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如读/写操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是Linux的设备驱动程序工作的基本流程。例如:当用户进程执行open( )调用时,open( )执行体将根据open所带的参数找到PCI设备驱动程序,并根据其相关联的PCI_fops数据结构,找到PCIopen子函数的入口点,接着就执行PCIopen函数体。
  (3)编写驱动程序子函数
  file_operation的数据结构中所定义的子函数的集合构成了具体的设备驱动的执行体。因此编写驱动程序子函数是开发过程中最为重要的一步,这些子函数要根据具体的需求来设计。工作中用到的PCI采集卡的主要功能函数有:A/D转换函数AD_INT_Start、AD_INT_Stop、AD_INT_Data,数字量采集与输出函数DI_Data、DO_Data,D/A转换函数AO_Data等。这里不予赘述。
2.3 PCI采集卡驱动程序的具体实现
  (1)获取PCI采集卡的基本配置信息
  PCI采集卡的驱动程序主要是完成对采集卡的寄存器和PCI总线控制器的PCI配置空间的设置和读取,用以启动采集卡,并按照一定的方式进行采集、传送、停止等。若要对这些寄存器进行设置和读取,就要知道这些寄存器的BaseAddress和偏移值、中断号等相关配置信息。这是对硬件操作的第一步。Linux操作系统对PCI设备提供了大量的初始化函数(这一点不同于Dos和Windows)。因此,在系统启动时,这特定的初始化函数会被调用,用来检测系统中存在的所有的PCI设备,并填充PCI设备的配置空间。因此,在开发PCI设备驱动时,只要执行相关的系统调用(由系统提供),就可获得所需要的PCI设备信息。
下面是几个常用的内核函数:
  ①pcibios_present( )
  ②int pcibios_find_device(int device_id,int vendor_id,
int index,int*bus_number,int*device_function)
    ③read_config_byte,read_config_word,read_config_dword
    ④write_config_byte,write_config_word,write_config_dword
    函数①的功能是:返回一个布尔值表明所运行的计算机是否具有支持PCI设备的能力;函数②的功能是:返回设备在总线上的位置及函数指针bus_number、device_function;函数③、④的功能是:通过调用该类函数,设备驱动程序实现寄存器空间的访问,包括读和写。
  (2)内存操作
  设备驱动的子函数运行在内核态,在设备驱动程序中动态开辟和释放内存。用kmalloc或get_free_pages直接申请页而不是用malloc和free;释放内存用的是kfree或free_pages。kmalloc等函数返回的是物理地址,而malloc等返回的是线性地址,且kmalloc最大只能开辟128KB。
  很多硬件需要一块比较大的连续内存用作DMA传送,并且需要一直驻留在内存,不能被交换到磁盘文件中去。但是kmalloc最多只能开辟128KB的内存,这可以通过牺牲一些系统内存的方法来解决。具体做法是:若机器是32MB的内存,在lilo.conf的启动参数中加上mem=30MB,这样Linux就认为此机器只有30MB的内存,剩下的2MB内存在vremap之后就可以为DMA所用。
  (3)Linux下的中断及中断处理
  Linux中的中断处理程序很有特色。它的一个中断处理程序可分为上半部(top half)和下半部(bottom half)二个部分。之所以会有上半部和下半部之分,完全是考虑到中断处理的效率。上半部的功能是“登记中断”,当一个中断发生时,它就把设备驱动程序中中断例程的下半部挂到该设备的下半部执行队列中去,然后就等待新的中断的到来。这样一来,上半部执行的速度就会很快,它就可以接受更多设备产生的中断。上半部之所以要快,是因为它是完全屏蔽中断的,如果不执行完,其他的中断就不能被及时处理,只能等到这个中断处理程序执行完毕以后。所以,要尽可能多地对设备产生的中断进行服务和处理,确保中断处理程序的速度。
要使用一个中断,必须先向系统登记。实现系统中断登记的系统调用形式如下:
  int request_irq(unsigned int irq,void(*handle)(int,void*,struct pt_regs*),unsigned int long flags,const char*device);
  其中:irq是要申请的中断号,handle是中断处理函数指针,flags是中断标识,device是设备名。
  如果登记成功,则返回0。这时在/proc/interrupts文件中可以看到自己请求的中断。
  (4)内核态和用户态下数据交换问题
  用户进程在执行特定系统调用使用设备时,系统就从用户态进入内核态下运行,这时用户进程的环境仍然可用。但在内核缓冲区和用户进程缓冲区间进行数据交换时,必须要用内核提供的专门函数。主要有:put_user( ),get_user( ),copy_to_user( ),copy_from_user( )等。
  通过这些调用来使用用户缓冲时还要进行用户缓冲读写权限的检验,否则调用数据交换函数时会出错。检验函数为:int verify_area(int access,void*u_addr,unsigned long size)。
3 将驱动程序嵌入内核
  由于驱动程序是在内核下运行,因此,要把编写的驱动程序嵌入内核。驱动程序可以按照二种方式编译,一种是编译进kernel,另一种是编译成模块(modules)。如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态卸载,不利于调试。所以推荐使用模块方式。这种方式可以用insmod命令来加载模块和用rmmod来卸载模块。
  在用insmod命令将编译好的模块调入内核时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数:其一是希望获得的设备号,如果为0,系统将选择一个没有被占用的设备号返回;其二是设备文件名;其三是用来登记驱动程序实际执行操作的函数指针。如果登记成功,则返回设备的主设备号;若不成功,则返回一个负值。
  下面是用模块方法将驱动程序加载进内核时用的主要功能函数体示例,也就是当执行inmod 命令时执行的函数体。
  int init_module(void)
  {
       int result;
       result=register_chrdev(254,″PCItest″,&PCI_fops);
       if(result<0) {
    printk(KERN_INFO ″test:can′t get major number\n″);
    return result;
       }
       if(PCItest==0) PCItest=result;/*dynamic*/
       return 0;
  }
  同样可以用 rmmod 命令卸载模块:
  void cleanup_module(void)
  {
       unregister_chrdev(254,″PCItest″);
  }
  在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备PCItest在系统字符设备表中占有的表项。
4  结束语
  设计Linux设备驱动程序有一定的模式,遵循这个模式,将会大大减轻设计程序的工作量。本文总结了工作中对一种PCI采集卡的驱动开发过程。同网卡的驱动相比,PCI采集卡驱动的开发是一件相对简单的工作,但它们同属于PCI设备,具有类似之处。所以,PCI采集卡的驱动开发对设计复杂的网络驱动程序是很有帮助的。
参考文献
1   Rubini A著,LISOLEG译.Linux设备驱动程序.北京:中国电力出版社,2000
2   蔡震.Linux系统下USB设备驱动程序的开发.计算机测量与控制,2003;11(2)
3   李善平,刘文蜂.Linux内核2.4版源代码分析大全.北京:机械工业出版社,2002
4   王学龙.嵌入式Linux系统设计与应用.北京:清华大学出版社,2001
5   Rusling D A著,朱珂译.Linux编程白皮书.北京:机械工业出版社,2000
 

此内容为AET网站原创,未经授权禁止转载。