一.Linux驱动程序插口
系统调用是操作系统内核与应用程序之间的插口,设备驱动程序则是操作系统内核与机器硬件的插口。几乎所有的系统操作最终映射到化学设备,不仅CPU、内存和少数其它设备,所有的设备控制操作都由该设备特殊的可执行代码实现,此代码就是设备驱动程序。操作系统内核须要访问两类主要设备:字符设备和块设备。与此相关主要有两类设备驱动程序,字符设备驱动程序和块设备驱动程序。Linux(也是所有UNIX)的基本原理之一是:系统企图使它对所有各种设备的输入、输出看上去就好像对普通文件的输入、输出一样。设备驱动程序本身具有文件的外部特点,它们都能使用象
open(),close(),read(),write()等系统调用。为使设备的存取能象文件一样处理,所有设备在目录中应有对应的文件名称,才可使用有关系统调用。
一般Linux驱动程序插口分为如下四层:
1).应用程序进程与内核的插口;
2).内核与文件系统的插口;
3).文件系统与设备驱动程序的插口;
4).设备驱动程序与硬件设备的插口。
二.驱动程序文件操作数据结构
每位驱动程序都有一个file-operation的数据结构,包含指向驱动程序内部函数的表针。file-operation的数据结构为:
structfile-operation{
int(*lseek)();
int(*read)();
int(*write)();
int(*readdir)();
int(*select)();
int(*ioctl)();
int(*mmap)();
int(*open)();
int(*close)();
int(*release)();
int(*fsync)();
int(*fasync)();
int(*check-media-change)();
int(*revalidate)();
内核中有两个表,一个用于字符设备驱动程序,一个用于块设备驱动程序。这两个表用于保存指向file-operation数据结构的表针,驱动程序内部函数的地址保存在这一结构。内核用主设备号作为索引访问file-operation结构,可以访问驱动程序子程序地址。SBS617设备采用了PCI总线字符设备的驱动程序实现方法。完成了设备驱动程序,经GNU软件编译,链接,形成一可加载模块,可以用于动态放入Linux操作系统内核,也可以在须要时从内核中卸除。
三.file_operations介绍
在结构file_operations里,强调了设备驱动程序所提供的入口点位置,分别是:
(1)lseek,联通文件表针的位置,似乎只能用于可以随机存取的设备。
(2)read,进行读操作,参数buf为储存读取结果的缓冲区,count为所要读取的数据宽度。返回值为负表示读取操作发生错误,否则返回实际读取的字节数。对于字符型,要求读取的字节数和返回的实际读取字节数都必须是inode->i_blksize的的倍数。
(3)write,进行写操作,与read类似。
(4)readdir,取得下一个目录入口点,只有与文件系统相关的设备驱动程序才使用。
(5)selec,进行选择操作,假如驱动程序没有提供select入口,select操作将会觉得设备早已打算好进行任何的I/O操作。
(6)ioctl,驱动程序特殊控制入口点,进行读、写以外的其它操作,参数cmd为自定义的命令。这是很有意思的部份,然后我会详细介绍;
(7)mmap,用于把设备的内容映射到地址空间,通常只有块设备驱动程序使用。
(8)open,打开设备打算进行I/O操作。返回0表示打开成功,返回正数表
示失败。假如驱动程序没有提供open入口,则只要/dev/driver文件存
在就觉得打开成功。
(9)release,即close操作。
设备驱动程序所提供的入口点,在设备驱动程序初始化的时侯向系统进行登记,便于系统在适当的时侯调用。
四.PCI字符设备驱动程序
要设计PCI设备驱动程序,必须进一步结合硬件设备和PCI总线的特点。设计PCI设备驱动程序的重要任务是寻找相应的硬件并实现对它的访问。作为外围设备的硬件必须响应三种地址空间的访问,即显存,IO,寄存器地址空间。前两种地址空间可以为PCI总线上的所有设备共享。寄存器空间占用化学地址,可以通过特殊的函数来访问配置寄存器。一旦可以访问配置寄存器,设备驱动程序就可以访问硬件了。每位设备的PCI配置寄存器均由256Bytes构成,其中64Bytes是标准化的,4Bytes标示了一个惟一的函数ID,通过这个ID驱动程序就可以定位该设备。
存取系统中的字符设备和存取系统文件一样。应用程序使用标准的系统调用来打开、读写和关掉设备,如同使用一个文件-样。当字符设备初始化时,通过向chrdevs字段中添加一个入口,设备驱动程序在系统内核中注册。chrdevs链表由device_struct数据结构组成。设备的主设备号拿来作为此chrdevs的索引,由于一个设备的主设备号是固定的。
LINUX系统里,通过调用register_chrdev向系统注册字符型设备驱动程序。register_chrdev定义为:
#includelinux/fs.h
#includelinux/errno.h
intregister_chrdev(unsignedintmajor,constchar*name,
structfile_operations*fops);
其中,major是为设备驱动程序向系统申请的主设备号,倘若为0则系统因此驱动程序动态地分配一个主设备号。name是设备名。fops就是上面所说的对各个调用的入口点的说明。此函数返回0表示成功。返回-EINVAL表示申请的主设备号非法,通常来说是主设备号小于系统所容许的最大设备号。返回-EBUSY表示所申请的主设备号正在被其它设备驱动程序使用。若果是动态分配主设备号成功,此函数将返回所分配的主设备号。
五.PCI设备启动与测量
PC显卡BIOS在系统启动时,可以手动检查PCI设备并配置设备的每一地址区。当驱动程序访问设备时,它的显存、I/O地址空间早已映射到进程的地址空间了。在驱动程序init_module()中,通过调用函数pcibios_find_device()函数返回设备在总线上的位置及函数表针,其中的包含文件及函数原型为:
#includeLinux/pci.h
#includeLinux/config.h
#includeLinux/bios32.h
intpcibios_find_device(unsignedshortvendor,
unsignedshortid,unsignedshortindex,
unsignedchar*bus,unsignedshort*function)
六.地址空间访问
在设备驱动程序检查到设备以后,一般要从三个地址空间读写数据,其中寄存器空间的读写尤为重要,由于只有通过它驱动程序才可能找到设备显存和I/O空间的映射地址。设备驱动程序通过调用以下函数实现寄存器空间的访问,其中的包含文件及函数原型为:
#includeLinux/bios32.h
intpcibios_read_config_byte(unsignedcharbus,
unsignedcharfunction,
unsignedcharwhere,
unsignedcharb*ptr)
intpcibios_write_config_byte(unsignedcharbus,
unsignedcharfunction,
unsignedcharwhere,
unsignedcharb*ptr)
类似的还有:
pcibios_read_config_word(),pcibios_write_config_word(),
pcibios_read_config_dword(),pcibios_write_config_dword()调用。
PCI设备最多有6个地址区,类型可以为显存区或I/O区。插口板可以通过配置寄存器的PCI_BASE_ADDRESS_0到PCI_BASE_ADDRESS_5来报告各地址区的实际地址位置。显存、IO空间的访问通过inb(),memcpy()等调用。其实可以通过pcibios_read_config_byte(),pcibios_write_config_byte()来访问配置寄存器的相应基地址值。
七.中断处理
对中断的处理是属于系统核心的部份,PC显卡BIOS为多数设备分配了一个惟一的中断号,在配置寄存器中保存,设备驱动程序通过pcibios_read_config_byte()函数读取相应的值,格式为:
xxx_irq=pcibios_read_config_byte(pci_bus,pci_device_fn,
PCI_INTERRUPT_LINE,
&pci_cofig->int_line)
操作系统中有中断寄存器,将特定的中断恳求与中断处理函数联系在一起,当中断发生时调用相应的中断处理函数处理。Linux操作系统下可用request_irq(),free_irq()实现中断的恳求,释放,其中包含文件及方式为:
#includeLinux/sched.h
intrequest_irq(unsignedintirq,
void(*handler)(intirq,voiddev_id,structpt_regs*regs),
unsignedlongflags,
constchar*device,
void*dev_id);
voidfree_irq(unsignedintirq,void*dev_id);
参数irq表示所要申请的硬件中断号。handler为向系统登记的中断处理子程序,中断形成时由系统来调用linux内核中的串口驱动,调用时所带参数irq为中断号,dev_id为申请时告诉系统的设备标示,regs为中断发生时寄存器内容。device为设备名,将会出现在/proc/interrupts文件里。
flag是申请时的选项,它决定中断处理程序的一些特点,有两种方法写中断方法设备驱动程序:即快中断方法和定时等待形式。采取快中断形式须要将request_irq()的第三个type类型参数设为SA_INTERRUPT。正常中断与快中断的区别在于:从正常中断返回时,内核可以借助机会调度更优先的进程执行;而快中断不进行调度立刻恢复被中断程序的执行.
在LINUX系统中,中断可以被不同的中断处理程序共享,这要求每一个共享此中断的处理程序在申请中断时在flags里设置SA_SHIRQ,这种处理程序之间以dev_id来分辨。假如中断由某个处理程序独占,则dev_id可以为NULL。request_irq返回0表示成功,返回-INVAL表示irq>15或handler==NULL,返回-EBUSY表示中断早已被占用且不能共享。
中断处理函数方式为:
voidxxx_irq_handler(intxxx_irq,void*dev_id,
structpt_regs*regs)
八.特殊控制函数ioctl()
ioctl()具有设备特殊性,不同于read(),write(),在于它容许应用程序访问、配置设备,并步入可能的操作模式。一般的read()、write()不能使用这种控制操作。ioctl()可以控制I/O通道。设备驱动的一个特征是要与其它设备硬件交换读/写的数据并须要同步控制。
多数的ioctl()由一系列的switch句子组成,ioctl()命令及操作选择考虑到硬件的特点和实际要实现的功能。写ioctl()程序之前,应选择相应的命令,不应当简单使用1-N的数字。选择ioctl()的命令有以下的考虑:
·首先命令码在系统中应当惟一,以防止与其它设备冲突,每位命令码应由多个比特域构成。
·参考两个文件来帮助选择ioctl()的命令,include/asm/ioctl.h及Documentation/ioctl_number.txt有如下定义:
命令码有四个8比特组,其相应取值的宏定义及涵义如下表:
命令码取值宏定义及涵义
比特组名称取值宏定义涵义
type_IOC_TYEBITS表示每位驱动程序惟一的类型标示
number_IOC_NRBITS表示序列号
direction_IOC_NONE,_IOC_READ,
_IOC_WRITE,_IOC_READ|WRITE表示数据传输的方向
size_IOC_SIZEBITS表示传输数据的大小
_IO(type,nr);
_IOR(type,nr,size);
_IOW(type,nr,size);
_IOC_DIR(nr);
_IOC_TYPE(nr);
_IOC_NR(nr);
_IOC_SIZE(nr);
这种设置与具体的硬件功能有关,可以参考有关的硬件指南。通过以上方法可以设置命令、获得设备参数及实现控制操作,完成设备驱动程序的重要功能。
对于设备驱动程序,ioctl()函数十分重要,用户可以通过它来控制设备函数,获取状态信息,进行数据的读写。
ioctl()在用户空间的方式为:
int(*ioctl)(structinode*inode,structfile*file,
unsignedintcmd,unsignedlongarg)
其中cmd相当于一个选择码,取决于使用的特殊控制命令,cmd命令一般在头文件中申明。直接的调用的格式为:
temp=ioctl(fd,XX_xxxx,param*);
/"fd/"是设备文件句柄。XX_xxxx是控制码。Param是一个参数结构的表针linux软件工程师,当调用ioctl()时,须要理解一些特殊参数结构,可以参考下边的四个表格。返回值0表示成功,-1失败。
九.调用Linux内核函数
Linux有许多内核函数可以调用。诸如;
1)memcpy_fromfs(*toptr,*fromptr,sizeof());
//用于从文件系统传输数据
2)memcpy_tofs(*toptr,*fromptr,sizeof());
//用于将数据传输到文件系统
#includeasm/segment.h
voidmemcpy_fromfs(void*toptr,constvoid*fromptr,unsignedlongn);
voidmemcpy_tofs(void*toptr,constvoid*fromptr,unsignedlongn);
在用户程序调用read、write时,由于进程的运行状态由用户态变为核态度,地址空间也变为核心地址空间。而read、write中参数buf是指向用户程序的私有地址空间的,所以不能直接访问,必须通过上述两个系统函数来访问用户程序的私有地址空间。memcpy_fromfs由用户程序地址空间往核心地址空间复制,memcpy_tofs则反之。参数toptr为复制的目的表针,fromptr为源表针,n为要复制的字节数。
3)ptr=vmalloc(sizeof());//动态分配显存
4)vfree(ptr);//动态释放显存
5)vremap(xxx_mapping[chn].pci_addr,
xxx_mapping[chn].len);
//映射PCI地址,
chn=current_map_chn.
6)作为系统核心的一部份,设备驱动程序在申请和释放显存时不是调用malloc
和free,而调用kmalloc和kfreewps for linux,定义为:
#includelinux/kernel.h
void*kmalloc(unsignedintlen,intpriority);
voidkfree(void*ptr);
参数len为希望申请的字节数,ptr为要释放的显存表针。priority为分配显存操作的优先级,即在没有足够空闲显存时怎么操作,通常用GFP_KERNEL。
7)与中断和显存不同,使用一个没有申请的I/O端口不会使CPU形成异常,也
就不会引起例如/"segmentationfault/"一类的错误发生。任何进程都可以访问
任何一个I/O端口。此时系统难以保证对I/O端口的操作不会发生冲突,甚至会为此而使系统崩溃。为此,在使用I/O端口前,也应当检测此I/O端口是否已有别的程序在使用,若没有,再把此端口标记为正在使用,在使用完之后释放它。
这样须要用到如下几个函数:
intcheck_region(unsignedintfrom,unsignedintextent);
voidrequest_region(unsignedintfrom,unsignedintextent,
constchar*name);
voidrelease_region(unsignedintfrom,unsignedintextent);
调用那些函数时的参数为:from表示所申请的I/O端口的起始地址;
extent为所要申请的从from开始的端口数;name为设备名,将会出现在
/proc/ioports文件里。check_region返回0表示I/O端口空闲,否则为正在
被使用。
在申请了I/O端口以后,就可以如下几个函数来访问I/O端口:
#includeasm/io.h
inlineunsignedintinb(unsignedshortport);
inlineunsignedintinb_p(unsignedshortport);
inlinevoidoutb(charvalue,unsignedshortport);
inlinevoidoutb_p(charvalue,unsignedshortport);
其中inb_p和outb_p插入了一定的延时以适应个别慢的I/O端口。
9)在设备驱动程序里,通常都须要用到计时机制。在LINUX系统中,时钟是
由系统接管linux内核中的串口驱动,设备驱动程序可以向系统申请时钟。与时钟有关的系统调用有:
#includeasm/param.h
#includelinux/timer.h
voidadd_timer(structtimer_list*timer);
intdel_timer(structtimer_list*timer);
inlinevoidinit_timer(structtimer_list*timer);
structtimer_list的定义为:
structtimer_list{
structtimer_list*next;
structtimer_list*prev;
unsignedlongexpires;
unsignedlongdata;
void(*function)(unsignedlongd);
};
其中expires是要执行function的时间。系统核心有一个全局变量JIFFIES
表示当前时间,通常在调用add_timer时jiffies=JIFFIES+num,表示在num个
系统最小时间间隔后执行function。系统最小时间间隔与所用的硬件平台有关,在核心里定义了常数HZ表示1秒内最小时间间隔的数量,则num*HZ表示num秒。系统计时到预定时间就调用function,并把此子程序从定时队列里删掉,因而假如想要每隔一定时间间隔执行一次的话,就必须在function里再一次调用add_timer。function的参数d即为timer上面的data项。
10)在设备驱动程序里,还可能会用到如下的一些系统函数:
#includeasm/system.h
#definecli()__asm____volatile__(/"cli/"::)
#definesti()__asm____volatile__(/"sti/"::)
这两个函数负责打开和关掉中断容许。
11)在设备驱动程序里,可以调用printk来复印一些调试信息,用法与printf类似。
printk复印的信息除了出现在屏幕上,同时还记录在文件syslog里。