Linux设备驱动程序设计

来源:互联网 发布:求下列矩阵的n次方幂 编辑:程序博客网 时间:2024/05/08 11:39

 摘要:本文介绍了Linux设备驱动程序设计的相关基本知识总结了2.4与2.6不同版本内核下Linux设备驱动程序的设计模版与编写方法,并给出了一个实例化的设备驱动程序样本。

关键词:静态链接模块可加载模块,字符设备,块设备,初始化,设备接口,中断请求,中断服务,设备号

一.Linux设备驱动程序设计基础

Linux设备驱动程序是内核与硬件间的一层软件接口,实现了高级应用程序与硬件互动。它可以通过两种方式集成入内核中:一是将其直接编译静态链接到内核,从而一劳永逸;二是通过称为Linux可加载模块(LKM)机制,将其编写成一种目标格式,实现成为可动态加载和卸载的驱动模块。前者用户可随时调用它而无需安装但增加了内核占用空间,并且更新需重编内核、重起系统;后者虽然使用前须先加载,但是更节省资源且灵活,也是通常采用的设备驱动设计方式。

Linux设备驱动程序有三种类型:字符设备、块设备和网络设备。字符设备一次I/O操作存取数据量不固定,只能顺序存取,如鼠标、磁带驱动器等设备。块设备一次I/O操作以固定大小的数据块为单位,如硬盘、软驱等。其中字符设备不经过系统的快速缓冲,而块设备经过系统的快速缓冲。网络设备是特殊处理的,它没有对应的设备文件,Linux使用套接口(socket),以文件I/O方式提供了对网络数据的访问。其中与文件系统相关的两种类型是字符设备和块设备。 (本文主要讨论字符设备和块设备驱动程序的编写。)

不管是何种类型,从结构上看,整个驱动程序可分为驱动程序初始化独立于设备的接口硬件I/O部分三个部分。驱动程序初始化部分负责将设备驱动程序装载到内核或从内核中卸载等;独立于设备的接口是设备驱动程序和文件系统连接的桥梁;而硬件I/O部分具体实现各种I/O操作。(如图)

从程序实现角度,设备驱动程序也可分为以下三个部分(1)自动配置和初始化子程序。负责检测所要驱动的硬件设备是否存在并能正常工作,如果该设备正常,则对这个设备及其相关的设备驱动程序需要的软件状态进行初始化。(2)服务于I/O请求的子程。主要是file operations结构的各个入口点的实现。这部分的实现支持文件系统调用(如openlosereadwrite)。(3)中断服务子程序。在Linux系统中,并不是直接从中断向量表中调用设备驱动程序的中断服务子程序,而是由Linux系统接收硬件中断,再由系统调用中断服务子程序。

Unix一样,Linux把设备均作为文件来对待这些文件一般称为特殊文件,它使用户或应用程序可按操纵普通文件的方式进行访问控制硬件设备。在Linux内中,设备驱动程序是作为文件系统的一个模块存在的。它向下负责和硬件设备的交互,向上通过一个通用的接口挂接到文件系统上,从而和系统的内核等联系起来,管理和控制各种设备,是软件和硬件设备的一个抽象层。

设备文件的属性包括:文件名、设备类型、主设备号次设备号主设备号是与驱动程序一一对应的。次设备号用来区分使用同一个驱动程序的个体设备。major()函数获得主设备号,minor()函数获得次设备号。与普通的目录和文件一样,对设备的操作也是通过对文件操作的file_operations结构体来调用驱动程序的设备服务子程序。作为实现驱动程序的最重要的数据结构,它为Linux提供的服务于I/O请求的子程序的代码实现提供了一系列入口点,它们在设备驱动程序初始化的时候向系统进行登记,以便系统在适当的时候调用。

Linux中,一个设备在使用之前必须向系统进行注册,设备注册是在设备初始化时完成的。在系统内核保持着一张字符(或块)设备注册表,表的结构是数组,其下标值就是主设备号,且每种字符(或块)设备占一个表项。例如,字符设备注册表是结构数组chrdevs[],而块设备注册表是结构数组blkdevs[]。Linux的内核模块被加载到内核后,它就可以根据设备注册表,任意地利用内核提供的各种资源和服务了。

当系统首次访问某硬件设备时,只要存在使用depmod”命令建立的模块从属关系树,与之对应的模块就可以自动加载。可加载内核模块通常情况下安装在系统/lib/modules目录的一个子目录下。

对于可加载的内核驱动程序模块,它类似Windows系统的动态连接库(dl1),并有两个主要接口函数:一个用于在模块加载时注册服务和申请资源,如init module();另一个用于在模块卸载时清除由前者所作的工作,从而使内核模块可以安全地卸载,如cleanup module()。编译后,root用户执行insmod命令加载模块时调用前一个函数,执行rmmod命令卸载模块时调用后一个函数。(不同内核版本接口函数有所不同,具体请参照下文。)

 

一.Linux设备驱动程序的编写

考虑Linux内核版本的差异,我们分旧的2.4内核与新2.6内核讨论:

A.        基于Linux 2.4内核的设备驱动程序编写

1.先给出Linux 2.4内核的设备驱动参考模板,并据此实现初始化(卸载)功能:

Linux设备驱动程序的初始化具体内容可用下面的伪函数进行描述:(为表达方便和实际编程时参考,采用类C语言的描述方法进行描述,其中括号[]间的内容表示可选。)

static int __init name_of_initialization_routine(void) {

/*完成硬件相关参数的设置、硬件初始化;申请内存(要用kmalloe)等资源;(注:若前面加static表示函数作用域限制在本文件之内;int,表示函数返回一个整型值,_init表示告诉gcc,生成的模块中,把该函数放在init这个sectioninit,是函数名,init section的代码/数据都只是在内核初始化的时候用到,所以初始化完成以后就可以释放了,内核的启动信息freeing init memory就是指的这个)*/

[cheek_region()测试I/O端口是否可用(是否被其它设备锁定);]

[用request_region()锁定所用的端口区域;]

 

#if(字符设备)

register_chrdev()向内核注册主设备号;

#endif

 

#if(块设备)

初始化struct blk_dev_struct结构变量bIk_dev[major],其major为注册设备号;

用函数register_blkdev()向内核注册主设备号;

#endif

 

#if(采用中断方式)

request_irq()将中断处理函数排队列以接收处理中断

#endif

 

#if(采用轮询方式)

queue_task()add_timer()等将相应函数排入合适的队列接收定时器报时

#endif

 

#if(需要DMA)

request_dma()

#endif

}

其中,用到的系数原形、主要数据结构有:

设备驱动程序的卸载对动态可加载驱动模块显得特别重要,它主要是回收分配给设备驱动程序的各种资源,某些部分正好和初始化程序相反,其处理过程可描述为:

static void __exit name_of_cleanup_routine(void) { //释放初始化时申请的各种资源(释放内存要用kfree);

#if(锁定了端口区域)

release_region()释放锁定的端口区域;

#endif

 

#if(字符设备)

unregister_chrdev()解除注册的主设备号;

#endif

 

#if(块设备)

清除struct blk_dev_struct结构变量;用unregister_blkdev()解除注册的主设备号;

#endif

 

#if(注册了DMA)

free_dma()释放DMA;

#endif

 

#if(注册了中断)

free_irq()释放已注册的中断;

#endif

 

#if(在定时器报时队列)

sleep_on()睡眠以等到处理函数不再注册从而删除;

#endif

}

 

其中,用到的函数原形:

设备驱动程序的许多请求和释放操作以及资源等的申请和释放等(如request_*free_*)可分另别放在方法函数openclose中进行,如申请释放内存、中断、DMA通道,初始化硬件等、推迟请求、及时释放有利于资源的充分利用。本文为讨论方便,将这些都放在init_modulecleanup_module中,在实际编程时可根据实际情况具体决定。要注意中断和DMA通道申请释放的顺序,尽量按文中给出的顺序,否则可能引起死锁。

 

       2.实现硬件I/O部分:

这部分可进一步为服务于I/O请求的子程序(即驱动程序的上半部分),如openreadwrite等和中断服务子程序(即驱动程序的下半部分)这部。由于涉及到具体的硬件,在此不作讨论。

 

B.       基于Linux 2.6内核的设备驱动程序编写

Linux 2.6内核设备驱动程序的统一框架,定义了各种即插即用硬件接口,子系统可通过这些接口与各个驱动程序进行通信。新驱动框架更加明确了总线和驱动程序之间的责任界限。

新内核还引入了sysfs文件系统为每个系统的硬件树进行分级处理,对可加载内核模块规定新的命名方法,不再使用旧版本标准的.o (object)扩展名,而是使用新的.ko扩展名。故"make"命令的成功完成将产生.ko后缀名的模块

Linux 2.6新内核驱动的编写与前面的2.4版基本方法相同,设备驱动程序参考模板如下:

鉴于内核版本改进的差异,新2.6内核的驱动设计应当注意:

2.6内核下,用户无论是在源代码中还是在Makefile文件中都不再需要对#define MODULE进行描述。内核搭建系统会自动对此类符号进行定义并校验。

若想对已有模块进行编译,并将其加载到2.6内核,必须先完成一些基本的结构变化:用户需要为MODULE_LICENSE()宏增加一个示例,例如MODULE_LICENSE("GPL")。否则,当用户利用此类结构加载模块时,在标准输出设备和系统日志上会显示一个坏模块的出错信息。这种2.4内核以后的版本才引入的宏,可以将模块定义为获得GPL Version 2或更新版本许可的模块。其它有效的值还可选"Proprietary""GPL v2""GPL and additional rights""Dual BSD/GPL"选择BSDGPL许可)以及"Dual MPL/GPL"选择Mozilla GPL许可

此外,旧版本内核设备驱动程序存在一个普遍问题:对初始化模块和卸载功能的名称进行假设。当开发人员编写旧版本内核下的硬件驱动程序时,如果使用缺省的名称init_module()cleanup_module(),那么就不需要对初始化模块和清除功能的名称进行记录。这种方法经常会出现错误,已逐渐被淘汰。在2.6内核下,用户必须使用module_init()宏和module_exit()宏对初始化和退出规程的名称进行记录。

 

C.       实例化的驱动程序设计举例(其中假设设备为Mouse

为便于参考,下面给出一个具体设备的驱动程序编写。同时为了具有通用性,将其硬件处理部分去掉,只用文字来叙述,仅保留一个字符设备驱动程序应有部分。这样做的目的不仅简化程序,而且同时还能起到了一个详细模版作用。

首先,需要的头文件有:<linux/fs.h><linux/sched.h><linux/kernel.h><linux/init.h>

<linux/module.h> <linux/delay.h> <linux/poll.h> <asm/uaccess.h>(若块设备需<blk.h>

 

int my_open(struct inode *inodestruct file *filp)

{int type=inode->i_rdev

if(type)filp->f_op=&my_fops//使应用程序和my_fops结构中函数相联系

else return -ENODEV

MOD_INC_USE_COUNT//加一设备的用次数,以便驱动程序可以被卸载

Return 0

}

 

int my_ioctl(struct inode *inode,struct file *flip,unsigned int cmd,unsigned long arg)

[switch(cmd){根据预先定义的cmdarg改变参数等}]

 

void my_inthandler(int irqvoid *dev_idstruct pt_regs *regs)//中断处理程序

[计算坐标位置、数据采样等;]

 

ssize_t my_read(struct file *flipchar *bufsize_t countloff_t *l)

{将中断处理函数通过数据采样,计算触击点坐标值放入*buf中;return读出的字节数;}

 

下面定义my_fop

struct file_operations my_fops=

{ NULL         /* LSEEK */

my_read       /* HEAD */

NULL         /* WRITE */

NULL         /* READDIR */

NULL         /* SELECT */

my_ioctl       /* IOCTL */

NULL         /* MMAP */

my_open       /* OPEN */

NULL        

my_release     /* CLOSE */

其它为NULL

};

 

通过该结构,内核和此驱动程序的各个方法函数相关联。对于设备的初始化,可参考初始化函数mydriver_init(void)。其中将注册主设备号函数改为register_chrdev (200,” Mouse”&my_fops)。将注册中断函数改为:

request_irq(10,my_inthandler,0,” Mouse”,&my_dev_id) //my_dev_idunsigned long变量

以上便是一个实例化的设备驱动程序开发,由于篇幅所限,不再给出其它的数据结构,如struct node *inodestruct file等。

原创粉丝点击