嵌入式linux设备驱动程序开发

2019-07-12 20:05发布

一、设备驱动程序简介     系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核与硬件设备之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序来看,硬件设备只是一个设备文件,可以向操作普通文件一样对硬件设备进行操作。      功能: (1)对硬件设备初始化和释放 (2)把数据从内核传送到硬件,从硬件读取数据 (3)读取应用程序传送给设备文件的数据和回送应用程序请求的数据 (4)检测和处理设备出现的错误          在linux中主要有两种设备文件:字符设备文件和块设备文件。他们主要区别是:对字符设备发出读写请求时,实际的硬件IO一般紧接着就发生了;而块设备利用一块系统内存做缓冲区,如果应用进程对设备请求能满足用户的要求,则返回请求的数据,如果不能,就调用请求函数来进行实际的IO操作。         用户进程都是通过设备文件来与实际的硬件打交道。每个设备文件都有其文件属性,如表示是字符设备还是块设备。另外每个文件都有两个设备号,第一个是主设备号,用于标识设备驱动程序,第二个是从设备号,用于标识使用同一设备驱动程序的不同的硬件设备。设备文件的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程无法访问到驱动程序。 二、设备驱动程序的特点 (1)内核代码 设备驱动程序是内核的一部分,如果驱动程序出错,则可能导致系统崩溃。 (2)内核接口 设备驱动程序必须为内核或其子系统提供一个标准接口。比如一个终端驱动程序必须为内核提供一个文件IO接口,一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同时SCSI子系统也必须为内核提供文件的IO接口及缓冲区。 (3)内核机制和服务 设备驱动程序使用一些标准的内核服务,如内存分配等。 (4)可装载 大多数的linux操作系统设备驱动程序都可以在需要时装载进内核,在不需要的时候从内核中卸载。装载 insmod    , 卸载  rmmod (5)可设置 linux操作系统设备驱动程序可以集成为内核的一部分,并可以根据需要把其中的某一部分集成到内核中,这只需要在系统编译时进行设置即可。 (6)动态性 在系统启动后且各个设备驱动程序初始化后,驱动程序将维护期控制的设备。
三、模块编程       linux内核中采用可加载的模块化设计,一般情况下,编译的linux内核是支持可插入式模块的,也就是将最基本的核心代码编译在内核中,其他的代码都可以选择在内核中或者编译为内核的模块文件。       linux设备驱动属于内核的一部分,可以直接编译进linux内核,随linux启动时加载; 也可以编译成一个可加载和删除的模块,使用insmod加载,rmmod删除。
四、模块相关命令 insmod  加载模块   rmmod 卸载模块    lsmod 显示内核中已加载的模块
五、模块编程流程 1、代码编程 用户空间的应用程序从main函数开始执行,内核空间的模块程序,以module_init()  作为加载模块被加载时的入口,以module_exit()作为模块卸载的出口。 2、模块编译 模块的编译和应用程序的编译也有很大的区别。使用make的扩展语法。
六、字符设备的驱动程序编写 A、设备编号 (1) 设备编号说明 对字符设备的访问是通过文件系统内的设备名称进行,特殊文件或设备文件,通常位于/dev目录下。使用  ls -l查看,第一列的c 表示该设备为字符设备。 在linux内核中,dev_t用来保存主设备号和从设备号,dev_t的前12位表示主设备号,后20位表示从设备号。设备号 和 dev_t 之间转换: #include MAJOR(dev_t   dev);   //获得主设备号 MINOR(dev_t   dev);   // 获得从设备号 MKDEV(int   major,    int    minor);   //将主设备号和从设备号转换为dev_t
驱动程序获得设备号:在linux中可以通过动态分配和静态分配设备号的方式。若用户提前知道所需要的设备编号,则可使用register_chrdev_region函数,若用户并不确知设备编号,则可使用alloc_chrdev_region函数进行动态分配。 无论采用哪种方式分配设备编号,在系统使用完设备时都需要使用函数 unregister_chrdev_region函数释放这些设备编号。 (2) 获得设备编号示例 在实际应用中,主设备号是一个全局变量,程序可以通过判断主设备号来确定动态分配或手动分配。 若采用手动分配,首先调用MKDEV宏来获得设备的dev_t结构,其次再调用register_chrdev_region函数注册设备,在该函数调用成功之后,可以在/proc/devices里看到名为name(用户在函数中定义)的设备了。        如果采用自动分配,用户直接调用函数alloc_chrdev_region即可,该函数调用成功后用户就可以看到在/proc/device里看到设备了。 if (scull_major){ dev_t  dev = MKDEV(scull_major,  scull_minor); result = register_chrdev_region(dev, scull_num_devs, "scull"); }else{ result = alloc_chrdev_region(&dev, 0, scull_num_devs, "scull"); scull_major = MAJOR(dev); } B、重要的数据结构 linux设备驱动程序中,最重要涉及3个重要的内核数据结构,分别为、file_operation,  file,  node。 inode结构用于表示文件;file结构用于表示打开的文件描述符,因为对于单个文件而言可能有多个表示打开的文件描述符,因此就可能有多个file结构,但他们都指向同一个inode结构;file_operation是linux驱动程序中最重要的一个结构,它包括了一组常见函数,这类结构的指针通常被称为fops。这个结构的每一个字段都必须指向驱动程序中实现特定操作的函数。
C、基本操作----open和release int  (*open) (struct  inode*,  struct  file *); 它主要提供驱动程序初始化的能力,从而为以后的操作完成初始化做准备,在大部分的驱动中,open主要完成以下功能: (1)检查设备特定的错误 (2)如设备初次打开,则对其进行初始化 (3)如有必要,更新f_op指针 (4)分配并填写 filp->private_data里的数据结构 通常使用函数 container_of,该函数可以返回包含cdev结构的结构体,然后再填写filp结构中的相关数据结构。 release接口函数释放设备:: 释放设备和关闭设备是完全不同的。当一个进程释放设备时,其他进程还能继续使用该设备,只是该进程暂时停止对该设备的使用。而当一个进程关闭设备时,其他进程必须重新打开此设备才能使用。 释放设备的主要工作: (1)递减计数器  MOD_DEC_USE_COUNT (2)在最后一次释放设备操作时关闭设备
D、基本操作 read    write 读写设备的任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,也就是将内核空间缓冲区中的数据复制到用户空间的缓冲区中或者相反。 #include ssize_t   (*read) (struct   file* filp, /*文件指针*/ ,    char* buff  /*指向用户缓冲区*/ ,     size_t  count, /*传入的数据长度*/,   loff_t * offp /*用户在文件中的位置*/) 内核空间地址和用户空间地址是有很大不同的,其中之一就是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况,故不能使用memcpy之类的函数来完成这样的操作。需要使用copy_to_user和 copy_from_user 函数,他们作用就是实现用户空间和内核空间的数据交换。 在应用程序中获取内存通常使用函数malloc,但在设备驱动程序中动态开辟内存有基于内存地址和基于页面的两类。其中,基于内存地址的函数由kmalloc,kmalloc返回的是物理地址,而malloc返回的是线性地址,因此在驱动程序中不能使用malloc函数。kmalloc申请空间有大小限制,长度为2的整数次方,并且不会对所获取的内存空间清零。 基于页为单位的内存函数族: get_zeroed_page: //获得一个已清零的页面 get_free_page://获得一个或几个连续页面 get_dma_pages:  ///获得用于DMA传输的页面 基于页的内存释放函数 free_page: unsigned long free_page(unsigned long addr)
E、proc文件系统 /proc 文件系统是一种内核和内核模块用来向进程发送信息的机制,是一个伪文件系统。可以让用户和内核内部数据结构进行交互,获取有关进程的有用信息,在运行时改变内核参数设置。 proc存在于内存中,而不是硬盘中。
七、块设备驱动程序编写 块设备通常以块(如512K字节)方式读写的设备,如IDE硬盘, SCSI硬盘,光驱等。 块设备驱动程序描述符结构: struct     blk_dev_struct{ request_queue_t    request_queue; queue_proc    *queue; void * data; }; 该结构中,请求队列是主体,包含了初始化之后的IO请求队列。对于函数指针queue,当其为非0时,就调用这个函数来找到具体设备的请求队列,这是为具有同一主设备号的多种同类设备而设的一个域,该指针也在初始化时设置好。指针data是辅助queue函数找到特定设备的请求队列,保存一些私有的数据。 所有块设备描述符都存放在blk_dev表 struct blk_dev_struct   blk_dev[MAX_BLKDEV]中,每个块设备都对应数组中的一项,可以使用主设备号进行检索。 每当用户进程对一个块设备发出一个读写请求时,首先调用块设备所公用的函数generic_file_read()和generic_file_write()。如果数据存在缓冲区中且缓冲区还可以存放数据,那么就与缓冲区进行数据交换。否则,系统会将相应的请求队列结构添加到其对应项的blk_dev_struct中。
编写流程: 块设备的驱动程序可以分为 注册和使用两部分,块设备驱动程序包括一个request请求队列,它是当内核安排一次数据传输时在列表中的一个请求队列,以最大化系统性能为原则进行排序。 重要数据结构:struct  device_struct  ,    struct   device_struct  blkdevs[MAX_BLKDEV];      struct  sbull_dev;  struct  file_operation   blk_fops 所有块驱动设备程序都调用内核函数 block_read(), block_wirte(), block_fsync(), 所以在块设备驱动程序入口中不包含这些函数,只需包括ioctl(), open()和release()。 (1)块设备初始化 内核使用主设备号来标志块驱动程序,但块设备号和字符主设备号是互不相干的。一个主设备号为32的块设备可以和具有相同主设备号的字符设备同时存在,因为他们具有各自独立的主设备号分配空间。 用来注册和注销块设备驱动程序的函数: if (register_blkdev(sbull_MAJOR, "sbull", &sbull_fops){ printk("Registering  block  device  major: %d failed ",  sbull_MAJOR); return EIO; } (2)request操作 块驱动程序中最重要的函数就是request函数,该函数执行数据读写相关的底层操作。 在内核安排一次数据传输时,它首先在一个表中对该请求队列,并以最大化系统性能为原则进行排序,然后,请求队列被传递到驱动程序的queue函数,该函数原型: void  request_fn(request_queue_t  *queue);