嵌入式LINUX操作系统字符设备驱动程序编写举例.docx
《嵌入式LINUX操作系统字符设备驱动程序编写举例.docx》由会员分享,可在线阅读,更多相关《嵌入式LINUX操作系统字符设备驱动程序编写举例.docx(29页珍藏版)》请在冰豆网上搜索。
嵌入式LINUX操作系统字符设备驱动程序编写举例
《嵌入式Linux应用开发菜鸟进阶》
第11章
对字符设备驱动的模块框架有一个宏观的认识:
#include
#include
#include
#include
︙
#include
这里根据实际的驱动需要添加头文件
staticintmem_major=251;
︙
/*这里定义驱动需要的一些静态数据或者指针,当然作为全局变量,一般不要轻易使用这些静态变量,它们很占内存,并且浪费资源*/
︙
实现file_operation中挂接的函数
staticconststructfile_operationsmem_operation={
.owner=THIS_MODULE,
︙
};
根据驱动需要实现相应的系统调用函数
staticintmymem_init(void)
{
︙
}
模块驱动的注册函数
staticvoidmymem_exit(void)
{
︙
}
模块的释放函数
MODULE_AUTHOR("LinHui");
MODULE_LICENSE("GPL");
定义模块编写的作者以及遵循的协议
module_init(mymem_init);
module_exit(mymem_exit);
定义模块初始化入口函数
以上就是一个驱动基本不变的部分,
针对字符变化的部分进行详细的讲解。
首先是字符设备的注册。
字符设备的注册主要分为4步:
设备号、分配设备号、定义并初始化file_operation结构体和字符设备的注册。
其中,设备号与分配设备号在11.1节中已经详述,这里不再重复。
下面介绍字符设备注册的详细步骤。
(1)设备号。
(2)分配设备号。
(3)定义并初始化file_operations结构体。
file_operations结构体用于连接设备号和驱动程序的操作。
在该结构体内部包含一组函数指针,这些函数用来实现系统调用。
通常情况下,要注册如下几个函数。
(1)structmodule*owner:
用来指向拥有该结构体的模块。
(2)ssize_tread(structfile*filp,char__user*buf,size_tcount,loff_t*f_ops):
用来从设备中读取数据。
其中,
❑filp为文件属性结构体指针。
❑buf为用户态函数使用的字符内存缓冲。
❑count为要读取的数据数。
❑f_ops为文件指针的偏移量。
❑ssize_twrite(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_ops):
用来向设备输入数据。
(3)intopen(structinode*inode,structfile*):
该函数用来打开一个设备文件。
(4)intrelease(structinode*inode,structfile*):
该函数用来关闭一个设备文件。
该结构体的初始化形式如下例:
structfile_operationsscull_fops={
.owner=THIS_MODULE,
.read=read,
.write=write,
.open=open,
.release=release,
}
内核内部使用structcdev结构来表示字符设备。
在进行内核调用设备的操作之前,必须分配或注册一个或者多个该结构体。
该结构体包含在头文件中,一般步骤如下。
首先定义该结构体:
structcdevmy_cdev;
然后即可初始化该结构体,使用如下函数进行初始化:
intcdev_init(structcdev*dev,structfile_operations*fops).
再后定义该结构体中的一个所有者字段:
my_cdev.owner=THIS_MODULE;
最后向模块添加该结构体:
intcdev_add(structcdev*dev,dev_tdev_num,usignedintcount);
其中,dev是cdev结构体,dev_num是该设备对应的第一个设备编号,count则是与该设备关联的设备编号数量。
以上就是一个字符设备驱动需要做的事情,当然在字符设备的注册过程中,可能还会涉及一些内核数据的处理,比如分配字符设备驱动需要的空间,以及初始化一些内核协议等工作。
因此针对不同驱动数据的处理要添加不同的Linux内核技术,这才是字符设备驱动开发的核心,这些内容都会在后续章节中通过实际的例子进行分析和学习。
11.2.2字符设备的释放
对于字符设备驱动的释放,需要做的工作就是对注册时或者数据处理时,申请到的字符设备驱动的核心数据予以释放。
我们知道Linux内核的资源相当珍贵,所以不用的时候就要释放出来,其中包括注销设备号、移除字符设备、释放申请到的Linux内核空间、释放申请到的Linux内核子系统相关的数据结构等。
一般的字符设备驱动的释放包含以下两方面内容。
(1)移除字符设备函数:
voidcdev_del(structcdev*dev);
(2)注销设备号函数:
unregister_chrdev_region(dev_tfirst,unsignedintcount);
以上两个函数一般用在模块出口函数中。
在介绍字符设备驱动的注册前我们就已经学习了字符设备驱动模型中的编程架构,下面将针对这些内容进行详细的解说。
1.确定一些版本信息和建立内核头文件
#include
#include
MODULE_AUTHOR("LinHui");
MODULE_LICENSE("GPL");
上面添加的是Linux内核驱动必需的一些头文件、编写驱动模块用到的协议的定义以及模块开发者的说明,这些是模型中的一部分。
2.建立系统调用与驱动程序之间的关联
将系统调用和驱动程序关联起来需要一个非常关键的数据结构—structfile_operations。
file_operations结构中每一个成员的名字都对应着一个系统调用。
用户进程利用系统调用在对设备文件进行读写等操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该内核函数。
这是Linux的设备驱动程序工作的基本原理。
驱动开发的主要工作就是编写这些系统调用子函数,并填充进file_operations数据结构的各个域,也就是编写XXX_read、XXX_write等函数和构建file_operations结构体。
3.驱动程序的编写(mymem.c)
❑包含基本的头文件和驱动需要的头文件。
❑编写基本功能函数,比如XXX_read()、XXX_write()、XXX_joctl()等。
这些函数被调用时系统进入核心态。
❑定义structfile_operations结构的对象,填充结构体。
结构体中功能的顺序不能改变,若一些功能没有实现就用NULL填充,已经实现的功能如read()、write()分别被添加到对应的位置。
这一步实现的是函数的注册。
到这里驱动程序的主体就写好了,现在需要把驱动程序嵌入内核。
❑注册设备驱动程序,使用register_chrdev_region或者alloc_chrdev_region函数注册字符型设备。
dev_tdevno=MKDEV(mem_major,0);
if(mem_major)
result=register_chrdev_region(devno,1,"mymem");
else{
result=alloc_chrdev_region(&devno,0,1,"mymem");
mem_major=MAJOR(devno);
}
此时已经完成了注册设备驱动程序的大部分内容。
当然,如果需要实现混杂设备驱动程序的编写以及自创建设备文件,则需要实现这些内容。
在用rmmod卸载模块时,mymem_exit函数被调用,它释放字符设备mymem在系统字符设备表中占有的表项。
staticvoidmymem_exit(void)
{
cdev_del(&cdev);
unregister_chrdev_region(MKDEV(mem_major,0),1);
}
到这里,mymem.c基本就编写完了。
可以说,一个简单的字符设备就写好了。
4.编译
到目前为止已经编写好了驱动程序,下面就要编写makefile文件了。
makefile文件用于编译我们所写的驱动模块代码编译文件,这个文件的格式必须要掌握,如果读者还没有掌握,那么复习一下基础知识,再接着看下面的代码:
ifneq($(KERNELRELEASE),)
obj-m:
=mymem.o
else
KDIR:
=/home/linhui/kernel/linux-2.6.29
all:
make-C$(KDIR)M=$(PWD)modulesARCH=armCROSS_COMPILE=arm-linux-
clean:
rm-f*.ko*.o*.mod.o*.mod.c*.symversmodul**~
endif
上面的makefile文件是编译Linux内核代码的一个样例,其中需要改变的只有KDIR中的内核路径代码,还有就是我们使用的具体代码运行的架构以及编译工具,在这里选择arm和arm-linux-。
当然,要注意Linux内核代码路径必须经过预先的编译才能成功地编译我们的代码。
也就是说,必须先编译一次内核代码并使用同样的代码架构和编译工具。
至此makefile已经编写完成,只需要执行make命令就可以完成驱动程序的编译了。
$make
驱动程序已经编译完成,现在把它安装到系统中。
$insmod-fmymem.o
安装成功后在/proc/devices文件中就可以看到设备test,并可以看到主设备号。
如果要卸载,则运行如下命令:
rmmodmymem
5.创建设备节点
上一章中已经介绍了如何手动地创建设备节点以及自创建设备文件的知识,这时就可以派上用场了。
手动创建方式如下:
$mkmod/dev/mymemcmajorminor
c指字符设备,major是主设备号,minor是从设备号,一般可以设置为0。
如果是自动创建设备驱动,则不需要做这一步。
至于详细的自动创建设备文件的内容,在前面几章中曾提到,在接下来的实际字符设备驱动的开发中也会用到。
第12章
我们开发一个基于内存的字符设备驱动。
该字符设备驱动的开发基于上一章用到的字符设备驱动的开发流程来实现。
在这个驱动中,需要在内存中分配一块4MB大小的内存空间,在驱动中提供对该内存的读写、控制和定位方法,用户空间的进程能够通过Linux相应的系统调用访问这片内存以及操作。
在这一章中,我们将学习到如下知识:
1.简单字符设备驱动mymem的数据结构的填充。
2.简单字符设备驱动mymem的注册与释放。
3.简单字符设备驱动mymem的打开和关闭以及llseek函数的实现。
4.简单字符设备驱动mymem的应用程序的编写和测试。
12.1简单字符驱动的数据结构
在开发一个简单的字符设备驱动之前,先要设计好整个驱动涉及的数据结构。
这非常关键,因为我们开发出来的驱动其实是对底层数据进行处理,考虑到字符设备驱动是基于内存的模拟硬件,因此设计一个合理的保存字符设备内容的结构非常关键。
12.1.1定义字符设备驱动的设备数据结构
这一章需要开发的是一个基于内存的简单字符驱动,所以驱动结构内容需要包含内存的首地址以及需要开辟出来的内存大小。
我们需要定义一个关于字符设备的头文件,这个文件用来保存该字符设备的设备信息。
在这里,我们的设备是基于一块4MB的内存,可以实现数据的读、写和定位3个简单的功能,因此定义一个mem_dev的设备结构体来保存内存的数据首地址和数据区的大小。
考虑到Linux内核的内存空间异常珍贵,所以内存空间不能分配过多,否则如果分配的内存大于1GB,那么程序和内核有可能出现崩溃现象。
鉴于此我们只分配4MB的空间用于实验。
创建一个头文件mymem.h,mymem.h中的实现内容如下:
#ifndef_MYMEM_H_
#define_MYMEM_H_
#ifndefMEMDEV_MAJOR
#defineMEMDEV_MAJOR251
#endif
#ifndefMEMDEV_SIZE
#defineMEMDEV_SIZE4096
#endif
definethestructofmemchardev.
structmem_dev{
char*data;poitertothememory
unsignedlongsize;definethesizeofthememory
};
#endif
头文件中的内容还包含了默认的主设备号251、需要分配内存的大小以及一个mem_dev的数据结构,其中的mem_dev结构体保存的是分配内存的首地址以及分配到的大小。
在以后的驱动编写中要养成一个习惯,就是编写一个驱动代码的头文件,将驱动中用到的一些常量、设备驱动用到的字符设备驱动数据结构保存在这个头文件中。
这样无论是从驱动的阅读上还是驱动结构上,都可以使驱动数据与驱动代码分开,以便于在驱动测试或者驱动修改时快速定位数据或者代码的位置,从而有利于驱动的开发。
同时这也是一名优秀的驱动开发者必备的一项技能。
12.1.2定义file_operation结构和挂接相应的系统调用函数
在定义了字符设备的数据结构和头文件之后,需要确定字符设备驱动的功能和需要实现的函数操作,也就是上层应用需要的系统调用。
我们设计的字符设备驱动需要实现字符设备的打开,并可以对内存空间的数据进行读、写、内存定位以及内存空间的释放等功能,因此需要实现open、read、write、llseek和release5个功能函数。
file_operation结构体如下。
staticconststructfile_operationsmem_operation={
.owner=THIS_MODULE,
.read=mem_read,
.write=mem_write,
.llseek=mem_llseek,
.open=mem_open,
.release=mem_release,
};
这里有一个技巧,就是当读驱动比较多时,只要看一下这个file_operation的结构,就会发现这个驱动所要完成的功能了。
当然,这只是针对功能相对简单的字符设备驱动程序,sysfs文件系统、I2C等设备就没有这么简单了。
12.2简单字符驱动设计
在定义好需要的驱动设备的数据结构后,就要开始设计驱动程序了。
在编写驱动程序之前,需要有一个清晰的字符设备驱动编程框架,也就是在没填充驱动实现内容之前需要的一个代码架构,具体如下。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#includedevice_create()funtionuse!
#include"mymem.h"
staticintmem_major=251;
structmem_dev*mem_devp;devstructpoiter
structcdevcdev;
staticdev_tdevno;
module_param(mem_major,int,S_IRUGO);
openthememorydevice
intmem_open(structinode*inode,structfile*filp)
{
}
intmem_release(structinode*inode,structfile*filp)
{
}
realizetheopenfuntion
staticssize_tmem_read(structfile*filp,char__user*buf,size_tsize,loff_t*ppos)
{
}
staticloff_tmem_llseek(structfile*filp,loff_toffset,intwhence)
{
}
staticconststructfile_operationsmem_operation={
.owner=THIS_MODULE,
.read=mem_read,
.write=mem_write,
.llseek=mem_llseek,
.open=mem_open,
.release=mem_release,
};
/*
staticintmalloc_device(dev_t*devno)
{
}
staticvoidmymem_exit(void)
{
}
MODULE_AUTHOR("LinHui");
MODULE_LICENSE("GPL");
module_init(mymem_init);
module_exit(mymem_exit);
如上所述,我们得到了一个字符设备驱动的基本编程框架,在这个框架里分别要实现mymem_init和mymem_exit驱动加载和退出函数,mem_read读函数,mem_write写函数,mem_llseek内存定位函数,mem_open字符驱动打开函数,mem_release字符驱动释放函数。
接下来,将分模块来实现以上这些函数。
到这里,读者应该对字符设备驱动编写有一个宏观的了解了。
“骨架”都搭好了,接下来就是“填肉”了。
12.2.1字符设备驱动的加载与卸载
mymem字符设备驱动的模块加载和卸载与上一章介绍的字符设备的注册与释放区别不大,但是在这里,我们将利用在前面几章中讲到的动态加载设备文件的相关知识,也就是说,不需要自创建设备文件,而是由内核自动完成。
字符设备驱动的加载—mymem_init():
staticintmymem_init(void)
{
intresult;
structclass*myclass;
applyfordevnumber
dev_tdevno=MKDEV(mem_major,0);
if(mem_major)
result=register_chrdev_region(devno,1,"mymem");
else{
result=alloc_chrdev_region(&devno,0,1,"mymem");
mem_major=MAJOR(devno);
}
if(result<0)
returnresult;
initializethecdevstruct(char)
cdev_init(&cdev,&mem_operation);
cdev.owner=THIS_MODULE;
cdev.ops=&mem_operation;
registerthechardevice
cdev_add(&cdev,MKDEV(mem_major,0),1);
cdev_add(&cdev,devno,1);
mallocforthemem_devstructanddevice
result=malloc_device(&devno);
mallocforthemem_dev
mem_devp=kmalloc(sizeof(structmem_dev),GFP_KERNEL);
if(!
mem_devp)
{
result=-ENOMEM;
unregister_chrdev_region(devno,1);
returnresult;
}
memset(mem_devp,0,sizeof(structmem_dev));
mallocfordev
mem_devp->size=MEMDEV_SIZE;
mem_devp->data=kmalloc(MEMDEV_SIZE,GFP_KERNEL);
memset(mem_devp->data,0,MEMDEV_SIZE);
sema_init(&mem_devp->sem,1);initializethesemaphore
myclass=class_create(THIS_MODULE,"my_device_driver");
device_create(myclass,NULL,MKDEV(mem_major,0),NULL,"mymem");
return0;
}
mymem字符设备驱动的卸载—mymem_exit():
在字符设备驱动的卸载中,需要将申请的字符设备进行释放,并且释放申请到的内存以及释放申请到的设备号。
staticvoidmymem_exit(void)
{
cdev_del(&cdev);
kfree(mem_devp);
unregister_chrdev_region(MKDEV(mem_major,0),1);
unregister_chrdev_region(devno,1);
}
12.2.2字符设备驱动的打开与关闭
在字符设备的驱动中,特别是在这些较为简单的字符设备中,一般都不会为驱动的open与close函数实现过多的内容。
但是这里有一点需要注意,在驱动开发中,大部分的Linux驱动工程师都会遵循一个“潜规则”,就是将文件的私有数据private_data指向设备结构体,在read()、write()、ioctl()、llseek()等函数中通过private_data访问设备结构体。
这是因为在这些函数的原型参数中没有structinode*这个结构体。
这样就很容易封装好设备结构与字符设备的内容。
而在字符设备的关闭中则不做任何动作,因为当应用程序关闭打开的驱动