摘要:本文介绍了怎样在嵌入在开发彩色LCD显示驱动的方式,并对Linux中的显示驱动程序结构和框架作一介绍。
关键字:ARM,帧缓冲(Framebuffer),MC928MXMX1。
常年以来,在常见的掌上笔记本(PDA)等大型手持式设备上,因为硬件条件等的限制,我们看见的显示元件一般是单色LCD,用户界面也十分简单,几乎看不到PC机上美观整齐的图形界面(GUI)支持。因为初期嵌入式处理器的速率有限,在处理图形和多媒体数据方面也变得力不从心。
随着高性能嵌入式处理器的普及和硬件成本的不断增加,尤其是Arm系列处理器的推出,嵌入式系统的功能也越来越强。在多媒体应用的带动下,彩色LCD也越来越多地应用到了嵌入式系统中,如新一代掌上笔记本(PDA)多采用TFT显示元件,支持彩色图形界面,图片显示和视频媒体播放。掌上笔记本(PDA)的操作系统有谷歌WindowCE,PalmOS等。而Linux做为开放源代码的操作系统也在市场中抢占了一席之地。因为Linux成本低廉,任何人都可以得到其源代码并在其基础上进行开发,成为各家厂商竭力发展的操作系统,加上其核心小,潜力可观。
在应用需求的带动下,Linux下也出现了许多图形界面软件包,如MiniGUI、Trolletech公司的EmbeddedQT等,其图形界面及开发工具与WindowsCE不相上下。在图形软件包的开发和移植工作中都牵涉到底层LCD的驱动问题。笔者参与了一个基于ARM9的PDA系统的开发,用的是诺基亚公司龙珠系列的MC928MXMX1。软件采用Linux2.4.18平台,编译器为gcc的ARM交叉编译器。
一.硬件平台
MC928MX1(以下简称MX1)是诺基亚公司基于ARM核心的第一款MCU,主要面向低端嵌入式应用。内部采用ARM920920T内核,并集成了SDRAM/Flash,LCD,USB,蓝牙(bluetooth),多媒体闪存卡(MMC),CMOS摄像头等控制器。
LCD控制器的功能是形成显示驱动讯号,驱动LCD显示器。用户只须要通过读写一系列的寄存器,完成配制和显示控制。MX1中的LCD控制器可支持单色/彩色LCD显示器。支持彩色TFT时,可提供4/8/12/16位颜色模式,其中16位颜色模式下可以显示65536种颜色。配置LCD控制器重要的一步是指定显示缓冲区,显示的内容就是从缓冲区中读出的,其大小由屏幕帧率和显示颜色数决定。在本例中,笔者采用的是夏普LQ035035QQ22DDDD54TFT显示模块,在240x320码率下可提供16位彩色显示。
二.Linux下的设备驱动
Linux将设备分为最基本的两大类,字符设备和块设备。字符设备是以单个字节为单位进行次序读写操作,一般不使用缓冲技术,如键盘等,驱动程序实现比较简单;而块设备则是以固定大小的数据块进行储存和读写的,如硬碟,软驱等。为提升效率,系统对于块设备的读写提供了缓存机制,由于涉及缓冲区管理,调度,同步等问题,实现上去比字符设备复杂的多。
Linux的设备管理是和文件系统揭秘结合的,各类设备名称都以文件的方式储存在/dev目录下,称为设备文件。应用程序可以打开,关掉linux lcd驱动模块,读写这种设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这种设备,系统为设备编了号,每位设备号又分为主设备号和次设备号。主设备号拿来分辨不同种类的设备,而次设备号拿来分辨同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬碟主设备号是3。在Linux的/dev/目录下使用ls-l命令可察看个设备文件的设备号。诸如,/dev/hda为块设备,主设备号3,次设备号0,是系统的第一块硬碟。/dev/hd1主设备号3,次设备号1,为系统的第二块硬碟。我们即将介绍的显示设备也是一个设备文
件/dev/fb,主设备号29。在编撰设备驱动程序的时侯,也要指明所操作设备的主设备号和次设备号。
Linux的特征之一,是为所有的文件,包括设备文件,提供了统一的操作函数插口,定义如下:
structfile_operations{
structmodule*owner;
loff_t(*llseek)(structfile*,loff_t,int);
ssize_t(*read)(structfile*,char*,size_t,loff_t*);
ssize_t(*write)(structfile*,constchar*,size_t,loff_t*);
int(*readdir)(structfile*,void*,filldir_t);
unsignedint(*poll)(structfile*,structpoll_table_struct*);
int(*ioctl)(structinode*,structfile*,unsignedint,unsignedlong);
int(*mmap)(structfile*,structvm_area_struct*);
int(*open)(structinode*,structfile*);
int(*flush)(structfile*);
int(*release)(structinode*,structfile*);
int(*fsync)(structfile*,structdentry*,intdatasync);
int(*fasync)(int,structfile*,int);
int(*lock)(structfile*,int,structfile_lock*);
ssize_t(*readv)(structfile*,conststructiovec*,unsignedlong,loff_t*);
ssize_t(*writev)(structfile*,conststructiovec*,unsignedlong,loff_t*);
ssize_t(*sendpage)(structfile*,structpage*,int,size_t,loff_t*,int);
unsignedlong(*get_unmapped_area)(structfile*,unsignedlong,unsignedlong,unsignedlong,unsignedlong);
};
结构体中的成员为一系列的插口函数,如用于读/写的read/write函数,用于控制的ioctl等。打开一个文件就是调用这个文件file_operations中的open操作。不同类型的文件有不同的file_operations成员函数。如普通的c盘数据文件,插口函数完成c盘数据块读写操作;而对于各类设备文件,则最终调用各自驱动程序中的I/O函数进行具体设备的操作。这样,应用程序根本不用考虑操作的是设备还是普通文件,可一律当成文件处理,具有特别清晰统一的I/O插口。所以file_operations是文件层次的I/O插口。
然而,因为外设的种类繁杂,操作方法也各不相同。如声音设备驱动要使用DMA通道,显示设备驱动要提供对内存的操作,硬碟驱动要处理复杂的缓冲区结构,网路设备驱动和socket联系紧密。假如file_operations中的函数都让驱动程序的开发人员来写,则就要处理大量的细节,几乎是不可能的。为了解决设备多样性的问题,Linux采用了特殊情况特殊处理的办法linux lcd驱动模块,为不同设备定义好了文件层次file_operations结构中的插口函数,其中处理了大多数设备相关的操作,如各类缓冲区的申请和释放等等,而具体操作底层硬件的一小部份则留给开发人员。所以Linux另外提供一个文件层到底层驱动程序的插口,一般为一个结构体linux重启命令,其中包含成员变量和函数表针。不同的设备驱动有不同的结构体。这样,一方面保证了文件层I/O插口file_operations的一致性,另一方面驱动程序的开发人员也不用了解太多细节,只著作于硬件相关的I/O操作就可以了。比如,一个有代表性的特殊设备是声音设备,其文件层的file_operations定义如下:
structfile_operationsoss_sound_fops={
owner:THIS_MODULE,
llseek:sound_lseek,
read:sound_read,
write:sound_write,
poll:sound_poll,
ioctl:sound_ioctl,
mmap:sound_mmap,
open:sound_open,
release:sound_release,
};
其中的sound_read,sound_write等函数Linux都已提供,处理了与声音设备相关的许多细节,如DMA的申请,释放和操作等。而文件层到驱动程序的插口为audio_driver结构,其中包含底层操作函数。文件层的sound_read,sound_write会在须要时调用audio_driver中的函数。开发人员只要编撰audio_driver中的函数就可以了,最大程度地减少了工作量。下边我们将听到,Linux为显示设备提供的帧缓冲驱动也是这些“文件层-驱动层”的插口形式。
三.Linux的帧缓冲设备
帧缓冲(framebuffer)是Linux为显示设备提供的一个插口,把内存具象后的一种设备,他准许下层应用程序在图形模式下直接对显示缓冲区进行读写操作。这些操作是具象的,统一的。用户毋须关心化学内存的位置、换页机制等等具体细节。这种都是由Framebuffer设备驱动来完成的。帧缓冲驱动的应用广泛linux ftp,在linux的桌面系统中,Xwindow服务器就是借助帧缓冲进行窗口的勾画。尤其是通过帧缓冲可显示汉字点阵,成为Linux汉化的惟一可行方案。
帧缓冲设备对应的设备文件为/dev/fb*,假如系统有多个显示卡,Linux下还可支持多个帧缓冲设备,最多可达32个,分别为/dev/fb0到/dev/fb31,而/dev/fb则为当前缺省的帧缓冲设备,一般指向/dev/fb0。其实在嵌入式系统中支持一个显示设备就够了。帧缓冲设备为标准字符设备,主设备号为29,次设备号则从0到31。分别对应/dev/fb0-/dev/fb31。通过/dev/fb,应用程序的操作主要有这几种:
1.读/写(read/write)/dev/fb:相当于读/写屏幕缓冲区。诸如用cp/dev/fb0tmp命令可将当前屏幕的内容拷贝到一个文件中,而命令cptmp>/dev/fb0则将图形文件tmp显示在屏幕上。
2.映射(map)操作:因为Linux工作在保护模式,每位应用程序都有自己的虚拟地址空间,在应用程序中是不能直接访问数学缓冲区地址的。因此,Linux在文件操作file_operations结构中提供了mmap函数,可将文件的内容映射到用户空间。对于帧缓冲设备,则可通过映射操作,可将屏幕缓冲区的化学地址映射到用户空间的一段虚拟地址中,然后用户就可以通过读写这段虚拟地址访问屏幕缓冲区,在屏幕上绘图了。实际上,使用帧缓冲设备的应用程序都是通过映射操作来显示图形的。因为映射操作都是由内核来完成,下边我们将见到,帧缓冲驱动留给开发人员的工作并不多。
3.I/O控制:对于帧缓冲设备,对设备文件的ioctl操作可读取/设置显示设备及屏幕的参数,如码率,显示颜色数,屏幕大小等等。ioctl的操作是由底层的驱动程序来完成的。
在应用程序中,操作/dev/fb的通常步骤如下:
1.打开/dev/fb设备文件。
2.用ioctrl操作取得当前显示屏幕的参数,如屏幕码率,每位象素点的比特数。依据屏幕参数可估算屏幕缓冲区的大小。
3.将屏幕缓冲区映射到用户空间。
4.映射后就可以直接读写屏幕缓冲区,进行绘图和图片显示了。
典型程序段如下:
#include
intmain()
intfbfd=0;
structfb_var_screeninfovinfo;
structfb_fix_screeninfofinfo;
longintscreensize=0;
/*打开设备文件*/
fbfd=open("/dev/fb0",O_RDWR);
/*取得屏幕相关参数*/
ioctl(fbfd,FBIOGET_FSCREENINFO,&finfo);
ioctl(fbfd,FBIOGET_VSCREENINFO,&vinfo);
/*估算屏幕缓冲区大小*/
screensize=vinfo.xres*vinfo.yres*vinfo.bits_per_pixel/8;
/*映射屏幕缓冲区到用户地址空间*/
fbp=(char*)mmap(0,screensize,PROT_READ|PROT_WRITE,MAP_SHARED,fbfd,0);
/*下边可通过fbp表针读写缓冲区*/
……
四.帧缓冲驱动的编撰
帧缓冲设备属于字符设备,与声音设备一样,也采用“文件层-驱动层”的插口形式。在文件层次上,Linux为其定义了
staticstructfile_operationsfb_fops={
owner:THIS_MODULE,
read:fb_read,/*读操作*/
write:fb_write,/*写操作*/
ioctl:fb_ioctl,/*控制操作*/
mmap:fb_mmap,/*映射操作*/
open:fb_open,/*打开操作*/
release:fb_release,/*关掉操作*/
};
其中的成员函数都在文件linux/driver/video/fbmem.c中定义。
因为显示设备的特殊性,在驱动层的插口中不但要包含底层函数,还要有一些纪录设备状态的数据。Linux为帧缓冲设备定义的驱动层插口为structfb_info结构,在include/linux/fb.h中定义。这个结构比较长,限于篇幅,文章中就不全部列举了。辛运的是,嵌入式系统要求的显示操作比较简单,只涉及到结构中少数几个成员,下边只对编撰驱动中要用到的几个关键成员作一说明。
fb_info中纪录了帧缓冲设备的全部信息,包括设备的设置参数,状态以及操作函数表针。每一个帧缓冲设备都必须对应一个fb_info结构。其中成员变量Modename为设备名称,fontname为显示字体,fbops为指向底层操作的函数的表针,这种函数是须要驱动程序开发人员编撰的。成员fb_var_screeninfo和fb_fix_screeninfo也是结构体。其中fb_var_screeninfo记录用户可更改的显示控制器参数,包括屏幕帧率和每位象素点的比特数。fb_var_screeninfo中的xres定义屏幕一行有多少个点,yres定义屏幕一列有多少个点,
bits_per_pixel定义每位点用多少个字节表示。而fb_fix_screeninfo中记录用户不能更改的显示控制器的参数,如屏幕缓冲区的化学地址,宽度。当对帧缓冲设备进行映射操作的时侯,就是从fb_fix_screeninfo中取得缓冲区化学地址的。里面所说的数据成员都是须要在驱动程序中设置的。
在了解了前面所述的概念后,编撰帧缓冲驱动的实际工作并不复杂,须要做的工作是:
1.编撰初始化函数:初始化函数首先初始化LCD控制器,设置显示模式和显示颜色数,之后分配LCD显示缓冲区。在Linux可通过kmalloc函数分配一片连续的空间。笔者采用的LCD显示方法为240x320,16位彩色。须要分配的显示缓冲区为240x320x2=150k字节,缓冲区一般分配在片外SDRAM中,起始地址保存在LCD控制器寄存器中。最后是初始化一个fb_info结构,填充其中的成员变量,并调用register_framebuffer(&fb_info)将fb_info登记入内核。
2.编撰结构fb_info中函数表针fb_ops对应的成员函数:对于嵌入式系统的简单实现,只须要下述三个函数就可以了:
structfb_ops{
……..
int(*fb_get_fix)(structfb_fix_screeninfo*fix,intcon,structfb_info*info);
int(*fb_get_var)(structfb_var_screeninfo*var,intcon,structfb_info*info);
int(*fb_set_var)(structfb_var_screeninfo*var,intcon,structfb_info*info);
…….
};
structfb_ops在include/linux/fb.h中定义。这种函数都是拿来设置/获取fb_info结构中的成员变量的。当应用程序对设备文件进行Ioctl操作时侯会调用它们,读者可参考前文中的应用程序反例。诸如,对于fb_get_fix(),应用程序传入的是fb_fix_screeninfo结构,在函数中对其成员变量形参,主要是smem_start(缓冲区起始地址)和smem_len(缓冲区宽度),最终返回给应用程序。而fb_set_var()函数的传入参数是fb_var_screeninfo,函数中须要对xres,yres,和bits_per_pixel形参。
驱动程序编撰完成后,开发者可选择将其编译为动态加载模块,或静态地编译入内核中。因为篇幅所限,有关这方面的内容请读者参考相关驱动程序文档。
五.结束语
因为篇幅所限,本文中仅对帧缓冲设备驱动的基本原理和框架做了简单介绍。辛运的是,在Linux的发布版本中,包含了大量的设备驱动程序源代码,其中drvers/video下提供了多种显示卡的帧缓冲设备驱动程序程序,用户自己的驱动程序可参考成熟的代码编撰或直接更改得到。
参考资料
《MC928MXMX1UserManual》
《Linuxframebufferdriverhowto》
作者简介
许庆丰Libo小组发起人。历任职于诺基亚半导体部,主要研究方向是嵌入式操作系统和应用。
文章引用自:
分类: