1、linux驱动程序的编写 linux驱动程序的编写一、实验目的1. 掌握linux驱动程序的编写方法2. 掌握驱动程序动态模块的调试方法3. 掌握驱动程序填加到内核的方法二、实验内容1. 学习linux驱动程序的编写流程2. 学习驱动程序动态模块的调试方法3. 学习驱动程序填加到内核的流程三、实验设备 PentiumII以上的PC机,LINUX操作系统,EL-ARM860实验箱四、linux的驱动程序的编写嵌入式应用对成本和实时性比较敏感,而对linux的应用主要体现在对硬件的驱动程序的编写和上层应用程序的开发上。嵌入式linux驱动程序的基本结构和标准Linux的结构基本一致,也支持模块化模
2、式,所以,大部分驱动程序编成模块化形式,而且,要求可以在不同的体系结构上安装。linux是可以支持模块化模式的,但由于嵌入式应用是针对具体的应用,所以,一般不采用该模式,而是把驱动程序直接编译进内核之中。但是这种模式是调试驱动模块的极佳方法。系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。同时,设备驱动程序是内核的一部分,它完成以下的功能:对设备初始化和释放;把数据从内核传送到硬件和从硬件读取数据;读取应用程序传送给
3、设备文件的数据和回送应用程序请求的数据;检测和处理设备出现的错误。在linux操作系统下有字符设备和块设备,网络设备三类主要的设备文件类型。字符设备和块设备的主要区别是:在对字符设备发出读写请求时,实际的硬件I/O一般就紧接着发生了;块设备利用一块系统内存作为缓冲区,当用户进程对设备请求满足用户要求时,就返回请求的数据。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。1 字符设备驱动结构Linux字符设备驱动的关键数据结构是cdev和file_operations结构体。1)cdev结构体在linux2.6内核中,使用cdev结构体描述一个字符设备,cdev结构体的定义如
4、下:struct cdev struct kobject kobj;/*内嵌的kobject对象*/ struct module *owner;/*所属模块*/ struct file_operations *ops;/*文件操作结构体*/ struct list_head list; dev_t dev;/*设备号*/ unsigned int count; ;cdev结构体的dev_t成员定义了设备号,为32位,其中12位主设备号,20位次设备号。使用下列宏可以从dev_t获得主设备号和次设备号。*dev_t这个不是structure,是简单变量,只用于保存一组major number和m
5、inor numberLinux提供一组mactor对其进行读写:MAJOR(dev_t dev);/读取设备的major numberMINOR(dev_t dev);/读取设备的minor number而使用下列宏则可以通过主设备号和次设备号生成dev_t;MKDEV(int major,int minor);/从一组指定的major number和minor number创建一个dev_tcdev结构体的另一个重要成员file_operations定义了字符设备提供给虚拟文件系统的接口函数。 Linux 2.6内核提供了一组函数用于操作cdev结构体:void cdev_init(str
6、uct cdev *,struct file_operations *);struct cdev *cdev_alloc(void);/*动态申请一个cdev空间内存*/void cdev_put(struct cdev *p);int cdev_add(struct cdev *,dev_t,unsigned);/*向系统添加、注册一个cdev*/void cdev_del(struct cdev *);/*向系统注销一个cdev*/cdev_init()函数用于初始化cdev的成员,并建立cdev和file_operations之间的连接,其源代码如下所示:void cdev_init(s
7、truct cdev *cdev,struct file_operations *fops) memset(cdev,0,sizeof *cdev); INIT_LIST_HEAD(&cdev-list); kobject_init(&cdev-kobj,&ktype_cdev_default); cdev-ops=fops;/*将传入的文件操作结构体指针赋值给cdev的ops*/ cdev_alloc()函数用于动态申请一个cdev内存,其源代码清单如下:struct cdev *cdev_alloc(void) struct cdev *p=kzalloc(sizeof(struct cd
8、ev),GFP_KERNEL); if(p) INIT_LIST_HEAD(&p-list); kobject_init(&p-kobj,&ktype_cdev_dynamic); return p;cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用通常发生在字符设备驱动模块卸载函数中。2)分配和释放设备号在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chr
9、dev_region()函数向系统申请设备号,这两个函数的原型为:int register_chrdev_region(dev_t from,unsigned count,const char *name);int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_regione用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入
10、第一个参数dev中。后者的优点在于它会自动避开设备号重复的冲突。相反地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型为:void unregister_chrdev_region(dev_t from,unsigned count);from:要分配设备编号范围的起始值,经常设置为0.count:所请求的连续设备编号的个数。Name:是和该设备范围关联的设备名称,它将出现在/proc/devices和sysfs中。3)file_operations结构体file_operations结
11、构体中的成员函数是字符设备驱动程序设计的主体内容,是这符设备驱动与内核的接口,是用户空间对linux进行系统调用的最终落实者。这些函数实际会在程序进行linux的open()、write()、read()、close()等系统调用时最终被调用。字符设备驱动程序中,具体实现这些函数,通常,比如file_operation中的read这个函数指针将指向这个具体的驱动程序中的函数xxx_read() 通常,一个设备驱动程序包括两个基本的任务:驱动设备的某些函数作为系统调用执行;而某些函数则负责处理中断(即中断处理函数)。而file_operations 结构的每一个成员的名称都对应着一个系统调用。用
12、户程序利用系统调用,比如在对一个设备文件进行诸如read操作时,这时对应于该设备文件的驱动程序就会执行相关的ssize_t (*read) (struct file *, char *, size_t, loff_t *);函数。在操作系统内部,外部设备的存取是通过一组固定入口点进行的,这些入口点由每个外设的驱动程序提供,由file_operations结构向系统进行说明,因此,编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。file_operations结构在kernel/include/linux/fs.h中可以找到。struct file_oper
13、ations struct module *owner;/*拥有该结构的模块的指针,一般为THIS_MODULES*/ loff_t (*llseek) (struct file *, loff_t, int);/*用来修改文件当前的读写位置*/ ssize_t (*read) (struct file *, char *, size_t, loff_t *); /*从设备中同步读取数据*/ ssize_t (*write) (struct file *, const char *, size_t, loff_t *); /*向设备发送数据*/ssize_t (*aio_read) (stru
14、ct file *, char *, size_t, loff_t *); /*初始化一个异步的读取操作*/ ssize_t (*aio_write) (struct file *, const char *, size_t, loff_t *); /*初始化一个异步的写入操作*/ int (*readdir) (struct file *, void *, filldir_t); /*仅用于读取目录,对于设备文件,该字符为null*/ unsigned int (*poll) (struct file *, struct poll_table_struct *); /*轮询函数,判断目前是否
15、可以进行非阻塞的读取或写入*/ int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); /*执行设备I/O的控制命令*/ int (*mmap) (struct file *, struct vm_area_struct *); /*用于请求将设备内存映射到进程地址空间*/ int (*open) (struct inode *, struct file *); /*打开*/ int (*flush) (struct file *); int (*release) (struct inode *, s
16、truct file *); /*关闭*/ int (*fsync) (struct file *, struct dentry *, int datasync); /*刷新待处理的数据*/ int (*fasync) (int, struct file *, int); /*通知设备FASYNC标志发生变化*/ int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t
17、 (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /*通常为NULL*/ unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);#ifdef MAGIC_ROM_PTR in
18、t (*romptr) (struct file *, struct vm_area_struct *);#endif /* MAGIC_ROM_PTR */;File_operations结构中的成员全部是函数指针,所以实质上就是函数的跳转表。每个进程对设备的操作,都会根据major、minor设备号,转换成对file_operations结构的访问。即在文件操作中使用的open、read、write、ioctl、close等,都会调用在file_operations中定义的相应的函数入口。其中主要的函数说明如下:1)open 是驱动程序用来完成设备初始化操作的, open还会增加设备计数,
19、以防止文件在关闭前模块被卸载出内核。open主要完成以下操作:检查设备错误(诸如设备未就绪或相似的硬件问题);如果是首次打开,初始化设备;标别次设备号;分配和填写要放在fileprivate_data内的数据结构;增加使用计数。2)read 用来从外部设备中读取数据。当其为NULL指针时,将引起read系统调用返回-EINVAL(“非法参数”)。函数返回一个非负值表示成功地读取了多少字节。3)write 向外部设备发送数据。如果没有这个函数,write 系统调用向调用程序返回一个-EINVAL。如果返回值非负,就表示成功地写入的字节数。4)release是当设备被关闭时调用这个操作。relea
20、se的作用正好与open相反。这个设备方法有时也称为close。它应该完成以下操作:使用计数减1;释放open分配在fileprivate_data中的内存,在最后一次关闭操作时关闭设备。5)llseek 是改变当前的读写指针。6)readdir 一般用于文件系统的操作。7)poll 一般用于查询设备是否可读可写或处于特殊的状态。8)ioctl 执行设备专有的命令。9)mmap 将设备内存映射到应用程序的进程地址空间。2 linux字符设备驱动程序编写的具体内容在linux中,字符设备驱动由以下几个部分组成:1)字符设备驱动模块加载与卸载函数在字符设备驱动模块加载函数中应该实现设备号的申请和c
21、dev的注册,而在卸载函数中应实现设备号的释放和cdev的注销。因为cdev是所有字符设备的抽象,在编写具体的字符设备驱动程序时,针对的是具体的字符设备,具体的字符设备肯定有着自已特征。所以一般的做法是,继承cdev,加上私有数据,信号量等信息。工程师通常习惯为设备定义一个设备相关的结构体,其包含该设备所涉及的cdev、私有数据及信号量等信息。常见设备结构体、模块加载和卸载函数形式如下所示:下面给出一个设备驱动的注册模版供参考/设备结构体struct xxx_dev_tstruct cdev cdev; private_data;/私有数据 semaprore;/信号量 xxx_dev; /设
22、备驱动模块加载函数 static int _init xxx_init(void) . cdev_init(&xxx_dev.cdev,&xxx_fops);/初始化cdev xxx_dev.cdev.owner=THIS_MODULE; /获取字符设备号 if(xxx_major) register_chrdev_region(xxx_dev_no,1,DEV_NAME); else alloc_chrdev_region(&xxx_dev_no,0,1,DEV_NAME); ret=cdev_add(&xxx_dev.cdev,xxx_dev_no,1);/注册设备 . /设备驱动模块卸载
23、函数 static void _exit xxx_exit(void) unregister_chrdev_region(xxx_dev_no,1);/释放占用的设备号 cdev_del(&xxx_dev.cdev);/注销设备 . 2)字符设备驱动的file_operations结构体中成员函数通过了解驱动程序的file_operations 结构,用户就可以编写出相关外部设备的驱动程序。首先,用户在自己的驱动程序源文件中定义file_operations结构,并编写出设备需要的各操作函数,对于设备不需要的操作函数用NULL初始化,这些操作函数将被注册到内核,当应用程序对设备相应的设备文件进
24、行文件操作时,内核会找到相应的操作函数,并进行调用。如果操作函数使用NULL,操作系统就进行默认的处理。定义并编写完file_operations结构的操作函数后,要定义一个初始化函数,比如函数名可device_init(),在linux初始化的时候要调用该函数,因此,该函数应包含以下几项工作:a. 对该驱动所使用到的硬件寄存器进行初始化。包括中断寄存器。b. 初始化设备相关的参数。一般来说每个设备要定义一个设备变量,用来保存设备相关的参数。c. 注册设备。Linux内核通过主设备号将设备驱动程序同设备文件相连。每个设备有且仅有一个主设备号。通过查看linux系统中/proc下的devices
25、文件,该文件记录已经使用的主设备号和设备名,选择一个没有使用的主设备号,调用下面的函数来注册设备。int register_chrdev(unsigned int,const char*,struct file_operations*),其中的三个参数代表主设备号,设备名,file_operations的结构地址。d. 注册设备使用的中断。注册中断使用的函数。int request_irq(unsigned irq,void(*handler)(int,void*,struct pt_regs*),unsigned long flags, const char* device, void* d
26、ev_id);其中,irq是中断向量。硬件系统将IRQn映射成中断向量。handler-中断处理函数。flags-中断处理中的一些选项的掩码。device-设备的名称dev_id-在中断共享时使用的id。e. 其他的一些初始化工作,比如给设备分配I/O,申请DMA通道等。当设备的驱动程序使用了如下的函数方式,则设备驱动可以动态的加载和卸载int _init device_init (void)void _exit device_exit(void)module_init(device _init);module_exit(device _exit);当然,也可以编译进内核中。File_oper
27、ations结构体中成员函数是字符设备驱动与内核的接口,是用户空间对linux进行系统调用最终的落实者。大多数字符设备驱动会实现read()、write()、ioctl()函数,常见的字符设备驱动的3个函数代码的形式如下所示: /*读设备*/ssize_t xxx_read(struct file *filp,char _user *buf,size_t count,loff_t *f_pos) . copy_to_user(buf,.,.); . /*写设备*/ssize_t xxx_write(struct file *filp,const char _user *buf,size_t c
28、ount ,loff_t *f_pos) . copt_from_user(.,buf.); . /*输入输出控制函数*/int xxx_ioct1(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg) . switch(cmd) case XXX_CMD1: . break; case XXX_CMD1: . break; default:/*不能支持的命令*/ return - ENOTTY;return 0; 设备驱动的读函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核
29、空间不能直接读写,count是要读的字节数,f_pos是读的位置相对于文件开头的偏移。设备驱动的写函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不能直接读写,count是要写的字节数,f_pos是写的位置相对于文件开头的偏移。由于用户空间和内核空间的内存不能够直接互相访问,要借助函数copy_from_user()完成用户空间到内核空间的复制,copy_to_user()完成内核空间到用户空间的复制,二者的原型分别为:unsigned long copy_from_user(void *to,const void _user *from,unsigned lon
30、g count);unsigned long copy_to_user(void _user *to,const void *from,unsigned long count);上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user(),如:int val;/*内核空间整型变量*/ Get_user(val,(int *)arg);/*用户到内核,arg是用户空间的地址*/ Put_user(val,(int *)arg);/*内核到用户,arg是用户空间的地址*/
31、读和写函数中的_user是一个宏,表明其后的指针指向用户空间,这个宏定义为:#ifdef _CHECKER_#define _user#else#define _user#endifI/O控制函数的cmd参数为事先定义的I/O控制命令,而arg为对应于该命令的参数。例如对于串行设备,如果SET_BAUDRATE是一道设置波特率的命令,那后面的arg就应该是波特率值。在字符设备的驱动中,还需要定义一个file_operations的实例xxx_fops,并将具体设备驱动的函数赋值给file_operations的成员,如下代码清单:Struct file_operations xxx_fops= .owner=THIS_MODULE;.read=xxx_read;.write=xxx_write;.ioctl=xxx_ioctl;其中xxx_fops在模块加载函数的cdev_init(&xxx_dev.cdev,&xxx
copyright@ 2008-2023 冰点文库 网站版权所有
经营许可证编号:鄂ICP备19020893号-2