现在的位置: 首页 > 综合 > 正文

宋宝华谈 ARM 的嵌入式 Linux 移植体验之四:设备驱动

2013年10月06日 ⁄ 综合 ⁄ 共 15521字 ⁄ 字号 评论关闭

宋宝华谈 ARM 的嵌入式 Linux 移植体验之四:设备驱动

        设备驱动程序是操作系统内核和机器硬件之间的接口,它为应用程序屏蔽硬件的细节,一般来说,Linux 的设备驱动程序需要完成如下功能:
                ·设备初始化、释放;
                ·提供各类设备服务;
                ·负责内核和设备之间的数据交换;
                ·检测和处理设备工作过程中出现的错误。
        Linux 下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得 Windows 的设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作,如open ()、close ()、read ()、write () 等。
        Linux 主要将设备分为二类:字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行;而块设备则以整个数据缓冲区的形式进行。在对字符设备发出读/写请求时,实际的硬件 I/O 一般就紧接着发生了;而块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的 I/O 操作。块设备主要针对磁盘等慢速设备。
        1.内存分配
        由于 Linux 驱动程序在内核中运行,因此在设备驱动程序需要申请/释放内存时,不能使用用户级的 malloc/free 函数,而需由内核级的函数kmalloc/kfree() 来实现,kmalloc() 函数的原型为:

 
        参数 size 为申请分配内存的字节数,kmalloc 最多只能开辟 128k 的内存;参数priority 说明若 kmalloc() 不能马上分配内存时用户进程要采用的动作:GFP_KERNEL 表示等待,即等 kmalloc() 函数将一些内存安排到交换区来满足你的内存需要,GFP_ATOMIC 表示不等待,如不能立即分配到内存则返回 0 值;函数的返回值指向已分配内存的起始地址,出错时,返回 0。
        kmalloc() 分配的内存需用 kfree() 函数来释放,kfree() 被定义为:

 
        其中 kfree_s () 函数原型为:

 
        参数 ptr 为 kmalloc() 返回的已分配内存的指针,size 是要释放内存的字节数,若为 0 时,由内核自动确定内存的大小。
        2.中断
        许多设备涉及到中断操作,因此,在这样的设备的驱动程序中需要对硬件产生的中断请求提供中断服务程序。与注册基本入口点一样,驱动程序也要请求内核将特定的中断请求和中断服务程序联系在一起。在 Linux 中,用request_irq() 函数来实现请求:

 
        参数 irq 为要中断请求号,参数 handler 为指向中断服务程序的指针,参数type 用来确定是正常中断还是快速中断(正常中断指中断服务子程序返回后,内核可以执行调度程序来确定将运行哪一个进程;而快速中断是指中断服务子程序返回后,立即执行被中断程序,正常中断 type 取值为 0 ,快速中断type 取值为 SA_INTERRUPT),参数 name 是设备驱动程序的名称。
        3.字符设备驱动
        我们必须为字符设备提供一个初始化函数,该函数用来完成对所控设备的初始化工作,并调用 register_chrdev() 函数注册字符设备。假设有一字符设备"exampledev",则其 init 函数为:

 
        其中,register_chrdev 函数中的参数 MAJOR_NUM 为主设备号,"exampledev"为设备名,exampledev_fops 为包含基本函数入口点的结构体,类型为file_operations。当执行 exampledev_init 时,它将调用内核函数register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。
        较早版本内核的 file_operations 结构体定义为(代码及图示):

 

较早版本内核的file_operations结构体
        随着内核功能的加强,file_operations 结构体也变得更加庞大。但是大多数的驱动程序只是利用了其中的一部分,对于驱动程序中无需提供的功能,只需要把相应位置的值设为 NULL。对于字符设备来说,要提供的主要入口有:open ()、release ()、read ()、write ()、ioctl () 等。
        open() 函数 对设备特殊文件进行 open() 系统调用时,将调用驱动程序的open () 函数:

 
        其中参数 inode 为设备特殊文件的 inode (索引结点) 结构的指针,参数 filp 是指向这一设备的文件结构的指针。open() 的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用 MINOR(inode-> i_rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0 表示成功,负数表示存在错误) 等;
        release() 函数 当最后一个打开设备的用户进程执行 close() 系统调用时,内核将调用驱动程序的 release () 函数:

 
        release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。
        read() 函数 当对设备特殊文件进行 read() 系统调用时,将调用驱动程序read() 函数:

 
        参数 buf 是指向用户空间缓冲区的指针,由用户进程给出,count 为用户进程要求读取的字节数,也由用户给出。
        read() 函数的功能就是从硬设备或内核内存中读取或复制 count 个字节到 buf  指定的缓冲区中。在复制数据时要注意,驱动程序运行在内核中,而 buf 指定的缓冲区在用户内存区中,是不能直接在内核中访问使用的,因此,必须使用特殊的复制函数来完成复制工作,这些函数在 include/asm/uaccess.h 中被声明:

 
        此外,put_user() 函数用于内核空间和用户空间的单值交互(如 char、int、long)。
        write() 函数 当设备特殊文件进行 write() 系统调用时,将调用驱动程序的 write() 函数:

 
        write ()的功能是将参数 buf 指定的缓冲区中的 count 个字节内容复制到硬件或内核内存中,和 read() 一样,复制工作也需要由特殊函数来完成:

 
        此外,get_user()函数用于内核空间和用户空间的单值交互(如 char、int、long)。
        ioctl() 函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:

 
        参数 cmd 为设备驱动程序要执行的命令的代码,由用户自定义,参数 arg 为相应的命令提供参数,类型可以是整型、指针等。
        同样,在驱动程序中,这些函数的定义也必须符合命名规则,按照本文约定,设备"exampledev"的驱动程序的这些函数应分别命名为 exampledev_open、exampledev_ release、exampledev_read、exampledev_write、exampledev_ioctl,因此设备"exampledev"的基本入口点结构变量exampledev_fops 赋值如下(对较早版本的内核):

 
        就目前而言,由于 file_operations 结构体已经很庞大,我们更适合用 GNU 扩展的 C 语法来初始化 exampledev_fops:

 
        看看第一章电路板硬件原理图,板上包含四个用户可编程的发光二极管(LED),这些 LED 连接在 ARM 处理器的可编程 I/O 口(GPIO)上,现在来编写这些 LED 的驱动:

 
        使用命令方式编译 led 驱动模块:

 
        以上命令将生成 leds.o 文件,把该文件复制到板子的 /lib 目录下,使用以下命令就可以安装 leds 驱动模块:

 
        删除该模块的命令是:

 
        4.块设备驱动
        块设备驱动程序的编写是一个浩繁的工程,其难度远超过字符设备,上千行的代码往往只能搞定一个简单的块设备,而数十行代码就可能搞定一个字符设备。因此,非得有相当的基本功才能完成此项工作。下面先给出一个实例,即 mtdblock 块设备的驱动。我们通过分析此实例中的代码来说明块设备驱动程序的写法(由于篇幅的关系,大量的代码被省略,只保留了必要的主干):

 
        从上述源代码中我们发现,块设备也以与字符设备 register_chrdev、unregister_ chrdev 函数类似的方法进行设备的注册与释放:

 
        但是,register_chrdev 使用一个向 file_operations 结构的指针,而register_blkdev 则使用 block_device_operations 结构的指针,其中定义的 open、release 和 ioctl 方法和字符设备的对应方法相同,但未定义 read 或者 write 操作。这是因为,所有涉及到块设备的 I/O 通常由系统进行缓冲处理。
        块驱动程序最终必须提供完成实际块 I/O 操作的机制,在 Linux 当中,用于这些 I/O 操作的方法称为"request(请求)"。在块设备的注册过程中,需要初始化 request 队列,这一动作通过 blk_init_queue 来完成,blk_init_queue 函数建立队列,并将该驱动程序的 request 函数关联到队列。在模块的清除阶段,应调用 blk_cleanup_queue 函数。
        本例中相关的代码为:

 
        每个设备有一个默认使用的请求队列,必要时,可使用 BLK_DEFAULT_QUEUE(major) 宏得到该默认队列。这个宏在 blk_dev_struct 结构形成的全局数组(该数组名为 blk_dev)中搜索得到对应的默认队列。blk_dev 数组由内核维护,并可通过主设备号索引。blk_dev_struct 接口定义如下:

 
        request_queue 成员包含了初始化之后的 I/O 请求队列,data 成员可由驱动程序使用,以便保存一些私有数据。
        request_queue 定义为:

 
        下图表征了 blk_dev、blk_dev_struct 和 request_queue 的关系:

blk_dev、blk_dev_struct和request_queue的关系
        下图则表征了块设备的注册和释放过程:

块设备的注册和释放过程
        5.小结
        本章讲述了 Linux 设备驱动程序的入口函数及驱动程序中的内存申请、中断等,并分别以实例讲述了字符设备及块设备的驱动开发方法。
原文链接:http://dev.yesky.com/53/2529553.shtml

抱歉!评论已关闭.