Linux 设备驱动程序框架
一、linux 的设备驱动程序与外界的接口可以分为三个部分:
1.驱动程序与操作系统内核的接口。通过 file_operations(include/linux/fs.h)数据
结构来完成的。
2.驱动程序与系统引导的接口。这部分利用驱动程序对设备进行初始化。
3.驱动程序与设备的接口。这部分描述了驱动程序如何与设备进行交互,与具体的设备
密切相关。
二、根据功能划分,设备驱动程序的代码有以下几部分:
1.驱动程序的注册和注销。
2.设备的打开和释放。
3.设备的读写操作。
4.设备的控制操作。
5.设备的中断和轮询处理。
三、驱动程序的注册和注销:
设备驱动程序可以在系统启动的时候初始化,也可以在需要的时候动态加载。字符设备
的 初 始 化 由 chr_dev_init() 完 成 , 包 括 对 内 存
(devfs_register_chrdev(MEM_MAJOR,"mem",&memory_fops)),终端(tty_init()),打印机
(lp_init()),鼠标(misc_init())等字符设备的初始化。
块设备初始化由 blk_dev_init()完成,这包括对 IDE 硬盘(ide_init()),软盘
(floppy_init()),光驱等块设备的初始化。
每个字符设备或是块设备的初始化都是通过 devfs_register_chrdev() 或是
devfs_register_blkdev()向内核注册。在关闭字符设备或是块设备时,还需要通过
devfs_unregister_chrdev()或是 devfs_unregister_blkdev()从内核中注销设备。
四、设备的打开和释放:
打开设备是由 open()来完成的。例如,打印机是用 lp_open()打开的,而硬盘是用
hd_open()打开的。在大部分设备驱动程序中,open 完成如下工作:
1.增加设备的是用计数。
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
2.检查设备的相关错误,如设备尚未准备好或是类似硬件的问题。
3.检查是首次打开,则初始化设备。
4.识别次设备号,如有必要则更新 f_op 指针。
5.如果需要,分配且设置要放在 filp->private_data 里的数据结构。
释放设备由 release()来完成,例如释放打印机是用 lp_release(),而释放终端设备是
用 tty_release()。释放设备的一般步骤包括:
1.释放在 filp->private_data 中的 open 分配的内存。
2.如果是最后一次释放,则关闭设备。
3.递减设别的使用计数。
五、设备的读写操作:
字符设备使用各自的 read()和 write()来进行数据读写。例如,对虚拟终端的读写是通
过 vcs_read()和 vcs_write()来进行数据读写的。
块设备使用通用的 generic_file_read()和 generic_file_write()来进行数据读写。这两个
通用函数向请求表添加读写请求,内核可以通过 ll_rw_block()优化请求顺序。由于是对内
存缓冲区而不是设备进行操作的,因而可以加快读写请求。如果内存缓冲区内没有要读入的
数据或是要将写请求写入设备,那么就要真正的执行数据传输。这是通过数据结构
request_queue 和 request_fn()来完成(include/linux/blkdev.h)。
六、设备的控制操作:
除了读写操作,有时还要控制设备。这可以通过设备驱动程序中的 ioctl()来完成。例
如 IDE 硬盘的控制可以通过 hd_ioctl(),对光驱的控制可以通过 cdrom_ioctl()。
与读写操作不同,ioctl()的用法与具体设备密切相关。以软驱的 floppy_ioctl()为例
(drivers/block/floppy.c):
static int fd_ioctl(struct inode *inode,
struct file *filp,
unsigned int cmd,
unsigned long param);
其中,cmd 的取值及含义都是与软驱有关的,比如,FDEJECT 表示弹出软盘。
除了 ioctl(),设备驱动程序还可能有其他控制函数,比如 llseek()等。
七、设备的轮询和中断处理:
对于不支持中断的设备,读写时需要轮询设备状态,以及是否需要继续进行数据传输。
例如,打印机。如果设备支持中断,则可按照中断方式进行。
由于嵌入式设备由于硬件种类非常丰富,在默认的内核发布版中不一定包括所有驱动程序。
所以进行嵌入式 Linux 系统的开发,很大的工作量是为各种设备编写驱动程序。除非系统不
使用操作系统,程序直接操纵硬件。嵌入式 Linux 系统驱动程序开发与普通 Linux 开发没有
区别。可以在硬件生产厂家或者 Internet 上寻找驱动程序,也可以根据相近的硬件驱动程序
来改写,这样可以加快开发速度。实现一个嵌入式 Linux 设备驱动的大致流程如下:
(1)查看原理图,理解设备的工作原理。一般嵌入式处理器的生产商提供参考电路,也可以
根据需要自行设计。
(2)定义设备号。设备由一个主设备号和一个次设备号来标识。主设备号惟一标识了设备类
型,即设备驱动程序类型,它是块设备表或字符设备表中设备表项的索引。次设备号仅由设
备驱动程序解释,区分被一个设备驱动控制下的某个独立的设备。
(3)实现初始化函数。在驱动程序中实现驱动的注册和卸载。
(4)设计所要实现的文件操作,定义 file_operations 结构。
(5)实现所需的文件操作调用,如 read、write 等。
(6)实现中断服务,并用 request_irq 向内核注册,中断并不是每个设备驱动所必需的。
(7)编译该驱动程序到内核中,或者用 insmod 命令加载模块。
(8)测试该设备,编写应用程序,对驱动程序进行测试
包括设备注册在内,设备驱动的初始化函数主要完成的功能是有以下 5项。
(1)对驱动程序管理的硬件进行必要的初始化。
对硬件寄存器进行设置。比如,设置中断掩码,设置串口的工作方式、并口的数据方向等。
(2)初始化设备驱动相关的参数。
一般说来,每个设备都要定义一个设备变量,用以保存设备相关的参数。在这一步骤里对设
备变量中的项进行初始化。
(3)在内核注册设备。
调用 register_chrdev()函数来注册设备。
(4)注册中断。
如果设备需要 IRQ 支持,则要使用 request_irq()函数注册中断。
(5)其他初始化工作。
初始化部分一般还负责给设备驱动程序申请包括内存、时钟、I/O 端口等在内的系统资源,
这些资源也可以在 open 子程序或者其他地方申请。这些资源不用时,应该释放,以利于资源
的共享。
若驱动程序是内核的一部分,初始化函数则要按如下方式声明:
int __init chr_driver_init(void);
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
其中__init 是必不可少的,在系统启动时会由内核调用 chr_driver_init,完成驱动程序的
初始化。
当驱动程序是以模块的形式编写时,则要按照如下方式声明:
int init_module(void)
当运行后面介绍的 insmod 命令插入模块时,会调用 init_module 函数完成初始化工作。
设备驱动开发的基本函数:
1.I/O 口函数
无论驱动程序多么复杂,归根结底,无非还是向某个端口或者某个寄存器位赋值,这个值只
能是 0 或 1。接收值的就是 I/O 口。与中断和内存不同,使用一个没有申请的 I/O 端口不会
使处理器产生异常,也就不会导致诸如“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);
调用这些函数时的参数为:
? from 表示所申请的 I/O 端口的起始地址;
? extent 为所要申请的从 from 开始的端口数;
? name 为设备名,将会出现在/proc/ioports 文件里;
? check_region 返回 0表示 I/O 端口空闲,否则为正在被使用。
在申请了 I/O 端口之后,可以借助 asm/io.h 中的如下几个函数来访问 I/O 端口:
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 端口。
2.时钟函数
在设备驱动程序中,一般都需要用到计时机制。在 Linux 系统中,时钟是由系统接管的,设
备驱动程序可以向系统申请时钟。与时钟有关的系统调用有:
#include
#include
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;
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
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 项。
3.内存操作函数
作为系统核心的一部分,设备驱动程序在申请和释放内存时不是调用 malloc 和 free,而代
之以调用 kmalloc 和 kfree,它们在 linux/kernel.h 中被定义为:
void * kmalloc(unsigned int len, int priority);
void kfree(void * obj);
参数 len 为希望申请的字节数,obj 为要释放的内存指针。priority 为分配内存操作的优先
级,即在没有足够空闲内存时如何操作,一般由取值 GFP_KERNEL 解决即可。
4.复制函数
在用户程序调用 read、write 时,因为进程的运行状态由用户态变为核心态,地址空间也变
为核心地址空间。由于 read、write 中参数 buf 是指向用户程序的私有地址空间的,所以不
能直接访问,必须通过下面两个系统函数来访问用户程序的私有地址空间。
#include
void memcpy_fromfs(void * to,const void * from,unsigned long n);
void memcpy_tofs(void * to,const void * from,unsigned long n);
memcpy_fromfs 由用户程序地址空间往核心地址空间复制,memcpy_tofs 则反之。参数 to为
复制的目的指针,from 为源指针,n为要复制的字节数。
在设备驱动程序里,可以调用 printk 来打印一些调试信息,printk 的用法与 printf 类似。
printk 打印的信息不仅出现在屏幕上,同时还记录在文件 syslog 里。
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔
Administrator
铅笔