linux设备驱动开发实例.docx

上传人:b****7 文档编号:8831426 上传时间:2023-02-02 格式:DOCX 页数:49 大小:177.85KB
下载 相关 举报
linux设备驱动开发实例.docx_第1页
第1页 / 共49页
linux设备驱动开发实例.docx_第2页
第2页 / 共49页
linux设备驱动开发实例.docx_第3页
第3页 / 共49页
linux设备驱动开发实例.docx_第4页
第4页 / 共49页
linux设备驱动开发实例.docx_第5页
第5页 / 共49页
点击查看更多>>
下载资源
资源描述

linux设备驱动开发实例.docx

《linux设备驱动开发实例.docx》由会员分享,可在线阅读,更多相关《linux设备驱动开发实例.docx(49页珍藏版)》请在冰豆网上搜索。

linux设备驱动开发实例.docx

linux设备驱动开发实例

·1基础知识

设备驱动的作用:

任何一个计算机系统的运行都是系统中软硬件协作的结果,没有硬件的软件是空中楼阁,没有软件的硬件则是一堆废铁。

硬件是底层基础,是所有软件得以运行的平台,代码最终会落实为硬件上的组合逻辑与时序逻辑;软件则实现了具体应用,它按照各种不同的业务需求而设计,满足了用户的需求。

软件和硬件不应该相互渗透到对方的领域。

为了尽快的完成设计,应用软件工程师不想也不必关心硬件,而硬件工程师也难掌握软件编程语言。

例如,应用软件工程师在使用printf函数输出信息的时候,他不用知道具体底层是怎么实现将信息输出到显示屏或者串口上的。

也就是说,应用软件工程师需要看到的是一个没有硬件的纯粹的软件世界,他不用知道底层的硬件原理,而是通过一些通用的接口函数就可以操作。

那么这些接口函数是怎么提供给上层的软件工程师的呢,那这个艰巨的任务就落在了驱动工程师的头上。

驱动程序在Linux内核里扮演着特殊的角色.它们是截然不同的"黑盒子",使硬件的特殊的一部分响应定义好的内部编程接口.它们完全隐藏了设备工作的细节.用户的活动通过一套标准化的调用来进行,这些调用与特别的驱动是独立的;设备驱动的角色就是将这些调用映射到作用于实际硬件的和设备相关的操作上.这个编程接口是这样,驱动可以与内核的其他部分分开建立,并在需要的时候在运行时"插入".这种模块化使得Linux驱动易写,以致于目前有几百个驱动可用.总而言之:

驱动就是linux给用户操作硬件提供的一个接口,它是一个存在于应用程序和实际设备间的软件层。

由此可见,设备驱动充当了硬件和应用软件之间的纽带,它使得应用软件只需要调用系统的一些应用编程接口(API)就可以让硬件去完成相应的操作。

在没有操作系统的情况下,我们可以根据硬件设备的特点利用汇编和C语言的混合编程来操作硬件。

在有操作系统的情况下,设备驱动的结构则由相应的操作系统定义,然后驱动工程师必须按照相应的架构设计设备驱动,这样,设备驱动才能良好的整合到操作系统的内核中。

并不是所有的计算机系统都一定要运行操作系统,在许多情况下操作系统是不必要的。

对于一些功能比较单一、控制并不复杂的系统,如公交车刷卡机、电冰箱、微波炉、简单的手机和小灵通等,并不需要多任务调度、文件系统、内存管理等复杂功能,用单任务架构完全可以很好的支持它们的工作。

一个无限循环中夹杂对设备中断的检测或者对设备的轮询是这种系统中软件的典型架构。

在这样的系统中,虽然不存在操作系统,但是设备驱动往往是必须存在的。

一般情况下,对每一种设备驱动都会定义

那么如果计算机系统中包含了操作系统,那么设备驱动会变得怎样呢?

首先我们要知道操作系统的功能。

操作系统的主要功能:

进程管理、内存管理、文件管理、设备控制和网络管理等。

操作系统的存在它要求设备驱动附加更多的代码和功能,把单一的“驱动硬件设备行动”变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的API。

此时我们需要将设备驱动融入到内核中。

为了实现这个融合,必须在所有的设备驱动中设计面向操作系统内核的接口,这样的接口由操作系统规定。

我们的linux操作系统是一个开源的操作系统,Linux的众多优良特性之一就是可以在运行时扩展由内核提供的特性的能力.这意味着你可以在系统正在运行着的时候增加内核的功能(也可以去除).每块可以在运行时添加到内核的代码,被称为一个模块.Linux内核提供了对许多模块类型的支持,包括但不限于设备驱动.每个模块由目标代码组成(没有连接成一个完整可执行文件),可以动态连接到运行中的内核中,通过insmod安装模块程序,以及通过rmmod移除模块.

Linux内核的划分如图所示:

计算机系统的硬件主要由CPU、存储器和外设组成。

随着IC制造工艺的发展,目前,芯片的集成度越来越高,往往在CPU内部就集成了存储器和外设适配器。

ARM、PowerPC、MIPS等处理器都集成了UART、IIC控制器、USB控制器、SDRAM控制器等,有的处理器还集成了片内RAM和Flash。

驱动针对的对象是存储器和外设(包括CPU内部集成的存储器和外设),而不是针对CPU核。

Linux将存储器和外设分为3种基本设备类型(字符设备、块设备、网络设备).每个模块常常实现3种设备类型中的1种,因此可分类成字符模块,块模块,网络模块.

3类设备如下:

字符设备:

一个字符(char)设备是一种可以当作一个字节流来存取的设备(如同一个文件);它是必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。

这样的驱动常常至少实现open,close,read,和write系统调用.它不经过系统的快速缓冲。

块设备:

如同字符设备,它可以用任意顺序进行访问,以块为单位进行操作,它会经过系统的快速缓冲。

块设备通过位于/dev目录的文件系统结点来存取.一个块设备(例如一个磁盘)应该是可以驻有一个文件系统的.Linux允许应用程序读写一个块设备象一个字符设备一样--它允许一次传送任意数目的字节.结果就是,块和字符设备的区别仅仅在内核在内部管理数据的方式上,因此在内核/驱动的软件接口上有所不同.

网络设备:

面向数据包的接收和发送而设计,它并不对应文件系统的节点。

除了网络设备以外,字符设备和块设备都被映射到linux文件系统的文件和目录,在linux中,一切皆文件。

通过文件系统的系统调用接口open/write/read/close等函数调用即可访问字符设备和块设备。

所有的字符设备和块设备都被统一的呈现给用户。

块设备比字符设备复杂,在它上面会首先建立一个磁盘/Flash文件系统,如FAT、Ext3、YAFFS、JFFS等。

这些文件系统规范了文件和目录在存储介质上的组织方式。

应用程序可以使用linux的系统调用接口编程,也可以使用C库函数。

linux设备驱动与整个软硬件系统的关系如图:

·2linux内核模块编程

linux内核的整体结构非常庞大,其包含的组件也非常多,那么我们怎么把需要的部分都包含在内核中呢?

一种方法时把所有需要的功能都编译到内核,利用makezImage可以实现,并会生成镜像文件,烧录到开发板上即可。

但是这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除部分功能,将不得不重新编译内核,而这个过程是比较耗费时间的。

那么有没有一种机制使得编译出的内核本身并不需要包含所有功能,而是在这些功能需要被使用的时候,其对应的代码可以被动态的加载到内核中呢?

linux提供了这种机制,称为模块(MODULE)。

模块的特点:

模块本身不被编译如内核镜像,从而控制了内核的大小;模块一旦被加载,它就和内核中的其他部分完全一样;模块可以被卸载,使用非常方便。

在进行内核模块编程之前,先要保证我们的开发板能够从nandflash启动进入命令行模式。

如果不行需要重新烧写镜像。

然后将linux-2.6.30.tar.bz2解压。

并且确保交叉编译器为4.3.3版本。

解压出来的linux内核它的源代码目录结构如下:

arch:

包含和硬件体系结构相关的代码,每种平台占一个相应的目录,如i386、ARM、PowerPC、MIPS等。

block:

块设备驱动程序I/O调度。

crypto:

常用加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法。

Documentation:

内核各部分的通用解释和注释。

drivers:

设备驱动程序,不同的驱动占用一个子目录(如char、block、net、mtd、iic)

fs:

支持的各种文件系统,如EXT、FAT、NTFS、JFFS2、YAFFS等。

include:

头文件,与系统相关的头文件被放置在include/linux子目录下。

init:

内核初始化代码。

ipc:

进程间通信的代码。

kernel:

内核的最核心部分,包括进程调度、定时器等,而和平台相关的一部分代码放在了arch/…/kernel目录下。

lib:

库文件代码。

mm:

内存管理代码,和平台相关的一部分代码放在arch/…/mm目录下。

net:

网络相关代码,实现了各种常见的网络协议。

scripts:

包含用于配置内核的脚步文件。

security:

主要包含SELinux模块。

sound:

ALSA、OSS音频设备的驱动核心代码和常见设备驱动。

usr:

实现了用于打包和压缩的cpio等。

linux内核主要由进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通信(IPC)等5个子系统组成。

如图:

内核模块编程常用函数说明如下:

·指定模块的初始化和清理退出函数的宏定义:

#include

module_init(init_function);//指明模块的入口函数

module_exit(exit_function);//指明模块的退出函数

module_init和module_exit是两个宏。

module_init:

指明模块的入口函数,当模块加载到内核时会自动调用init_function函数,该函数初始化成功,则返回0;失败,则返回错误编码。

在linux内核中,错误编码是一个负值,在中定义,包含-ENODEV、-ENOMEM之类的符号值;

module_exit:

指明模块的退出函数,当模块被去除时会自动调用exit_function函数,模块卸载函数要完成与模块加载函数相反的功能。

如:

若模块加载函数注册了XXX,则模块卸载函数应该注销XXX。

若模块加载函数动态申请了内存,则模块卸载函数应释放该内存。

若模块加载函数申请了硬件资源(中断、DMA通道、I/O端口和I/O内存等)的占用,则模块卸载函数应释放这些硬件资源。

若模块加载函数开启了硬件,则卸载函数中一般要关闭硬件。

init_function和exit_funciton的函数原型如下:

staticint__initinit_function(void);//静态表示只能在本源程序中调用

staticvoid__exitexit_function(void);

__init:

是一个给内核的暗示,给定的函数只用于初始化操作.所有标识为__init的函数在连接的时候都放在.init.text这个区段内。

模块加载者在模块加载后会丢掉这个初始化函数,使它的内存可做其他用途。

__exit:

标识这个代码是只用于模块卸载操作。

和__init一样,__exit也可以使对应函数在运行完成后自动回收内存。

·模块的打印函数:

#include

intprintk(constchar*fmt,…);

该函数类似于C语言程序中的printf函数。

用于在内核模块中打印信息。

常用的形式为:

printk(“helloinit\n”);或printk(KERN_INFO“helloinit\n”);其中KERN_INFO和后面的字符串之间没有逗号,只是用空格隔开。

用printk,内核会根据日志级别,可能把消息打印到当前控制台上,这个控制台通常是一个字符模式的终端、一个串口打印机或是一个并口打印机。

可选项如下:

#defineKERN_EMERG0/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/

  #defineKERN_ALERT1/*报告消息,表示必须立即采取措施*/

  #defineKERN_CRIT2/*临界条件,通常涉及严重的硬件或软件操作失败*/

  #defineKERN_ERR3/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/

  #defineKERN_WARNING4/*警告条件,对可能出现问题的情况进行警告*/

  #defineKERN_NOTICE5/*正常但又重要的条件,用于提醒*/

  #defineKERN_INFO6/*提示信息,如驱动程序启动时,打印硬件信息*/

  #defineKERN_DEBUG7/*调试级别的消息*/

·几个常见模块宏声明

MODULE_LICENSE(license);//遵循的协议,指明为GPL(通用公共协议)

MODULE_AUTHOR(author);//表示作者

MODULE_DESCRIPTION(description);//具体描述

MODULE_VERSION(version_string);//版本信息

通常写法如下:

MODULE_LICENSE("GPL");

MODULE_AUTHOR("stars984@");

MODULE_DESCRIPTION("thisismyfirstmoduleprogram");

MODULE_VERSION("1.0");

实例如下:

在linux下编写驱动模块程序有两种方式:

第一种:

单独编译:

首先编写通用的驱动Makefile程序如下:

obj-m:

=hello.o//obj-m:

一个makefile符号,内核建立系统用来决定当前目录下的哪个模块应当被建立。

只需要更换这个地方就可以了,Makefile是通用的

KDIR:

=/lib/modules/`uname-r`/build//内核所在目录,此时是在虚拟机上模拟。

如果是想将模块在开发板上运行,需要修改该地址为你刚刚linux-2.6.30.tar.bz2安装的目录,如/opt/linux-2.6.30.4。

如果最后执行make的时候提示163的错误,则在linux-2.6.30.4目录下执行makemodules即可。

PWD:

=$(shellpwd)//当前路径

all:

make-C$(KDIR)M=$(PWD)modules//-C表示去到内核目录中M=表示表示生成的模块*.ko在当前目录

clean:

rm-rf*.o.*.cmd*.ko*.mod.c.tmp_versions

解析make-C$(KDIR)M=$(PWD)modules:

表示编译生成为模块。

这个命令开始是改变它的目录到用-C选项提供的目录下(就是说,你的内核源码目录).它在那里会发现内核的顶层Makefile.这个M=选项使Makefile在试图建立模块目标前,回到你的模块源码目录.

然后编写第一个hello.c模块程序如下:

#include//linux内核的头文件

#include//linux驱动模块的头文件

#include//linux模块入口函数和退出函数的头文件

MODULE_LICENSE("GPL");//定义宏,表示所要遵循的协议

MODULE_AUTHOR("stars984@");//表示作者

staticint__inithello_init(void)//入口函数

{

printk("hello_init\n");

return0;

}

staticvoid__exithello_exit(void)//出口函数

{

printk("hello_exit\n");

}

module_init(hello_init);//内核注册函数

module_exit(hello_exit);//内核退出函数

因为我此时是在虚拟机上模拟,所以直接执行make。

成功生成一个hello.ko。

可以使用modinfohello.ko获得模块的信息。

然后执行insmodhello.ko安装模块,此时在执行dmesg|tail–n10可以看到helloinit。

通过lsmod可以查看你安装的模块(其实执行lsmod命令,实际上读取并分析的是/proc/modules文件,它等价于cat/proc/modules,加载hello.ko成功之后,在内核的/sys/module/下回生成一个目录hello)。

执行rmmodhello表示去除模块hello。

再用dmesg|tail–n10可以看到又有一条信息输出:

helloexit。

如果是用开发板的内核(linux-2.6.30.tar.bz2),则修改Makefile为指定的linux-2.6.30.tar.bz2的解压路径。

然后执行make,如果提示163错误,则到linux-2.6.30.4里面去执行makemodules然后再回到源目录执行make,成功会生成一个hello.ko,然后利用超级终端,从nandflash启动,然后输入命令rz,然后点击菜单里面的发送,选择发送文件就可以将hello.ko发送到开发板上去。

然后再开发板上运行insmodhello.ko,利用dmesg|tail–n10查看信息。

利用lsmod可以查看是否有你安装的模块,用rmmod可以去除模块。

如果开发板从nandflash启动进入不了命令行,则重新烧写。

用DNW连接开发板。

1烧uboot、3烧linuxkernel、6烧文件系统(root_qt_4.5_2.6.30.4_256MB_20100601.bin)、b烧LOGO。

其中hello_init这个函数是在insmod的时候调用的,hello_exit是在rmmod的时候调用的。

第二种:

加入到linux内核编译菜单三态(<*>编入内核编为模块<>不编译)

在linux内核中增加程序需要完成以下3项工作:

·将编写的源代码复制到linux内核源代码所在的响应目录。

·在目录的Kconfig文件中增加新源代码对应项目的编译配置选项。

·在目录的Makefile文件中增加对新源代码的编译条目。

1>.cphello.c/opt/linux-2.6.30.4/drivers/char

2>.cd/opt/linux-2.6.30.4/drivers/char

3>.viKconfig-->添加如下代码:

configHELLO//这里的HELLO,后面在Makefile里面的时候要用CONFIG_HELLO

tristate"helloworld"//表示采用3态模式

dependsonARCH_S3C2440//依赖于S3C2440的架构

4>.修改同目录下的Makefile-->viMakefile-->添加如下代码:

obj-$(CONFIG_HELLO)+=hello.o

5>.进到linux-2.6.30.4下执行makemenuconfig,选择DeriverDrivers--->Characterdevices--->helloworld(选择编译为模块)

6>.在linux-2.6.30.4目录下执行makemodules(表示编译所有的模块。

而makezImage表示编译到内核里面去,并生成镜像文件),会在driver下的char里面生成hello.ko,然后进到drivers/char中,用ftp把hello.ko传到开发板,在开发板上运行insmodhello.ko,rmmodhello,lsmod等操作。

·指定模块参数的函数:

#include

module_param(variable,type,perm);

该函数用户创建模块参数,可以被用户在模块加载时传入。

其中variable表示变量名称,type表示变量类型,可以是bool、charp、int、short等。

perm表示权限,如S_IRUGO表示0444,即所有者,同组和其他人都是只读权限。

实例test.c:

#include

#include

#include

MODULE_LICENSE("GPL");

MODULE_AUTHOR("stars984@");

staticchar*name=NULL;//定义char*类型变量

staticintn=0;//定义int类型变量

module_param(name,charp,0444);

module_param(n,int,0444);

staticint__initfunc_init(void)

{

printk("---funcinit---\n");

printk("%s--->%d\n",name,n);

return0;

}

staticvoid__exitfunc_exit(void)

{

printk("%s--->%d\n",__FILE__,__LINE__);

printk("---funcexit---\n");

}

module_init(func_init);

module_exit(func_exit);

此时执行make生成test.ko,然后执行insmodtest.koname=wangxiaon=1001则可以给内核传递参数。

另外,这里用到了两个宏:

__FILE__和__LINE__。

其中__FILE__表示文件所在的完整路径和文件名,__LINE__表示所在的行号。

·全局变量current

#include

structtask_struct*current;

利用这个变量可以打印当前进程ID和产生该进程执行的命令名。

实例:

printk(KERN_INFO"Theprocessis\"%s\"(pid%i)\n",current->comm,current->pid);

·导出符号

linux2.6的“/proc/kallsyms”文件对应着内核符号表,它记录了符号以及符号所在的内存地址。

模块可以使用如下宏导出符号到内核符号表:

EXPORT_SYMBOL(符号名);

EXPORT_SYMBOL_GPL(符号名);

导出的符号将可以被其他的模块使用,使用前声明一下即可。

EXPORT_SYMBOL_GPL()只适用于包含GPL许可的模块。

例如编写一个求加法和减法的函数如下:

#include

#include

MODULE_LICENSE("GPL");

intadd(inta,intb)

{

returna+b;

}

intsub(inta,intb)

{

returna–b;

}

EXPORT_SYMBOL(add);

EXPORT_SYMBOL(sub);

此时修改hello.c如下:

#include

#include

#include

#include

#include

MODULE_LICENSE("GPL");

MODUL

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 农学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1