1、第6章 Linux 设备驱动程序,在嵌入式系统设计中,设备驱动程序起着举足轻重的作用。在Linux操作系统中,应用程序不能直接访问硬件,而是需要通过Linux内核提供的系统调用访问设备文件,最后由设备驱动程序直接和硬件打交道。设备驱动程序提供给应用程序的是一些标准的接口,包括读/写操作,而隐藏了各种硬件寄存器的操作。嵌入式系统根据不同的需求,外部设备多种多样,设备驱动程序在项目开发中占有重要地位。,本章主要内容:,6.1 概述 6.2 Linux设备驱动模型 6.3 一个简单的设备驱动程序 6.4 设备驱动程序与硬件 6.5 用户程序和内核之间传递数据 6.6 中断技术 6.7 软中断和tas
2、klets 6.8 /proc文件系统,6.1 概述,Linux设备驱动程序在目录/dev下对应有一个或几个设备文件。应用程序通过系统调用open( )打开设备文件,然后通过read( )、write( )和ioctl( ) 等文件,对设备进行操作。因此编写设备驱动程序时,就要实现对应的功能。 早期版本的Linux设备目录/dev下的文件并不一定有实际的设备对应,有大量的节点实际上是没有用的,或者说目前是没有用的。至于未来什么时候安装了新设备的时候可能会用得上,那是难以预料的事情。这些节点都是由系统管理员建立的,脚本/dev/MAKEDEV可以帮助系统管理员来做这件事情。当增加一个新设备,或者
3、修改一个设备的主、从设备号时,除了要告诉内核这些变动外,还要修改脚本MAKEDEV。一个典型系统的/dev目录下会有成百上千个节点。,6.2 Linux设备驱动模型,Linux的设备驱动模型提供了一个统一的数据模型,使得系统中所有的设备在底层都有一个统一的接口。设备驱动模型包括了用户程序udev、内核数据结构kobject以及文件系统sysfs等。设备驱动模型是一种层次结构,各种数据结构及程序之间的关系还是有些复杂。对于用户来说,最直接看到的就是目录/dev下的设备文件。在本节中,希望能够清楚描述从设备驱动程序的组织结构到用户程序udev如何管理目录/dev下的节点。,6.2.1 sysfs文
4、件系统,sysfs文件系统是类似于proc文件系统的特殊文件系统,一般挂载在目录/sys下。目录/sys中的内容是在系统启动后才在内存中建立起来,作用是将系统中的设备组织成层次结构,向用户程序提供详细的内核数据结构信息。,6.2.2 内核相关数据结构,设备驱动模型相关的数据结构包括了kobject、kset、ktype和subsystem等。目录/sys下的每一个目录对应一个内核对象kobject。Kobject是Linux2.6引入的新的设备管理机制,在内核中由struct kobject表示,该结构提供了基本的对象管理,并在嵌入到内核更复杂、更庞大的数据结构(如 bus、devices和d
5、rivers等)中使用,避免一些通用机制的重复实现。这些机制包括: 对象引用计数;维护对象链表(集合);对象上锁; 在用户空间的表示。通过kobject使所有设备在底层都具有统一的接口,kobject提供基本的对象管理,是构成Linux2.6设备模型的核心结构,它与sysfs文件系统紧密关联,每个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录。kobject是组成设备模型的基本结构,都是嵌入在称为“容器”的对象中。,6.3 一个简单的设备驱动程序,这一节来看一个简单的设备驱动程序。为了简单起见,暂时不设计硬件,仅对Linux设备驱动程序作一个初步的了解。传统上,unix
6、/Linux设备分为字符设备和块设备两类。在Linux系统中,用命令“ls-l /dev”查看目录/dev下的设备文件,输出类似以下的信息:,第一列属性的头一个字符表示该设备的类型,c表示字符设备,b表示块设备。所谓字符设备就是指该设备是以字符为单位进行输入或输出操作;而块设备则是以块为单位进行输入或输出操作。第五列和第六列分别是设备的主设备号和次设备号,如设备rtc的主设备号是10,次设备号是135。 现在来实现一个简单的设备驱动程序,暂时不涉及硬件。在这个例子中,完成以下几项任务:1在目录/dev下建立设备文件/dev/ex1;2打开设备文件/dev/ex1时,打印“exl open!”的
7、信息;3往设备文件写入任意数据时,打印“exl write,”的信息;4从设备文件进行读取操作时,打印“exl read.”的信息。,首先来看一下怎么向内核注册一个设备驱动程序。注册设备驱动程序需要以下步骤: 1申请主设备号。 2为设备数据结构分配内存空间。 3登记设备文件的操作函数,如open、write和read等。 4向内核登记主设备号。 5在目录/dev和/sys下建立节点。,这个简单的设备驱动程序ex1.c完整的代码如下:,#include #include #include #include #include #include #include static dev_t ex1_d
8、ev_number; struct class * ex1_class; static struct cdev ex1_deve; #define DEVICE_NAME “ex1”,int ex1_open(struct inode * inode,struct file * file) printk(“ex1 open! n”);return 0; int ex1_read(struct file *file,char *buf,int count,int *ppos) printk(“ex1 read.n”);return 0; int ex1_write(struct file *fi
9、le,char *buf,int count,int *ppos) printk(“ex1 write.n”);return count; static struct file_operations ex1_fops=.owner=THIS_MODULE,.open=ex1_open,.read=ex1_read,.write=ex1_write,;,static int_init ex1_init(void) alloc_chrdev_region(&ex1_dev_number,0,1,DEVICE_NAME); ex1_class=class_create(THIS_MODULE,DEV
10、ICE_NAME); cdev_init(&ex1_devs,&ex1_fops); ex1_devs.owner=THIS_MODULE; ex1_devs.ops=&ex1_fops; cdev_add(&sex1_devs,ex1_dev_number,1); class_device_create(ex1_class,NULL,ex1_dev_number,NULL,“ex1”); printk(“QCD:ex1 driver init./n”); return 0; ,static void_exit ex1_exit(void) cdev_del(&ex1_devs); unreg
11、ister_chrdev_region(MAJOR(ex1_dev_number),1); class_device_destroy(ex1_class,MKDEV(MAJOR(ex1_dev_number),0); class_destroy(ex1_class); module_init(ex1_init); module_exit(ex1_exit); module_author(“qcd”); module_license(“gp1”);,6.4 设备驱动程序与硬件,6.3节中的设备驱动程序并没有真正地操作硬件,而嵌入式系统写设备驱动程序的主要目的就是让硬件按照用户的要求工作。在本节中
12、,介绍设备驱动程序如何去操作硬件。在前面的描述中,尽量避免谈到具体的系统平台,在这里还是选用S3C2410处理器的系统来介绍设备驱动程序。 现在来看设备驱动程序如何控制GPIO接口。ARM处理器的GPIO接口可以配置为输入或输出口。作为输入引脚时,可以用于检测外部电平的变化,如某个模块是否正常工作等。作为输出引脚时,可以控制LED的亮灭、控制继电器工作等,其实也就是从I/O口输出高电平或是低电平。特别要注意的是,处理器的I/O引脚的驱动能力是有限的,必要时需要在外部加上驱动电路,如图6-4所示。,图6-4 驱动电路,图6-4(a)其实是一个反相器。要注意各种三极管的参数,像9014/9015集
13、电极的最大电流是100mA,如果用于像继电器控制或是红外发射管驱动之类就需要类似于2N2907或是其他允许更大驱动电流的三极管,甚至是MOS管。 在嵌入式系统设计时,要先确定需要使用到几个GPIO脚,哪些I/O脚是作为输入,哪些是作为输出。在操作系统启动时(或者是在BootLoader启动时),需要对各个GPIO端口进行配置。当然也可以在加载设备驱动时再来进行端口的配置。,从ARM处理器的手册上可以查到,要使用I/O引脚输出高电平或是低电平,只需要往指定的寄存器上写入1或是0。例如,S3C2410的端口F的数据寄存器地址是0x56000054,想让端口F所有的引脚都输出低电平,则往地址0x56
14、000054写入0即可。当然前提是端口F要配置成输出端口。,6.5 用户程序和内核之间传递数据,现在知道如何去控制硬件进行工作了,但是还想更进一步。在6.4节中,设备ex1无论写入任何数据,就是将I/O引脚GPF6的电平取反,这不符合人们的习惯。可能有人希望是给ex1写入“1”时LED点亮,反之,写入“0”时LED熄灭。这就意味着需要在用户空间和内核空间传递数据。因为地址映射的问题,使用了MMU的系统在不同用户程序之间、用户程序和内核之间传递数据需要一些特殊的手段。这在嵌入式系统中是经常遇到的,从设备读取数据或是向设备写入数据,如串口通信,都需要在用户程序和设备驱动程序(内核空间)之间交换数据
15、。,Linux内核有一系列的函数用于内核和用户进程交换数据,函数名称和作用如表6-2所列。,表6-2 内核和用户控件数据交换函数,下面来看这些函数是怎么样使用。还是用6.3节的ex1设备来进行修改,作以下几个改进:,向ex1设备写入1时,LED点亮;写入0时,LED熄灭;写入其他数据无效。 从ex1设备读取数据时,返回端口GPF6当前数据,即端口F引脚6的电平状态。 函数ex1_read()和ex1_write()进行修改,修改部分用黑体表示: #define GPFDAT (*(volatile unsigned char *)(S3C24XX_VA_GPIO+0x54) int ex1_r
16、ead(struct file * file ,char * buf,int count ,int * ppos) unsigned char temp;printk(“ex1 write.n”); temp=(GPFDAT6)return 0;,int ex1_write(struct file * file,char *buf ,int count,int *ppos) char c; printk(“ex1 write.n”); get_user(c,buf); if(c=1)GPFDAT|=(0x016); else if(c=0)GPFDAT return count; 在ex1_w
17、rite()函数中,先通过get_user()从用户缓冲区复制一个字符到变量c,如果字符变量c是为1,则将GPF6置为高电平;如果字符变量c是0,则将GPF6置为低电平。可以通过LED的亮灭来观察电平的变化。在用户空间测试的方法还是用以下命令测试:,echo 1 /dev/ex1 上述命令会使GPF6输出高电平。 函数ex1_read()读取GPF6引脚电平状态,将电平“0”或“1”转变为字符0或1的ASCII码,再把数据复制到用户空间。用户程序去读取设备文件“/dev/ex1”时,将会得到字符0或1.,6.6 中断技术,实际嵌入式系统中的硬件设备远比ex1复杂,不是简单的在需要的时候对寄存器
18、进行读写。一般设备的速度都比处理器慢得多,处理器需要使用中断机制来解决这种速度的不匹配。什么叫中断?从软件的角度来看,中断就是把正在进行的工作停下来,去处理更紧急的事情。中断完成后,当然就要继续原来的工作,这就需要硬件的支持,即处理器要有中断控制器。S3C2410支持56个中断源,每一个中断源产生中断时,会向CPU提供中断向量。在中断向量有限的情况下,就需要共享中断向量。,程序状态寄存器的各个位如图所示。,其中位6和位7置为1时,分别屏蔽FIQ和IRQ。,有中断产生时,ARM处理器将程序计数器pc设置到向量表的位置。向量表一般是放在地址0x00000000的位置,注意这里指的是虚拟地址。对于I
19、RQ中断,ARM处理器响应中断时跳转到地址0x18,Linux在这个地址是中断响应程序的总入口。中断响应时,CPU是处于用户模式或是系统模式,Linux根据中断前的不同模式跳转到相应处理程序。在第4章Linux操作系统移植的时候,要实现一个汇编的宏get_irqnr_and_base,在这里就要调用get_irqnr_and_base来获取中断号。之后就调用do_IRQ()。那么函数do_IRQ()是如何找到特定程序的中断处理程序呢?,6.7 软中断和tasklets,设备ex1的中断处理函数非常简单,因此很快就完成了中断处理。在很多情况下,并非如此,而是要处理设备的很多数据。但Linux的中
20、断是不能嵌套的,如果中断处理占用很多时间,将会影响到系统的运行。基本上是希望中断处理程序执行得越快越好。因此Linux把中断处理分成两部分来完成,这两部分被称为上半部和下半部。上半部指的就是中断处理程序,例如对于设备ex1,上半部指的就是ex1_int_handler()。,那么下半部指的又是什么呢?下半部的意义还是比较容易理解的,但是早期的Linux版本提供了一种实现下半部的机制,称为BH(Bottom Half),字面上的意思也是“下半部”,两者之间很容易混淆。不过这种机制在Linux-2.6版本以后被新机制取代了。读者可以通过旧版本的Linux内核分析书籍了解BH。,总线是所有模块或设备
21、共同使用的公共信息通路,每个模块或设备都通过开关电路与总线上的相应信号线相连。一般开关电路会选择使用多路选择器方式(用于单向总线)或者三态门(用于双向总线)等。目前,Linux有三种下半部的实现机制:软件中断(Softirqs)tasklets和工作队列(work queues)。软中断的使用需要经过比较谨慎的考虑,因为系统中只允许有32个软件中断。目前也只使用了几个软件中断,这可以从文件include/linux/interrupt.h中看到:,/* PLEASE,avoid to allocate new softirgs,if you need not_really_highfreque
22、ncy threaded job scheduling.For almost all the purposestasklets are more than enough.F.e.all serial decvice BHs etal.should be converted to tasklets,not to softirqs. */ enum HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, #ifdef CONFIG_HIGH
23、_RES_TIMERSHRTIMER_SOFTIRQ, #endif;,这个枚举类型定义了软中断号,同时也是相应软中断的优先级别。软中断号小的优先执行。注释中希望大家尽量少使用软件中断,对于大多数情况,用tasklet就足够了。事实上tasklet也是通过软中断实现。软中断号HI_SOFTIRQ和TASKLET_SOFTIRQ分别是高优先级别的tasklet和一般的tasklet。事实上,在系统启动时,软中断的初始化就只注册这两个tasklet的软中断:,void _init softirq_init(void) open_softirq(TASKLET_SOFTIRQ,tasklet_act
24、ion,NULL); open_softirq(HI_SOFTIRQ,tasklet_hi_action,NULL); 其他软中断,如网络子系统的软中断,则是在网路设备初始化时注册。 tasklet通过数据结构tasklet_struct来描述,在include/interrupt.h中定义: struct tasklet_struct struct tasklet_struct * next; unsigned long state; atomic_t count; void(*func)(unsigned long); unsigned long data;,其中最重要的成员是func,它
25、是tasklet的处理程序。state表示tasklet目前正处在什么状态,如正在运行等。count是tasklet引用计数,不为0时tasklet禁止执行。 创建tasklet有两种方式,第一种方式是利用以下两个之一进行定义: #define DECLARE_TASKLET(name,func,data) struct tasklet_struct name=NULL,0,ATOMIC_INIT(0),func,data #define DECLARE_TASKLET_DISABLED(name,func,data) struct tasklet_struct name=NULL,0,ATO
26、MIC_INIT(1),func,data 实际上就是创建一个tasklet_struct结构,并对结构体的各个成员进行初始化。两个宏的差别是前者把引用计数count置为0,处于激活状态,后者把count置为1,处于禁止状态。 另一种方式就是通过函数task_init()动态初始化。task_init()并不为tasklet分配内存,而是对已有的结构体进行初始化。,6.8 /proc文件系统,Linux内核中有几种特殊文件系统,如/proc、sysfs和tmpfs,这几个文件系统并没有类似于硬盘的固定存储介质,而是建立在内存中,其文件内容在读/写的时候才根据系统信息动态生成,在系统掉电后也就消失了。/proc文件系统在Linux操作系统中既可以用于查看系统状态信息,也可以用于程序调试,还可以用于驱动程序的调试。,谢谢观赏!,