[精选]第11章 嵌入式Linux设备驱动开发7685.pptx
本章的要求第11章、嵌入式Linux设备驱动开发 uLinux设备驱动的基本概念 uLinux设备驱动程序的基本功能 uLinux设备驱动的运作过程 u常见设备驱动接口函数 u掌握字符设备驱动程序的编写u掌握键盘设备驱动程序的编写 u了解块设备的编写流程本章的主要内容11.1 设备驱动概述 11.2 字符设备驱动编程 11.3 GPIO驱动程序实例 11.4 块设备驱动编程 11.5 中断编程 11.6 按键驱动程序实例 11.7 实验内容test驱动 11.1 设备驱动概述设备驱动简介及驱动模块 操作系统是通过各种驱动程序来驾驭硬件设备的,它为用户屏蔽了各种各样的设备,驱动硬件是操作系统最基本的功能,并且提供统一的操作方式。设备驱动程序是内核的一部分,硬件驱动程序是操作系统最基本的组成部分,在Linux内核源程序中也占有60%以上。因此,熟悉驱动的编写是很重要的。Linux内核中采用可加载的模块化设计(LKMs,Loadable Kernel Modules),一般情况下编译的Linux内核是支持可插入式模块的,也就是将最基本的核心代码编译在内核中,其他的代码可以编译到内核中,或者编译为内核的模块文件(在需要时动态加载)。内核模块的主要相关命令常见的驱动程序是作为内核模块动态加载的,比如声卡驱动和网卡驱动等,而Linux最基础的驱动,如CPU、PCI总线、TCP/IP协议、APM(高级电源管理)、VFS等驱动程序则直接编译在内核文件中。有时也把内核模块叫做驱动程序,只不过驱动的内容不一定是硬件罢了,比如ext3文件系统的驱动。因此,加载驱动就是加载内核模块。lsmod列出当前系统中加载的模块,其中左边第一列是模块名,第二列是该模块大小,第三列则是使用该模块的对象数目。rmmod是用于将当前模块卸载。insmod和modprobe是用于加载当前模块,但insmod不会自动解决依存关系,即如果要加载的模块引用了当前内核符号表中不存在的符号,则无法加载,也不会去查在其他尚未加载的模块中是否定义了该符号;modprobe可以根据模块间依存关系以及/etc/modules.conf文件中的内容自动加载其他有依赖关系的模块。设备分类(1)Linux系统的设备分为三类:字符设备、块设备和网络设备。字符设备通常指像普通文件或字节流一样,以字节为单位顺序读写的设备,如并口设备、虚拟控制台等。字符设备可以通过设备文件节点访问,它与普通文件之间的区别在于普通文件可以被随机访问(可以前后移动访问指针),而大多数字符设备只能提供顺序访问,因为对它们的访问不会被系统所缓存。但也有例外,例如帧缓存(framebuffer)是一个可以被随机访问的字符设备。块设备通常指一些需要以块为单位随机读写的设备,如IDE硬盘、SCSI硬盘、光驱等。块设备也是通过文件节点来访问,它不仅可以提供随机访问,而且可以容纳文件系统(例如硬盘、闪存等)。Linux可以使用户态程序像访问字符设备一样每次进行任意字节的操作,只是在内核态内部中的管理方式和内核提供的驱动接口上不同。设备分类(2)网络设备通常是指通过网络能够与其他主机进行数据通信的设备,如网卡等。内核和网络设备驱动程序之间的通信调用一套数据包处理函数,它们完全不同于内核和字符以及块设备驱动程序之间的通信(read(),write()等函数)。Linux网络设备不是面向流的设备,因此不会将网络设备的名字(例如eth0)映射到文件系统中去。$ls l/devcrw-rw-1 root uucp 4,64 08-30 22:58 ttyS0/*串口设备,c表示字符设备*/brw-r-1 root floppy 2,0 08-30 22:58 fd0/*软盘设备,b表示块设备*/设备号 设备号是一个数字,它是设备的标志。就如前面所述,一个设备文件(也就是设备节点)可以通过mknod命令来创建,其中指定了主设备号和次设备号。主设备号表明设备的类型(例如串口设备、SCSI硬盘),与一个确定的驱动程序对应;次设备号通常是用于标明不同的属性,例如不同的使用方法,不同的位置,不同的操作等,它标志着某个具体的物理设备。高字节为主设备号,底字节为次设备号。例如,在系统中的块设备IDE硬盘的主设备号是3,而多个IDE硬盘及其各个分区分别赋予次设备号1、2、3$ls l/devcrw-rw-1 root uucp 4,64 08-30 22:58 ttyS0/*主设备号4,此设备号64*/驱动层次结构 设备驱动程序与外界的接口 设备驱动程序的特点(1)(1)内核代码:设备驱动程序是内核的一部分,如果驱动程序出错,则可能导致系统崩溃。(2)内核接口:设备驱动程序必须为内核或者其子系统提供一个标准接口。比如,一个终端驱动程序必须为内核提供一个文件I/O接口;一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同时SCSI子系统也必须为内核提供文件的I/O接口及缓冲区。(3)内核机制和服务:设备驱动程序使用一些标准的内核服务,如内存分配等。设备驱动程序的特点(2)(4)可装载:大多数的Linux操作系统设备驱动程序都可以在需要时装载进内核,在不需要时从内核中卸载。(5)可设置:Linux操作系统设备驱动程序可以集成为内核的一部分,并可以根据需要把其中的某一部分集成到内核中,这只需要在系统编译时进行相应的设置即可。(6)动态性:在系统启动且各个设备驱动程序初始化后,驱动程序将维护其控制的设备。如果该设备驱动程序控制的设备不存在也不影响系统的运行,那么此时的设备驱动程序只是多占用了一点系统内存罢了。11.2 字符设备驱动编程 设备驱动程序工作原理模块在调用insmod命令时被加载,此时的入口点是init_module()函数,通常在该函数中完成设备的注册。同样,模块在调用rmmod命令时被卸载,此时的入口点是cleanup_module()函数,在该函数中完成设备的卸载。在设备完成注册加载之后,用户的应用程序就可以对该设备进行一定的操作,如open()、read()、write()等,而驱动程序就是用于实现这些操作,在用户应用程序调用相应入口函数时执行相关的操作。重要数据结构-file_operaions结构 struct file_operations loff_t(*llseek)(struct file*,loff_t,int);ssize_t(*read)(struct file*filp,char*buff,size_t count,loff_t*offp);ssize_t(*write)(struct file*filp,const char*buff,size_t count,loff_t*offp);int(*readdir)(struct file*,void*,filldir_t);unsigned 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*,struct file*);int(*fsync)(struct file*,struct dentry*);int(*fasync)(int,struct file*,int);int(*check_media_change)(kdev_t dev);int(*revalidate)(kdev_t dev);int(*lock)(struct file*,int,struct file_lock*);重要数据结构-inode结构struct file mode_t f_mode;/*标识文件是否可读或可写,FMODE_READ或FMODE_WRITE*/dev_t f_rdev;/*用于/dev/tty*/off_t f_pos;/*当前文件位移*/unsigned short f_flags;/*文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC*/unsigned short f_count;/*打开的文件数目*/unsigned short f_reada;struct inode*f_inode;/*指向inode的结构指针*/struct file_operations*f_op;/*文件索引指针*/;早期版本的字符设备注册(1)早期版本的设备注册使用函数register_chrdev(),调用该函数后就可以向系统申请主设备号,如果register_chrdev()操作成功,设备名就会出现在/proc/devices文件里。在关闭设备时,通常需要解除原先的设备注册,此时可使用函数unregister_chrdev(),此后该设备就会从/proc/devices里消失。其中主设备号和次设备号不能大于255。早期版本的字符设备注册(2)设备号相关函数(1)在linux2.6的版本中,用dev_t类型来描述设备号(dev_t是32位数值类型,其中高12位表示主设备号,低20位表示次设备号)。用两个宏MAJOR和MINOR分别获得dev_t设备号的主设备号和次设备号,而且用MKDEV宏来实现逆过程,即组合主设备号和次设备号而获得dev_t类型设备号。分配设备号有静态和动态的两种方法。静态分配(register_chrdev_region()函数)是指在事先知道设备主设备号的情况下,通过参数函数指定第一个设备号(它的次设备号通常为0)而向系统申请分配一定数目的设备号。动态分配(alloc_chrdev_region())是指通过参数仅设置第一个次设备号(通常为0,事先不会知道主设备号)和要分配的设备数目而系统动态分配所需的设备号。通过unregister_chrdev_region()函数释放已分配的(无论是静态的还是动态的)设备号。设备号相关函数(2)字符设备注册(1)在Linux内核中使用struct cdev结构来描述字符设备,我们在驱动程序中必须将已分配到的设备号以及设备操作接口(即为struct file_operations结构)赋予struct cdev结构变量。首先使用cdev_alloc()函数向系统申请分配struct cdev结构,再用cdev_init()函数初始化已分配到的结构并与file_operations结构关联起来。最后调用cdev_add()函数将设备号与struct cdev结构进行关联并向内核正式报告新设备的注册,这样新设备可以被用起来了!。如果要从系统中删除一个设备,则要调用cdev_del()函数。字符设备注册(2)打开设备 打开设备的函数接口是open,根据设备的不同,open函数接口完成的功能也有所不同,但通常情况下在open函数接口中要完成如下工作。递增计数器,检查错误。如果未初始化,则进行初始化。识别次设备号,如果必要,更新f_op指针。分配并填写被置于filp-private_data的数据结构。其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。因此使用计数器就可以很好地完成这项功能。释放设备释放设备的函数接口是release()。要注意释放设备和关闭设备是完全不同的。当一个进程释放设备时,其他进程还能继续使用该设备,只是该进程暂时停止对该设备的使用;而当一个进程关闭设备时,其他进程必须重新打开此设备才能使用它。释放设备时要完成的工作如下。递减计数器MOD_DEC_USE_COUNT(最新版本已经不再使用)。释放打开设备时系统所分配的内存空间(包括filp-private_data指向的内存空间)。在最后一次释放设备操作时关闭设备。读写设备 读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,也就是将内核空间缓冲区里的数据复制到用户空间的缓冲区中或者相反。内核空间和用户空间的数据交换内核空间地址和用户空间地址是有很大区别的,其中一个区别是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况。所以不能使用诸如memcpy()之类的函数来完成这样的操作。在这里要使用copy_to_user()或copy_from_user()等函数,它们是用来实现用户空间和内核空间的数据交换的。ioctl 大部分设备除了读写操作,还需要硬件配置和控制(例如,设置串口设备的波特率)等很多其他操作。在字符设备驱动中ioctl函数接口给用户提供对设备的非读写操作机制。获取内存(1)在应用程序中获取内存通常使用函数malloc(),但在设备驱动程序中动态开辟内存可以以字节或页面为单位。其中,以字节为单位分配内存的函数有kmalloc(),注意的是,kmalloc()函数返回的是物理地址,而malloc()等返回的是线性虚拟地址,因此在驱动程序中不能使用malloc()函数。与malloc()不同,kmalloc()申请空间有大小限制。长度是2的整次方,并且不会对所获取的内存空间清零。以页为单位分配内存的函数如下所示:get_zeroed_page():获得一个已清零页面。get_free_page():获得一个或几个连续页面。get_dma_pages():获得用于DMA传输的页面。与之相对应的释放内存用也有kfree()或free_page函数族。获取内存(2)获取内存(3)获取内存(4)打印信息 在内核空间要用函数printk()而不能用平常的函数printf()。printk()还可以定义打印消息的优先级。proc文件系统(1)/proc文件系统是一个伪文件系统,它是一种内核和内核模块用来向进程发送信息的机制。这个伪文件系统让用户可以和内核内部数据结构进行交互,获取有关系统和进程的有用信息,在运行时通过改变内核参数来改变设置。与其他文件系统不同,/proc存在于内存之中而不是在硬盘上。读者可以通过“ls”查看/proc文件系统的内容。proc文件系统(2)11.3 GPIO驱动程序实例 GPIO工作原理 FS2410开发板的S3C2410处理器具有117个多功能通用I/O(GPIO)端口管脚,包括GPIO 8个端口组,分别为GPA(23个输出端口)、GPB(11个输入/输出端口)、GPC(16个输入/输出端口)、GPD(16个输入/输出端口)、GPE(16个输入/输出端口)、GPF(8个输入/输出端口)、GPH(11个输入/输出端口)。根据各种系统设计的需求,通过软件方法可以将这些端口配置成具有相应功能(例如:外部中断或数据总线)的端口。为了控制这些端口,S3C2410处理器为每个端口组分别提供几种相应的控制寄存器。其中最常用的有端口配置寄存器(GPACON GPHCON)和端口数据寄存器(GPADAT GPHDAT)。因为大部分I/O管脚可以提供多种功能,通过配置寄存器(PnCON)设定每个管脚用于何种目的。数据寄存器的每位将对应于某个管脚上的输入或输出。所以通过对数据寄存器(PnDAT)的位读写,可以进行对每个端口的输入或输出。LED和蜂鸣器驱动电路可知使用可知使用S3C2410处理处理器的通用器的通用I/O口口GPF4、GPF5、GPF6和和GPF7分分别直接驱动别直接驱动LED D12、D11、D10以及以及D9,而使,而使用用GPB0端口驱动蜂鸣器。端口驱动蜂鸣器。4个个LED分别在对应端口分别在对应端口(GPF4GPF7)为低电)为低电平时发亮,而蜂鸣器在平时发亮,而蜂鸣器在GPB0为高电平时发声。为高电平时发声。这这5个端口的数据流方向个端口的数据流方向均为输出。均为输出。主要控制寄存器(1)主要控制寄存器(2)为了驱动LED和蜂鸣器,首先通过端口配置寄存器将5个相应寄存器配置为输出模式。然后通过对端口数据寄存器的写操作,实现对每个GPIO设备的控制(发亮或发声)。在下一个小节中介绍的驱动程序中,s3c2410_gpio_cfgpin()函数和s3c2410_gpio_pullup()函数将进行对某个端口的配置,而s3c2410_gpio_setpin()函数实现向数据寄存器的某个端口的输出。GPIO驱动程序 阅读并运行11-3-2$make clean;make/*驱动程序的编译*/$insmod gpio_drv.ko/*加载gpio驱动*/$cat/proc/devices/*通过这个命令可以查到gpio设备的主设备号*/$mknod/dev/gpio c 252 0 /*假设主设备号为252,创建设备文件节点*/$arm-linux-gcc o gpio_test gpio_test.c$./gpio_test运行结果为4个LED轮流闪烁,同时蜂鸣器以一定周期发出声响。11.4 块设备驱动编程块设备驱动块设备通常指一些需要以块(如512字节)的方式写入的设备,如IDE硬盘、SCSI硬盘、光驱等。它的驱动程序的编写过程与字符型设备驱动程序的编写有很大的区别。块设备驱动编程接口相对复杂,不如字符设备明晰易用。块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动程要重点考虑的问题。系统中使用缓冲区与访问请求的优化管理(合并与重新排序)来提高系统性能。块设备驱动工作流程块设备驱动程序的编写流程同字符设备驱动程序的编写流程很类似,也包括了注册和使用两部分。但与字符驱动设备所不同的是,块设备驱动程序包括一个request请求队列。它是当内核安排一次数据传输时在列表中的一个请求队列,以最大化系统性能为原则进行排序。重要数据结构(1)每个块设备物理实体由一个gendisk结构体来表示,每个gendisk可以支持多个分区。每个gendisk中包含了本物理实体的全部信息以及操作函数接口。整个块设备的注册过程是围绕gendisk来展开的。struct gendisk int major;/*主设备号*/int first_minor;/*第一个次设备号*/int minors;/*次设备号个数,一个块设备至少需要使用一个次设备号,而且块设 备的每个分区都需要一个次设备号,因此这个成员等于1,则表明该块 设备是不可被分区的,否则可以包含minors 1 个分区。*/char disk_name32;/*块设备名称,在/proc/partions中显示*/struct hd_struct*part;/*分区表*/struct block_device_operations*fops;/*块设备操作接口,与字符设备的 file_operations结构对应*/struct request_queue*queue;/*I/O请求队列*/void*private_data;/*指向驱动程序私有数据*/sector_t capacity;/*块设备可包含的扇区数*/*其他省略*/;重要数据结构(2)struct block_device_operations int(*open)(struct inode*,struct file*);int(*release)(struct inode*,struct file*);int(*ioctl)(struct inode*,struct file*,unsigned,unsigned long);long(*unlocked_ioctl)(struct file*,unsigned,unsigned long);long(*compat_ioctl)(struct file*,unsigned,unsigned long);int(*direct_access)(struct block_device*,sector_t,unsigned long*);int(*media_changed)(struct gendisk*);int(*revalidate_disk)(struct gendisk*);int(*getgeo)(struct block_device*,struct hd_geometry*);struct module*owner;块设备驱动程序也包含一个在中定义的block_device_operations结构块设备并不提供read()、write()等函数接口。对块设备的读写请求都是以异步方式发送到设备相关的request 队列之中。块设备注册和初始化(1)块设备注册和初始化(2)(1)向内核注册使用register_blkdev()函数对设备进行注册。其中参数major为要注册的块设备的主设备号,如果其值等于0,则系统动态分配并返回主设备号。参数name为设备名,在/proc/devices中显示。如果出错,则该函数返回负值。与其对应的块设备的注销函数为unregister_blkdev()。其参数必须与注册函数中的参数相同。如果出错则返回负值。(2)申请并初始化请求队列这一步要调用blk_init_queue()函数来申请并初始化请求队列。其中参数rfn是请求队列的处理函数指针,它负责执行块设备的读、写请求。参数lock为自旋锁,用于控制对所分配的队列的访问。int register_blkdev(unsigned int major,const char*name);int unregister_blkdev(unsigned int major,const char*name);struct request_queue*blk_init_queue(request_fn_proc*rfn,spinlock_t*lock)块设备注册和初始化(3)(3)初始化并注册gendisk结构 首先使用alloc_disk()函数动态分配gendisk结构,接下来,对gendisk结构的主设备号(major)、次设备号相关成员(first_minor和minors)、块设备操作函数(fops)、请求队列(queue)、可包含的扇区数(capacity)以及设备名称(disk_name)等成员进行初始化。在完成对gendisk的分配和初始化之后,调用add_disk()函数向系统注册块设备。在卸载gendisk结构的时候,要调用del_gendisk()函数。块设备请求处理 块设备驱动中一般要实现一个请求队列处理函数来处理队列中的请求。从块设备的运行流程,可知请求处理是块设备的基本处理单位,也是最核心的部分。对块设备的读写操作被封装到了每一个请求中。11.5 中断编程中断编程接口(1)实际上,有很多Linux的驱动都是通过中断的方式来进行内核和硬件的交互。中断机制提供了硬件和软件之间异步传递信息的方式。硬件设备在发生某个事件时通过中断通知软件进行处理。中断实现了硬件设备按需获得处理器关注的机制,与查询方式相比可以大大节省CPU资源的开销。申请中断使用request_irq()调用,释放中断使用free_irq()调用。int request_irq(unsigned int irq,void(*handler)(int irq,void*dev_id,struct pt_regs*regs),unsigned long irqflags,const char*devname,oid*dev_id);void free_irq(unsigned int irq,void*dev_id);中断编程接口(2)其中irq是要申请的硬件中断号。在Intel平台,范围是015。参数handler为将要向系统注册的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号、设备id以及寄存器值。设备id就是在调用request_irq()时传递给系统的参数dev_id。参数irqflags是中断处理的一些属性,其中比较重要的有SA_INTERRUPT。这个参数用于标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序只屏蔽正在处理的中断。还有一个SA_SHIRQ属性,设置了以后运行多个设备共享中断,在中断处理程序中根据dev_id区分不同设备产生的中断。参数devname为设备名,会在/dev/interrupts中显示。参数dev_id在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map()找到中断对应的设备。11.6 按键驱动程序实例按键工作原理(1)LED和蜂鸣器是最简单的GPIO的应用,都不需要任何外部输入或控制。按键同样使用GPIO接口,但按键本身需要外部的输入,即在驱动程序中要处理外部中断。按键硬件驱动原理图如图11-7所示。在图11-7的4 X 4矩阵按键(K1K16)电路中,使用4个输入/输出端口(EINT0、EINT2、EINT11和EINT19)和4个输出端口(KSCAN0KSCAN3)。按键工作原理(2)按键电路的主要端口 按键驱动程序原理(1)因为通常中断端口是比较珍贵且有限的资源,所以在本电路设计中,16个按键复用了4个中断线。那怎么样才能及时而准确地对矩阵按键进行扫描呢?某个中断的产生表示,与它所对应的矩阵行的4个按键中,至少有一个按键被按住了。因此可以通过查看产生了哪个中断,来确定在矩阵的哪一行中发生了按键操作(按住或释放)。例如,如果产生了外部2号线中断(EINT2变为低电平),则表示K7、K8、K9和K15中至少有一个按键被按住了。这时候4个EINT端口应该通过GPIO配置寄存器被设置为外部中断端口,而且4个KSCAN端口的输出必须为低电平。按键驱动程序原理(2)在确定按键操作所在行的位置之后,我们还得查看按键操作所在列的位置。此时要使用KSCAN端口组,同时将4个EINT端口配置为通用输入端口(而不是中断端口)。在4个KSCAN端口中,轮流将其中某一个端口的输出置为低电平,其他3个端口的输出置为高电平。这样逐列进行扫描,直到按键所在列的KSCAN端口输出为低电平,此时按键操作所在行的EINT管脚的输入端口的值会变成低电平。例如,在确认产生了外部2号中断之后,进行逐列扫描。若发现在KSCAN1为低电平时(其他端口输出均为高电平),GPF2(EINT2管脚的输入端口)变为低电平,则可以断定按键K8被按住了。实际的按键动作会在短时间(几毫秒至几十毫秒)内产生信号抖动。例如,当按键被按下时,其动作就像弹簧的若干次往复运动,将产生几个脉冲信号。一次按键操作将会产生若干次按键中断,从而会产生抖动现象。因此驱动程序中必须要解决去除抖动所产生的毛刺信号的问题。按键驱动程序阅读并运行示例11-6。$make clean;make/*驱动程序的编译*/$insmod butt_dev.ko/*加载buttons设备驱动*/$cat/proc/devices /*通过这个命令可以查到buttons设备的主设备号*/$mknod/dev/buttons c 252 0 /*假设主设备号为252,创建设备文件节点*/$make clean;make/*驱动程序的编译*/$insmod gpio_drv.ko/*加载gpio驱动*/$cat/proc/devices/*通过这个命令可以查到gpio设备的主设备号*/$mknod/dev/gpio c 251 0 /*假设主设备号为251,创建设备文件节点*/然后编译并运行驱动测试程序。$arm-linux-gcc o butt_test butt_test.c$./butt_test测试程序中,首先打开按键设备文件和gpio设备(包括4个LED和蜂鸣器)文件,接下来,根据按键的输入值(按键ID)的二进制形式,LED D9D12发亮(例如,按下11号按键,则D9、D10和D12会发亮),而蜂鸣器当每次按键时发出声响。11.7 实验内容test驱动 实验目的和实验内容1实验目的该实验是编写最简单的字符驱动程序,这里的设备也就是一段内存,实现简单的读写功能,并列出常用格式的Makefile以及驱动的加载和卸载脚本。读者可以体会到字符设备驱动的整个编写流程。2实验内容该实验要求实现对虚拟设备(一段内存)的打开、关闭、读写的操作,并要通过编写测试程序来测试虚拟设备及其驱动运行是否正常。实验运行结果$make clean;make$./test_drv_load$gcc o test test.c$./testInput some words to kernel(enter quit to exit):Hello,everybody!The read string is from kernel:Hello,everybody!/*从内核读取的数据*/Input some words to kernel(enter quit to exit):This is a simple driverThe read string is from kernel:This is a simple driverInput some words to kernel(enter quit to exit):quitThe read string is from kernel:quit$./test_drv_unload$dmesg|tail n 10The major of the test device is 250/*当加载模块时打印*/This is open operation/*当打开设备时打印*/This is release operation/*关闭设备时打印*/Test device uninstalled/*当卸载设备时打印*/思考与练习 根据书上的提示,将本章中所述的按键驱动程序进行进一步的改进,并在开发板上进行测试。