s3c2440驱动学习.docx
- 文档编号:9969490
- 上传时间:2023-02-07
- 格式:DOCX
- 页数:82
- 大小:539.76KB
s3c2440驱动学习.docx
《s3c2440驱动学习.docx》由会员分享,可在线阅读,更多相关《s3c2440驱动学习.docx(82页珍藏版)》请在冰豆网上搜索。
s3c2440驱动学习
驱动学习
开发板:
TQ2440
开发环境:
fedora10
Linux内核代码:
linux-2.6.30.4
编译器:
arm-linux-gcc-4.3.2
ok,现在我们正式开始学习编写linux的驱动吧。
我个人觉得学习驱动是最有必要的。
为什么呢?
你到了一个公司大部分是进行驱动开发的,你们或许会问还可以进行内核开发啊,是的,我们还可以进行内核开发的或者bootloader的开发啊。
但是,不要说内核有多难学先,你觉得一般的商家会专注于内核开发吗?
答案就自己寻求吧。
我们先讲讲驱动分类,驱动分为字符设备的驱动、块设备驱动还有就是网络设备。
这里我们先重点讲解字符设备驱动先。
字符设备驱动是一种按字节来访问的设备,所以这样的驱动设备通常实现open、close、read系统调用。
不懂没事,接下来你会懂的。
实践才是真理,所以我们先来写一个最简单的驱动吧,让大家熟悉一下编写驱动的流程吧。
我们就编写一个简单的打印helloworld的驱动吧。
一下是它的源代码。
#include
#include
MODULE_LICENSE("GPL");//用于声明模块的许可证
staticint__initEmbedSky_hello_init(void)//这个是驱动的加载函数,也是必须的
{
printk("<1>\nHello,EmbedSky!
\n");
printk("<1>\nThisisfirstdriverprogram.\n\n");
return0;
}
staticvoid__exitEmbedSky_hello_exit(void)//这个是驱动的卸载函数,也是必须的
{
printk("<1>\nExit!
\n");
printk("<1>\nGoodbyeEmbedSky!
\n\n");
}
module_init(EmbedSky_hello_init);//你想让你的驱动运行起来啊,你就必须有这个
module_init,当你的驱动要加载进内核的时候,内核只识别这个module_init,然后在连接到上面的函数。
module_exit(EmbedSky_hello_exit);//这里的作用和上面的module_init是一样的,只不过意义是相反的
MODULE_AUTHOR("");//这个是作者的信息,可有可无吧
MODULE_DESCRIPTION("SKY2440/TQ2440BoardFirstmoduletest");//一看就知道是描述信息了
看到了吧,这个就是驱动的大概格式了,没什么好说的。
语法上又没什么的。
ok,兄弟们你们自己来好好记住一下上面这段代码的格式吧。
以后的字符设备驱动差不多就这个摸样的了。
至于注释也写好在旁边了。
假如你已经写好了一个驱动了,接下来该怎么做了呢?
其实说到底驱动最后还是加进内核去运行的,所以这就涉及到了驱动程序安装的知识点。
驱动程序的安装有两种方式,第一种是模块方式,第二种是直接编译进内核,至于什么是模块方式或者直接编译进内核方式我们在内核的移植的时候有讲解过,这里就不再重复啰嗦了,不然有的盆友们就不爽啦。
这里我就讲讲如何修改相应的配置文件,增加内核的配置菜单选项吧。
这里涉及到两个文件一个是makefile和kconfig,makefile的作用就是编译,具体的来说就是写好一个程序你就必须写好它的makefile文件。
kconfig的作用就是使得写好的驱动能增加到图形配置菜单中,简单来说,就是你想在内核配置菜单中看到你的内核选项,你就必须修改kconfig文件。
ok,下面我们就来讲讲如何修改makefile文件以及kconfig文件吧。
假如我们写好了一个驱动程序,然后呢放在了drivers/char目录下,而且文件名为EmbedSky_hello.c。
首先,我们先修改同目录下的makefile文件吧,如何修改呢?
很多时候一打开一看怎么密密麻麻的呢,其实没关系的,你没看到吗?
大部分的格式其实是一摸一样的,这就简单了,我们也依葫芦画瓢吧,我们加这一句:
obj-$(CONFIG_TQ2440_HELLO)+=EmbedSky_hello.o
对于这个语句是什么意思,我也不想在这里说的太详细了,最右边那个是我们的驱动文件的文件名,至于右边那个CONFIG是必须的,而那个TQ2440_HELLO呢,是我们等一下在Kconfig文件中要用到的。
接着我们修改同目录下的Kconfig文件,找到menu“Characterdevices”在这下面开始添加吧,添加的内容如下:
configTQ2440_HELLO
tristate"SKY2440/TQ2440BoardHellomoduletest"
dependsonARCH_S3C2440
defaultmifARCH_S3C2440
help
SKY2440/TQ2440BoardFirstmoduletest.
在这里我也简单讲解一下上面这几条语句吧,最上面的config是必须的,这是它的格式这样说吧。
下面一句呢,是它在内核配置单上显示的文字了,下面的那句是它的依赖,也就是说只有ARCH_S3C2440配置好了,我们才能配置这一驱动。
最后的是它的帮助信息,以后你按照它的格式照着写就好了。
懂了吧!
接着就是照着之前讲的如何编译内核编译一下内核了。
输入:
makemenuconfig,将刚刚的模块选择为M吧,然后编译内核,把内核镜像烧进开发板。
最后使用命令makeSUBDIR=drivers/char/modules再编译。
得到EmbedSky_hello.ko这个就是我们要的驱动模块了,我们把它加到开发板,加载的方式有很多种,我一般是用串口加进开发板的。
在开发板的lib目录下输入rz即可下载了。
ok,假如你已经成功下载驱动模块到开发板了,这时候你需要做的是加载驱动模块了,为了形象生动点,我就截图了吧:
到这里为止,大家已经基本会了驱动开发的流程了,所以接下来我们就要痛苦一阵子了,为什么这么说呢?
因为要进行理论学习了,我也非常讨厌理论的学习,没办法,只能说驱动所迫吧!
那……,就让驱动的暴风雨来的更猛烈些吧。
这里我们先讲讲几个和字符设备驱动相关的知识点。
第一:
设备号
字符设备是通过字符设备文件来存取的。
这样说好像很笼统的样子,我们进入linux操作系统吧,进入/dev目录下,然后输入命令ls–l,接着下面会显示一大堆的东西出来了,可以看到有c打头的,有b打头的,c打头的就是我们的字符设备文件了,其中有2个数(由一个逗号隔开)这些数字就是主次设备号了。
那么设备号用来做什么的呢?
设备号是连接字符设备文件和字符设备驱动的。
这里有两个要理解的概念了。
第一:
字符设备驱动,就是我们前面写的驱动吧。
字符设备文件呢?
这个也是要创建的,至于怎么创建后面会讲到的,创建好字符设备文件之后,我们通过操作字符设备文件就可以操作字符设备驱动了。
而他们之间的联系就是通过设备号的。
讲讲主次设备号吧,主设备号是用来标识与设备文件相连的驱动程序。
次设备号被驱动程序用来辨识操作的是哪个设备。
简简单来说就是主设备号是哪一类设备,次设备号就是哪一类设备的那个一个设备。
在内核中描述设备号是用dev_t来描述的。
那么怎么从dev_t中提取主次设备号呢?
请看:
MAJOR(dev_tdev)得到主设备号。
MINOR(dev_tdev)得到次设备号。
我们接下来要讲的内核如何分配主设备号了。
可以采用静态申请和动态申请两种方法,
静态申请
1、根据Documentation/devcices.txt,确定一个没使用的主设备号。
2、使用register_chrdev_region函数注册设备号
intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name)
功能:
注册从from开始的count个设备号
from:
要注册的第一个设备号
count:
要注册的设备号个数
name:
设备名
动态分配
使用alloc_chrdev_region分配设备号,它有一个缺点就是既然是动态分配那么之前就是不知道设备号的,所以我们在安装了驱动之后,我们可以从/proce/devices中查询设备号
intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsigne
dcount,constchar*name)
dev:
分配到的设备号
baseminor:
起始次设备号
count:
要注册的设备号个数
name:
设备名
在你不用这个设备号之后,当然你就要注销掉它了。
voidunregister_chrdev_region(dev_tfrom,unsignedcount)
第二:
创建设备文件
这里也是有两种方法创建设备文件,第一种是使用mknod命令手工创建,第二种是自动创建
手工创建
mknodfilenametypemajorminor一看就知道什么意思了,就不具体讲解了,不会的XX吧!
例子:
mknodserialc1000
自动创建
自动创建分2.4内核和2.6内核的,但是2.4内核的比较少用了,就不讲了。
我们是利用udev来实现设备文件的自动创建的,在驱动中先调用class_create为设备创建一个class即类。
再为设备调用device_create创建对应的设备。
大家不用急,具体的实例在后面会提到的。
structclass*myclass=class_create(THIS_MODULE
“my_deviceer”);
device_create(myclass,NULL,MKDEV(major_num,0),NULL,
“my_device”);
第三:
重要的数据结构
Structfile:
只要打开一个文件,在内核中就会有一个和打开的文件相关联的structfile了。
其实我们暂时不用管这么多的,我只要记住在这个结构体里有几个重要的成员就好了,分别是:
loff_tf_pos//文件读写位置
structfile_operation*f_op
StructInode:
用来记录文件的物理信息,打开的文件就有一个StructInode对应着。
而且只对应着一个,什么意思呢?
就是说每个文件同时打开很多次,也只对应着一个structInode。
还是那句话其实我们暂时不用管这么多的,我只要记住在这个结构体里重要的成员就好了:
dev_ti_rdev//设备号
Structfile_operations:
一个函数指针的集合,对于字符设备来说这个结构体是看的最多的了。
下面是它的完整的面貌:
structfile_operations{
int(*seek)(structinode*,structfile*,off_t,int);
int(*read)(structinode*,structfile*,char,int);
int(*write)(structinode*,structfile*,off_t,int);
int(*readdir)(structinode*,structfile*,structdirent*,int);
int(*select)(structinode*,structfile*,int,select_table*);
int(*ioctl)(structinode*,structfile*,unsinedint,unsignedlong);
int(*mmap)(structinode*,structfile*,structvm_area_struct*);
int(*open)(structinode*,structfile*);
int(*release)(structinode*,structfile*);
int(*fsync)(structinode*,structfile*);
int(*fasync)(structinode*,structfile*,int);
int(*check_media_change)(structinode*,structfile*);
int(*revalidate)(dev_tdev);
}
这个结构的每一个成员的名字都对应着一个系统调用。
用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。
这是linux的设备驱动程序工作的基本原理。
既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。
第四:
设备操作
ok,我们现在来讲讲设备操作吧,所谓的设备操作就是一些函数的编写啦!
一起来看看吧!
打开操作:
int(*open)(structinode*inode,structfile*file)
{
}
关闭操作:
void(*release)(structinode*inode,structfile*file)
{
}
向设备写操作:
ssize_t(*write)(structfile*filp,constchar__user*buff,size_tcount,loff_toffp*)
{
}
向设备读操作:
ssize_t(*read)(structfile*filp,constchar__user*buff,size_tcount,loff_toffp*)
{
}
filp是文件指针,count是请求传输的数据量,buff参数指向数据缓存,最后,offp指出文件当前的访问位置。
在这里要说明一点的是:
read和write方法的参数buff参数是用户空间的。
因此,它不能被内核代码直接引用,因为用户空间指针在内核空间是根本无效的。
所以在read和write中要用内核提供的专门的函数,如下:
intcopy_from_user(void*to,constvoid__user*from,intn)
intcopy_to_user(void__user*to,constvoid*from,intn)
大家可能有点急了,怎么讲了这么多的理论的东西呢,别急我再讲多一个我们就进行实际的编程吧!
ioctl驱动方法:
int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg)
我们来好好讲解一下如何使用ioctl吧,这个函数还是比较重要,而且相对有点难度的。
1、定义命令
在编写ioctl代码之前,首先需要的是定义命令。
Ioctl命令编码被划分为几个段位:
类型,序号,传送方向,参数大小。
其中在Documentation/ioctl-number.txt文件中罗列了在内核中已经使用的幻数。
Type:
表明是哪个设备的命令。
Number:
表明设备命令中的第几个,8位宽。
Direction:
数据传输方向,_IOC_NONE(没有数据传输),_IOC_READ,_IOC_WRITE,数据传送是从应用程序的观点来看的。
_IOC_READ意思是从设备读。
Size:
用户数据的大小。
有相关的宏来帮助定义命令:
_IO(type,nr)没有参数的命令
_IOR(type,nr,datatype)从驱动中读取数据
_IOW(type,nr,datatype)写数据到驱动
_IOWR(type,nr,datatype)双向传送
定义范例:
#defineMEN_IOC_MAGIC‘m’//定义幻数
#defineMEN_IOCSET_IOW(MEN_IOC_MAGIC,0,int)
2、函数的实现
(1)返回值:
一般执行一个switch语句,命令出错时,返回-EINVAL(非法参数)。
(2)参数使用:
如果是整数直接使用,如果是指针,先要确保这个用户地址是否有效,因此使用前需进行正确的检查。
不需要检查:
Copy_from_user
Copy_to_user
Get_user
Put_user
需要检查:
__get_user
__put_user
参数检查函数:
intaccess_ok(inttype,constvoid*addr,unsignedlongsize)
第一个参数是VERIFY_READ或者VERIFY_WRITE,用来检查是读内存还是写内存。
addr参数是要操作的用户内存地址,size是操作的长度。
返回1是成功的。
(3)命令操作
Ok,让我们来看一个实例吧!
这个实例是驱动四个LED灯。
这个实例是基于TQ2440开发板的,我们先来简单分析一下硬件先吧!
在TQ2440中的四个LED灯对应的管脚是GPB5、GPB6、GPB7、GPB8。
下面是原理图:
可得,为低电平时LED灯点亮!
先前学了这么多理论知识,现在是大显身手的时候了!
请看下面代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*加载模式后,执行cat/proc/devices命令可以看到设备名称*/
#defineDEVICE_NAME"GPIO-Control"
/*ioctl用到的命令,应用程序上的ioctl用到的命令的值也是0、1的*/
#defineIOCTL_GPIO_ON1
#defineIOCTL_GPIO_OFF0
/*用来指定LED所用的GPIO脚,相关定义已经在内核中定义好了*/
staticunsignedlonggpio_table[]=
{
S3C2410_GPB5,//37
S3C2410_GPB6,//38
S3C2410_GPB7,//39
S3C2410_GPB8,//40
};
/*指定GPIO为输出功能,相关定义已经在内核中定义好了*/
staticunsignedintgpio_cfg_table[]=
{
S3C2410_GPB5_OUTP,//01
S3C2410_GPB6_OUTP,//01
S3C2410_GPB7_OUTP,//01
S3C2410_GPB8_OUTP,//01
};
/*这个函数就是到时我们通过应用程序传递参数的时对应的函数了*/
staticinttq2440_gpio_ioctl(structinode*inode,structfile*file,unsignedintcmd,unsignedlongarg)
{
if(arg>4)
{
return-EINVAL;
}
switch(cmd)
{
caseIOCTL_GPIO_ON:
//从应用程序接收到的是这个命令的话,就输出低电平
s3c2410_gpio_setpin(gpio_table[arg],0);/*内核自带的函数,设置对应的IO口为0*/
return0;
caseIOCTL_GPIO_OFF:
//从应用程序接收到的是这个命令的话,就输出高电平
s3c2410_gpio_setpin(gpio_table[arg],1);
return0;
default:
return-EINVAL;
}
}
/*写好的操作函数就是通过这个结构注册进内核的了*/
staticstructfile_operationsdev_fops={
.owner=THIS_MODULE,
.ioctl=tq2440_gpio_ioctl,
};
/*在这里我们把LED当做是混杂设备,所以我们用到了这个结构体,包括的信息就是里面已经定义的东西*/
staticstructmiscdevicemisc={
.minor=MISC_DYNAMIC_MINOR,//等于255
.name=DEVICE_NAME,//GPIO-Control
.fops=&dev_fops,
};
staticint__initdev_init(void)//__init表示的意思就是内核在启动的时候就被内核强制加载运行
{
intret;
inti;
for(i=0;i<4;i++)
{
s3c2410_gpio_cfgpin(gpio_table[i],gpio_cfg_table[i]);
s3c2410_gpio_setpin(gpio_table[i],0);
}
ret=misc_register(&misc);//混杂设备的注册
printk(DEVICE_NAME"initialized\n");
returnret;
}
/*就是和加载项对应的卸载了*/
staticvoid__exitdev_exit(void)
{
misc_deregister(&misc);
}
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- s3c2440 驱动 学习