设备驱动1.docx
- 文档编号:14257627
- 上传时间:2023-06-21
- 格式:DOCX
- 页数:32
- 大小:296.21KB
设备驱动1.docx
《设备驱动1.docx》由会员分享,可在线阅读,更多相关《设备驱动1.docx(32页珍藏版)》请在冰点文库上搜索。
设备驱动1
驱动程序介绍设备驱动1
驱动分类
——字符设备驱动
字符设备:
字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现open,close,read,write系统调用。
——块设备驱动
块设备:
——在大部分的unix系统,块设备不能按字节处理数据,只能一次传送一个活多个长度是512字节(或一个更大的2次幂的数)的整块数据。
——而linux则允许块设备传送任意数目的字节。
因此,块和字符设备的区别仅仅是驱动的与内核的接口不同。
——网络接口驱动
网络接口:
任何网络事务都通过一个接口来进行,一个接口通常是一个硬件设备(eth0),但是它也可以是一个纯粹的软件设备,比如回环接口(lo)。
一个网络接口负责发送和接收数据报文。
字符设备与块设备的区别:
块设备是可以进行随机访问的,而字符设备不能。
在linux系统中,块设备也可以进行字节访问。
驱动程序安装
——模块方式
——直接编译进内核:
修改Kconfig、修改Makefile,即可。
将要编译进内核的代码(比如hello.c)cp进内核源码树的/kernel/drivers/char。
在char目录下改写Kconfig。
然后再makemenuconfig的时候便能看见helloworld项(Kconfig是用来在menuconfig中增加菜单的,menuconfig配置后的结果保存在.config中);再修改/char目录下的Makefile添加obj-$(CONFIG_HELLO_WORLD)+=hello.o(Makefile根据配置去选择CONFIG_HELLO_WORLD的值)。
如此之后便能编译内核了(进入源码树编译)。
编译好的内核位于arch/arm/boot/uImage
驱动程序使用
A:
linux用户程序通过设备文件(又名:
设备节点)来使用驱动程序操作字符设备和块设备
Q:
设备(字符、块)文件在何处?
——在/dev/目录下
字符设备驱动程序1.设备号2.创建设备文件3.设备注册4.重要数据结构5.设备操作
主次设备号
字符设备通过字符设备文件来存取。
字符设备文件由使用ls–l的输出的第一列的“c”标识。
如果使用ls–l命令,会看到在设备文件项中有2个数(由一个逗号分隔)这些数字就是设备文件的主次设备编号(举例说明,进入/dev/目录,ls–l)
Q:
内核中如何描述设备号?
A:
dev_t其实质为unsignedint32位整数,其中高12位(4K)为主设备号,低20位(64K)为次设备号
Q:
如何从dev_t中分解出主设备号?
A:
MAJOR(dev_tdev)
Q:
如何从dev_t中分解出此设备号?
A:
MINOR(dev_tdev)
设备号
每个设备文件对应有自己的设备号
驱动程序也有自己的设备号
如果两者的设备号对应相同,那么设备文件便和设备驱动建立关联
设备号作用
——主设备号用来标识与设备文件相连的驱动程序。
次编号被驱动程序用来辨别操作的是哪个设备。
主设备号用来反映设备类型
此设备号用来区分同类型的设备
分配主设备号
Linux内核如何给设备分配主设备号?
——静态申请和动态分配两种方法
静态申请
——方法:
1.根据documentation/devices.txt,确定一个没有使用的主设备号
2.使用register_chrdev_region函数注册设备号
——优点:
简单
——缺点:
一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。
intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name)
功能——申请使用从from开始的count个设备号(主设备号不变,次设备号增加)
参数——from:
希望申请使用的设备号
——count:
希望申请使用设备号数目
——name:
设备名(体现在/proc/devices)
动态分配(让内核自动来分)
——方法:
使用alloc_chrdev_region分配设备号
——有点:
简单,易于驱动推广(因为内核知道哪些驱动有没使用)
——缺点:
无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)
——解决办法:
安装驱动后,从/proc/devices中查询设备号
Intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name)
——功能:
请求内核动态分配count个设备号,且次设备号从baseminor开始
——参数:
dev:
分配到的设备号
Baseminor:
起始设备号
Count:
需要分配的设备号数目
Name:
设备名(体现在/proc/devices)
注销设备号
不论使用何种方法分配设备号,都应该在不再使用它们时释放这些设备号
voidunregister_chrdev_region(dev_tfrom,unsignedcount)
——功能:
释放从from开始的count个设备号
创建设备文件
2种方法:
——1.使用mknod命令手工创建
Mknod用法:
mknodfilenametypemajorminor
——filename:
设备文件名
——type:
设备文件类型(b/c)
——major:
主设备号
——minor:
次设备号
例:
mknodserial0c1000
——2.自动创建
重要结构
在linux字符设备驱动程序设计中,有3种非常重要的数据结构:
structfile;structinode;structfile_operations
Structfile
代表一个打开的文件。
系统中每个打开的文件在内核空间都有一个关联的structfile。
它由内核在打开文件时创建,在文件关闭后释放。
(如果有3个程序打开同一个文件,那么也有3个structfile)
——重要成员:
loff_tf_pos//文件读写位置,loff_t其实是个整形
Structfile_operations*f_op
Structinode
用来记录文件的物理上的信息。
因此,它和代表打开文件的file结构是不同的。
一个文件可以对应多个file结构,但只有一个inode结构.
——重要成员:
dev_ti_rdev:
设备号
Structfile_operations
一个函数指针的集合(更像是一个对应关系表,把应用程序中对文件的操作转化为驱动程序中相应的函数),定义能在设备上进行的操作。
结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留为NULL.
例:
mem_fops
Structfile_operationsmem_fops={
.owner=THIS_MODULE;
.llseek=mem_seek;
.read=mem_read;
.write=mem_write;
.ioctl=mem_ioctl;
.open=mem_open;
.release=mem_release;
};
内核代码导读
设备注册
在linux2.6内核中,字符设备使用structcdev来描述.
字符设备的注册可分为如下3个步骤:
——1.分配cdev
Structcdev的分配可使用cdev_alloc函数来完成
Structcdev*cdev_alloc(void)
——2.初始化cdev
Structcdev的初始化使用cdev_init函数来完成
Voidcdev_init(structcdev*cdev,conststructfile_operations*fops)
——参数:
cdev:
待初始化的cdev结构
:
fops:
设备对应的操作函数集
——3.添加cdev
Structcdev的注册使用cdev_add函数来完成
Intcdev_add(structcdev*p,dev_tdev,unsignedcount)
——参数:
p:
待添加到内核的字符设备结构
dev:
设备号
count:
添加的设备个数、
设备操作实现
完成了驱动程序的注册,下一步该做什么呢?
——实现设备所支持的操作(即是file_operations中的函数指针集)
——int(*open)(structinode*,structfile*)
在设备文件上的第一个操作,并不要求驱动程序一定要实现这个方法。
如果该项为NULL,设备的打开操作永远成功。
Open这个函数指针名可以改,比如改为上述mem_fops中的mem_open,但是其参数的类型是固定的,不能更改的。
下同。
——void(*release)(structinode*,structfile*)
当设备文件被关闭时调用这个操作。
与open相仿,release也可以没有
——ssize_t(*read)(structfile*,char__user*,size_tloff_t)
从设备中读取数据
——ssize_t(*write)(structfile*,constchar__user*,size_tloff_t)
向设备发送数据
——unsignedint(*poll)(structfile*,structpoll_table_struct*)
对应select系统调用
——int(*ioctl)(structinode*,structfile*,unsignedint,unsignedlong)
控制设备
——int(*mmap)(structfile*,structvm_area_struct*)
将设备映射到进程虚拟地址空间中
——off_t(*llseek)(structfile*,loff_t,int)
修改文件的当前读写位置,并将新位置作为返回值
——参数:
要操作的文件,移动的偏移量,移动的起始位置(有三种取值,头、当前位置、尾)
那么如何实现上述函数的呢?
OPEN方法
OPEN方法是驱动程序用来为以后的操作完成初始化准备工作的。
在大部分驱动程序中,open完成如下工作:
——初始化设备
——标明次设备号
RELEASE方法
RELEASE方法的作用正好与open相反。
这个设备方法有时也称为close,它应该:
——关闭设备
读和写
读和写方法都完成类似的工作:
从设备中读取数据到用户空间;将数据传递给驱动程序,它们的原型也相当相似:
ssize_txxx_read(structfile*filp,char__user*buff,size_tcount,loff_t*offp);
ssize_txxx_write(structfile*filp,char__user*buff,size_tcount,loff_t*offp);
对于2个方法,filp是文件指针,count是请求传输的数据量。
buff参数指向数据缓存。
最后,offp指出文件当前的访问位置(buff和count来自用户空间,filp和offp来自内核)
Read和write方法的buff参数是用户空间指针。
因此,它不能被内核代码直接引用(而应由内核提供的专门函数来引用),理由如下:
用户空间指针在内核空间时可能根本是无效的——没有那个地址的映射
内核提供了专门的函数用于访问用户空间的指针,例如:
——intcopy_from_user(void*to,constvoid__user*from,intn)
对应写操作,为真则是写失败
——intcopy_to_user(void__user*to,constvoid*from,intn)
对应读操作,为真则是读失败
设备注销
字符设备的注销使用cdev_del函数来完成
Intcdev_del(structcdev*p)
——参数:
p:
要注销的字符设备结构
例:
字符设备驱动程序:
memdev.c
(分析驱动程序不像应用程序那样从头到尾看,应该看入口module_init())
(分析一个字符设备驱动程序,首先分析初始化、分析fileoperations的各函数(open、read、write、seek))
驱动调试技术
调试技术分类
对于驱动程序设计来说,核心问题之一就是如何完成调试。
当前常用的驱动调试技术科分为:
打印调试,调试器调试,查询调试。
——打印调试(printk)
在调试应用程序时,最常用的调试技术是打印,就是在应用程序中合适的点调用printf。
当调试内核代码的时候,可以用printk完成类似任务。
合理使用printk
在驱动开发时,printk非常有助于调试。
但当正式发行驱动程序时,应当去掉这些打印语句。
但你有可能很快又发现,你又需要在驱动程序中实现一个新功能(或者修复一个bug),这时你又要用到那些被删除的打印语句。
这里介绍一种是用printk的合理方法,可以全局地打开或关闭它们,而不是简单地删除。
#ifdefPDEBUG
#definePLOG(fmt,args…)printk(KERN_DEBUG”scull:
”fmt,##args)
#else
#definePLOG(fmt,args..)//donothing
#endif
Makefile作如下修改:
——DEBUG=y
ifeq($(DEBUG),y)
DEBFLAGS=-O2–g–DPDEBUG//D的作用是相当于#define
else
DEBFLAGS=-O2
endif
CFLAGS+=$(DEBFLAGS)
——调试器调试(kgdb)
——查询调试(/proc文件系统)
并非控制
并发与竞态
——并发:
多个执行单元同时被执行
——竞态:
并发的执行单元对共享资源(硬件资源和软件上的全局变量等)的访问导致的竞争状态
例:
If(copy_from_user(&(dev->data[pos]),buf,count))
Ret=-EFAULT;
Gotoout;
假设有2个进程试图同时向一个设备的相同位置写入数据,就会造成数据混乱(对应于多核情况)
处理并发的常用技术是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作共享资源。
在Linux内核中主要通过semaphore(信号量)机制和spin_lock(自旋锁)机制实现。
信号量
Linux内核的信号量在概念和原理上与用户态的信号量是一样的,但是它不能在内核之外使用,它是一种睡眠锁。
如果有一个任务想要获得已经被占用的信号量时,信号量会将这个进程放入一个等待队列,然后让其睡眠。
当持有信号量的进程将信号释放后,处于等待队列中的任务将被唤醒,并让其获得信号量。
——信号量在创建时需要设置一个初始值,表示允许有几个任务同时访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
——当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果释放后信号量的值为非正数,表明有任务等待当前信号量,因此要唤醒等待该信号量的任务。
信号量的实现也是与体系结构相关的,定义在
1.定义信号量structsemaphoresem;
2.初始化信号量
Voidsema_init(structsemaphore*sem,intvall)该函数用户初始化设置信号量的初值,它设置信号量sem的值为val
Voidinit_MUTEX(structsemaphore*sem)该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。
Voidinit_MUTEX_LOCKED(structsemaphore*sem)该函数用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。
定义及初始化的工作可由如下宏一步完成:
DECLARE_MUTEX(name):
定义一个信号量name,并初始化它的值为1
DECLARE_MUTEX_LOCKED(name):
定义一个信号量name,但它把它的初始值设置为0,即锁在创建时就处在已锁状态。
3.获取信号量
voiddown(structsemaphore*sem)获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文使用该函数。
该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行(此时处于TASK_UNINTERRUPTIBLE的状态)。
——intdown_interruptible(structsemaphore*sem):
获取信号量sem。
如果信号量不可用,进程将被设置为TASK_INTERRUPTIBLE(可被信号和中断唤醒)类型的睡眠状态。
该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR
——down_killable(structsemaphore*sem):
获取信号量sem。
如果信号量不可用,进程将被置为TASK_KILLABLE类型的睡眠状态
注:
down()函数(linux2.4)现已不建议继续使用。
建议使用down_killable()或down_interruptible()函数
4.释放信号量
Voidup(structsemaphore*sem):
该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。
自旋锁
自旋锁最多只能被一个可执行单元持有。
自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环(一直占有CPU),一直等待下去,在那里看是否该自旋锁的保持者已经释放了锁,“自旋”就是这个意思。
——spin_lock_init(x):
该宏用于初始化自旋锁x,自旋锁在使用前必须先初始化
——spin_lock(lock):
获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,直到该自旋锁的保持者释放。
——spin_trylock(lock):
试图获取自旋锁lock,如果能立即获得锁,并返回真,否则立即返回假。
它不会一直等待被释放
——spin_unlock(lock):
释放自旋锁lock,它与spin_trylock或spin_lock配对使用
信号量PK自旋锁
——信号量可能允许有多个持有者,而自旋锁在任何时候只能允许一个持有者。
当然也有信号量叫互斥信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量
——信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我们就应该选择信号量。
Ioctl设备控制设备驱动2
大部分驱动除了需要具备读写设备的能力外,还需要具备对硬件控制的能力。
例如,要求设备报告错误信息,改变波特率,这些操作常常通过ioctl方法来完成
用户使用方法
在用户空间,使用ioctl系统调用来控制设备,原型如下:
intioctl(intfd,unsignedlongcmd,...)
原型中的点表示这是一个可选的参数,存在与否依赖于控制命令(第2个参数)是否涉及到与设备的数据交互。
返回值为假则操作失败
驱动ioctl方法
ioctl驱动方法有和用户空间版本不同的原型:
int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg)
cmd参数从用户空间传下来,可选的参数arg以一个unsignedlong的形式传递,不管它是一个整数或一个指针。
如果cmd命令不涉及数据传输,则第3个参数arg的值无任何意义
ioctl实现
如何实现ioctl方法?
——步骤:
1.定义命令;2.实现命令
定义命令(32位)
在编写ioctl代码之前,首先需要定义命令。
为了防止对错误的设备使用正确的命令,命令号应该在系统范围内是唯一的。
Ioctl命令编码被划分为几个位段,include/asm/ioctl.h中定义了这些位字段:
类型(幻数)、序号、传送方向、参数的大小。
Documentation/ioctl-number.txt文件中罗列了在内核中已经使用了的幻数。
定义ioctl命令的正确方法是使用4个位段,这个列表中介绍的符号定义在
——type:
幻数(类型):
表明哪个设备的命令,在参考了ioctl-number.txt之后选出,8位宽。
_IO_TYPE(cmd)!
=...
——number:
序号,表明设备命令中的第几个,8位宽。
_IO_NR(cmd)!
=....
——direction:
数据传送的方向,可能的值是_IOC_NONE(没有数据传输),_IOC_READ,_IOC_WRITE。
数据传送是从应用程序的观点来看的,_IOC_READ意思是从设备读(_IO_DIR(cmd)!
=...)
——size:
用户数据的大小。
(13/14位宽,视处理器而定)_IO_SIZE(cmd)
内核提供了下列宏来帮助定义命令:
——_IO(type,nr):
没有参数传递的命令。
(那么direction的值为_IOC_NONE,size的值为0)
——_IOR(type,nr,datatype):
从驱动中读数据(4个值已经确定)
——_IOW(type,nr,datatype):
写数据到驱动
——_IOWR(type,nr,datatype):
双向传送,type和number成员作为参数被传递
定义命令(范例)
#defineMEM_IOC_MAGIC'm'//定义幻数,一个字母刚好是8位
#defineMEM_IOCSET
_IOW(MEM_IOC_MAGIC,0,int)
#defineMEM_IOCGQSET
_IOR(MEM_IOC_MAGIC,1,int)
Ioctl函数实现
定义好了命令,下一步就是要实现ioctl函数,loctl函数的实现包括3步:
——1.返回值:
ioctl函数的实现通常是根据命令执行的一个switch语句。
但是,当命令号不能匹配任何一个设备所支持的命令时,通常返回-EINVAL(“非法参数”)
——2.参数使用:
如果是一个整数,可以直接使用。
如果是指针,我们必须确保这个用户地址是有效地,因此使用前需进行正确的检查:
不需要检测:
——copy_from_user
——copy_to_user
——get_user
——put_user
需要检测:
——__get_uesr
——__put_user:
传给用户空间
intaccess_o
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 设备 驱动