linux驱动程序的编写.docx
- 文档编号:15145182
- 上传时间:2023-07-01
- 格式:DOCX
- 页数:18
- 大小:108.08KB
linux驱动程序的编写.docx
《linux驱动程序的编写.docx》由会员分享,可在线阅读,更多相关《linux驱动程序的编写.docx(18页珍藏版)》请在冰点文库上搜索。
linux驱动程序的编写
linux驱动程序的编写
一、实验目的
1.掌握linux驱动程序的编写方法
2.掌握驱动程序动态模块的调试方法
3.掌握驱动程序填加到内核的方法
二、实验内容
1.学习linux驱动程序的编写流程
2.学习驱动程序动态模块的调试方法
3.学习驱动程序填加到内核的流程
三、实验设备
PentiumII以上的PC机,LINUX操作系统,EL-ARM860实验箱
四、linux的驱动程序的编写
嵌入式应用对成本和实时性比较敏感,而对linux的应用主要体现在对硬件的驱动程序的编写和上层应用程序的开发上。
嵌入式linux驱动程序的基本结构和标准Linux的结构基本一致,也支持模块化模式,所以,大部分驱动程序编成模块化形式,而且,要求可以在不同的体系结构上安装。
linux是可以支持模块化模式的,但由于嵌入式应用是针对具体的应用,所以,一般不采用该模式,而是把驱动程序直接编译进内核之中。
但是这种模式是调试驱动模块的极佳方法。
系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。
设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。
同时,设备驱动程序是内核的一部分,它完成以下的功能:
对设备初始化和释放;把数据从内核传送到硬件和从硬件读取数据;读取应用程序传送给设备文件的数据和回送应用程序请求的数据;检测和处理设备出现的错误。
在linux操作系统下有字符设备和块设备,网络设备三类主要的设备文件类型。
字符设备和块设备的主要区别是:
在对字符设备发出读写请求时,实际的硬件I/O一般就紧接着发生了;块设备利用一块系统内存作为缓冲区,当用户进程对设备请求满足用户要求时,就返回请求的数据。
块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。
1字符设备驱动结构
Linux字符设备驱动的关键数据结构是cdev和file_operations结构体。
1)cdev结构体
在linux2.6内核中,使用cdev结构体描述一个字符设备,cdev结构体的定义如下:
structcdev{
structkobjectkobj;/*内嵌的kobject对象*/
structmodule*owner;/*所属模块*/
structfile_operations*ops;/*文件操作结构体*/
structlist_headlist;
dev_tdev;/*设备号*/
unsignedintcount;
};
cdev结构体的dev_t成员定义了设备号,为32位,其中12位主设备号,20位次设备号。
使用下列宏可以从dev_t获得主设备号和次设备号。
*dev_t这个不是structure,是简单变量,只用于保存一组majornumber和minornumber.Linux提供一组mactor对其进行读写:
MAJOR(dev_tdev);//读取设备的majornumber
MINOR(dev_tdev);//读取设备的minornumber
而使用下列宏则可以通过主设备号和次设备号生成dev_t;
MKDEV(intmajor,intminor);//从一组指定的majornumber和minornumber创建一个dev_t
cdev结构体的另一个重要成员file_operations定义了字符设备提供给虚拟文件系统的接口函数。
Linux2.6内核提供了一组函数用于操作cdev结构体:
voidcdev_init(structcdev*,structfile_operations*);
structcdev*cdev_alloc(void);/*动态申请一个cdev空间内存*/
voidcdev_put(structcdev*p);
intcdev_add(structcdev*,dev_t,unsigned);/*向系统添加、注册一个cdev*/
voidcdev_del(structcdev*);/*向系统注销一个cdev*/
cdev_init()函数用于初始化cdev的成员,并建立cdev和file_operations之间的连接,其源代码如下所示:
voidcdev_init(structcdev*cdev,structfile_operations*fops)
{
memset(cdev,0,sizeof*cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj,&ktype_cdev_default);
cdev->ops=fops;/*将传入的文件操作结构体指针赋值给cdev的ops*/
}
cdev_alloc()函数用于动态申请一个cdev内存,其源代码清单如下:
structcdev*cdev_alloc(void)
{
structcdev*p=kzalloc(sizeof(structcdev),GFP_KERNEL);
if(p){
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj,&ktype_cdev_dynamic);
}
returnp;
}
cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。
对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用通常发生在字符设备驱动模块卸载函数中。
2)分配和释放设备号
在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型为:
intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name);
intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name);
register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_regione用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中。
后者的优点在于它会自动避开设备号重复的冲突。
相反地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型为:
voidunregister_chrdev_region(dev_tfrom,unsignedcount);
from:
要分配设备编号范围的起始值,经常设置为0.
count:
所请求的连续设备编号的个数。
Name:
是和该设备范围关联的设备名称,它将出现在/proc/devices和sysfs中。
3)file_operations结构体
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,是这符设备驱动与内核的接口,是用户空间对linux进行系统调用的最终落实者。
这些函数实际会在程序进行linux的open()、write()、read()、close()等系统调用时最终被调用。
字符设备驱动程序中,具体实现这些函数,通常,比如file_operation中的read这个函数指针将指向这个具体的驱动程序中的函数xxx_read().
通常,一个设备驱动程序包括两个基本的任务:
驱动设备的某些函数作为系统调用执行;而某些函数则负责处理中断(即中断处理函数)。
而file_operations结构的每一个成员的名称都对应着一个系统调用。
用户程序利用系统调用,比如在对一个设备文件进行诸如read操作时,这时对应于该设备文件的驱动程序就会执行相关的ssize_t(*read)(structfile*,char*,size_t,loff_t*);函数。
在操作系统内部,外部设备的存取是通过一组固定入口点进行的,这些入口点由每个外设的驱动程序提供,由file_operations结构向系统进行说明,因此,编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。
file_operations结构在kernel/include/linux/fs.h中可以找到。
structfile_operations{
structmodule*owner;/*拥有该结构的模块的指针,一般为THIS_MODULES*/
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*);
/*向设备发送数据*/
ssize_t(*aio_read)(structfile*,char*,size_t,loff_t*);
/*初始化一个异步的读取操作*/
ssize_t(*aio_write)(structfile*,constchar*,size_t,loff_t*);
/*初始化一个异步的写入操作*/
int(*readdir)(structfile*,void*,filldir_t);
/*仅用于读取目录,对于设备文件,该字符为null*/
unsignedint(*poll)(structfile*,structpoll_table_struct*);
/*轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
int(*ioctl)(structinode*,structfile*,unsignedint,unsignedlong);
/*执行设备I/O的控制命令*/
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);
/*通知设备FASYNC标志发生变化*/
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);/*通常为NULL*/
unsignedlong(*get_unmapped_area)(structfile*,unsignedlong,unsignedlong,unsignedlong,unsignedlong);
#ifdefMAGIC_ROM_PTR
int(*romptr)(structfile*,structvm_area_struct*);
#endif/*MAGIC_ROM_PTR*/
};
File_operations结构中的成员全部是函数指针,所以实质上就是函数的跳转表。
每个进程对设备的操作,都会根据major、minor设备号,转换成对file_operations结构的访问。
即在文件操作中使用的open、read、write、ioctl、close等,都会调用在file_operations中定义的相应的函数入口。
其中主要的函数说明如下:
1)open是驱动程序用来完成设备初始化操作的,open还会增加设备计数,以防止文件在关闭前模块被卸载出内核。
open主要完成以下操作:
检查设备错误(诸如设备未就绪或相似的硬件问题);如果是首次打开,初始化设备;标别次设备号;分配和填写要放在file→private_data内的数据结构;增加使用计数。
2)read用来从外部设备中读取数据。
当其为NULL指针时,将引起read系统调用返回-EINVAL(“非法参数”)。
函数返回一个非负值表示成功地读取了多少字节。
3)write向外部设备发送数据。
如果没有这个函数,write系统调用向调用程序返回一个-EINVAL。
如果返回值非负,就表示成功地写入的字节数。
4)release是当设备被关闭时调用这个操作。
release的作用正好与open相反。
这个设备方法有时也称为close。
它应该完成以下操作:
使用计数减1;释放open分配在file→private_data中的内存,在最后一次关闭操作时关闭设备。
5)llseek是改变当前的读写指针。
6)readdir一般用于文件系统的操作。
7)poll一般用于查询设备是否可读可写或处于特殊的状态。
8)ioctl执行设备专有的命令。
9)mmap将设备内存映射到应用程序的进程地址空间。
2linux字符设备驱动程序编写的具体内容
在linux中,字符设备驱动由以下几个部分组成:
1)字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应实现设备号的释放和cdev的注销。
因为cdev是所有字符设备的抽象,在编写具体的字符设备驱动程序时,针对的是具体的字符设备,具体的字符设备肯定有着自已特征。
所以一般的做法是,继承cdev,加上私有数据,信号量等信息。
工程师通常习惯为设备定义一个设备相关的结构体,其包含该设备所涉及的cdev、私有数据及信号量等信息。
常见设备结构体、模块加载和卸载函数形式如下所示:
下面给出一个设备驱动的注册模版供参考
//设备结构体
structxxx_dev_t
{structcdevcdev;
private_data;//私有数据
semaprore;//信号量
}xxx_dev;
//设备驱动模块加载函数
staticint_initxxx_init(void)
{...
cdev_init(&xxx_dev.cdev,&xxx_fops);//初始化cdev
xxx_dev.cdev.owner=THIS_MODULE;
//获取字符设备号
if(xxx_major)
{register_chrdev_region(xxx_dev_no,1,DEV_NAME);
}
else
{alloc_chrdev_region(&xxx_dev_no,0,1,DEV_NAME);
}
ret=cdev_add(&xxx_dev.cdev,xxx_dev_no,1);//注册设备
...
}
//设备驱动模块卸载函数
staticvoid_exitxxx_exit(void)
{unregister_chrdev_region(xxx_dev_no,1);//释放占用的设备号
cdev_del(&xxx_dev.cdev);//注销设备¸
...
}
2)字符设备驱动的file_operations结构体中成员函数
通过了解驱动程序的file_operations结构,用户就可以编写出相关外部设备的驱动程序。
首先,用户在自己的驱动程序源文件中定义file_operations结构,并编写出设备需要的各操作函数,对于设备不需要的操作函数用NULL初始化,这些操作函数将被注册到内核,当应用程序对设备相应的设备文件进行文件操作时,内核会找到相应的操作函数,并进行调用。
如果操作函数使用NULL,操作系统就进行默认的处理。
定义并编写完file_operations结构的操作函数后,要定义一个初始化函数,比如函数名可device_init(),在linux初始化的时候要调用该函数,因此,该函数应包含以下几项工作:
a.对该驱动所使用到的硬件寄存器进行初始化。
包括中断寄存器。
b.初始化设备相关的参数。
一般来说每个设备要定义一个设备变量,用来保存设备相关的参数。
c.注册设备。
Linux内核通过主设备号将设备驱动程序同设备文件相连。
每个设备有且仅有一个主设备号。
通过查看linux系统中/proc下的devices文件,该文件记录已经使用的主设备号和设备名,选择一个没有使用的主设备号,调用下面的函数来注册设备。
intregister_chrdev(unsignedint,constchar*,structfile_operations*),其中的三个参数代表主设备号,设备名,file_operations的结构地址。
d.注册设备使用的中断。
注册中断使用的函数。
intrequest_irq(unsignedirq,void(*handler)(int,void*,structpt_regs*),unsignedlongflags,constchar*device,void*dev_id);其中,irq是中断向量。
硬件系统将IRQn映射成中断向量。
handler-----中断处理函数。
flags-----中断处理中的一些选项的掩码。
device-----设备的名称
dev_id------在中断共享时使用的id。
e.其他的一些初始化工作,比如给设备分配I/O,申请DMA通道等。
当设备的驱动程序使用了如下的函数方式,则设备驱动可以动态的加载和卸载
int__initdevice_init(void)
void__exitdevice_exit(void)
module_init(device_init);
module_exit(device_exit);
当然,也可以编译进内核中。
File_operations结构体中成员函数是字符设备驱动与内核的接口,是用户空间对linux进行系统调用最终的落实者。
大多数字符设备驱动会实现read()、write()、ioctl()函数,常见的字符设备驱动的3个函数代码的形式如下所示:
/*读设备*/
ssize_txxx_read(structfile*filp,char_user*buf,size_tcount,loff_t*f_pos)
{
...
copy_to_user(buf,...,...);
...
}
/*写设备*/
ssize_txxx_write(structfile*filp,constchar_user*buf,size_tcount,loff_t*f_pos)
{
...
copt_from_user(...,buf...);
...
}
/*输入输出控制函数*/
intxxx_ioct1(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg)
{
...
switch(cmd){
caseXXX_CMD1:
...
break;
caseXXX_CMD1:
...
break;
default:
/*不能支持的命令*/
return-ENOTTY;
}
return0;
}
设备驱动的读函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不能直接读写,count是要读的字节数,f_pos是读的位置相对于文件开头的偏移。
设备驱动的写函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不能直接读写,count是要写的字节数,f_pos是写的位置相对于文件开头的偏移。
由于用户空间和内核空间的内存不能够直接互相访问,要借助函数copy_from_user()完成用户空间到内核空间的复制,copy_to_user()完成内核空间到用户空间的复制,二者的原型分别为:
unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongcount);
unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongcount);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。
如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user(),如:
intval;/*内核空间整型变量*/
…
Get_user(val,(int*)arg);/*用户到内核,arg是用户空间的地址*/
…
Put_user(val,(int*)arg);/*内核到用户,arg是用户空间的地址*/
读和写函数中的__user是一个宏,表明其后的指针指向用户空间,这个宏定义为:
#ifdef__CHECKER__
#define__user
#else
#define__user
#endif
I/O控制函数的cmd参数为事先定义的I/O控制命令,而arg为对应于该命令的参数。
例如对于串行设备,如果SET_BAUDRATE是一道设置波特率的命令,那后面的arg就应该是波特率值。
在字符设备的驱动中,还需要定义一个file_operations的实例xxx_fops,并将具体设备驱动的函数赋值给file_operations的成员,如下代码清单:
Structfile_operationsxxx_fops={
.owner=THIS_MODULE;
.read=xxx_read;
.write=xxx_write;
.ioctl=xxx_ioctl;
…
};
其中xxx_fops在模块加载函数的cdev_init(&xxx_dev.cdev,&xxx
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- linux 驱动程序 编写
![提示](https://static.bingdoc.com/images/bang_tan.gif)