长期以来,在常见的掌上电脑(PDA)等小型手持式设备上,由于硬件条件等的限制,我们看到的显示器件通常是单色LCD,用户界面也非常简单,几乎看不到PC机上美观整齐的图形界面(GUI)支持。由于早期嵌入式处理器的速度有限,在处理图形和多媒体数据方面也显得力不从心。
随着高性能嵌入式处理器的普及和硬件成本的不断降低,尤其是Arm系列处理器的推出,嵌入式系统的功能也越来越强。在多媒体应用的推动下,彩色LCD也越来越多地应用到了嵌入式系统中,如新一代掌上电脑(PDA)多采用TFT显示器件,支持彩色图形界面,图片显示和视频媒体播放。掌上电脑(PDA)的操作系统有微软Window CE, PalmOS等。而Linux做为开放源代码的操作系统也在市场中占据了一席之地。由于Linux成本低廉,任何人都可以得到其源代码并在其基础上进行开发,成为各家厂商极力发展的操作系统,加上其核心小,潜力可观。
在应用需求的推动下,Linux下也出现了许多图形界面软件包,如MiniGUI、Trolletech公司的Embedded QT等,其图形界面及开发工具与Windows CE不相上下。在图形软件包的开发和移植工作中都牵扯到底层LCD的驱动问题。笔者参与了一个基于ARM9的PDA系统的开发,用的是摩托罗拉公司龙珠系列的MC928MX1。软件采用Linux 2.4.18平台,编译器为gcc的ARM交叉编译器。
一. 硬件平台
MC928MX1(以下简称MX1)是摩托罗拉公司基于ARM核心的第一款MCU,主要面向高端嵌入式应用。内部采用ARM920T内核,并集成了SDRAM/Flash,LCD,USB,蓝牙(bluetooth),多媒体闪存卡(MMC),CMOS摄像头等控制器。
LCD控制器的功能是产生显示驱动信号,驱动LCD显示器。用户只需要通过读写一系列的寄存器,完成配制和显示控制。MX1中的LCD控制器可支持单色/彩色LCD显示器。支持彩色TFT时,可提供4/8/12/16位颜色模式,其中16位颜色模式下可以显示65536种颜色。配置LCD控制器重要的一步是指定显示缓冲区,显示的内容就是从缓冲区中读出的,其大小由屏幕分辨率和显示颜色数决定。在本例中,笔者采用的是夏普LQ035Q2DD54 TFT 显示模块,在240x320分辨率下可提供16位彩色显示。
二. Linux下的设备驱动
Linux将设备分为最基本的两大类,字符设备和块设备。字符设备是以单个字节为单位进行顺序读写操作,通常不使用缓冲技术,如鼠标等,驱动程序实现比较简单;而块设备则是以固定大小的数据块进行存储和读写的,如硬盘,软盘等。为提高效率,系统对于块设备的读写提供了缓存机制,由于涉及缓冲区管理,调度,同步等问题,实现起来比字符设备复杂的多。
Linux的设备管理是和文件系统解密结合的,各种设备名称都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开,关闭,读写这些设备文件,完成对设备的操作,就象操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘主设备号是3。在Linux的/dev/目录下使用ls -l命令可察看个设备文件的设备号。例如,/dev/hda为块设备,主设备号3,次设备号0,是系统的第一块硬盘。/dev/hd1主设备号3,次设备号1,为系统的第二块硬盘。我们将要介绍的显示设备也是一个设备文件/dev/fb,主设备号29。在编写设备驱动程序的时候,也要指明所操作设备的主设备号和次设备号。
Linux的特点之一,是为所有的文件,包括设备文件,提供了统一的操作函数接口,定义如下:
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);
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 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 iovec *, 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);
};
结构体中的成员为一系列的接口函数,如用于读/写的read/ write函数,用于控制的ioctl等。打开一个文件就是调用这个文件file_operations中的open操作。不同类型的文件有不同的file_operations成员函数。如普通的磁盘数据文件,接口函数完成磁盘数据块读写操作;而对于各种设备文件,则最终调用各自驱动程序中的I/O函数进行具体设备的操作。这样,应用程序根本不用考虑操作的是设备还是普通文件,可一律当作文件处理,具有非常清晰统一的I/O接口。所以file_operations是文件层次的I/O接口。
但是,由于外设的种类繁多,操作方式也各不相同。如声音设备驱动要使用DMA通道,显示设备驱动要提供对显存的操作,硬盘驱动要处理复杂的缓冲区结构,网络设备驱动和socket联系紧密。如果file_operations中的函数都让驱动程序的开发人员来写,则就要处理大量的细节,几乎是不可能的。为了解决设备多样性的问题,Linux采用了特殊情况特殊处理的办法,为不同设备定义好了文件层次file_operations结构中的接口函数,其中处理了大多数设备相关的操作,如各种缓冲区的申请和释放等等,而具体操作底层硬件的一小部分则留给开发人员。所以Linux另外提供一个文件层到底层驱动程序的接口,通常为一个结构体,其中包含成员变量和函数指针。不同的设备驱动有不同的结构体。这样,一方面保证了文件层I/O接口file_operations的一致性,另一方面驱动程序的开发人员也不用了解太多细节,只专著于硬件相关的I/O操作就可以了。例如,一个有代表性的特殊设备是声音设备,其文件层的file_operations定义如下:
struct file_operations oss_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的桌面系统中,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/fb0 tmp命令可将当前屏幕的内容拷贝到一个文件中,而命令cp tmp > /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 <linux/fb.h>
int main()
{
int fbfd = 0;
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
long int screensize = 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为其定义了
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/driver/video/fbmem.c中定义。
由于显示设备的特殊性,在驱动层的接口中不但要包含底层函数,还要有一些纪录设备状态的数据。Linux为帧缓冲设备定义的驱动层接口为struct fb_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对应的成员函数:对于嵌入式系统的简单实现,只需要下列三个函数就可以了:
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在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下提供了多种显示卡的帧缓冲设备驱动程序程序,用户自己的驱动程序可参考成熟的代码编写或直接修改得到。