导读-- 在UNIX系统里,对用户程序而言,设备驱动程序隐藏了设备的具体细节,对各种不同设备提供了一致的接口,一般来说是把设备映射为一个特殊的设备文件......
b>
参数说明:
参数irq表示所要申请的
硬件中断号。handler为向系统登记的中断处理子
程序,中断产生时由系统来调用,调用时所带参数irq为中断号,dev_id为申
请时告诉系统的设备标识,regs为中断发生时寄存器内容。device为设备名,
将会出现在/proc/interrupts文件里。flag是申请时的选项,它决定中断处理
程序的一些特性,其中最重要的是中断处理程序是快速处理程序(flag里设置
了SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT),快速处理程序
运行时,所有中断都被屏蔽,而慢速处理程序运行时,除了正在处理的中断外,
其它中断都没有被屏蔽。在LINUX系统中,中断可以被不同的中断处理程序共享,
这要求每一个共享此中断的处理程序在申请中断时在flags里设置SA_SHIRQ,
这些处理程序之间以dev_id来区分。如果中断由某个处理程序独占,则dev_id
可以为NULL。request_irq返回0表示成功,返回-INVAL表示irq$#@62;15或
handler==NULL,返回-EBUSY表示中断已经被占用且不能共享。
作为系统核心的一部分,设备
驱动程序在申请和释放内存时不是调用malloc
和free,而代之以调用kmalloc和kfree,它们被定义为:
#include $#@60;linux/kernel.h$#@62;
void * kmalloc(unsigned int len, int priority);
void kfree(void * obj);
参数len为希望申请的字节数,obj为要释放的内存指针。priority为分配内存操
作的优先级,即在没有足够空闲内存时如何操作,一般用GFP_KERNEL。
与中断和内存不同,使用一个没有申请的I/O端口不会使CPU产生异常,也
就不会导致诸如“segmentation fault"一类的错误发生。任何进程都可以访问
任何一个I/O端口。此时系统无法保证对I/O端口的操作不会发生冲突,甚至会
因此而使系统崩溃。因此,在使用I/O端口前,也应该检查此I/O端口是否已有
别的程序在使用,若没有,再把此端口标记为正在使用,在使用完以后释放它。
这样需要用到如下几个函数:
int check_region(unsigned int from, unsigned int extent);
void request_region(unsigned int from, unsigned int extent,
const char *name);
void release_region(unsigned int from, unsigned int extent);
调用这些函数时 问篺rom表示所申请的I/O端口的起始地址;
extent为所要申请的从from开始的端口数;name为设备名,将会出现在
/proc/ioports文件里。check_region返回0表示I/O端口空闲,否则为正在
被使用。
在申请了I/O端口之后,就可以如下几个函数来访问I/O端口:
#include $#@60;asm/io.h$#@62;
inline unsigned int inb(unsigned short port);
inline unsigned int inb_p(unsigned short port);
inline void outb(char value, unsigned short port);
inline void outb_p(char value, unsigned short port);
其中inb_p和outb_p插入了一定的延时以适应某些慢的I/O端口。
在设备驱动程序里,一般都需要用到计时机制。在LINUX系统中,时钟是由
系统接管,设备驱动程序可以向系统申请时钟。与时钟有关的系统调用有:
#include $#@60;asm/param.h$#@62;
#include $#@60;linux/timer.h$#@62;
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
inline void init_timer(struct timer_list * timer);
struct timer_list的定义为:
struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data;
void (*function)(unsigned long d);
};
其中expires是要执行function的时间。系统核心有一个全局变量JIFFIES
表示当前时间,一般在调用add_timer时jiffies=JIFFIES+num,表示在num个
系统最小时间间隔后执行function。系统最小时间间隔与所用的硬件平台有关,
在核心里定义了常数HZ表示一秒内最小时间间隔的数目,则num*HZ表示num
秒。系统计时到预定时间就调用function,并把此子程序从定时队列里删除,
因此如果想要每隔一定时间间隔执行一次的话,就必须在function里再一次调
用add_timer。function的参数d即为timer里面的data项。
在设备驱动程序里,还可能会用到如下的一些系统函数:
#include $#@60;asm/system.h$#@62;
#define cli() __asm__ __volatile__ ("cli"::)
#define sti() __asm__ __volatile__ ("sti"::)
这两个函数负责打开和关闭中断允许。
#include $#@60;asm/segment.h$#@62;
void memcpy_fromfs(void * to,const void * from,unsigned
long n);
void memcpy_tofs(void * to,const void * from,unsigned long
n);
在用户程序调用read 、write时,因为进程的运行状态由用户态变为核心
态,地址空间也变为核心地址空间。而read、write中参数buf是指向用户程
序的私有地址空间的,所以不能直接访问,必须通过上述两个系统函数来访问用
户程序的私有地址空间。memcpy_fromfs由用户程序地址空间往核心地址空间
复制,memcpy_tofs则反之。参数to为复制的目的指针,from为源指针,n
为要复制的字节数。
在设备驱动程序里,可以调用printk来
打印一些调试信息,用法与printf
类似。printk打印的信息不仅出现在屏幕上,同时还记录在文件syslog里。
3.3、LINUX系统下的具体实现
在LINUX里,除了直接修改系统核心的源代码,把设备驱动程序加进核心里
以外,还可以把设备驱动程序作为可加载的模块,由系统管理员动态地加载它,
使之成为核心地一部分。也可以由系统管理员把已加载地模块动态地卸载下来。
LINUX中,模块可以用C语言编写,用gcc编译成目标文件(不进行链接,作
为*.o文件存在),为此需要在gcc命令行里加上-c的参数。在编译时,还应该在
gcc的命令行里加上这样的参数:-D__KERNEL__ -DMODULE。由于在不链接时,
gcc只允许一个输入文件,因此一个模块的所有部分都必须在一个文件里实现。
编译好的模块*.o放在/lib/modules/xxxx/misc下(xxxx表示核心版本,如
在核心版本为2.0.30时应该为/lib/modules/2.0.30/misc),然后用depmod -a
使此模块成为可加载模块。模块用insmod命令加载,用rmmod命令来卸载,并可
以用lsmod命令来查看所有已加载的模块的状态。
编写模块程序的时候,必须提供两个函数,一个是int init_module(void),
供insmod在加载此模块的时候自动调用,负责进行设备驱动程序的初始化工作。
init_module返回0以表示初始化成功,返回负数表示失败。另一个函数是void
cleanup_module (void),在模块被卸载时调用,负责进行设备驱动程序的清除
工作。
在成功的向系统注册了设备驱动程序后(调用register_chrdev成功后),
就可以用mknod命令来把设备映射为一个特别文件,其它程序使用这个设备的时
候,只要对此特别文件进行操作就行了。
附录:参考文献
1、 《UNIX
操作系统设计与实现》
陈华瑛、李建国主编
电子工业出版社出版
2、 《Linux Kernel Hackers Guide》
作者:Michael K. Johnson
3、 《Kernel Jorn》
作者:Alessandro Rubini & Georg Zezchwitz
连载于《Linux Journal》1996年36期
4、 Linux核心源代码(核心版本2.0.30)
5、 Linux-HOWTO