1、模块驱动实验指导内核驱动设计入门模块方式驱动实验一、实验目的 学习在LINUX下进行驱动设计的原理 掌握使用模块方式进行驱动开发调试的过程二、实验内容在PC机上编写简单的虚拟硬件驱动程序并进行调试,实验驱动的各个接口函数的实现,分析并理解驱动与应用程序的交互过程。三、预备知识 有C语言基础。 掌握在Linux下常用编辑器的使用。 掌握Makefile 的编写和使用。 掌握Linux下的程序编译与交叉编译过程。 有驱动开发的基本知识。四、实验设备及工具硬件:PC机Pentium 500以上, 硬盘40G以上,内存大于128M。软件:PC机操作系统REDHAT LINUX 9.0 MINICOM
2、AMR-LINUX开发环境五、实验原理Linux中的驱动设计是嵌入式Linux开发中十分重要的部分,它要求开发者不仅要熟悉Linux的内核机制、驱动程序与用户级应用程序的接口关系、考虑系统中对设备的并发操作等等,而且还要非常熟悉所开发硬件的工作原理。这对驱动开发者提出了比较高的要求,这个实验主要是给大家进入驱动设计提供一个简单入门的一个实例,并不需要提供太多与硬件相关的内容,这部分应该是通过仔细阅读芯片厂家提供的资料来解决。驱动程序的作用是应用程序与硬件之间的一个中间软件层,驱动程序应该为应用程序展现硬件的所有功能,不应该强加其他的约束,对于硬件使用的权限和限制应该由应用程序层控制。但是有时驱
3、动程序的设计是跟所开发的项目相关的,这时就可能在驱动层加入一些与应用相关的设计考虑,主要是因为在驱动层的效率比应用层高,同时为了项目的需要可能只强化或优化硬件的某个功能,而弱化或关闭其他一些功能;到底需要展现硬件的哪些功能全都由开发者根据需要而定。驱动程序有时会被多个进程同时使用,这时我们要考虑如何处理并发的问题,就需要调用一些内核的函数使用互斥量和锁等机制。驱动程序主要需要考虑下面三个方面:提供尽量多的选项给用户,提高驱动程序的速度和效率,尽量使驱动程序简单,使之易于维护。Linux的驱动开发调试有两种方法,一种是直接编译到内核,再运行新的内核来测试;二是编译为模块的形式,单独加载运行调试。
4、第一种方法效率较低,但在某些场合是唯一的方法。模块方式调试效率很高,它使用insmod工具将编译的模块直接插入内核,如果出现故障,可以使用rmmod从内核中卸载模块。不需要重新启动内核,这使驱动调试效率大大提高。我们的实验在PC机和UP-NETARM2410-S上都可以运行,编译时使用不同的编译器就可以了。1.驱动程序与应用程序的区别应用程序一般有一个main函数,从头到尾执行一个任务;驱动程序却不同,它没有main函数,通过使用宏module_init(初始化函数名); 将初始化函数加入内核全局初始化函数列表中,在内核初始化时执行驱动的初始化函数,从而完成驱动的初始化和注册,之后驱动便停止等
5、待被应用软件调用。驱动程序中有一个宏moudule_exit(退出处理函数名)注册退出处理函数。它在驱动退出时被调用。应用程序可以和GLIBC库连接,因此可以包含标准的头文件,比如 ,在驱动程序中是不能使用标准C库的,因此不能调用所有的C库函数,比如输出打印函数只能使用内核的printk函数,包含的头文件只能是内核的头文件,比如。2.内核版本与编译器的版本依赖当模块与内核链接时,insmod会检查模块和当前内核版本是否匹配,每个模块都定义了版本符号_module_kernel_version,这个符号位于模块文件的ELF头的.modinfo段中。只要在模块中包含,编译器就会自动定义这个符号。每
6、个内核版本都需要特定版本的编译器的支持,高版本的编译器并不适合低版本的内核,比如UP-NETARM2410-S实验仪中的LINUX-2.4.19的内核需要2.95.3的GCC版本编译器。Linux-2.4版本的insmod 命令装载模块时,首先从/lib/modules目录和内核相关的子目录中查找模块文件,如果需要从当前目录装载,使用insmod module.o。3.主设备号和次设备号传统方式中的设备管理中,除了设备类型外,内核还需要一对称作主次设备号的参数,才能唯一标识一个设备。主设备号相同的设备使用相同的驱动程序,次设备号用于区分具体设备的实例。比如PC机中的IDE设备,一般主设备号使用
7、3,WINDOWS下进行的分区,一般将主分区的次设备号为1,扩展分区的次设备号为2、3、4,逻辑分区使用5、6.。设备操作宏MAJOR()和MINOR()可分别用于获取主次设备号,宏MKDEV()用于将主设备号和次设备号合并为设备号,这些宏定义在include/linux/kdev_t.h中。对于LINUX中对设备号的分配原则可以参考Documentation/devices.txt。对于查看/dev目录下的设备的主次设备号可以使用如下命令:/mnt/yaffsls /dev -l crw- 1 root root 5, 1 Jan 1 00:00 console crw- 1 root ro
8、ot 5, 64 Jan 1 00:00 cua0 crw- 1 root root 5, 65 Jan 1 00:00 cua1 crw-rw-rw- 1 root root 1, 7 Jan 1 00:00 full drwxr-xr-x 1 root root 0 Jan 1 00:00 keyboard crw-r- 1 root root 1, 2 Jan 1 00:00 kmemcrw-r- 1 root root 1, 1 Jan 1 00:00 memdrwxr-xr-x 1 root root 0 Jan 1 00:00 mtddrwxr-xr-x 1 root root 0
9、 Jan 1 00:00 mtdblockcrw-rw-rw- 1 root root 1, 3 Jan 1 00:00 nullcrw-r- 1 root root 1, 4 Jan 1 00:00 portcrw- 1 root root 108, 0 Jan 1 00:00 pppcrw-rw-rw- 1 root root 5, 2 Jan 1 00:00 ptmxcrw-r-r- 1 root root 1, 8 Jan 1 00:00 randomlr-xr-xr-x 1 root root 4 Jan 1 00:00 root - rd/0crw-rw-rw- 1 root ro
10、ot 5, 0 Jan 1 00:00 ttycrw- 1 root root 4, 64 Jan 1 00:11 ttyS0crw- 1 root root 4, 65 Jan 1 00:00 ttyS1crw-r-r- 1 root root 1, 9 Jan 1 00:00 urandomcrw-rw-rw- 1 root root 1, 5 Jan 1 00:00 zero4.设备文件设备类型、主次设备号是内核与设备驱动程序通信时所使用的,但是对于开发应用程序的用户来说比较难于理解和记忆,所以Linux使用了设备文件的概念来统一对设备的访问接口,在引入设备文件系统(devfs)之前Li
11、nux将设备文件放在/dev目录下,设备的命名一般为设备文件名数字或字母表示的子类,例如/dev/hda1、/dev/hda2等。在Linux2.4内核中引入了设备文件系统(devfs),所有的设备文件作为一个可以挂装的文件系统,这样就可以被文件系统进行统一管理,从而设备文件就可以挂装到任何需要的地方。命名规则也发生了变化,一般将主设备建立一个目录,再将具体的子设备文件建立在此目录下。比如在UP-NETARM2410-S中的MTD 设备为:/dev/mtdblock/0。5.设备驱动程序接口通常所说的设备驱动程序接口是指结构file_operations,它定义在include/linux/f
12、s.h中。 file_operations数据结构说明struct file_operations struct module *owner; 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 *); int (*readdir) (struct file *, void *, filldir_t); unsig
13、ned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, str
14、uct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iov
15、ec *, unsigned long, loff_t *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);#ifdef MAGIC_ROM_PTR int (*romptr) (struct file *, struct vm_area_struct *);#e
16、ndif /* MAGIC_ROM_PTR */;file_operations结构是整个Linux内核的重要数据结构,它也是file、inode结构的重要成员,表5.1.1中分别说明结构中主要的成员:表5.1.1 file_operations结构Ownermodule的拥有者。Llseek重新定位读写位置。Read从设备中读取数据。Write向字符设备中写入数据。Readdir只用于文件系统,对设备无用。Ioctl控制设备,除读写操作外的其他控制命令。Mmap将设备内存映射到进程地址空间,通常只用于块设备。Open打开设备并初始化设备。Flush清除内容,一般只用于网络文件系统中。Rele
17、ase关闭设备并释放资源。Fsync实现内存与设备的同步,如将内存数据写入硬盘。Fasync实现内存与设备之间的异步通讯。Lock文件锁定,用于文件共享时的互斥访问。Readv在进行读操作前要验证地址是否可读。Writev在进行写操作前要验证地址是否可写。在嵌入式系统的开发中,我们一般仅仅实现其中几个接口函数:read、write、ioctl、open、release,就可以完成应用系统需要的功能。6.file 数据结构说明struct file struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmn
18、t; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error; unsigned long f_version; /* needed for tty driver, and maybe others */
19、void *private_data; /* preallocated helper kiobuf to speedup O_DIRECT */ struct kiobuf *f_iobuf; long f_iobuf_lock; file结构中与驱动相关的重要成员说明我们将struct file 结构指针定义为flip,以便于下面说明。表5.1.2 file结构中与驱动相关的成员f_mode标识文件的读写权限f_pos当前读写位置,类型为loff_t是64位的数,只能读不能写f_flag文件标志,主要用于进行阻塞/非阻塞型操作时检查f_op文件操作的结构指针,内核在OPEN操作时对此指针赋值
20、。private_dataOpen系统调用在调用驱动程序的open方法前,将此指针值NULL,驱动程序可以将这个字段用于任何目的,一般用它指向已经分配的数据,但在内核销毁file结构前要在release方法中释放内存。f_dentry文件对应的目录项结构,一般在驱动中用filp-f_dentry-d_inode访问索引节点时用到它。7.驱动接口的实现过程我们先看看实验代码框架其中, static struct file_operations demo_fops = 完成了将驱动函数映射为标准接口,devfs_registe()和register_chrdev()函数完成将驱动向内核注册。sta
21、tic struct file_operations demo_fops = owner: THIS_MODULE, write: demo_write, read: demo_read, ioctl: demo_ioctl, open: demo_open, release:demo_release,;上面的这种特殊表示方法不是标准C的语法,这是GNU编译器的一种特殊扩展,它使用名字对进行结构字段的初始化,它的好处体现在结构清晰,易于理解,并且避免了结构发生变化带来的许多问题。 Open方法Open方法提供给驱动程序初始化设备的能力,从而为以后的设备操作做好准备,此外open操作一般还会递增
22、使用计数,用以防止文件关闭前模块被卸载出内核。在大多数驱动程序中Open方法应完成如下工作:1. 递增使用计数2. 检查特定设备错误。3. 如果设备是首次打开,则对其进行初始化。4. 识别次设备号,如有必要修改f_op指针。5. 分配并填写filp-private_data中的数据。 Release方法与open方法相反,release 方法应完成如下功能:1. 释放由open分配的filp-private_data中的所有内容2. 在最后一次关闭操作时关闭设备3. 使用计数减一 Read和Write方法ssize_t demo_write(struct file *filp,const ch
23、ar * buffer, size_t count,loff_t *ppos)ssize_t demo_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)read 方法完成将数据从内核拷贝到应用程序空间,write方法相反,将数据从应用程序空间拷贝到内核。对于者两个方法,参数filp是文件指针,count是请求传输数据的长度,buffer是用户空间的数据缓冲区,ppos是文件中进行操作的偏移量,类型为64位数。由于用户空间和内核空间的内存映射方式完全不同,所以不能使用象memcpy之类的函数,必须使用如下函数:un
24、signed 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); Read的返回值1. 返回值等于传递给read系统调用的count参数,表明请求的数据传输成功。2. 返回值大于0,但小于传递给read系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次读取的方法。3. 返回值0,表示到达文件的末尾。4. 返回值为
25、负数,表示出现错误,并且指明是何种错误。5. 在阻塞型io中,read调用会出现阻塞。 Write的返回值1. 返回值等于传递给write系统调用的count参数,表明请求的数据传输成功。2. 返回值大于0,但小于传递给write系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次读取的方法。3. 返回值0,表示没有写入任何数据。标准库在调用write时,出现这种情况会重复调用write。4. 返回值为负数,表示出现错误,并且指明是何种错误。错误号的定义参见5. 在阻塞型io中,write调用会出现阻塞。 ioctl方法ioctl方法主要用于对
26、设备进行读写之外的其他控制,比如配置设备、进入或退出某种操作模式,这些操作一般都无法通过read/write文件操作来完成,比如在UP-NETARM2410-S中的SPI设备通道的选择操作,无法通过write操作控制,这就是ioctl操作的功能。用户空间的ioctl函数的原型为:int ioctl(inf fd,int cmd,)其中的代表可变数目的参数表,实际中是一个可选参数,一般定义为:int ioctl(inf fd,int cmd,char *argp)驱动程序中定义的ioctl 方法原型为:int (*ioctl) (struct inode *inode, struct file
27、*file,unsigned int cmd, unsigned long arg)inode 和 filp两个指针对应应用程序传递的文件描述符fd,cmd不会被修改地传递给驱动程序,可选的参数arg则无论用户应用程序使用的是指针还是其他类型值,都以unsigned long的形式传递给驱动。 ioctl方法的命令编号确定由于为了防止向不该控制的设备发出正确的命令,LINUX驱动的ioctl方法中的cmd参数推荐使用唯一编号,编号方法并根据如下规则定义:编号分为4个字段:1. type(类型):也称为幻数,8位宽。2. number(号码):顺序数,8位宽。3. direction(方向):如
28、果该命令有数据传输,就要定义传输方向,2位宽,可使用的数值:a) _IOC_NONEb) _IOC_READc) _IOC_WRITE 4. size(大小):数据大小,宽度与体系结构有关,在ARM上为14位。这些定义在中可以找到。其中还定义了一些用于构造命令号的宏:#define _IOC_NRBITS 8#define _IOC_TYPEBITS 8#define _IOC_SIZEBITS 14#define _IOC_DIRBITS 2#define _IOC_NRMASK (1 _IOC_NRBITS)-1)#define _IOC_TYPEMASK (1 _IOC_TYPEBITS
29、)-1)#define _IOC_SIZEMASK (1 _IOC_SIZEBITS)-1)#define _IOC_DIRMASK (1 _IOC_DIRBITS)-1)#define _IOC_NRSHIFT 0#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)#define _IOC_NONE 0U#define _IOC_WRITE 1U#define _IOC_READ 2U#define _IOC(dir,type,nr,size) (dir) _IOC_DIRSHIFT) | (type) _IOC_TYPESHIFT) | (nr) _IOC_NRSHIFT) | (size) _IOC_DIRSHIFT) & _IOC_DIRMASK)#define _IOC_TYPE(nr) (
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1