版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第八章嵌入式Linux驱动程序本章含有多个微课视频,请择机观看第八章嵌入式Linux驱动程序(1)设备驱动开发概述驱动程序处理过程/设备驱动程序框架
(2)内核设备模型功能/Sysfs和实现机制/Platform总线/设备树(扩展)(3)字符设备驱动程序及其实例关键数据结构/驱动框架/实例/GPIO/
pinctrl框架(扩展)/I2C总线(4)块设备与网络设备驱动程序驱动程序框架/关键数据结构/设计方法/实例ARM-Linux驱动程序的4种经典方法1.mmap映射型设计方法(单片机工程师的最爱)2.传统file_operatiopns设计方法(盛久不衰,沿用至今)3.platform总线型设计方法(2.6版本主流)4.设备树(内核3.x版本后流行)
设备驱动程序开发概述PartOne8.1设备驱动程序是应用程序和硬件设备之间的一个软件层,它向下负责和硬件设备的交互,向上通过一个通用的接口挂接到文件系统上,从而使用户或应用程序可以无需考虑具体的硬件实现环节。由于设备驱动程序为应用程序屏蔽了硬件细节,在用户或者应用程序看来,硬件设备只是一个透明的设备文件,应用程序对该硬件进行操作就像是对普通的文件进行访问和控制硬件设备(如打开、关闭、读和写等)。Linux设备驱动程序概述
作为Linux内核的重要组成部分,设备驱动程序主要完成以下的功能:(1)对设备初始化和释放。(2)把数据从内核传送到硬件和从硬件读取数据。(3)读取应用程序传送给设备文件的数据和回送应用程序请求的数据。(4)检测错误和处理中断。Linux设备驱动程序可以分为两个主要组成部分:(1)对子程序进行自动配置和初始化,检测驱动的硬件设备是否正常,能否正常工作。(2)设备服务子程序和中断服务子程序,这两者分别是驱动程序的上下两部分。驱动上部分即设备服务子程序的执行是系统调用的结果,并且伴随着用户态向核心态的演变,在此过程中还可以调用与进程运行环境有关的函数,比如sleep()函数。驱动程序的下半部分即中断服务子程序。Linux设备驱动程序分类
1.字符设备字符设备是传输数据以字符为单位进行的设备,字符设备驱动程序通常实现open、close、read和write等系统调用函数,常见的字符设备有键盘、串口、控制台等。通过文件系统节点可以访问字符设备,例如/dev/tty1和/dev/lp1。2.块设备所谓块设备是指对其信息的存取以“块”为单位。块设备和字符设备一样可以通过文件系统节点来访问。在大多数linux系统中,只能将块设备看作多个块进行访问,一个块设备通常是1024B数据。3.网络设备网络设备驱动通常是通过套接字(Socket)等接口来实现操作。任何网络事务处理都可以通过接口来完成和其他宿主机数据的交换。内核和网络设备驱动程序之间的通信与字符设备驱动程序和块设备驱动程序与内核的通信是完全不同的。8.1.1Linux设备驱动程序分类1.字符设备字符设备是传输数据以字符为单位进行的设备,字符设备驱动程序通常实现open、close、read和write等系统调用函数,常见的字符设备有键盘、串口、控制台等。通过文件系统节点可以访问字符设备,例如/dev/tty1和/dev/lp1。字符设备和普通文件系统之间唯一的区别是普通文件允许往复读写,而大多数字符设备驱动仅是数据通道,只能顺序读写。此外,字符设备驱动程序不需要缓冲且不以固定大小进行操作,它与用户进程之间直接相互传输数据。2.块设备所谓块设备是指对其信息的存取以“块”为单位。如常见的光盘、硬磁盘、软磁盘、磁带等,块长大小通常取512B、1024B或4096B等。块设备和字符设备一样可以通过文件系统节点来访问。在大多数linux系统中,只能将块设备看作多个块进行访问,一个块设备通常是1024B数据。块设备的特点是对设备的读写是以块为单位的,并且对设备的访问是随机的。块设备和字符设备的区别主要在于内核内部的管理上,其中应用程序对于字符设备的每个I/O操作都会直接传递给系统内核对应的驱动程序;而应用程序对于块设备的操作要经过系统的缓冲区管理间接地传递给驱动程序处理。
3.网络设备网络设备驱动通常是通过套接字(Socket)等接口来实现操作。任何网络事务处理都可以通过接口来完成和其他宿主机数据的交换。内核和网络设备驱动程序之间的通信与字符设备驱动程序和块设备驱动程序与内核的通信是完全不同的。8.1.2驱动程序的处理过程:块设备为例如果逻辑I/O层请求读取块设备的第j块,假设请求到来时驱动程序处于空闲状态,那么驱动程序立刻执行该请求,由于外设速度相比CPU要慢很多,因此进程会在该数据块缓存上阻塞,并调度新的进程运行。但是如果驱动程序同时正在处理另一个请求,那么就将请求挂在一个请求队列中,对应的请求进程也阻塞于所请求的数据块。当完成一个请求的处理时,设备控制器向系统发出一个中断信号。结束中断的处理方法是将设备控制器和通道的控制块均置为空闲状态,然后查看请求队列是否为空。如果为空则驱动程序返回,反之则继续处理下一个请求。如果传输错误,则向系统报告错误或者进行相应进程重复执行处理。对于故障中断,则向系统报告故障,由系统进一步处理。1.内存与I/O端口编写驱动程序大多数情况下其本质都是对内存和I/O端口的操作。(1)内存Linux通常有以下几种地址类型:用户虚拟地址物理地址总线地址内核逻辑地址内核虚拟地址(2)I/O端口有两个重要的内核调用可以保证驱动程序使用正确的端口,它们定义在include/linux/ioport.h中。int__check_region(structresource*,resource_size_t,resource_size_t);该函数的作用是查看系统I/O表,看是否有别的驱动程序占用某一段I/O口。structresource*__request_region(structresource*, resource_size_tstart, resource_size_tn, constchar*name,intflags);根据CPU系统结构的不同,CPU对I/O端口的编址方式通常有两种:第一种是I/O映射方式,如x86处理器为外设专门实现了一个单独的地址空间,称为I/O地址空间,CPU通过专门的I/O指令来访问这一空间的地址单元;第二种是内存映射方式,RSIC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为了内存的一部分,此时CPU访问I/O端口就像访问一个内存单元,不需要单独的I/O指令。这两种方式在硬件实现上的差异对软件来说是完全可见的。2.并发控制
在驱动程序中经常会出现多个进程同时访问相同的资源时可能会出现竞态(racecondition),即竞争资源状态,因此必须对共享资料进行并发控制。Linux内核中解决并发控制最常用的方法是自旋锁(spinlocks)和信号量(semaphores)。(1)自旋锁自旋锁是一个互斥现象的设备,它只能是两个值:locked(锁定)或unlocked(解锁)。它通常作为一个整型值的单位来实现。在任何时刻,自旋锁只能有一个保持者,也就是说在同一时刻只能有一个进程获得锁。(2)信号量信号量是一个结合一对函数的整型值,这对函数通常称为P操作和V操作。自旋锁和信号量有很多相似之处但又有些本质的不同。其相同之处主要有:首先它们对互斥来说都是非常有用的工具;其次在任何时刻最多只能有一个线程获得自旋锁或信号量。不同之处主要有:首先自旋锁可在不能睡眠的代码中使用,如在中断服务程序(ISR)中使用,而信号量不可以;其次自旋锁和信号量的实现机制不一样;最后通常自旋锁被用在多处理器系统。总体而言,自旋锁通常适合保持时间非常短的情况,它可以在任何上下文中使用,而信号量用于保持时间较长的情况,只能在进程上下文中使用。互斥锁(互斥量)互斥锁(Mutex,全称MutualExclusion)是一种同步原语,用于防止多个线程或进程同时访问共享资源,从而避免竞态条件和数据不一致的问题。互斥锁确保任何时候只有一个线程可以持有锁并访问资源,其他线程必须等待直到锁被释放。互斥锁的基本原理包括:在编程中,互斥锁通常通过特定的库或语言特性来实现。例如,在C语言中,可以使用POSIX线程库(pthread)提供的pthread_mutex_t类型来实现互斥锁。当一个线程想要访问共享资源时,它首先尝试锁定互斥锁。如果锁已经被其他线程持有,则线程将被阻塞,直到锁被释放。锁定(Lock)一旦线程成功锁定互斥锁,它就可以安全地访问共享资源。访问资源线程完成对共享资源的访问后,必须释放锁,以便其他线程可以访问资源。解锁(Unlock)如果线程在持有互斥锁的同时尝试再次锁定它,这可能导致死锁。为了避免这种情况,通常要求线程在尝试重新锁定之前先解锁。死锁避免自旋锁和互斥锁之间的区别自旋锁是忙等待(busy-waiting)的,而互斥锁是休眠等待(sleep-waiting)的(又叫做休眠锁)。01需要访问共享资源的进程或线程必须先获取互斥锁才能访问。如果互斥锁已经被另一个进程或线程占有,这个尝试获取的进程或线程将被阻塞,直到互斥锁被释放。互斥锁通常使用原子操作和系统调用的组合来实现。互斥锁的主要优点是防止忙等待,从而可以节省CPU周期并降低功耗。互斥锁是为共享资源提供互斥的锁。02自旋锁不会在锁已被占有时阻塞进程或线程,而是会在循环中重复检查锁的状态,直到它空闲为止。这比阻塞和解除阻塞进程或线程更有效,尤其是在锁只占用很短时间的情况下。但是,如果锁占用时间过长,自旋锁会浪费大量CPU时间,并且其他进程或线程可能会因缺乏CPU资源造成饥饿。自旋锁是一种使用忙等待来同步对共享资源访问的锁。3.阻塞与非阻塞在驱动程序的处理过程中我们提到了阻塞的概念,这里进行以下说明。阻塞(blocking)和非阻塞(nonblocking)是设备访问的两种不同模式,前者在I/O操作暂时不可进行时会让进程睡眠,而后者在I/O操作暂时不可进行时并不挂起进程,它或者放弃,或者不停地查询,直到可以进行操作为止。(1)阻塞与非阻塞操作阻塞操作是指在执行设备操作时,若不能获得资源则进程挂起,直到满足可操作的条件再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列中移走,直到等待条件被满足。非阻塞操作是在不能进行设备操作时并不挂起,它会立即返回,使得应用程序可以快速查询状态。(2)异步通知异步通知是指一旦设备准备就绪,则该设备会主动通知应用程序,这样应用程序就不需要不断地查询设备状态,通常把异步通知称为信号驱动的异步I/O(SIGIO),这有点类似于硬件上的中断。中断处理Linux将中断分为两个部分:上半部分(tophalf)和下半部分(bottomhalf)。上半部分的功能是注册中断,下半部完成了中断处理程序的大部分工作中断的Bottom-half机制,包括了softirq(软中断)、tasklet(任务队列)、workqueue(工作队列)等,从2.3版本开始,Linux为实现下半部的机制主要引入了tasklet()和软中断。软中断是一组静态定义的下半部接口,可在所有处理器上同时执行-这就要求软中断执行的函数必须可重入。当软中断在访问临界区时需要用到同步机制,如自旋锁。软中断主要针对时间严格要求的下半部使用,如网络和SCSI。任务队列(tasklet)下面是tasklet的定义:structtasklet_struct{ structtasklet_struct*next;//指向下一个tasklet unsignedlongstate;//tasklet的状态 atomic_tcount;//计数,1表示禁止 void(*func)(unsignedlong);//处理函数指针 unsignedlongdata;//处理函数参数};在interrupt.h中可以看到tasklet的数据结构,其状态定义了两个位的含义:enum{TASKLET_STATE_SCHED,/*正在运行*/ TASKLET_STATE_RUN /*已被调度,准备运行*/};工作队列(workqueue)接口在linux2.5版本中引入,取代了任务队列接口。工作队列与tasklet的主要区别在于tasklet在软中断上下文中运行,代码必须是原子的;工作队列函数在一个内核线程上下文中运行,并且可以在延迟一段时间后才执行,因而具有更多的灵活性,并且工作队列可以使用信号量等能够sleep的函数。另外,工作队列的中断服务程序和tasklet非常类似,唯一不同就是它调用schedule_work()来调度下半部处理,而tasklet使用tasklet_schedule()函数来调度下半部处理。工作队列(workqueue)工作队列的queue核心代码如下所示。staticvoidinsert_work(structcpu_workqueue_struct*cwq,
structwork_struct*work,structlist_head*head,
unsignedintextra_flags){
structglobal_cwq*gcwq=cwq->gcwq;
set_work_cwq(work,cwq,extra_flags);
list_add_tail(&work->entry,head);
//head参数表示的是每cpu一个的全局队列
if(__need_more_worker(gcwq))//如果是诸如高优先级之类的工作或者当前已经没有//空闲的工作者了,那么唤醒一个工作者,系统起码要保持一个空闲的工作者进程以备用。
wake_up_worker(gcwq);}驱动程序在使用工作队列时的主要步骤是:(1)当驱动程序不使用默认的工作队列时,驱动程序可以创建一个新的工作队列。(2)当驱动程序需要延迟时,根据需要静态或者动态创建工作队列。(3)将工作队列任务插入工作队列。5.设备号用户进程与硬件的交流是通过设备文件进行的,硬件在系统中会被抽象成为一个设备文件,访问设备文件就相当于访问其所对应的硬件。每个设备文件都有其文件属性(c/b),表示是字符设备还是块设备。每个设备文件的设备号有两个:第一个是主设备号,标识驱动程序对应一类设备的标识;第二个是从设备号,用来区分使用共用的设备驱动程序的不同硬件设备。设备号在linux2.6内核中,主从设备被定义为一个dev_t类型的32位数,其中前12位表示主设备号,后20位表示从设备号。另外,在include/linux/kdev.h中定义了如下的几个宏来操作主从设备号。#defineMAJOR(dev) ((unsignedint)((dev)>>MINORBITS))#defineMINOR(dev) ((unsignedint)((dev)&MINORMASK))#defineMKDEV(ma,mi) (((ma)<<MINORBITS)|(mi))上述宏分别实现从32位dev_t类型数据中获得主设备号、从设备号及将主设备号和从设备号转换为dev_t类型数据的功能。设备驱动程序框架Linux的设备驱动程序可以分为以下部分。(1)驱动程序与内核的接口,这是通过关键数据结构file_operations来完成的。(2)驱动程序与系统引导的接口,这部分利用驱动程序对设备进行初始化。(3)驱动程序与设备的接口,描述了驱动程序如何与设备进行交互,这与具体设备密切相关。根据功能划分,设备驱动程序代码通常可分为以下几个部分:(1)驱动程序的注册与注销(2)设备的打开与释放:引用计数(3)设备的读写操作(4)设备的控制操作(5)设备的轮询和中断处理根据功能划分,设备驱动程序代码通常可分为以下几个部分:(1)驱动程序的注册与注销对于字符设备或者是块设备,关键的一步还要向内核注册该设备,linux操作系统也专门提供了相应的功能函数,如字符设备注册函数register_chrdev()、块设备注册函数register_blkdev()。在设备关闭时,要在内核中注销该设备,操作系统也相应的提供了注销设备的函数unregister_chrdev()、unregister_blkdev(),并释放设备号。在内核中使用一个数组chrdevs[]保存所有字符设备驱动程序的信息,在fs/char_dev.c中该数组的数据结构如下所示:staticstructchar_device_struct{ structchar_device_struct*next; unsignedintmajor; unsignedintbaseminor; intminorct; charname[64]; structcdev*cdev; }*chrdevs[CHRDEV_MAJOR_HASH_SIZE];字符设备驱动程序的注册其实就是将字符设备驱动程序插入到该数组中。Linux通过字符设备注册函数register_chrdev()来完成注册功能。其函数原型如下:int__register_chrdev(unsignedintmajor,unsignedintbaseminor, unsignedintcount,constchar*name, conststructfile_operations*fops)Linux通过字符设备注销函数unregister_chrdev来完成注销功能。其函数原型如下:void__unregister_chrdev(unsignedintmajor,unsignedintbaseminor,unsignedintcount,constchar*name)块设备比字符设备要复杂,但是块设备驱动程序也需要一个主设备号来标识。块设备驱动程序的注册时通过register_blkdev()函数实现的。其函数原型如下:int__register_blkdev(unsignedintmajor,constchar*name)(2)设备的打开与释放打开设备是由调用定义在incliude/linux/fs.h中的file_operations结构体中的open()函数完成的。open()函数主要完成的主要工作:增加设备的使用计数。检测设备是否异常,及时发现设备相关错误,防止设备有未知硬件问题。若是首次打开,首先完成设备初始化。读取设备次设备号。其函数原型如下:int(*open)(structinode*,structfile*);释放设备由release()完成,包括以下几件事情:释放open时系统为之分配的内存;释放所占用的资源,并进行检测,关闭设备,并递减设备使用计数。其函数原型如下:int(*release)(structinode*,structfile*);(3)设备的读写操作字符设备对数据的读写操作是由各自的read()函数和write()函数来完成的。对块设备的读写操作,由文件block_devices.C中定义的函数blk_read()和blk_write()完成。真正需要读写的时候由每个设备的request()函数根据其参数cmd与块设备进行数据交换。(4)设备的控制操作ioctl()函数就是驱动程序提供的控制函数。该函数的使用和具体设备密切相关。在linux内核版本2.6.35以前,ioctl()函数原型如下:int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg);在2.6.35以后,系统已经完全删除了structfile_operations中的ioctl函数指针,剩下unlocked_ioctl和compat_ioctl,取而代之的是unlocked_ioctl,主要改进就是不再需要上大内核锁(调用之前不再先调用lock_kernel()然后再unlock_kernel())。如下是unlocked_ioctl和compat_ioctl的原型:long(*unlocked_ioctl)(structfile*,unsignedint,unsignedlong);long(*compat_ioctl)(structfile*,unsignedint,unsignedlong);(5)设备的轮询和中断处理对于支持中断的设备,可以按照正常的中断方式进行。但是对于不支持中断的设备,过程就相对繁琐,在确定是否继续进行数据传输时都需要轮询设备的状态。驱动程序的加载
通常linux驱动程序可通过两种方式进行加载:一种是将驱动程序编译成模块形式进行动态加载,常用命令有insmod(加载)、rmmod(卸载)等;另一种是静态编译,即将驱动程序直接编辑放进内核。动态加载模块设计使Linux内核功能更容易扩展。而静态编译方法对于在要求硬件只是完成比较特定、专一的功能的一些嵌入式系统中,具有更高的效率。以网卡DM9000为例说明驱动程序的加载过程
内核设备模型PartTwo8.2内核设备模型
在Linux2.6内核及后续版本中,设备模型为设备驱动程序管理、描述设备抽象数据结构之间关系等提供了一个有效的手段,其主要功能包括
:(1)电源管理和系统关机(2)与用户空间通信(3)热插拔(hotplug)设备管理(4)设备类型管理(5)对象生命周期处理linux内核各目录代码量对比linux内核设备模型带来的好处十分之多。首先linux设备模型是一个具有清晰结构的组织所有设备和驱动的树状结构,用户就可以通过这棵树去遍历所有的设备,建立设备和驱动程序之间的联系;其次,Linux驱动模型把很多设备共有的一些操作抽象出来,大大减少重复开发的可能;再次,Linux设备模型提供了一些辅助的机制,比如引用计数,让开发者可以安全高效的开发驱动程序。同时,Linux设备模型还提供了一个非常有用的虚拟的基于内存的文件系统sysfs。Sysfs解释了内核数据结构的输出、属性以及它们之间及用户空间的连接关系。sysfs
sysfs给用户提供了一个从用户空间去访问内核设备的方法,它在Linux里的路径是/sys。这个目录并不是存储在硬盘上的真实的文件系统,只有在系统启动之后才会建起来。可以使用tree/sys这个命令显示sysfs的结构。由于信息量较大,这里只列出第一层目录结构:/sys|--block|--bus|--class|--dev|--devices|--firmware|--fs|--kernel|--module`--power如果只是单纯要访问设备,一般很少会直接操作sysfs,因为sysfs非常繁琐和底层化现象严重,大部分情况下可以使用更加方便的DeviceKit或者libudev。Block目录从块设备的角度来组织设备,其下的每个子目录分别对应系统中的一个块设备;值得注意的是sys/block目录从内核2.6.26已经正式转移到sys/class/block中。Sys/block目录虽然为了向后兼容保持存在,但是其中的内容已经变为指向它们在sys/devices/中真实设备的符号链接文件。Bus目录从系统总线这个角度来组织设备,内核设备按照总线类型分层放置的目录结构,它是构成linux统一设备模型的一部分。Class目录把看以类别的角度看待设备,比如PCI设备或者USB设备等,该目录是按照设备功能分类的设备模型,是linux统一设备模型的一部分。Dev目录下维护一个按照字符设备或者块设备的设备号链接到硬件设备的符号链接,在内核2.6.26首次引入。Devices目录是所有设备的大本营,系统中的任一设备在设备模型中都由一个device对象描述,是sysfs下最重要的目录。该目录结构就是系统中实际的设备拓扑结构。Firmware目录包含了一些比较低阶的子系统,比如ACPI、EFI等,是系统加载固件机制的对用户空间的接口。Fs目录里列出的是系统支持的所有文件系统,但是目前只有fuse、gfs2等少数文件系统支持sysfs接口。Kernel目录下包含的是一些内核的配置选项,如slab分配器等。Modules目录下包含的是所有内核模块的信息,内核模块实际上和设备之间存在对应联系,通过这个目录可以找到设备。Power目录存放的是系统电源管理的数据,用户可以通过它来查询目前的电源状态,甚至可以直接“命令”系统进入休眠等省电模式。sysfs的实现机制kobject
“内核对象”(kernel
object)的设备管理机制,该机制是基于一种底层数据结构,通过这个数据结构,可以使所有设备在底层都具有一个公共接口,便于设备或驱动程序的管理和组织。从面向对象的角度来说,kobject可以看作是所有设备对象的基类。Kobject结构struct
kobject{
const
char
*name;//指向设备名称的指针struct
list_head
entry;//挂接到所在kset中去的单元struct
kobject
*parent;//指向父对象的指针struct
kset
*kset;//所属kset的指针struct
kobj_type
*ktype;//指向其对象类型描述符的指针struct
sysfs_dirent*sd;//指示在sysfs中的目录项struct
kref
kref;//对象引用计数unsignedint
state_initialized:1;//标记:初始化unsignedint
state_in_sysfs:1;//标记在sysfs中;
。。。。。。}kref的定义非常简单,其结构体里只有一个原子变量。struct
kref{
atomic_trefcount;};Ktype域是一个指向kobj-type结构的指针,表示该对象的类型。
struct
kobj_type{
void
(*release)(struct
kobject*kobj);
const
struct
sysfs_ops*sysfs_ops;
struct
attribute**default_attrs;};Ksetkobject通常通过kset组织成层次化的结构,kset是具有相同类型的kobject的集合Kset的定义如下:struct
kset{
struct
list_headlist;//用于连接该kset中所有kobject的链表头
spinlock_tlist_lock;//迭代时用的锁
struct
kobjectkobj;//指向代表该集合基类的对象
const
struct
kset_uevent_ops*uevent_ops;//指向一个用于处理集合中kobject对象的热插拔结构操作的结构体};结构体中的kref域表示该对象引用的计数(引用计数本质上就是利用kref实现的),内核通过kref实现对象引用计数管理。
kref的定义非常简单,其结构体里只有一个原子变量。struct
kref{
atomic_trefcount;};krefLinux内核定义了下面三个函数接口来使用kref:void
kref_init(struct
kref*kref);void
kref_get(struct
kref*kref);int
kref_put(struct
kref*kref,void
(*release)(struct
kref*kref));内核提供两个函数kref_get()、kref_put(),分别用于增加和减少引用计数,当引用计数为0时,所有该对象使用的资源将被释放。Kref在使用前必须通过kref_init()函数初始化,其函数原型如下:staticinlinevoidkref_init(structkref*kref){atomic_set(&kref->refcount,1)}Ktype域是一个指向kobj-type结构的指针,表示该对象的类型。Ktype的定义如下:struct
kobj_type{
void
(*release)(struct
kobject*kobj);//
const
struct
sysfs_ops*sysfs_ops;
struct
attribute**default_attrs;};Ktype设备模型的组织-platform总线
Platform总线就是从2.6
内核开始引入的一种虚拟总线,主要用来管理CPU的片上资源,具有更好的移植性。目前,大部分的驱动都是用Platform总线编写的,除了极少数情况之外如构建内核最小系统之内的而且能够采用CPU存储器总线直接寻址的设备。Platform总线模型主要包括platform_device、platform_bus、platform_driver三个部分。本节有微课Platform总线模型的platform_driver机制将设备的本身资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过标准接口进行申请和使用,具有很高的安全性和可靠性。而模型中的platform_device是一个具有自我管理功能的子系统。当platform模型中总线上有设备,又有驱动的时候,就会进行设备与驱动匹配的过程,总线起到了沟通设备和驱动的桥梁作用。1.Platformbus初始化Platform总线的初始化是在/drivers/base/platform.c中的platform_bus_init()完成的,代码如下:int__initplatform_bus_init(void){ interror; early_platform_cleanup(); error=device_register(&platform_bus); if(error) returnerror; error=bus_register(&platform_bus_type); if(error) device_unregister(&platform_bus); returnerror;}这段初始化代码调用device_register向内核注册(创建)了一个名为“platform_bus”的设备.后续platform的设备都会以此为parent。在sysfs中表示为所有platform类型的设备都会添加在platform_bus所代码的目录下/sys/devices/platform。然后这段初始化代码又调用bus_register注册了platform_bus_type2.platformdevice注册在最底层,Linux系统中的每一个设备都是由一个device数据结构来代表的,该结构定义在<linux/device.h>中。device结构体用于描述设备相关的信息设备之间的层次关系,以及设备与总线、驱动的关系。Platform_device是对device的封装。Platform设备通过struct
platform_device
来进行描述。structplatform_device{ constchar *name;//平台设备的名称 int id;//设备的ID,当ID=-1的时候,表示设备名称只有一个,否则表示设备编号
structdevice dev; u32 num_resources; structresource *resource; conststructplatform_device_id *id_entry; /*MFDcellpointer*/ structmfd_cell*mfd_cell; /*archspecificadditions*/ structpdev_archdata archdata;};3.platformdriver的注册系统中的每个驱动程序由一个device_driver对象描述。Platform设备是一种特殊的设备,它与处理器是通过CPU地址数据控制总线或者GPIO连接的。Platform_driver既具有一般device的共性,也有自身的特殊属性。Platform_driver的描述如下所示:structplatform_driver{ int(*probe)(structplatform_device*);//指向设备探测函数 int(*remove)(structplatform_device*);/指向设备移除函数 void(*shutdown)(structplatform_device*);//指向设备关闭函数 int(*suspend)(structplatform_device*,pm_message_tstate);//指向设备挂起函数 int(*resume)(structplatform_device*);//指向设备恢复函数 structdevice_driverdriver;//驱动基类 conststructplatform_device_id*id_table;//平台设备id列表};设备树为什么要引入设备树?本质上,设备树改变了使用硬编码方法将设备配置信息嵌入内核代码的方式——即使用引导程序以结构化的方式将设备信息传递给内核。那么,设备树是否需要描述系统中的所有硬件信息呢?答案是否定的。可以动态检测的设备通常不需要在设备树中被描述,比如USB设备。然而对于USB控制器,由于它无法被动态识别,所以需要在设备树中对其进行描述。同样,PCI设备可以被动态检测到(比如以太网卡),所以通常不需要在设备树中被描述,但是PCI桥因为不能被动态检测到而需要出现在设备树描述中。设备树的使用从高层次来看,设备树是描述系统硬件组件的层次结构。系统中的每个组件都由树中的一个节点描述。树中的每个节点都有一个名称和一组描述硬件组件特性的属性。在ARM/RISC-V架构中,设备树通常由引导程序或固件生成,并作为引导过程的一部分传递给操作系统。设备树存储在内存中,并由操作系统访问以确定系统硬件组件的配置。设备树设备树的主要作用:一是平台标识,所谓平台标识就是板级识别,让内核知道当前使用的是哪个开发板,这里识别的方式是根据root节点下的compatible字段来匹配。二是运行时配置,比如在内核启动的时候ramdisk的配置,比如bootargs的配置,ramdisk的起始和结束地址。三是设备信息集合,这也是最重要的信息,集合了各种设备控制器设备树的工作流设备树编译器(DTC):用于将设备树编译为系统可识别的二进制文件的工具。位于内核目录下scripts/dtc,也可以手动安装:sudoapt-getinstalldevice-tree-compiler工具。设备树源码(DTS):设备树描述文件。该文件是一种程序员可以看懂的文件格式,但是U-Boot和Linux只能识别二进制文件,不能直接识别该文件。所以需要把.dts文件编译成.dtb文件。设备树源码信息(DTSI):可包括在设备树描述文件中的头文件。设备树块(DTB):系统可读设备树二进制blob文件,在系统中烧录以供执行。设备树设备树用树状结构描述设备信息,它有以下特性:每个设备树文件都有一个根节点,每个设备都是一个节点。节点间可以嵌套,形成父子关系,这样就可以方便地描述设备间的关系。每个设备的属性都用一组key-value对(键值对)来描述。每个属性的描述用“。”表示结束。/{//根节点node1{//node1是节点名,是板子的子节点key=value;//node1的属性...node2{
//node2是node1的子节点key=value;//node2的属性...}}2.键值对
在设备树中,键值对(key-value)是描述属性的方式,比如,Linux驱动中可以通过设备节点中的"compatible"属性查找设备节点(一般表示设备名及厂家)。Linux设备树语法中定义了一些具有规范意义的属性,包括:compatible,
address,
interrupt等。这些信息能够在内核初始化的时候,自动解析生成相应的设备信息。此外,还有一些特殊的设备通用的属性,这些属性一般不能被内核自动解析生成相应的设备信息,但是内核可以解析提取函数使用,常见的属性有
"mac_addr","gpio","clock","power"。"regulator"
等等。3.address
几乎所有的设备都需要与CPU的I/O端口相连,所以其I/O端口信息就需要在设备节点中说明。其中最常用的属性有:#address-cells,用来描述子节点"reg"属性地址表中首地址的cell的数量。#size-cells,用来描述子节点"reg"属性地址表中地址长度的cell的数量。有了这两个属性,子节点中的"reg"就可以描述一块连续的地址区域4.interrupts
一个计算机系统中大量设备都是通过中断求CPU服务的,设备节点需要指定中断号。常用的属性有:interrupt-controller
,一个中断控制器节点。#interrupt-cells是中断控制器节点的属性,用来描述子节点中“interrupts”属性使用了父节点中的interrupts属性的具体值。interrupt-parent标识此设备节点属于哪一个中断控制器。interrupts,一个中断标识符列表,表示每一个中断输出信号。
设备树中中断的部分涉及的内容比较多,interrupt-controller表示这个节点是一个中断控制器,需要注意的是,一个SoC可能有不止一个中断控制器5.gpio
在端口中gpio也是最常见的I/O口,设备树中常用的属性有:"gpio-controller",用来说明该节点描述的是一个gpio控制器。"#gpio-cells",用来描述gpio使用节点的属性中一个cell的内容。可以通过reg指定引脚也可以通过pin指定引脚,在设备树中如何指定引脚完全取决于驱动程序,既可以获取pin属性值也可以获取reg属性值。设备树示例让我们举一个例子来说明设备树是如何工作的。假设我们有一个系统,其中包括一个UART(通用异步收发器)和一个SPI(串行外设接口)控制器。该系统的设备树可能如下所示:在这个例子中,顶层节点是根节点(/),它包含子节点:chosen和uart0。chosen节点包含一个名为bootargs的属性,用于将命令行参数传递给内核。uart0节点描述了UART控制器,它包含一组描述控制器特性的属性。例如,compatible属性指定应被用于UART控制器的设备驱动程序(在本例中,为ns16550a),reg属性指定UART控制器的内存映射寄存器的基地址和大小。同样,spi0节点描述了SPI控制器,包含一组描述控制器特性的属性。例如,compatible属性指定了应被用于SPI控制器的设备驱动程序(在本例中,为spi-gpio),reg属性指定了SPI控制器的内存映射寄存器的基地址和大小。dm9000网卡设备树示例ethernet@18000000{ compatible="davicom,dm9000"; reg=<0x180000000x20x180000040x2>; interrupt-parent=<&gpn>; interrupts=<74>; local-mac-address=[0000deadbeef]; davicom,no-eeprom;reset-gpios=<&gpf12GPIO_ACTIVE_LOW>; vcc-supply=<ð0_power>; };设备树编译器dtc的使用方法基本命令格式如下:dtc[选项]-I输入格式-O输出格式-o输出文件输入文件假设已有my_device.dts文件,编译为my_device.dtb。则可以使用如下命令:dtc-Idts-Odtb-omy_device.dtbmy_device.dts如果编译成功,会生成my_device.dtb。注意:编译生成的.dtb文件需与内核版本严格匹配,建议直接从内核源码中获取dtc工具链以保证兼容性。编译生成的.dtb文件可以首先在Linux内核配置中启用DeviceTree支持,然后在如U-Boot中指定.dtb文件,比如:setenvbootargs"root=/dev/mmcblk0p2rootfs=ext4rwconsole=ttyS0,115200"loadmmc0:1$kernel_addr_rzImageloadmmc0:1$fdt_addr_rmy_device.dtbbootz$kernel_addr_r-$fdt_addr_r最后将.dtb文件与内核镜像一同烧录到嵌入式设备的存储介质中即可。字符设备驱动设计框架PartThree8.38.3.1字符设备的重要数据结构字符设备驱动程序编写通常都要涉及到三个重要的内核数据结构,分别是file_operations结构体、file结构体和inode结构体。File_operations为用户态应用程序提供接口,是系统调用和驱动程序关联的重要数据结构。File结构体在内核代码include/linux/fs.h中定义,表示一个抽象的打开的文件,file_operations结构体就是file结构的一个成员。Inode结构表示一个文件,而file结构表示一个打开的文件。这正是二者间最重要的关系。每个进程为每个打开的文件分配一个文件描述符,每个文件描述符对应一个file结构,同一个文件被不同的进程打开后,在不同的进程中会有不同的file文件结构,其中包括了文件的操作方式(只读\只写\读写),偏移量,以及指向inode的指针等等。这样,不同的file结构指向了同一个inode节点。字符设备驱动程序的重要数据结构
structfile_operations{ structmodule*owner; loff_t(*llseek)(structfile*,loff_t,int); ssize_t(*read)(structfile*,char__user*,size_t,loff_t*); ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);
long(*unlocked_ioctl)(structfile*,unsignedint,unsignedlong);……file结构体使用open打开文件时,传入的flags、mode等参数会被记录在内核中对应的structfile结构体里(f_flags、f_mode)打开字符设备节点时,内核中也有对应的structfile注意这个结构体中的结构体:structfile_operations*f_op,这是由驱动程序提供的。file结构体文件对象(file)用于记录已打开文件的状态信息,一个文件可以被多个进程多次打开,每次打开一个文件都需要创建一个新的file。8.3.2字符设备驱动框架字符设备驱动程序的初始化流程一般可以用如下的过程来表示:(1)定义相关的设备文件结构体(如file_operation()中的相关成员函数的定义)。(2)向内核申请主设备号(建议采用动态方式)。(3)申请成功后,通过调用MAJOR()函数获取主设备号。(4)初始化cdev的结构体,可以通过调用cdev_init()函数实现。(5)通过调用cdev_add()函数注册cdev到内核。(6)注册设备模块,主要使用module_init()函数和module_exit()函数。这里介绍一下字符设备的分配和初始化,它有两种不同的方式。cdev_alloc()函数用于动态分配一个新的cdev结构体并初始化。一般如果建立新的cdev结构体可以使用该方式,这里给出一个参考代码:structcdev*my_cdev=cdev_alloc();my_cdev->owner=THIS_MODULE;my_cdev->ops=&fops;如果需要把cdev结构体嵌入到指定设备结构中,可以采用静态分配方式。cdev_init()函数可以初始化一个静态分配的cdev结构体,并建立cdev和file_operation之间的连接。与cdev_alloc()唯一不同的是,cdev_init()函数用于初始化已经存在的cdev结构体。这里给出一段参考代码:structcdevmy_cdev;cdev_init(&my_cdev,&fops);my_cdev.owner=THIS_MODULE;编写一个字符设备的驱动程序,首先要注册一个设备号。内核提供了三个函数来注册一组字符设备编号,这三个函数分别是:alloc_chrdev_region()、register_chrdev_region()和register_chrdev()。其中register_chrdev()在上节已经介绍过。这里首先介绍的是alloc_chrdev_region()函数,该函数用于动态申请设备号范围,通过指针参数返回实际分配的起始设备号。Register_chrdev_region()函数用于向内核申请分配已知可用的设备号(次设备号通常为0)范围。下面是该函数原型:intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name)。参数from是要分配的设备号的dev_t类型数据,表示了要分配的设备编号的起始值,参数count表示了允许分配设备编号的范围。register_chrdev()是一个老版本内核的设备号分配函数,不过新内核对其还是兼容的。Register_chrdev()兼容了动态和静态两种分配方式。Register_chrdev()不仅分配了设备号,同时也注册了设备。这是register_chrdev()与前两个函数的最大区别。也就是说,如果使用alloc_chrdev_region()或register_chrdev_region()分配设备号,还需要对cdev结构体初始化。而register_chrdev()则把对cdev结构体的操作封装在了函数的内部。所以在一般的字符设备驱动程序中,不会看到对cdev的操作。与注册分配字符设备编号的方法类似,内核提供了两个注销字符设备编号范围的函数unregister_chrdev_region()和unregister_chrdev()。这两个函数实际上都调用了__unregister_chrdev_region()函数,原理是一样的。Register_chrdev()函数封装了cdev结构的操作,而alloc_chrdev_region()或register_chrdev_region()只提供了设备号的注册,并未真正的初始化一个设备,只有cdev这个表示设备的结构体初始化了,才可以说设备初始化了。这里举出字符设备驱动程序的常见的两种编程架构。架构一:staticint__initxxx_init(void){...register_chrdev(xxx_dev_no,DEV_NAME,&fops);}staticvoid__exitxxx_exit(void){
unregister_chrdev(xxx_dev_no,DEV_NAME);...}module_init(xxx_init);module_exit(xxx_exit);架构二:
structxxx_dev_t{
structcdevcdev;
...}xxx_dev;staticint__initxxx_init(void){
...
cdev_init(&xxx_dev.cdev,&xxx_fops);xxx_dev.cdev.owner=THIS_MODULE;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);
...}module_init(xxx_init);module_exit(xxx_exit);这两个结构中,前一个应用register_chrdev函数封装了cdev,后面可以直接定义file_operations结构体提供系统调用接口。后一种架构用alloc_chrdev_region注册设备号,然后用cdev_init初始化了一个设备,接着用cdev_add添加了该设备。两种架构在模块卸载函数中,分别用相应的卸载函数实现。当file_operations结构与设备关联在一起后,就可以在驱动的架构中补全file_operations的内容,实现一个完整的驱动架构,比如:staticunsignedintxxx_open(){…}staticunsignedintxxx_ioctl(){…}structfile_operationsfops={.owner=THIS_MODULE,.open=xxx_open,.ioctl=xxx_ioctl,//注意新式写法这里应是
.unlocked_ioctl=xxx_ioctl};staticint__initxxx_init(void){...register_chrdev(xxx_dev_no,DEV_NAME,&fops);}staticvoid__exitxxx_exit(void){unregister_chrdev(xxx_dev_no,DEV_NAME);...}module_init(xxx_init);module_exit(xxx_exit);字符设备驱动程序设计
驱动程序编写应用程序调用open函数打开hello_drv这个设备,打开以后可以使用write函数向hello_drv的写缓冲区writebuf中写入数据(不超过100个字节),也可以使用read函数读取读缓冲区readbuf中的数据操作,操作完成以后应用程序使用close函数关闭chrdevbase设备。Makefile测试程序编写驱动编写好以后是需要测试的,一般编写一个简单的测试程序,测试程序运行在用户空间。测试程序很简单通过输入相应的指令来对hello_drv设备执行读或者写操作。(本案例有微课)GPIO驱动概述PartFoue8.4“通用输入输出”GPIO(generalpurposeinputoutput)是嵌入式系统中最简单,最常用的I/O接口。GPIO是一组可编程控制的管脚,由多个寄存器同时控制。通过设置对应的寄存器可以达到设置GPIO口对应状态与功能如读取数据状态,设置输入输出方向,清零,中断使能等功能。GPIO的驱动主要作用就是读取GPIO口的内容,或者设置GPIO口的状态。GPIO是与硬件体系密切相关,在linux内核目录下的相关文件中我们可以发现针对不同硬件芯片的GPIO定义和使用方法,如本书涉及的S5PV210芯片linux内核中也有相应的驱动程序支持(如在/drivers/gpio/)。当然,linux内核也提供了一个模型框架,能够使用统一的接口来操作GPIO,这个架构被称作"gpiolib",系统通过gpiolib.c文件来描述该架构。说明文档可见Documention/gpio.txt。8.4.1gpiolib关键数据结构Gpiolib架构下最重要的数据结构是gpio_chip结构体和gpio_desc结构体。gpio_chip结构体的部分定义如下:Structgpio_chip{…...int(*request)(structgpio_chip*chip,unsignedoffset);//申请gpio资源;void(*free)(structgpio_chip*chip,unsignedoffset);//释放gpio资源;int(*direction_input)(structgpio_chip*chip,unsignedoffset);//设置GPIO口方向的操作int(*get)(structgpio_chip*chip,unsignedoffset);int(*direction_output)(structgpio_chip*chip,unsignedoffset,intvalue);//设置GPIO口方向的操作int(*set_debounce)(structgpio_chip*chip,unsignedoffset,unsigneddebounce);void(*set)(structgpio_chip*chip,unsignedoffset,intvalue);//设置GPIO口高低电平值操作int(*to_irq)(structgpio_chip*chip,unsignedoffset);void(*dbg_show)(structseq_file*s,structgpio_chip*chip);intbase;u16ngpio;…...};8.4.2GPIO的申请和注册GPIO的申请在gpiolib架构下是通过gpio_request_array函数实现的。这里的申请的主要标识就是检测GPIO描述符desc->flags的FLAG_REQUESTED标识,如果已申请该标识是1,否则是0,往往多个gpio会作为一个数组来进行申请。当然,如前文所述,用户也可以使用linux系统已经支持的芯片GPIO接口驱动来完成目标系统设计。扩展:pinctrl框架(1)引脚控制核心:pin控制框架的核心层。其中的默认状态,睡眠状态或空闲状态是指引脚控制的电源管理状态。(2)引脚控制复用器:提供引脚多路复用功能。(3)引脚控制配置:提供Pin配置设置功能。Linux内核提供了pinctrl子系统(引脚控制子系统),旨在统一不同SoC平台上的pin管理。IIC总线驱动设计PartFive8.5I2C总线驱动图8-7Linux内核I2C总线驱动框架Linux内核提供了一个通用的方法来实现I2C设备驱动。这个通用的设备驱动由I2C_dev.c文件实现。在/drivers/I2C目录下
----Algos/
一些I2C总线适配器通信的算法
----Busses/
I2C总线驱动的方法
----Chips/
I2C设备驱动
----I2C-boardinfo.c
----I2C-core.c
I2C核心文件,用于联系设备驱动和总线驱动,重要函数为I2C_add_addapter、I2C_add_driver和I2C_transfer函数
----I2C-dev.c
通用的I2C设备驱动
----Kconfig
----MakefileLinux内核的I2C总线驱动程序框架如图8-7所示。I2C总线驱动程序主要由3个部分组成:I2Ccore(I2C核心),adapter(适配器),client(设备驱动)
。(1)I2Ccore(I2C核心)是I2C总线驱动程序体系结构的核心,它为总线设备驱动提供统一的接口,通过这些接口来访问在特定I2C设备驱动程序中实现的功能,并实现从I2C
总线驱动体系结构中添加和删除总线驱动的方法等。(2)adapter部分代表I2C适配器驱动,adapter是各个适配器驱动所构成的集合,主要实现各相应适配器数据结构I2C_adapter的具体的通信传输算法(I2C_algorithm),此算法管理I2C控制器及实现总线数据的发送接收等操作。(3)Client部分则代表挂载在I2C总线上的设备驱动,Client部分是各个I2C设备构成的集合,主要实现各描述I2C设备的数据结构I2C_client及其私有部分,并通过I2Ccore提供的接口实现设备的注册,提供设备可使用的地址范围及地址检测成功后的回调函数。I2C总线驱动I2Ccore处于控制中心的I2Ccore实现了控制策略,具体I2C总线的适配器和设备的驱动实现了具体设备可用的机制,控制策略和底层机制通过中间的函数接口相联系。正是中间的函数接口使得控制策略与底层机制无关,从而使得控制策略具有良好的可移植性和重用性。在实际设计中,I2C核心提供的接口不需要修改,只需针对目标总线适配器驱动和设备驱动进行必要修改即可。8.5.3关键数据结构1.I2C适配器一个I2C适配器对应I2C_adapter结构体。I2C_adapter是对硬件上的适配器的抽象,相当于整个I2C驱动的控制器。它的作用就是产生总线时序。I2C_adapter数据结构描述如下:structI2C_adapter{ structmodule*owner; unsignedintclass; /*classestoallowprobingfor*/ conststructI2C_algorithm*algo;/*thealgorithmtoaccessthebus*/ void
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 秋季学期小学数学教研组工作计划
- 电子计算机公司品牌经理述职报告
- 2025-2026学年上海市静安区高一(下)期末数学试卷(含解析)
- 进城务工人员随迁子女城市认同感的心理梗阻机制与教育融合路径-基于进城务工子女寄宿制学校心理咨询室案例的实证
- 舜宇光学科技投资者日:加速“光学与AI”转型
- 无人基础及应用 15
- 新苏教版科学六年级上册第一单元第4课《自热的“暖宝宝”》教学设计
- 某造船厂焊接作业制度
- 佛山事业编试题及答案
- 社区干部面试题及答案
- 2026及未来5年中国流动住房市场数据分析及竞争策略研究报告
- 2026西语翻译面试题目及答案
- GB/T 10128-2026金属材料室温扭转试验方法
- 2026贵州省专业技术人员继续教育公需科目考试题库
- 2026年秋季新教材统编版九年级上册道德与法治全册知识点背诵提纲精简版
- 2026-2030中国激光打印机行业发展现状与市场前景趋势洞察报告
- 透析患者急性左心衰护理
- 2026年屠宰兽医卫生检验员考试题库(附答案)
- 2026年高考(北京卷)英语试题及答案
- 《义务教育语文课程标准2025》
- 家居门店礼仪培训
评论
0/150
提交评论