1、Linux设备驱动程序开发,认识目标板各设备型号及开发中注意事项 驱动程序(BSP)在嵌入式系统中的重要性和所处位置 Linux驱动程序的概念、驱动结构、对中断和内存的处理、设备驱动的初始化 Linux下设备驱动程序开发框架和流程 Linux下模块化驱动程序设计(动态加载) 设备驱动加入Linux内核中 实验:编写驱动程序框架+测试用例(字符型设备)两种初始化方式,主要内容,目标板各设备介绍,设备开发中的注意事项,了解清楚设备型号接口大小,详细阅读用户手册和硬件设计规格说明书 了解清楚电源的输出功率 插拔接口的顺序,是否支持热插拔和带电插拔 遇到问题要多一点细心,沉着冷静分析问题,作好笔记,分
2、析问题的范围扩大,把每一个新的想法都去试一试,有时就试出来了。 善于利用网络资源,BSP的重要性,BSP:Board Support Package(板级支持包),介于硬件与操作系统之间一层, 属于操作系统的一部分,所以同一体系结构下移植操作系统就是编写BSP过程。,用户通过一组与具体设备无关的标准化的调用来完成相应的操作(如:open,write,read,close),同时完全隐蔽了设备的工作细节 操作系统分为两层:用户态:只能运行系统上的应用程序内核态:权限高,处理内存的映射和分配,访问外设空间和处理器的特殊状态寄存器,用户空间的数据不能直接通过指针传给内核在用户态和内核态之间传递数据(
3、copy_from_user、copy_to_user、get_user、put_user) 驱动程序是内核的一部分,可以使用中断、DMA等操作 驱动程序提供访问硬件设备寄存器的函数 开发一个性能可靠、可移植性好,可配置性好,规范化的BSP可以大在提高系统各方面性能,BSP的重要性,Linux驱动程序概念,设备CPU并不是系统中唯一的智能设备,每个物理设备都拥有自己的控制器 (键盘、鼠标和串行口 ),每个硬件控制器都有各自的控制和状态寄存器(CSR) 并且各不相同 ,这些CSR被用来启动和停止、初始化设备及对设备进行诊断,在 Linux中管理硬件设备控制器的代码并不放置在每个应用程序中而是由内
4、核统一 管理。这些处理和管理硬件控制器的软件就是Linux设备驱动程序。 Linux核心设备驱动是一组运行在特权级上的内存驻留底层硬件处理共享库, 它们负责管理各个设备。 设备驱动程序的流程: 设备初始化和释放把数据从内核送到硬件和从硬件读取数据读取应用程序传 送给设备文件的数据和回送应用程序请求的数据检测和处理设备出现的错误,Linux下设备驱动的共性,Linux下的设备驱动都有以下几方面特点: 核心代码 设备驱动是内核的一部分,象内核中其它代码一样,出错将导致系统的严重损伤。 一个编写不好的设备驱动甚至能使系统崩溃并导致文件系统的破坏和数据丢失。 内核接口 设备驱动必须为Linux内核或者
5、其从属子系统提供一个标准接口。 内核机制与服务 设备驱动可以使用标准的内核服务如内存分配、中断发送和等待队列等。 动态可加载 Linux设备驱动可以在内核模块发出加载请求时加载,同时在不再使用时卸载。 这样内核能有效地利用系统资源。 (insmod,rmmod) 可配置 Linux设备驱动可以和内核一起编译。当内核被编译时,哪些内核被链入内核是可配置的。 在系统启动时会由内核调用驱动程序初始化,设备驱动程序的分类,字符设备 无缓存,每次读写一个字符的设备 当发出读/写请求时,实际的硬件I/O马上发生 一般要包含open read write close ioctl等系统调用的实现 块设备 通常
6、是指诸如磁盘、内存、Flash等可以容纳文件系统的存储设备。 块设备也是通过文件系统来访问,与字符设备的区别是:内核管理数据的方式不同,利用一块内存缓冲区,满足硬件要求时发生 它允许象字符设备一样以字节流的方式来访问,也可一次传递任意多的字节。 网络接口设备 通常它指的是硬件设备,但有时也可能是一个软件设备(如回环接口loopback),它们由内核中网络子系统驱动,负责发送和接收数据包。 它们的数据传送往往不是面向流的,因此很难将它们映射到一个文件系统的节点上。,每一个设备文件属性:主设备号(major)和次设备号(minor) 主设备号和次设备号能够唯一地标识一个设备 128(kernel
7、V2.0以前), 256(kernel v2.2 v2.4 v2.5)其中:0和255保留 主设备号相同的设备使用相同的驱动程序(I8250),次设备号用于区分具体设备的不同硬件(com1,com2) 动态获取主设备号,MAJOR=0 Linux下对设备号的分配请参考Documentation/devices.txt 设备文件 (mknod) Linux使用设备文件来统一对设备的访问接口,将设备文件放在/dev/目录下 设备的命名一般为设备文件名+数字或者字母表示的子类,例如/dev/hda1, /dev/hda2等 Linux 2.4以后引入了设备文件系统(devfs)的概念,所有的设备文件
8、作为一个可以挂装的文件系统,这样就可以被文件系统统一管理,从而设备文件就可以挂装到任何需要的地方。一般将主设备建立一个目录,再将具体的子设备文件建立在此目录下。例如,/dev/mtdblock0,设备驱动程序的属性,在系统内部,I/0设备的存/取通过一组固定的入口点来进行, 这组入口点是由每个设备的设备驱动程序提供的 驱动程序使用的2个重要结构 struct file struct file_operations(重点了解),设备驱动程序的结构,struct file,设备驱动程序的结构,struct file数据结构 file结构代表一个“打开的文件”。它有内核在open时创建而且在clos
9、e前做为参数传递给如何操作在设备上的函数。在文件关闭后,内核释放这个数据结构。一个“打开的文件”与由struct inode表示的“磁盘文件”有所不同 定义位于include/fs.h struct file结构与驱动相关的成员 mode_t f_mode 标识文件的读写权限 loff_t f_pos 当前读写位置 unsigned int_f_flag 文件标志,主要进行阻塞/非阻塞型操作时检查 struct file_operation * f_op 文件操作的结构指针 void * private_data 驱动程序一般将它指向已经分配的数据,在跨系统调用时保存状态信息 struct d
10、entry* f_dentry 文件对应的目录项结构,设备驱动程序的结构,struct file_operations int (*lseek) (struct inode *inode,struct file *file,off_t off, int pos); int (*read) (struct inode *inode,struct file *file,char *buf, int count); int (*write) (struct inode *inode,struct file *file,const char *buf, int count); int (*readdi
11、r) (struct inode *inode,struct file *file,struct dirent *dirent, int count); int (*select) (struct inode *inode,struct file *file,int sel_type, select_table *wait); int (*ioctl) (struct inode *inode,struct file *file,unsigned int cmd,unsigned int arg); int (*mmap) (void); int (*open) (struct inode *
12、inode,struct file *file); int (*release) (struct inode *inode,struct file *file); int (*fsync) (struct inode *inode,struct file *file); ;,设备驱动程序的结构,设备驱动程序接口( struct file_operations), 标记化方法: static struct file_operations demo_fops = owner: THIS_MODULE, write: demo_write, read: demo_read, ioctl: demo_
13、ioctl, open: demo_open, release: demo_release, NULL, ;,设备驱动程序的结构,设备驱动程序接口( struct file_operations ) 通常所说的设备驱动程序接口是指struct file_operations ,它的定义位于include/linux/fs.h中,初始化时注册到系统内部。 常用操作 lseek 移动文件指针的位置,只能用于可以随机存取的设备 read 从设备中读取数据 write 向字符设备中写数据 readdir 取得下一个目录入口点,只有与文件系统相关设备 select 进行选择操作. ioctl 控制设备,
14、实现除读写操作以外的其他控制命令 mmap 设备的内容映射到地址空间,用块设备 open 打开设备并进行初始化 release 关闭设备并释放资源,设备驱动程序的结构,中断是现代微处理器的一个重要功能 Linux驱动程序中的中断处理函数#includeint request_irq(unsigned int irq, /硬件中断号void(*handler)(int, void*, struct pt_regs *),/登记中断处理子程序 unsigned long flag, /确定快速(所有中断屏蔽)或慢速const char *dev_name, /设备名 void *dev_id);
15、/申请时告诉系统的设备标识void free_irq(unsigned int irq, void* dev_id);/释放中断 Polling方式收发数据,Linux对中断的处理,内存分配也同样是系统核心部分 Linux驱动程序申请和释放内存不是malloc和free#includevoid *kmalloc(unsigned int len, int priority);/len为申请字节数,priority分配内存操作的优先级 Void kfree(void *obj); /释放内存指针 调试驱动程序时打印一些调试信息,通常使用printk同时会记录在文件syslog里面 ,用法和pri
16、ntf类似,Linux对内存的处理,设备注册-设备驱动在初始化时向系统进行登记,以便系统适当的时候调用。Linux通过调用register_chrdev函数#include#includeint register_chrdev(unsigned int major,const char *name,struct file_operations *fops);major:设备驱动向系统申请的主设备号,如果是0,则系统动态分配一个主设备号。name:设备名。fops:各个调用的入口点函数说明。此函数正确返回0,返回-EINVAL申请主设备号非法,返回-EBUSY申请的主设备号被其它设备正使用,如果
17、动态分配主设备号返回主设备号。注册成功会出现在/proc/device文件里,每个设备在/dev目录中都有一个文件,Linux设备驱动的初始化,设备卸载-设备驱动在卸载时对应的主设备号必须释放。Linux在cleanup_module()函数下调用unregister_chrdev函数#include#includeint unregister_chrdev(unsigned int major,const char *name);主设备号major和设备名name。在系统中对比,如果不等返回-EINVAL卸载失败,major大于最大的设备号返回-EINVAL卸载失败。,Linux设备驱动的初
18、始化,设备驱动初始化函数主要完成的功能(1)对驱动程序管理的硬件进行必要的初始化对硬件寄存器进行设置,如:设置中断掩码,设置串口工作方式。 (2)初始化设备驱动相关参数每个设备都要定义一个设备变量,用以保存设备相关参数。 (3)在内核中注册设备调用register_chrdev函数 (4)注册中断如果设备需要IRQ支持,注册中断:request_irq函数(5)其它初始化工作如:设备分配I/O,申请DMA通道等。,Linux设备驱动的初始化,Linux设备驱动的开发流程,在进行嵌入式系统的开发,大部分工作量是为各种设备编写驱动程序,除非 系统不使用操作系统,程序直接操纵硬件。Linux系统中,
19、内核提供保护机制, 用户不能直接访问硬件。 Linux v2.0 v2.2 v2.4主要是驱动的增加 根据设备的共性,操作系统提供给驱动程序的支持大致相同。重要特征如下 (1)读/写 几乎所有设备都有输入和输出,每个驱动负责本设备的读写操作,操作系统的 其它部分不需要知道对设备的具体读/写操作是怎么进行的,由驱动程序完成 这些功能。只需要初始化时把读写注册到系统。 (2)中断 在现在计算机结构中有重要地位,操作系统必须提供驱动程序响应中断的能力 ,一般把一个中断处理程序注册到系统中,当硬件产生中断时,调用驱动程序 处理程序,Linux提供中断共享,多个设备共用一个中断。 (3)时钟 主要是处理
20、一些超时,延时。预定时间过后,回调注册时钟函数,Linux设备驱动的开发流程,Linux下大致流程: (0)查看原理图及用户手册,详细了解设备的工作原理 (1)定义主、次设备号,也可以动态获取 (2)实现驱动初始化和清除函数。如果驱动程序采用模块化方式,则要实现模块初始化和清除函数。 (3)设计所要实现的文件操作,定义file_operations结构。 (4)实现所需实现的文件操作调用,如:read,write等。 (5)实现中断服务函数,并用request_irq向内核注册。 (6)将驱动编译到内核或编译成模块,用insmod命令加载。 (7)生成设备节点文件(mknod /dev/xxx
21、 c 225 0)。(8) 编写测试驱动,测试该设备。,模块化驱动程序设计,宏内核Linux核心是一种整体式内核即单一的大程序,核心中所有的功能部件都可以对其全部内部数据结构和例程进行访问。核心的另外一种形式是微内核结构,此时核心的所有功能部件都被拆成独立部分,这些部分之间通过严格的通讯机制进行联系。如果给内核增加一个新成分的配置过程非常费时。 动态加载与卸载操作系统部件Linux模块就是这样一种可在系统启动后的任何时候动态连入核心的代码块。当我们不再需要它时又可以将它从核心中卸载并删除。Linux模块多指设备驱动,但它们并没有被编译到内核中,而是被编译并链接成一组目标文件。Linux为我们提
22、供了两个命令:使用insmod来显式加载核心模块,使用rmmod来卸载模块。,模块化驱动程序设计,模块化动态加载的优缺点好处在于可以让核心保持很小的尺寸同时非常灵活。一旦Linux模块被加载则它和普通内核代码一样都是核心的一部分。对系统性能和内存利用有负面影响,它们具有与其他核心代码相同的权限与职责;也可以象所有内核代码和设备驱动一样使内核崩溃。,模块化驱动程序设计,内核资源模块需要使用核心资源。例如模块需要调用核心内存分配例程kmalloc()来分配内存。模块在构造时并不知道kmalloc()在内存中何处,这样核心必须在使用这些模块前修改模块中对kmalloc()的引用地址。核心在其核心符号
23、表中维护着一个核心资源链表,这样当加载模块时它能够解析出模块中对核心资源的引用。Linux还允许存在模块堆栈,它在模块之间相互调用时使用。例如VFAT文件系统模块可能需要FAT文件系统模块的服务,因为VFAT文件系统多少是从FAT文件系统中扩展而来。某个模块对其他模块的服务或资源的需求类似于模块对核心本身资源或服务的请求。不过此时所请求的服务是来自另外一个事先已加载的模块。每当加载模块时,核心将把新近加载模块输出的所有资源和符号添加到核心符号表中。,模块化驱动程序设计,当试图卸载某个模块时,核心需要知道此模块是否已经没有被使用,同时它需要有种方法来通知此将卸载模块。模块必须能够在从核心种删除之
24、前释放其分配的所有系统资源,如核心内存或中断。当模块被卸载时,核心将从核心符号表中删除所有与之对应的符号。版本控制可加载模块具有使操作系统崩溃的能力,而编写较差的模块会带来另外一种问题。当你在一个或早或迟构造的核心而不是当前你运行的核心上加载模块时将会出现什么结果?一种可能的情况是模块将调用具有错误参数的核心例程。核心应该使用严格的版本控制来对加载模块进行检查以防止这种这些情况的发生。,模块化驱动程序设计,两个重要函数: 模块入口点init_module()的任务就是为以后调用模块的函数做准备; 对于模块第二个入口点cleanup_module(),仅当模块被卸载前才被调用。 在kernel
25、v2.3以后提供了一种新的方法来命名,这两个函数可以定义my_init和my_cleanup然后源代码后使用: #include Module_init(my_init); Module_exit(my_cleanup); 这样好处:每个模块都可以有自己初始化和卸载函数,多个模块时不会有函数重名。 在kernel v2.2后,该两个函数定义有变化:_init定义在执行一次后从内存中删除,申请的内存也回收。_init定义只能与内核一起编译时有用,模块时无效。对于_exit定义时不会编译入内核。对模块同样无效。,模块的加载,insmod的工作过程 insmod程序必须找到要求加载的核心模块。请求加
26、载核心模块一般被保存在/lib/modules/kernel-version 中。这些核心模块和系统中其他程序一样是已连接的目标文件,但是它们被连接成可重定位映象。即映象没有被连接到在特定地址上运行。这些核心模块可以是a.out或ELF文件格式。insmod将执行一个特权级系统调用来找到核心的输出符号。这些都以符号名以及数值形式,如地址值成对保存。核心输出符号表被保存在核心维护的模块链表的第一个module结构中,同时module_list指针指向此结构。只有特殊符号被添加到此表中,它们在核心编译与连接时确定,不是核心每个符号都被输出到其模块中。例如设备驱动为了控制某个特定系统中断而由核心例程
27、调用的“request_irq”符号。可以通过使用ksyms工具或者查看/proc/ksyms来观看当前核心输出符号。insmod将模块读入虚拟内存并通过使用来自核心输出符号来修改其未解析的核心例程和资源的引用地址。这些修改工作采取由insmod程序直接将符号的地址写入模块中相应地址来修改内存中的模块映象。当insmod修改完模块对核心输出符号的引用后,它将再次使用特权级系统调用来申请足够的空间来容纳新核心。核心将为其分配一个新的module结构以及足够的核心内存来保存新模块, 并将它放到核心模块链表的尾部。 然后将其新模块标志为UNINITIALIZED。,模块的加载,模块的加载,例图给出了
28、一个加载两个模块:VFAT和FAT后的核心链表示意图。不过图中没有画出链表中的第一个模块: 用来存放核心输出符号表的一个伪模块。lsmod可以帮助我们列出系统中所有已加载的核心模块以及相互间依赖关系。它是通过重新格式化从核心module结构中建立的/proc/modules来进行这项工作的。核心为其分配的内存被映射到insmod的地址空间, 这样它就能访问核心空间。insmod将模块拷贝到已分配空间中, 如果为它分配的核心内存已用完,则它将再次申请。注意:模块不会总是加载到相同地址,在两个不同Linux系统中也不会加载到相同位置。另外此重定位工作包括使用适当地址来修改模块映象。这个新模块也希望
29、将其符号输出到核心中,insmod将为其构造输出符号映象表。每个核心模块必须包含模块 初始化和模块清除例程,它们的符号被设计成故意不输出,但是insmod必须知道这些地址,这样它可以将它们传递给核心。所有这些工作做完之后,insmod将调用初始化代码并执行一个特权级系统调用将模块的初始化与清除例程地址传递给核心。,模块的加载,当将一个新模块加载到核心中间时,核心必须更新其符号表并修改那些被新模块使用的老模块。那些依赖于其他模块的模块必须维护在其符号表尾部维护一个引用链表并在其module数据结构中指向它。例图中VFAT 依赖于FAT文件系统模块。所以FAT模块包含一个对VFAT模块的引用;这个
30、引用在加载VFAT模块时添加。核心调用模块的初始化例程,如果成功它将安装此模块。模块的清除例程地址被存储在其module结构中,它将在模块卸载时由核心调用。最后模块的状态被设置成RUNNING。,模块的卸载,模块可以通过使用rmmod命令来删除,如果核心中的其他部分还在使用某个模块,则此模块不能被卸载。例如系统中安装了多个VFAT文件系统,那么将不能卸载VFAT模块。执行lsmod可以看到每个模块的引用记数。模块的引用记数被保存在其映象的第一个长字中。这个字同时还包含AUTOCLEAN和VISITED标志。请求加载模块使用这两个标志域。如果模块被标记成AUTOCLEAN则核心知道此模块可以自动
31、卸载。VISITED标志表示此模块正被一个或多个文件系统部分使用;只要有其他部分使用此模块则这个标志被置位。,最简单模块事例,#define MODULE #include int init_module(void) printk(“Hello,worldn“); return 0; void cleanup_module(void) printk(“Goodbye cruel worldn“); root# arm-linux-gcc D_KERNEL_ -I /usr/src/kenelSB/include DKBUILD_BASENAME= hello DMODULE -c o hell
32、o.o hello.c root# insmod ./hello.o Hello,world root# rmmod hello Goodbye cruel world root#,样例模块化驱动程序、测试程序、Makefile文件和模块化驱动的操作步骤: 以demo.c为例的操作步骤,字符设备的例子,1、编译: Makefile的形式参考代码 root# make clean root# make 命令行的形式 root# arm-linux-gcc D_KERNEL_ -I /usr/src/kenelSB/include DKBUILD_BASENAME=demo DMODULE -c
33、o demo.o demo_drv.c2、在系统中为驱动程序模块建立一个设备节点 root# mknod /dev/demo_drv c 125 0 其中/dev/demo_drv标识设备名为demo_drv,“c”说明是字符设备,125是指定的主设备号, 0是次设备号 root# lsmod 3、加载驱动 root# insmod demo.o root# lsmod 4、运行测试代码 root#。Chmod a+x test /修改文件 root#。/test,字符设备的例子,卸载驱动 root# rmmod demo root# insmod demo.o demo_param = 8
34、root# lsmod root# ./test root# rmmod demo root# rm demo_drv rf root# make clean,字符设备的例子,open 提供给驱动程序初始化设备的能力,为后续的操作做准备 此外一般会递增使用计数,防止文件关闭前模块被卸载 通常情况下,open完成如下工作: 递增使用计数 检查特定设备错误 如果设备是首次打开,则对其进行初始化 识别次设备号,如有必要,则修改f_op指针 分配并填写filp-private_data中的数据 release 与open正好相反 释放由open分配的filp-private_data中的数据 在最后一
35、次关闭操作时关闭设备 使用计数减一,字符设备DEMO例子框架,read和write read将数据从内核拷贝到应用程序空间,write则将数据从应用程序空间拷贝到内核。 由于用户空间和内核空间的内存映射方式不同,所以在内核和用户空间传输数据的时候需要使用如下的函数 unsigned long copy_to_user(void *to, const void* from, unsigned long count); unsigned long copy_from_user(void *to, const void *from, unsigned long count); 在阻塞型IO中,rea
36、d和write调用可能会出现阻塞 read调用当前无数据可读,而又没有数据马上可读,这时会睡眠并且等待,write调用也会出现这样的情况 等待队列机制wait_queue_head_t;(定义在中) 如果声明了等待队列并完成初始化,进程就可以睡眠,可以调用sleep_on的不同变体来完成睡眠(函数声明位于中) 大多数情况下应使用“可中断”的函数,如interruptible_sleep_on。 睡眠进程被唤醒并不一定代表有数据,也有可能是被其他的信号唤醒,所以醒来后需要测试condition.,字符设备DEMO例子框架,设备驱动加到Linux内核中,设备驱动程序编写调试完成后若是常用驱动,将该
37、驱动加到内核中。需要修改Linux源代码,然后重新编译内核。 (1)将设备驱动程序文件(mydriver.c)复制到/linux/drivers/char目录中。修改该目录下mem.c文件的 int chr_drv_init()函数中增加: #ifdef CONFIG_MYDRIVER /配置内核时赋值 device_init(); #endif (2)在/linux/driver/char目录下config.in文件,在 comment Character devices 语句下面加上bool support for mydriver CONFIG_MYDRIVER 那么在配置内核时的字符设
38、备对话框中会出现 Support for mydriver选中这个选项时,设备驱动就加到了内核中.重新编译内核。 (3)在/linux/driver/char目录下Makefile文件中添加一行 Obj - $(CONFIG_MYDRIVER) + = mydriver.o,设备驱动加到Linux内核中,不必理会mem.c,只要有_init,内核初始化时便会执行它。 如果设备驱动程序成功加进,在Linux文件系统下/proc/devices可以看到当前设备的信息。如果注册有中断可以在/proc/interrupts 下记录有当时中断情况,是否申请正常。还可以在/proc下创建一个文件用来存放设备相关信息,这样可以了解设备使用情况。 采用命令cat来直接察看信息rootfa /proc# cat interrupts13: 0 DMA timer14: 2774 timer26: 0 usb-ohci37: 0 cs89x052: 45 serial_s3c2410_rx53: 311 serial_s3c2410_tx54: 0 serial_s3c2410_err,