最新电子科技大学嵌入式linux设备驱动程序的开发.docx
- 文档编号:9682048
- 上传时间:2023-05-20
- 格式:DOCX
- 页数:18
- 大小:24.73KB
最新电子科技大学嵌入式linux设备驱动程序的开发.docx
《最新电子科技大学嵌入式linux设备驱动程序的开发.docx》由会员分享,可在线阅读,更多相关《最新电子科技大学嵌入式linux设备驱动程序的开发.docx(18页珍藏版)》请在冰点文库上搜索。
最新电子科技大学嵌入式linux设备驱动程序的开发
Linux将所有外部设备看成是一类特殊文件,称之为“设备文件”,如果说系统调用是Linux内核和应用程序之间的接口,那么设备驱动程序则可以看成是Linux内核与外部设备之间的接口。
设备驱动程序向应用程序屏蔽了硬件在实现上的细节,使得应用程序可以像操作普通文件一样来操作外部设备。
1.字符设备和块设备
Linux抽象了对硬件的处理,所有的硬件设备都可以像普通文件一样来看待:
它们可以使用和操作文件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O控制操作,而驱动程序的主要任务也就是要实现这些系统调用函数。
Linux系统中的所有硬件设备都使用一个特殊的设备文件来表示,例如,系统中的第一个IDE硬盘使用/dev/hda表示。
每个设备文件对应有两个设备号:
一个是主设备号,标识该设备的种类,也标识了该设备所使用的驱动程序;另一个是次设备号,标识使用同一设备驱动程序的不同硬件设备。
设备文件的主设备号必须与设备驱动程序在登录该设备时申请的主设备号一致,否则用户进程将无法访问到设备驱动程序。
在Linux操作系统下有两类主要的设备文件:
一类是字符设备,另一类则是块设备。
字符设备是以字节为单位逐个进行I/O操作的设备,在对字符设备发出读写请求时,实际的硬件I/O紧接着就发生了,一般来说字符设备中的缓存是可有可无的,而且也不支持随机访问。
块设备则是利用一块系统内存作为缓冲区,当用户进程对设备进行读写请求时,驱动程序先查看缓冲区中的内容,如果缓冲区中的数据能满足用户的要求就返回相应的数据,否则就调用相应的请求函数来进行实际的I/O操作。
块设备主要是针对磁盘等慢速设备设计的,其目的是避免耗费过多的CPU时间来等待操作的完成。
一般说来,PCI卡通常都属于字符设备。
所有已经注册(即已经加载了驱动程序)的硬件设备的主设备号可以从/proc/devices文件中得到。
使用mknod命令可以创建指定类型的设备文件,同时为其分配相应的主设备号和次设备号。
例如,下面的命令:
[root@garyroot]#mknod/dev/lp0c60
将建立一个主设备号为6,次设备号为0的字符设备文件/dev/lp0。
当应用程序对某个设备文件进行系统调用时,Linux内核会根据该设备文件的设备类型和主设备号调用相应的驱动程序,并从用户态进入到核心态,再由驱动程序判断该设备的次设备号,最终完成对相应硬件的操作。
2.设备驱动程序接口
Linux中的I/O子系统向内核中的其他部分提供了一个统一的标准设备接口,这是通过include/linux/fs.h中的数据结构file_operations来完成的:
structfile_operations{
structmodule*owner;
loff_t(*llseek)(structfile*,loff_t,int);
ssize_t(*read)(structfile*,char*,size_t,loff_t*);
ssize_t(*write)(structfile*,constchar*,size_t,loff_t*);
int(*readdir)(structfile*,void*,filldir_t);
unsignedint(*poll)(structfile*,structpoll_table_struct*);
int(*ioctl)(structinode*,structfile*,unsignedint,unsignedlong);
int(*mmap)(structfile*,structvm_area_struct*);
int(*open)(structinode*,structfile*);
int(*flush)(structfile*);
int(*release)(structinode*,structfile*);
int(*fsync)(structfile*,structdentry*,intdatasync);
int(*fasync)(int,structfile*,int);
int(*lock)(structfile*,int,structfile_lock*);
ssize_t(*readv)(structfile*,conststructiovec*,unsignedlong,loff_t*);
ssize_t(*writev)(structfile*,conststructiovec*,unsignedlong,loff_t*);
ssize_t(*sendpage)(structfile*,structpage*,int,size_t,loff_t*,int);
unsignedlong(*get_unmapped_area)(structfile*,unsignedlong,unsignedlong,unsignedlong,unsignedlong);
};
当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过file_operations结构访问驱动程序提供的函数。
例如,当应用程序对设备文件执行读操作时,内核将调用file_operations结构中的read函数。
2.设备驱动程序模块
Linux下的设备驱动程序可以按照两种方式进行编译,一种是直接静态编译成内核的一部分,另一种则是编译成可以动态加载的模块。
如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态地卸载,不利于调试,所有推荐使用模块方式。
从本质上来讲,模块也是内核的一部分,它不同于普通的应用程序,不能调用位于用户态下的C或者C++库函数,而只能调用Linux内核提供的函数,在/proc/ksyms中可以查看到内核提供的所有函数。
在以模块方式编写驱动程序时,要实现两个必不可少的函数init_module()和cleanup_module(),而且至少要包含和两个头文件。
在用gcc编译内核模块时,需要加上-DMODULE-D__KERNEL__-DLINUX这几个参数,编译生成的模块(一般为.o文件)可以使用命令insmod载入Linux内核,从而成为内核的一个组成部分,此时内核会调用模块中的函数init_module()。
当不需要该模块时,可以使用rmmod命令进行卸载,此进内核会调用模块中的函数cleanup_module()。
任何时候都可以使用命令来lsmod查看目前已经加载的模块以及正在使用该模块的用户数。
设备驱动程序结构
了解设备驱动程序的基本结构(或者称为框架),对开发人员而言是非常重要的,Linux的设备驱动程序大致可以分为如下几个部分:
驱动程序的注册与注销、设备的打开与释放、设备的读写操作、设备的控制操作、设备的中断和轮询处理。
驱动程序的注册与注销
向系统增加一个驱动程序意味着要赋予它一个主设备号,这可以通过在驱动程序的初始化过程中调用register_chrdev()或者register_blkdev()来完成。
而在关闭字符设备或者块设备时,则需要通过调用unregister_chrdev()或unregister_blkdev()从内核中注销设备,同时释放占用的主设备号。
设备的打开与释放
打开设备是通过调用file_operations结构中的函数open()来完成的,它是驱动程序用来为今后的操作完成初始化准备工作的。
在大部分驱动程序中,open()通常需要完成下列工作:
1.检查设备相关错误,如设备尚未准备好等。
2.如果是第一次打开,则初始化硬件设备。
3.识别次设备号,如果有必要则更新读写操作的当前位置指针f_ops。
4.分配和填写要放在file->private_data里的数据结构。
5.使用计数增1。
释放设备是通过调用file_operations结构中的函数release()来完成的,这个设备方法有时也被称为close(),它的作用正好与open()相反,通常要完成下列工作:
1.使用计数减1。
2.释放在file->private_data中分配的内存。
3.如果使用计算为0,则关闭设备。
设备的读写操作
字符设备的读写操作相对比较简单,直接使用函数read()和write()就可以了。
但如果是块设备的话,则需要调用函数block_read()和block_write()来进行数据读写,这两个函数将向设备请求表中增加读写请求,以便Linux内核可以对请求顺序进行优化。
由于是对内存缓冲区而不是直接对设备进行操作的,因此能很大程度上加快读写速度。
如果内存缓冲区中没有所要读入的数据,或者需要执行写操作将数据写入设备,那么就要执行真正的数据传输,这是通过调用数据结构blk_dev_struct中的函数request_fn()来完成的。
设备的控制操作
除了读写操作外,应用程序有时还需要对设备进行控制,这可以通过设备驱动程序中的函数ioctl()来完成。
ioctl()的用法与具体设备密切关联,因此需要根据设备的实际情况进行具体分析。
设备的中断和轮询处理
对于不支持中断的硬件设备,读写时需要轮流查询设备状态,以便决定是否继续进行数据传输。
如果设备支持中断,则可以按中断方式进行操作。
三、PCI驱动程序实现
1.关键数据结构
PCI设备上有三种地址空间:
PCI的I/O空间、PCI的存储空间和PCI的配置空间。
CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用。
内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。
Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该变量存放了与该设备相关的所有信息。
好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标来表示次设备号。
在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用:
pci_driver
这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe()和卸载设备的函数remove():
structpci_driver{
structlist_headnode;
char*name;
conststructpci_device_id*id_table;
int(*probe)(structpci_dev*dev,conststructpci_device_id*id);
void(*remove)(structpci_dev*dev);
int(*save_state)(structpci_dev*dev,u32state);
int(*suspend)(structpci_dev*dev,u32state);
int(*resume)(structpci_dev*dev);
int(*enable_wake)(structpci_dev*dev,u32state,intenable);
};
pci_dev
这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等:
structpci_dev{
structlist_headglobal_list;
structlist_headbus_list;
structpci_bus*bus;
structpci_bus*subordinate;
void*sysdata;
structproc_dir_entry*procent;
unsignedintdevfn;
unsignedshortvendor;
unsignedshortdevice;
unsignedshortsubsystem_vendor;
unsignedshortsubsystem_device;
unsignedintclass;
u8hdr_type;
u8rom_base_reg;
structpci_driver*driver;
void*driver_data;
u64dma_mask;
u32current_state;
unsignedshortvendor_compatible[DEVICE_COUNT_COMPATIBLE];
unsignedshortdevice_compatible[DEVICE_COUNT_COMPATIBLE];
unsignedintirq;
structresourceresource[DEVICE_COUNT_RESOURCE];
structresourcedma_resource[DEVICE_COUNT_DMA];
structresourceirq_resource[DEVICE_COUNT_IRQ];
charname[80];
charslot_name[8];
intactive;
intro;
unsignedshortregs;
int(*prepare)(structpci_dev*dev);
int(*activate)(structpci_dev*dev);
int(*deactivate)(structpci_dev*dev);
};
2.基本框架
在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:
初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。
下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。
/*指明该驱动程序适用于哪一些PCI设备*/
staticstructpci_device_iddemo_pci_tbl[]__initdata={
{PCI_VENDOR_ID_DEMO,PCI_DEVICE_ID_DEMO,
PCI_ANY_ID,PCI_ANY_ID,0,0,DEMO},
{0,}
};
/*对特定PCI设备进行描述的数据结构*/
structdemo_card{
unsignedintmagic;
/*使用链表保存所有同类的PCI设备*/
structdemo_card*next;
/*...*/
}
/*中断处理模块*/
staticvoiddemo_interrupt(intirq,void*dev_id,structpt_regs*regs)
{
/*...*/
}
/*设备文件操作接口*/
staticstructfile_operationsdemo_fops={
owner:
THIS_MODULE,/*demo_fops所属的设备模块*/
read:
demo_read,/*读设备操作*/
write:
demo_write,/*写设备操作*/
ioctl:
demo_ioctl,/*控制设备操作*/
mmap:
demo_mmap,/*内存重映射操作*/
open:
demo_open,/*打开设备操作*/
release:
demo_release/*释放设备操作*/
/*...*/
};
/*设备模块信息*/
staticstructpci_driverdemo_pci_driver={
name:
demo_MODULE_NAME,/*设备模块名称*/
id_table:
demo_pci_tbl,/*能够驱动的设备列表*/
probe:
demo_probe,/*查找并初始化设备*/
remove:
demo_remove/*卸载设备模块*/
/*...*/
};
staticint__initdemo_init_module(void)
{
/*...*/
}
staticvoid__exitdemo_cleanup_module(void)
{
pci_unregister_driver(&demo_pci_driver);
}
/*加载驱动程序模块入口*/
module_init(demo_init_module);
/*卸载驱动程序模块入口*/
module_exit(demo_cleanup_module);
上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。
需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、__exit等标志符,以使同普通函数区分开来。
构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。
3.初始化设备模块
在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:
检查PCI总线是否被Linux内核支持;
检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
读出配置头中的信息提供给驱动程序使用。
当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代码:
staticint__initdemo_init_module(void)
{
/*检查系统是否支持PCI总线*/
if(!
pci_present())
return-ENODEV;
/*注册硬件驱动程序*/
if(!
pci_register_driver(&demo_pci_driver)){
pci_unregister_driver(&demo_pci_driver);
return-ENODEV;
}
/*...*/
return0;
}
驱动程序首先调用函数pci_present()检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。
在2.4以前的内核中,需要手工调用pci_find_device()函数来查找PCI设备,但在2.4以后更好的办法是调用pci_register_driver()函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。
staticint__initdemo_probe(structpci_dev*pci_dev,conststructpci_device_id*pci_id)
{
structdemo_card*card;
/*启动PCI设备*/
if(pci_enable_device(pci_dev))
return-EIO;
/*设备DMA标识*/
if(pci_set_dma_mask(pci_dev,DEMO_DMA_MASK)){
return-ENODEV;
}
/*在内核空间中动态申请内存*/
if((card=kmalloc(sizeof(structdemo_card),GFP_KERNEL))==NULL){
printk(KERN_ERR"pci_demo:
outofmemory\n");
return-ENOMEM;
}
memset(card,0,sizeof(*card));
/*读取PCI配置信息*/
card->iobase=pci_resource_start(pci_dev,1);
card->pci_dev=pci_dev;
card->pci_id=pci_id->device;
card->irq=pci_dev->irq;
card->next=devs;
card->magic=DEMO_CARD_MAGIC;
/*设置成总线主DMA模式*/
pci_set_master(pci_dev);
/*申请I/O资源*/
request_region(card->iobase,64,card_names[pci_id->driver_data]);
return0;
}
4.打开设备模块
在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。
在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。
staticintdemo_open(structinode*inode,structfile*file)
{
/*申请中断,注册中断处理程序*/
request_irq(card->irq,&demo_interrupt,SA_SHIRQ,
card_names[pci_id->driver_data],card)){
/*检查读写模式*/
if(file->f_mode&FMODE_READ){
/*...*/
}
if(file->f_mode&FMODE_WRITE){
/*...*/
}
/*申请对设备的控制权*/
down(&card->open_sem);
while(card->open_mode&file->f_mode){
if(file->f_flags&O_NONBLOCK){
/*NONBLOCK模式,返回-EBUSY*/
up(&card->open_sem);
return-EBUSY;
}else{
/*
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 最新 电子科技大学 嵌入式 linux 设备 驱动程序 开发