的CRAMFS,而在根文件系统下我们还可以挂载一个可读写的YAFFS2文件系统。这样既可以提高存储设备的利用率,又可以保证数据的正常读取。YAFFS2(Yet Another nandFlash FileSytem2)是专门针对NandFlash设备的一种文件系统。虽然YAFFS2类似于JFFS/JFFS2文件系统,但由于采用了增强的碎片整理机制和均衡磨损技术,不但它的读写速度较JFFS/JFFS2有很大的提高,其使用寿命和稳定性可靠性也有很大的提升。这使得该文件系统可以在大容量芯片中得到推广使用。所以说YAFFS文件系统具有很大的优势和发展潜力。3.4.1建立根文件系统Linux内核启动以后,会参数linux_cmd_line的引导而跳转到根文件系统的入口地址,来完成文件系统的初始化和挂载任务。根文件的制作方法分为两种:一是自己手动建立;而是使用一些开发套件来实现根文件系统的快速建立。手动建立在调试过程中会比较麻烦,本平台使用一个开发套件来帮助完成基本文件系统的建立。Busybox就是这样的一种开发套件,其本身是由Bruce Perens发起的一项计划,本来是用来协助Debian在发行套件中用来建立安装磁盘。但由于Busybox不仅小巧而且有很强大的功能,导致了其在嵌入式系统中应用广泛。具体的方法是:首先下载Busybox常用的可靠版本(www.busybox.net),解压后类似于内核的配置过程,直接到相应的目录中执行make menuconfig命令,从而可以进入Busybox配置界面(这个界面类似于内核配置图)。在Applets下面选择适合平台的命令,然后退出保存。其次是执行make,make install。这时就会在Busybox的目录下生成_install目录,这个目录就已经包含了配置好的命令工具包。本平台中只要把这个目录拷贝到工作目录,并且更名为rootfs。这样我们就在该目录的基础上建立了根文件系统。在rootfs/下,首先在/rootfs/dev下建立必须的设备文件。关于设备的建立,可以参考Filesystem Hierarchy Standard来确定需要的哪些设备文件,下一步就是完成/rootfs/etc这个目录下的3个比较重要的文件inittab,fstab,init.d/rcS(或者是rc.d/rc.sysinit,这个由inittab决定)。最后,加入需要的链接库文件就可以了。但是这部分的实现是比较繁琐的,因为走到这一步时,我们对于应用中到底需要哪些具体的库文件并不是很清楚,这
基于ARM9的嵌入式Linux系统研究及设备驱动程序的开发28就导致了我们制作的文件系统的大小并不是太好控制的。而通常情况下这里有两种经验性的处理方法:一是采用readelf这个工具包来找出链接库;二是参考linux相关网站上所提供的文件系统,来进一步确定我们平台中所需要的链接库文件。3.4.2制作YAFFS2文件系统根文件系统做好后,可以通过NFS(http://nfs.sourceforge.net/nfs-howto/)来测试文件系统是否可行,最后我们要把这个文件系统烧进目标板。可以有两种方式来应用YAFFS2文件系统,一是CRAMFS+YAFFS2的组合,一种是纯YAFFS2文件系统格式。在本平台中内核和根文件系统的应用十分稳定,所以从节省空间、降低成本、保护系统的角度来看,选用CRAMFS这种高压缩率、只读文件系统,具有很大的现实意义[45]。对于CRAMFS只读文件系统,我们采用mkcramfs/rootfs~/root.img这几个命令来生成cramfs镜像,而/rootfs本身就是之前建立好的目录,~/root.img作为生成镜像的指定保存目录来用。由于YAFFS2文件系统就是根据NandFlash设备来设计的,而在本文的应用中文件系统恰好设置成从NandFlash中启动的,这就需要我们在内核中加入支持NandFlash启动的设备驱动代码,并在arch\\arm\\mach-s3c2410\\devs.c文件中添加有关NandFlash分区的代码。在本平台中将NandFlash分为bootloader、kernel、root三个彼此相连的分区。下面是具体的执行过程:在相应的网站中下载YAFFS2(GPL)源码,首先解压后在YAFFS2目录下执行:patch-ker.sh~/Linux2.6.29.5命令,这样就可以在相应的目录中产生YAFFS2目录。而下一步就是在该目录下执行make menuconfig命令来在内核的配置相应的驱动程序,等到这些都顺利完成之后保存退出设置。其次执行make命令,Make结束后就生成了一个有挂载yaffs2文件系统能力的内核镜像。而在Uboot源文件中,将NandFlash分成几个分区,此时这里的
分区必须和前面所提到的devs.c文件里的分区保持一致,因为内核和文件系统的地址均在前面都到确认,所以最后启动时也会到相应的地址去寻找指令。在本平台中,我们将Flash分成Uboot、kernel、root、user四个分区。其中CRAMFS文件系统挂载于根目录下,而YAFFS2文件系统挂载于用户目录下。在YAFFS2源文件的utils目录下,执行make命令就可以生成mkyaffs2image镜像工具,而执行./mkyaffs2image则可以生成yaffs2文件系统。当然也可以将YAFFS2作为动态模块来编译到内核中,因为根文件系统是CRAMFS格式,如此这样可以使得文件系统更加灵活。文件系统最后的目的就是在内核启动完成后进行加载,而文件系统不止一种挂载方式,具体的说有两种方式:手动和自动挂载。文件的挂载显然是要在无人的情况下自动的进行,这就需要对/etc/init.d/rcS的脚本进行编辑。在这个里面可以
兰州理工硕士论文29对时间进行设置、配置交换分区大小、采集主机名等等。等到脚本都设置完成之后,就可以将文件系统烧写到相应分区中去,到此处我们就成功的添加了对于文件系统的支持。图3.10文件系统配置图3.5本章小结本章重点介绍了Uboot和Linux内核的移植过程,并针对本平台的储存方式,加入了相应的支持代码。并且制作了相应的文件系统,为下一步的驱动的开发设计提供了可靠,稳定的软件平台。
基于ARM9的嵌入式Linux系统研究及设备驱动程序的开发30第4章设备驱动设备驱动程序是Linux内核的重要组成部分,控制了操作系统和硬件设备之间的交互。它通过向用户提供针对系统硬件的标准化接口,使得用户在程序设计的过程中不需要特别关注设备的工作细节。在Linux系统的运行状态中,分为三层结构,顶层的是用户层的应用程序,中层是内核的运行,而底层则是设备驱动层,内核和驱动紧密相连,任何用户层的应用程序,想要控制对应的硬件设备,都必须通过驱动层才能实现。4.1设备驱动简介Linux系统中设备一般可以分为三种类型:字符设备、块设备和网络设备。文中所涉及的LCD驱动属于字符型设备驱动程序。在对字符设备发出读、写请求时,实际的硬件I/O一般就紧接着发生了。Linux系统访问设备就像访问文件一样,例如打开关闭设备系统调用open()和close()函数。设备驱动的工作流程是这样的:在加载时首先需要调用入口函数init_module(),这主要是将设备的寄存器和控制器进行初始化设置。而这个入口函数中最典型的子函数就是register_chrdev()和register_blkdev(),前者完成向内核注册字符设备的任务,后者实现了块设备在内核中的注册过程。当这些设备在内核中注册成功后,系统会向这个成功注册的设备分配主设备号和自定义的次设备号,同时还要和文件系统建立起一定的联系。字符设备驱动在向Linux内核注册的过程中,首先其对应的向量表chrdevs会添加device_struct这个数据结构,同时在注册成功时,系统分配的设备标识符会作为这个向量表的指针。对于device_struct这个针对设备的数据结构,其一般由两个部分组成:一个是指向该设备名称的字符型指针,另一个是对于这个设备进行文件操作的一组返回值为布尔型的指针。而对于这些设备的文件操作其实就存在于设备驱动程序中,当需要打开、读写这些设备的时候,都通过上面的文件操作来实现的。而注册登记的过程,实际上就是将模块所能识别的文件操作调用函数和设备驱动中的设备结构体相关联起来,形成类似于管道的功能。一旦这种关系建立起来,内核可以通过模块与设备之间的管道关系,实现对设备的操作。但是这个时候内核和应用程序并没有直接的接口函数,应用程序只能通过在文件系统中创建节点的方式,来和设备文件结构体形成关联。在Linux中系统中通过mknod()创建针对这个设备的文件节点,这样应用程序就可以通过文件系统来直接操作外部设备了。这也实现了Linux对设备的读写就如同对文件的读写。当设备驱动卸载的时候,这个过程和加载的时候刚好相反,
兰州理工硕士论文31首先应用程序通过文件系统首先删除掉对应的节点,之后内核会根据
设备号来释放其占用的硬件资源,来实现对具体设备的卸载过程。下图是设备驱动的结构图。应用程序设备文件驱动模块硬件内核Open,read,writeOpen,read,write以设备号建立节点返回主设备号Insmod()注册图4.1驱动原理图4.2 LCD设备驱动设计4.2.1帧缓冲设备结构帧缓冲(framebuffer)是Linux为显示设备提供的一个接口,把显存抽象成一种设备,针对图形化界面下的应用程序显示问题,提供了一种可以直接读写显示缓存区的方案。这种设置对于用户层是透明的,即用户无法知晓具体的显存大小、存储机制等等。用户只要通过对该接口的调用,可以直接完成诸如画图和图形框的显示。该方案也是Linux中显示汉字的唯一解决方案,除此之外这是PC机中解决窗口显示的主要方式。帧缓冲设备对应的设备驱动文件为/dev/fb,当系统有不止有一个显卡时,Linux也支持多达32个帧缓冲设备的显示,其文件编号分别从/dev/fb0到/dev/fb31,而一般情况下指向/dev/fb0的帧缓冲设备通常是/dev/fb,即为当前默认的帧缓冲设备。在本平台的系统中,只有一个帧缓冲设备,所以对于缓存的显示设置是足够的。帧缓冲设备为典型的字符设备:其主设备号为29,从设备号从0到31总共有32个,这分别对应/dev/fb0-/dev/fb31。通过默认的帧缓冲设备,应用程序的操作方式有:
基于ARM9的嵌入式Linux系统研究及设备驱动程序的开发32(1)对/dev/fb的读写:相当于对缓冲区的读写操作。比如使用命令cp/dev/fb0 tmp,其可以将此时的屏幕的内容复制到一个具体的文件中,而cp tmp>/dev/fb0这个命令则将缓冲区的图形或者数据投影到屏幕上。(2)映射操作:在现代计算机技术中,出于对数据和代码的保护,通常情况并不支持应用程序对内存的具体内容的进行直接的读写操作,这种模式称为保护模式。每个应用程序都有相对独立的虚拟地址空间。Linux作为先进的操作系统也不例外。所以为了将文件的内容映射到用户空间,Linux针对文件操作中提供了一个称为mmap的函数。对于帧缓冲设备,同样可以通过映射操作,将显示缓冲区的物理地址映射到用户空间的一段虚拟地址中。映射完成后用户就可以通过这段虚拟地址,对屏幕缓冲区进行数据的读写操作或者在屏幕上绘制图形。事实上应用程序的图形显示功能都是通过映射操作来实现的。而映射操作运行于系统的内核态,所以我们要做的工作一般不多。(3)I/O控制:帧缓冲设备中对于输入输出部分的控制,如屏幕的像素大小、分辨率、色彩深度等等主要是通过底层的驱动来完成的。在应用程序中,操作/dev/fb的一般步骤如下:(1)打开/dev/fb设备文件。(2)通过调用函数ioctrl来获取当前屏幕的参数,如屏幕大小,每个像素点RGB位数。由此可以计算出相应的显示缓冲区的大小。(3)将显示缓冲区的内容映射到用户空间中去。(4)映射后完成后就可以通过读写显示缓冲区,来实现图形显示和图形的绘制。典型程序段如下:#include
兰州理工硕士论文33/*将显示缓冲区映射到用户地址空间*/fbp=(char*)mmap(0,screensize,PROT_READ|PROT_WRITE,MAP_SHARED,fbfd,0);4.2.2帧缓冲驱动的编写帧缓冲设备是字符设备的一种,采用“文件层-驱动层”的关联方式。文件层中,Linux对它的定义为:static struct file_operations fb_fops={owner:THIS_MODULE,read:fb_read,write:fb_write,ioctl:fb_ioctl,mmap:fb_mmap,open:fb_open,release:fb_release,};这些函数在Linux的目录(linux/driver/video/fbmem.c)中有对应的定义。显示设备不仅接收来自于用户层的数据和图形,而且还要对内核态的控制命令作出相应,这就使得显示设备的驱动设计很特别,其不仅要通过底层的函数来实现屏幕的显示和图形的绘制,还要和内核态交换诸如设备记录信息的通信。这个信息交换函数,主要是指
struct fb_info结构体。在本平台的系统中,对于显示的要求不是很复杂,只用到了该数据结构中的少数几个成员,以下是对嵌入式系统中常用的成员做个简介,这有利于我们采取有针对性的开发。fb_info这个结构体纪录了包括帧缓冲设备的参数、状态和相关操作的全部信息。不同的帧缓冲设备对应不同的fb_info结构,这是嵌入式系统中关于显示部分需要开发的地方。Fb_info的主要变量有Modename、fontname、fbops这三个,前者代表设备名称,中间是显示字体,后者是一个指向具体的底层操作的指针。除了上述的结构体需要驱动开发人员根据相应的硬件特点有针对性的进行编写之外,还包括以下两个结构体:fb_var_screeninfo和fb_fix_screeninfo。其中fb_var_screeninfo记录用户可更改的显示控制器参数,包括像素数量和每个像素点的大小;fb_var_screeninfo中的xres用于表示一行中的像素点数量,而yres定义屏幕一列的像素点个数,bits_per_pixel则表示一个像素点所占用的字节数[46]。而fb_fix_screeninfo与前面的结构体fb_var_screeninfo相反,只记录用户不可更改的参数,如显示缓冲区的物理地址。一旦用户程序要进行映射操作时,就可以从fb_fix_screeninfo中读取到对应的物理地址。在掌握了上述的原理之后,编写帧缓冲驱动的工作是比较简单的,具体要做
基于ARM9的嵌入式Linux系统研究及设备驱动程序的开发34的就是:1.编写初始化函数:首先初始化LCD控制器,这其中主要是针对显示模式和颜色进行设置,然后在SDRAM中为LCD分配缓冲区。这主要通过kmalloc函数来实现对一片连续的空间的分配。本文采用的LCD像素大小为240x320,有24位色彩,需要分配的缓冲区大小为240x320x3=230KB,缓冲区的起始地址保存在LCD控制寄存器中。最后对fb_info结构进行设置,包括填写成员变量,调用注册函数register_framebuffer(&fb_info)将fb_info注册进入内核。2.编写fb_info中指向缓冲区操作的指针fb_ops:对于本平台的要求,只需要写好以下几个函数就行:struct fb_ops{……..int(*fb_get_fix)(struct fb_fix_screeninfo*fix,int con,struct fb_info*info);int(*fb_get_var)(struct fb_var_screeninfo*var,int con,struct fb_info*info);int(*fb_set_var)(struct fb_var_screeninfo*var,int con,struct fb_info*info);…….};struct fb_ops用来设置/获取fb_info结构中的成员变量的。当应用程序对设备文件进行I/O操作时候会调用它们。例如对于fb_get_fix(),应用程序传入的是fb_fix_screeninfo结构,在函数中对其成员变量赋值,主要是smem_start(缓冲区起始地址)和smem_len(缓冲区长度),最终返回给应用程序。而fb_set_var()函数的传入参数是fb_var_screeninfo,函数中需要对xres,yres,和bits_per_pixel赋值。4.3 USB设备驱动设计4.3.1 USB的具体构成USB驱动程序存在于不同的内核子系统和USB硬件控制器之间,USB核心为USB驱动程序提供了一个用于访问和控制USB硬件的接口,而不必考虑系统当前存在的各种不同类型的USB硬件控制器。USB是一个非常复杂的设备,内核为我们提供了一个称为USB的核心的子系统来处理大部分的复杂性。USB设备有以下三个部分组成:配置、接口和端点。如下图所示:
兰州理工硕士论文35USB HostUSB Device DriverUSB CoreUSBHCD(EHCI/OHCI/UHCI)HOST ControllerUSB Device DriverUSB CoreUSBHCD(EHCI/OHCI/UHCI)Device ControllerHardwareUSB Device图4.2 USB结构图利用端点通信是USB通信的特点,端点又分为四种方式:中断、批量、等时、控制。而USB端点的数据传输采用半双工的方式,即在一个确定的时间点上,数据要么是从主机到设备,要么就是设备到主机,这种工作方式类似于单向管道。根据上面所说的可以得出这样的结论:USB设备的设置非常灵活,且有不止一种配置方式,而USB的接口也具有以上类似的多样性功能。而驱动程序的工作方式是首先将USB对象注册到USB子系统中,之后利用制造商
和设备标识来判断硬件是否已经成功安装,之后就是利用USB的串行通道和控制器来实现数据的传输。上面我们简要说明了驱动程序的基本理论,在写一个设备驱动程序之前,我们还要了解以下两个概念:模块和设备文件[47]。模块:我们知道一个程序从代码写作到最后的运行,首先是在代码写作完成后用编译器进行编译以生成目标对象,之后通过链接器将目标对象链接起来,最后是生成可执行的程序。而模块其实就是目标对象文件,因为没有链接所以不能独立运行,但是模块可以被装载进内核而运行,从而可以增强内核的功能。Linux下对于硬件的驱动有两种方式:一是直接加载到内核代码中,随内核的启动而运行相应的设备;二是以模块方式,编译生成一个目标对象文件,在应用程序需要的时候再加载到内核空间运行,所以通常对于驱动就是指驱动模块。设备文件:在文件系统中设备是文件系统创建的一个节点,这个节点在逻辑上以文件的形式存在。但这又不同于常见的文件,设备文件的特点是其与硬件息息相关,内核为硬件指定了主从设备号和其他硬件信息,这些信息都是以节点的形式存在于文件系统中。其中主设备号用于区别不同的硬件设备,且对应着确定的驱动程序,而从设备号主要用于区分同一类设备的不同属性或者使用方式。主设备号一般存在于/proc/devices文件中,而设备节点存在于相应的文件系统中。一
基于ARM9的嵌入式Linux系统研究及设备驱动程序的开发36旦我们要使用某个具体的外部设备,首先是调用驱动的对象目标模块,这个模块可以是已经运行于内核态,也可以是调用的时候动态加载进入内核的。等驱动模块加载成功后,其会由内核分配到一个主设备号和从设备号。主设备号如上文所说决定了驱动的类型,可以使字符设备、也可以是块设备、还可以是网络设备。之后通过设备号将设备和驱动关联起来,此时系统就知道了这个设备所对应的驱动程序。等这些完成后文件系统会根据设备号,创建一个设备文件节点。这样就可以通过对文件的操作来实现对设备的诸如:打开、读写、关闭等操作。4.3.2 USB驱动程序结构分析写一个USB的驱动程序最基本的要做四件事:驱动程序要支持的设备、注册USB驱动程序、探测和断开、提交和控制urb。驱动程序支持的设备:一般使用struct usb_device_id这个结构体来控制单一的USB驱动程序。在本平台中,我们根据USB的特征,对程序中的定义进行一定的修改。注册USB驱动程序:structusb_driver是USB驱动设计中的重要数据结构,包含了很多函数和变量。struct usb_driver结构体要初始设置的有5个字段,其框架如下所示:static struct usb_driver skel_driver={.owner=THIS_MODULE,.name=\sconnect,.id_table=skel_table,};这部分的流程图如下所示:
兰州理工硕士论文37开始初始化设备,如产品ID向系统注册USB设备注册成功?向系统提供设备驱动结构体信息成功提供?结束YNY N图4.3驱动流程图struct module*owner:这是一个指向该驱动模块的指针。USB通过对它来对驱动计数,保证它能正常的运行或者卸载。const char*name:这是一个指向常量字符的指针,其指向驱动的名称。这个指针所指向的常量字符在整个驱动中是独一无二的。一般将它与驱动模块设为同一名称。int(*probe)(struct usb_interface*intf,const structusb_device_id*id):这是指向驱动探测函数的指针。只要当USB接口有空闲,并且驱动接口有请求的时候,这个函数就要被调用。void(disconnect)(struct usb_interface*intf):指向驱动断开函数的指针。如果系统要卸载某个USB接口或者驱动时,就调用这个函数。const struct usb_device_id*id_table:指向设备ID表的指针。这个表中存放了可以被驱动识别和支持的USB设备。只有设置好这个变量,USB的所有函数才能被识别和相应。usb_register_driver函数是能把structusb_driver注册到USB核心的注册函数。当USB驱动模块初始化之后,上述的工作就已经完成了。static int__init usb_skel_init(void){int result;/*驱动注册到USB子系统中*/result=usb_register(&skel_driver);