驱动简介
Linux设备驱动程序是内核的一部分,它完成以下功能:
对设备初始化和释放
把数据从内核传送到硬件和从硬件读取数据
读取应用程序传送给设备文件的数据和回送应用程序请求的数据
检测和处理设备出现的错误。
系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。Linux设备驱动程序为应用程序屏蔽了硬件细节,在应用程序看来,Linux硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。每个设备驱动程序都具有以下几个特性:
1. 具有一整套的和硬件设备通讯的例程,并且提供给操作系统一套标准的软件接口;
2. 具有一个可以被操作系统动态地调用和移除的自包含组件;
3. 可以控制和管理用户程序和物理设备之间的数据流。
驱动类型
Linux设备分为三种:字符设备、块设备和网络设备。
字符设备是指存取时没有缓存,只能顺序访问的设备,一般不能进行任意长度的I/O请求。典型的字符设备包括鼠标、串行口、键盘等。字符设备接口支持面向字符的I/0操作,它不经过系统的快速缓存,所以它们负责管理自己的缓冲区结构。下面所描述的I2C接口属于字符设备。
块设备的读/写都有缓存来支持,并且块设备必须能够随机存取,字符设备则没有这个要求。块设备主要是针对磁盘等慢速设备设计的,以免损耗过多的CPU时间来等待。
网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD Unix的Socket机制。在系统和驱动程序之间定义有专门的数据结构进行数据的传递。系统里支持对发送资料和接受资料的缓存,提供流量控制机制,提供对多协议的支持。
主次设备号
Linux给每个设备都分配一个主设备和次设备号。主设备号一般用来定义这个设备的类型。例如软驱的主设备号是2,并行端口的主设备号是6。次设备号是一个8位的数字,它指定一个特定的设备,例如一台电脑可以有2个软驱,它们都有主设备号2,但是第一个软驱的次设备号为1,而第二个软驱的次设备号为2。
在任何程序使用设备驱动程序之前,设备驱动程序应该向系统进行登记,以便系统在适当的时候调用。向系统增加一个驱动程序即给它一个主设备号,这一过程在驱动程序(模块)的初始化过程中完成,调用如下函数:
int register chrdev(major,*name ,*fops)
参数major是所请求的主设备号,name是设备的名字,它们将在/proc/devices文件中出现,fops是一个指向跳转表的指针,利用这个跳转表完成对设备函数的调用。
从系统中卸载一个模块时,应该释放主设备号。这一操作可以在cleanup_module中调用如下函数完成:
int unregister chrdev (major,*name)
参数是要释放的主设备号和相应的设备名。内核对与这个名字和设备号对应的名字进行比较,如果不同或者主设备号超出了允许的范围或是并未分配给这个设备,内核返回ENINVAL。
文件操作
Linux具有设备的无关性,它把每个设备都抽象为文件系统的一个文件。Linux为每个设备在/dev目录建立一个文件。例如,第一个软驱在文件系统中的文件名为/dev/fd。可以使用以下命令来建立设备文件:
mknod /dev/device_name device_type major_number ninor_number
其中device_name是此设备的文件,device_type是设备的类型(c表示字符设备,b表示块设备)。
Linux系统把设备当作文件一样来访问,访问文件和设备有以下函数:seek、read、write、poll、io-control、memory map、open、flush、release、check、lock等。编写设备驱动程序的主要工作就是编写子函数,并填充file-operations的各个域。并不一定要实现所有的函数,只需要实现设备必须的函数就可以了。
设备驱动使用类型为struct file_operations的一个数据结构来与上面的文件访问函数对应。一般的字符设备驱动程序适用的file-operations结构如下:
struct file_operation dev_fiops{
dev_lseek,
dev_read,
dev_write,
dev_ioctl,
dev_open,
dev_release,
};
lseek:用来修改一个文件当前的读写位置,并将新位置做为返回值返回。出错时返回一个负的返回值。
open:来为以后的操作完成作初始化准备工作的。此外,open还增加设备计数(MOD_INC_USE_COUNT),以便防止文件在关闭前模块被卸载出内核。大部分驱动程序中open完成如下工作:
检查设备相关错误,如设备未就绪或类似的硬件问题。
如果是首次打开,初始化设备。
识别次设备号,如有必要更新fop指针。
分配和填写要放在file->private_data里的数据结构。增加使用计数。驱动程序从来不知道被打开的设备名字,它仅仅知道设备号。
release:使用计数减1,释放在file->private_data中open分配的内存,在最后一次关闭操作时关闭设备。如果open没有被调用,release也不会调用。它们在系统调用间的关系保证了模块使用计数永远是一致的(MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT)。
read、write:通过这两个函数可以像使用文件那样向设备传送数据,ssize(*write)(*filp, *buff, count, *offp)和 ssize(*read)(*filp, *buff, count, *offp)其中filp是文件指针,buff是指向用户的缓冲区,count是传入数据的长度,offp是用户在文件中的位置。当成功时返回值就是写入或读取的数据长度。用write函数向打开的文件写数据,用read函数从打开的文件中读数据,完成到用户空间和来自用户空间的整个数据段的复制。
利用函数copy_to_user和copy_from_user来完成用户空间和内核空间数据的传输。
Unsigned long copy_to_user(*to, *from, count)和unsigned long copy_from_user(*to, *from, count)其中to是指向数据目的缓冲区,from是指向数据源缓冲区,count是数据的长度。当成功时,返回值就是写入或读出长度,失败返回-EFAULT。
ioctl:最常用的通过设备驱动完成控制动作的方法。ioctl的调用为驱动程序执行“命令”提供了一个与设备相关的入口点。与read和其他方法不同,ioctl是与设备相关的,允许应用程序访问被驱动硬件的特殊功能:配置设备以及进入或退出操作模式,这些控制操作通常无法通过read/write文件操作完成。
下面以I2C驱动的编写为例进行简要的说明:
驱动结构
在***系统中,I2C接口主要执行读写操作,完成与**部分的数据收发工作。
根据I2C接口所需要的功能,驱动程序的file_operations结构如下:
static struct file_operations si2c_ops={
open: si2c_open,
release: si2c_release,
ioctl: si2c_ioctl,
};
驱动中主要函数如下:
int si2c_init (void):初始化I2C控制器,在系统中注册驱动,并初始化通信处理器CPM。
static int si2c_open (struct inode *inode, struct file *file):打开设备的第一个操作,标示设备打开,并进行加一计数。
static int si2c_release (struct inode *inode, struct file *file):当驱动程序关闭时,系统调用该函数。与open函数对应。
static int si2c_ioctl (struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg):应用程序对驱动的所有操作都通过ioctl来调用。
static void si2c_interrupt (void *dev_id):负责处理收发数据和出错时产生的中断。
static void si2c_reset_params (volatile iic_t *iip):重新设置I2C CPM中控制通道的参数。
static void si2c_force_close (void):使用CPM_CR_CLOSE_RXBD命令关闭I2C通信。
extern ssize_t si2c_read (si2c_request_t *req):读I2C总线上的数据。
extern ssize_t si2c_write(si2c_request_t *req):写I2C总线上的数据。
应用程序通过si2c_ioctl来对I2C控制器进行读写操作,由si2c_ioctl分别对读写函数进行调用。进行读写操作时,I2C控制器使用中断来与驱动进行数据交互。
驱动实现
流程图如图
安装驱动程序时,系统会调用初始化函数si2c_init( )进行初始化工作。在初始化程序中,对I2C设备进行注册,使用register_chrdev( ) 返回主设备号。
I2C接口使通用I/O的PB26、PB27做为信号,在初始化I2C时必需对PB口的状态寄存器进行配置,使这两根信号线实现I2C接口功能。
接下来,在CPM的RAM中为I2C控制器的2个发送缓冲标识符和2个接收缓冲标识符申请空间。使用m8xx_cpm_dpalloc( )函数来申请地址空间,返回申请到的地址。将申请到的缓冲区地址指针分别赋给指针tbase和rbase。
在使用I2C控制器前,必须先配置好CPM ram中的I2C控制器的相关参数,不用的参数置零。做为从设备,多址板使用I2C地址为0x34。在初始化过程中,还需禁止中断,防止影响初始化工作。
图I2C驱动流程及si2c_ioctl函数结构
应用程序调用si2c_ioctl( )函数来控制驱动。分别使用I2C_CMD_READ和I2C_CMD_WRITE执行读写命令。在ioctl中这两个命令会分别调用si2c_read( )和si2c_write( )函数。驱动程序与用户缓冲区交互使用函数copy_from_user( )和copy_to_user( ),前者从用户缓冲区读数据,后者将数据复制到用户数据缓冲区。
读数据:因为I2C控制器做为从设备,在进行读操作之前,只需要初始化接收缓冲标识符,并准备好接收缓冲区,这里的接收缓冲区由ioctl函数通过参数传入。在读函数打开中断,启动I2C控制器进行读操作之后,等待中断产生,待中断返回后,检查状态寄存器是否出错,进行相应操作后返回状态值。
写数据:在启动一次写操作前,驱动程序需预先配置好发送描述符,将描述符指向的ioctl传入的发送缓冲区。些函数打开中断,启动I2C控制器后,等待中断发生,待中断返回后,检查状态寄存器,并返回状态值。
I2C控制器使用中断与驱动通信,中断由Linux系统管理,在Linux系统里,对中断的处理是属于系统核心的部分,读写函数与中断程序交互的操作由信号量实现。读写函数通过interruptible_sleep_on (&iic_wait)进入等待队列,等待中断发生。进入中断处理程序后,将控制器的中断标志位清零,并通过wake_up_interruptible (&iic_wait)唤醒读写函数,返回等待的位置。
驱动调用结束后,系统会使用si2c_release( )函数来进行减一操作并关闭I2C控制器。当使用rmmod name命令卸载驱动程序时,系统会调用cleanup_module( ),释放申请的存储空间,注销驱动设备。
驱动程序编写完毕,编写Makefile文件,具体格式如下:
KERNELDIR = /home/adhoc/linux-2.4.4
LD = powerpc-linux-gcc
CFLAGS = -D__KERNEL__ -I/home/adhoc/linux-2.4.4/include -Wall
-Wstrict-prototypes -O2 -fomit-frame-pointer -fno-strict-aliasing
-D__powerpc__ -fsigned-char -msoft-float -pipe -fno-builtin -ffixed-r2
-Wno-uninitialized -mmultiple -mstring -mcpu=860 -DMODULE
-DMODVERSIONS -include
/home/adhoc/linux-2.4.4/include/linux/modversions.h
all: clean i2c.o
i2c.o: i2c.c
$(LD) $(CFLAGS) -c $^ -o $@
clean:
rm -f *.o *. core
Makefile完成之后,在驱动文件所在目录下运行make命令,编译生成可执行文件i2c.o, 使用mknod /dev/i2c c 42 0在系统/dev目录下建立设备文件节点,驱动主设备号42,次设备号0,然后用insmod命令将驱动安装在系统中,供应用程序调用。
调试过程
设备驱动程序仅仅处理硬件,如何使用硬件的问题属于应用程序。要测试驱动程序的正确性,就应该编写相应的应用程序,对驱动的各种功能进行测试。
在Linux系统中,应用程序通过open、read、write、ioctl等命令来调用驱动程序。下面以一段调用驱动写操作的应用程序为例,给出系统对应用程序的响应过程。
int main(){
int file_desc;
si2c_request_t *i2c_data,*temp;
int len,i;
i2c_data=(si2c_request_t *)malloc(sizeof(si2c_request_t));
temp=(si2c_request_t *)malloc(sizeof(si2c_request_t));
i2c_data->dlen=1;
for(i=0;idlen;i++) {
i2c_data->data=i;
}
printf("start test .../n");
file_desc = open("/dev/i2c",O_RDWR);
if(file_desc<0){
printf("Can't open device file:%s/n",DEVICE_NAME);
exit(-1);
}
ioctl(file_desc,I2C_CMD_WRITE,i2c_data);
len=i2c_data->dlen;
printf("len=%d./n",len);
close(file_desc);
free(i2c_data);
free(temp);
return 0;
}
1. 用户程序使用open打开设备节点文件,这时操作系统内核知道该驱动程序工作了,就调用fops方法中的open函数进行相应的工作。open方法一般返回的是文件标示符,实际上并不是直接对它进行操作的,而是有操作系统的系统调用在背后工作。
2. 当用户使用write函数操作设备文件时,操作系统调用syswrite函数,该函数首先通过文件标识符得到设备节点文件对应的inode指针和flip指针。inode指针中有设备号信息,能够告诉操作系统应该使用哪一个设备驱动程序,flip指针中有fops信息,可以告诉操作系统相应的fops方法函数在哪里可以找到。
3. 然后这时syswrite才会调用驱动程序中的write方法来对设备进行写的操作。其中1是在用户空间进行的,2-3是在内核空间进行的。通过系统调用sys_write将用户的write函数和操作系统的write函数联系在了一起.
在多址硬件系统中,I2C接口作为从属设备,而从属设备必须有主设备的驱动才能工作,因此要测试驱动程序,还必须模拟出一个主设备。我们用单片机来模拟主设备的工作情况。在测试过程中,可以使用printf函数将驱动中收到或发送的数据打印出来,方便观察和调试。