图形用户接口
一、实验目的:
1、了解嵌入式系统图形界面的基本编程方法;
2、学习图形库的制作。
二 、 实验原理:
1、Frame Buffer
显示屏的整个显示区域,在系统内会有一段存储空间与之对应。通过改变存储空间的内容达到改变现实信息的目的。改存储空间被称为Frame Buffer,或显存。显示屏上的每一个点都与Frame Buffer里的某一个位置对应。 Frame Buffer的读写直接对显存进行操作。用户可以将Frame Buffer看成是显示内存的一个映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作可以立即反应在屏幕上。这种操作是抽象的,统一的。用户不必关心物理显存的位置、换页机制等等具体细节,这些都是由Frame Buffer设备驱动来完成的。但FrameBuffer本身不具备任何运算数据的能力,就只好比是一个暂时存放水的水池。CPU将运算后的结果放到这个水池,水池再将结果流到显示器.中间不会对数据做处理。应用程序也可以直接读写这个水池的内容。
Frame Buffer通常就是从内存空间分配所得,并且它是由连续的字节空间组成。由于屏幕的现实操作通常是从左到右逐点像素扫描,从上到下逐行扫描,直到扫描到右下角,然后再折返到左上角,而Frame Buffer里的数据则是按地址递增的顺序被提取,当Frame Buffer里的最后一个字节被提取后,在返回到Frame Buffer的首地址,所以屏幕同一行上相邻的两像素被映射到Frame Buffer里是连续的,某一行的最末像素与它下一行的首像素反映在Frame Buffer里也是连续的,并且屏幕最左上角的像素对应的Frame Buffer的第一单元空间,最右下角的像素对应Frame Buffer的最后一个单元空间。
2、Frame Buffer与色彩
计算机反映自然界的颜色是通过RGB(Red-Green-Blue)值来表示的。如果在屏幕某一点显示某种颜色,则必须给出相应的RGB值。Frame Buffer为屏幕提供的内容就必须能够从Frame Buffer里得到每一个像素的RGB值。像素的RGB值可以从Frame Buffer里得到,或是从调色板间接得到。
RGB的格式:
RGB32 ----使用32位来表示一个像素,RGB分量各用去8位,剩下的8位用作Alpha通道或者不用;
RGB24 ----使用24位来表示一个像素,RGB分量都用8位表示,取值范围为0~255;
RGB555----是16位的格式,分量都用5位表示,剩下的1位不用:
X R R R R R G G G G G B B B BB
RGB565----是16位的另一个格式,5位用于R,6位用于G,5位用于B:
R R R R R G G G G G G B B B B B
本实验系统使用的是16位TFTLCD,像素分辨率为640×480,选用的是RGB565格式。3、LCD控制器
在Frame Buffer与显示屏之间还需要一个中间件,该中间件负责从Frame Buffer里提取数据进行处理,并传输到显示屏上。
PXA270处理器内部集成LCD控制器,提供了一个从PXA270出来器到Passive或Active显示屏的接口。LCDC的作用是将Frame Buffer里的数据传输到LCDC内部,然后经过处理,输出数据到LCD的输入引脚上。
三、 实验内容:
1、图形库的制作:编写点、线、圆、星、矩形、菱形、正三角形等函数,编译生成xx.o文件,形成静态链接库libxc.a,(同时尝试形成动态链接库libxc.so)供应用程序的调用;
2、编写一个应用程序(界面游戏),调用图形库,调试运行;
3、理解Frame Buffer的字符显示原理,将字符显示功能加到应用程序中;
4、理解Frame Buffer显示bmp格式图像的原理,编写程序,使Frame Buffer能够显示图像。
四、 实验步骤:
(一) 图形库的制作:
对于本实验中,仅编写点、线、圆、星、矩形、菱形、正三角形等程序,制作的图形库最重要的便是函数的调用和画图的算法。画点是其他图形的基础,其他的图形都调用到画点的程序;对于算法,一个好的算法,能够提高代码的执行效率,更重要的是能够使画出的图形更美观。
1. 画点draw_point:
void draw_point(int x,int y,int color)
{
long offset = 0;
offset=(y*vinfo.xres+x)*vinfo.bits_per_pixel/8;
*(unsignedchar *)(fbp + offset + 0) = color & 0xff;
*(unsignedchar *)(fbp + offset + 1) = (color>>8)&0xff;
}
其中的3个参数分别为点的x、y坐标值和点的颜色。
1. 画线 draw_line:(程序见附录)
给出任意两点绘制出的直线,采用的是直线斜率表达式的方式,在逐个取点,调用draw_point进行绘制。此方法需注意的问题:
a.对于斜率不存在的竖直线的绘制;
b.逐个取点的方式有两种,将x的值逐个增加,通过表达式计算出其y的值,表达式是算出的y是取整后的值,或者是将y的值逐个增加,计算出取整后x的值。开始仅采用第一种方式,所带来的问题是,当所要绘制的直线斜率增大时,可以明显地看出直线只是断断续续的,效果不理想。因此将上述两种方法同时使用,即对直线进行两次描点,但两种方式得到的点并不完全重复,使得直线连续;
3. 画圆 draw_circle:
画圆采用的是极坐标的方式,调用数学库中的sin、cos函数,将坐标进行转化。
j=0.5*pi/r;
for(i=0;i<=2*r;i++)
{
x=r*cos(j*i)+x0;
y=r*sin(j*i);
draw_point(x,y+y0,color);
draw_point(x,abs(y-y0),color);
}
同样采用的是逐点扫描的方法,在平行与x轴的直径上,需扫描2*r个点,而半圆的角度变化为pi,因此定义j=0.5*pi/r,即每次增加的角度。调用画点的函数,确定一个x值后,可以画出上下两个点。
这中算法的问题在于,当圆的半径很小时,圆上所能够取到的整数点的个数也将变少,导致画出的圆也是断断续续的。
4. 画星 draw_star:
画星形的程序是建立在画圆的基础上的,在一个圆上取出5个点,每相邻的两个点所对应的弧度为0.4*pi,再调用画线的函数进行连接。
5. 画正三角形 draw_triangle:
类似与画星形的画法,在圆上取3个点,每相邻的两个点所对应的弧度为2.0*pi/3,调用画线的函数将相邻的两个点连接,形成正三角形。
6. 画矩形 draw_rectangle:
通过给出的矩形的左上顶点的坐标值及其长和宽,确定出其他3个顶点的位置,调用画线函数连接。
7. 画菱形 draw_rhombus:
通过给出的菱形的两对角线的交点,以及交点距上顶点和左顶点的距离,确定出菱形4个顶点的坐标值,调用画线函数连接。
以上即7个图形的函数,最主要的是画点画线画圆的函数,其他的图形函数建立在点线圆的函数上,同时也可以拓展出其他的图形,如平行四边形、梯形、椭圆等。
将写好的画图程序分别编译:
$ arm-linux-gcc -c draw_point.c
$ arm-linux-gcc -c draw_circle.c
$ arm-linux-gcc -c draw_line.c
$ arm-linux-gcc -c draw_rhombus.c
$ arm-linux-gcc -c draw_triangle.c
$ arm-linux-gcc -c draw_rectangle.c
$ arm-linux-gcc -c draw_star.c
通过上述编译将生成:draw_point.o 、draw_line.o 、draw_circle.o、draw_star.o、 draw_rectangle.o 、draw_rhombus.o 、draw_triangle.o
共7个xx.o文件。
(1) 生成静态库libxc.a:
$ ar cq libxc.adraw_point.o draw_line.o draw_circle.o draw_star.o draw_rectangle.o draw_rhombus.o draw_triangle.o
(2) 生成动态库libxc.so(此时不需要先生成xx.o的文件):
$ arm-linux-gcc -o libxc.so-shared draw_circle.c draw_point.c draw_triangle.c draw_line.c draw_rectangle.c draw_rhombus.c draw_star.c
思考:静态库和动态库的区别:
静态库的名字一般是libxxx.a; 利用静态库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中,编译后的执行程序不需要外部的函数库支持。当然这也是静态库的缺点,因为如果静态库改变了,那么应用程序必须重新编译。
动态库的名字一般是libxxx.so; 相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,应用程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进应用程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响应用程序,所以动态函数库的升级比较方便。
由于本实验形成的图形库的很小,里面的供调用的函数也有限,更多的是采用简单易行的静态库连接方式。
(一) 主程序的开发
总体介绍:该程序是一个互动的小游戏,界面的设计是调用生成的图形库。游戏界面的底部分别为星形、圆、三角、菱形、矩形,并分别对应键盘上的1~5数字;游戏开始时,首先选择的是游戏的等级(1~4个等级),等级越高,其图形下落的速度越快;游戏一共5个回合,每个回合对应产生一个图形的掉落,在图形掉到底部前,正确输入其对应列的数字即进入下一个回合;当图形掉落到底部时,游戏结束,调用字符显示“Game Over!”,并在下一行显示总共赢得回合数,当5个回合全部结束时,游戏结束,并调用字符显示“Excellent!”。游戏结果如下图所示:
![](http://hi.csdn.net/attachment/201112/12/0_13236994584FKR.gif)
程序(game.c)的设计及问题:
(1) 在应用程序中,操作/dev/fb0的一般步骤如下:
a.打开/dev/fb0设备文件。
b.用 ioctl 操作取得当前显示屏幕的参数,如屏幕分辨率,每个像素点的比特数。根据屏幕参数可计算屏幕缓冲区的大小。
c.将屏幕缓冲区映射到用户空间(mmap)。
d.映射后就可以直接读写屏幕缓冲区,进行绘图和字符的显示了。
主要的程序片段:
long screensize=0;
fp = open("/dev/fb0", O_RDWR);
ioctl(fp, FBIOGET_VSCREENINFO, &vinfo);
screensize = vinfo.xres * vinfo.yres * vinfo.bits_per_pixel / 8;
fbp=(char*)mmap(0,screensize,PROT_READ|PROT_WRITE,MAP_SHARED, fp, 0);
实验中关于fb0设备的探究:fb0设备只是Frame Buffer设备中的一个,如果系统有多个显示卡,可支持多个Frame Buffer设备,最多可达 32个,分别为/dev/fb0 到/dev/fb31,而/dev/fb 则为当前缺省的Frame Buffe设备,通常指向/dev/fb0。本实验系统中,在文件系统的构建过程时,创建的/dev目录下:
$ mknod fb0 c 29 0 ,仅只创建了fb0这一个设备文件。
(2) 线程的使用:
实验需实现图形在下落的过程中同时能进行键盘的输入(主要用到是“scanf”),由于scanf是阻塞型,当使用scanf一直读取键盘输入时,将阻塞图 形的移动。解决此问题的途径是合理地使用线程。具体使用方法:
调用函数pthread_create来创建一个线程,它的原型为:
externint pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr, void*(*__start_routine) (void *), void *__arg));
其中第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
主要程序片段:
void *thread_func() {
…..
scanf("%d",&m);
}
int main()
{
int rc=0;
……
rc= pthread_create( &mythread, NULL, thread_func, NULL);
……
}
实验中将“scanf”放入到一个子线程中,在主程序(即主线程)中,成功创建线程后继续执行以下的图形下落的程序,而子线程中一直在扫描键盘,当符合条件时,则退出并关闭该线程,在主程序中相应地作出判断。线程的使用有效地解决了实验中两个程序同时执行的问题。
(3) 简单动画效果的实现:
Frame帧:你所看到的屏幕的图像,或者在一个窗口中的图像,叫做一帧。实验中实现图形的下落的动画,如果采用整个屏幕一帧一帧地,连续图像的刷新,将会消耗很多的时间,并且此时主程序的代码也会相应地增多,执行的效率不高。考虑到在制作图形库里的各个函数时,每一个画图函数里均有一个“color”参数,实现一个区域内的图形动画就比较简单。
do{
draw_circle(x0,y0+25*j,25,color);
usleep(100*1000);
draw_circle(x0,y0+25*j,25,0);
j=j+1; } while(m!=k &&j!=15);
首先设定需要显示图形的颜色,如圆的颜色是蓝色,显示100ms后,在同样的位置,将该圆的颜色设置为0,即黑色,与背景的颜色相同,就可以看到刚才显示的圆消失;保持x值不变,增大y值,在其下方即可显示下一个圆。利用循环和颜色的变化实现了图形下落的动画效果,这种方式不需要重新绘制外框,使得主程序的代码简单,并且数据的计算量很小,执行的时间也较短。
(4) Frame Buffer字符显示:
在LCD绘制字符,可以将每一个字符看作是一幅图像,字符的大小对应于图像的大小,字符的笔画对应于图像的内容。实现字符转化成图像简单方法是“字模提取”,把任意的字符转换为一个字节型的数组,数组元素中的每一位代表LCD上的一个像素点,当为1时,调用画点函数进行画点填充,为0时,不进行调用。实验中的字符数在8×16的面积上显示,即该字符的宽和高各为8、16个像素。动手制作字符显示时,首先下载了一个定义好的字符头文件(font_8*16.h),在该头文件中,定义了一个fontdata_8x16[4096]的数组,其中字符的排列是按照ASCII表的顺序。如字母“A”的字模是数组中的fontdata_8x16[65*16]~ fontdata_8x16[66*16-1]。如下左图所示:
主程序中字符显示的子程序如上右图所示,其中子函数的前两个参数是显示字符左上顶点的坐标,第三个参数是要显示的字符。ASCII表中的字符的字模的起始点在数组中对应的下标为该字符的ASCII的值乘以16;然后,从上到下,从左到右每个像素进行扫描,当list&1<<(7-j)=1时,调用画点函数画出,否则,不进行调用描点。当要显示一个词或一个句子的时,首先将该词或句子定义成字符串数组,通过变换字符左上角的坐标改变整个字符显示的位置,在用循环语句重复调用字符显示的子程序即可。如显示“Game Over”:
int main{
…..
char str1[]="Game Over!";
…..
for(i=0;i<strlen(str1);i++)
{ show_char(480+10*i,450,str1[i],color); }
}
(5) 程序的编译调试:
编译生成show_char.o文件,并重新创建静态库libxc.a.
$ arm-linux-gcc –o game game.c -lxc -lm -lpthread -L ./
说明:-lxc –lm -lpthread 分别是连接自己制作的静态图形库、数学库和线程库;图形库libxc.a存放在当前目录下。
将生成的game可执行文件拷贝到/exp目录下,在目标机上执行;
$ ./game
提示错误:error while loading sharedlibraries: libpthread.so.0: cannot open shary
分析:回顾文件系统的创建,在/lib目录下仅有ld-2.3.2.so、libc-2.3.2.so、libm-2.3.2.so共3个动态链接库。当执行game时,需要动态链接库libpthread.so.0的支持,但目标机上的链接库中没有,导致了上述的错 误。
解决方法: 将交叉编译器链接库中的libpthread-0.10.so拷贝到/lib下,并作软链接ln -s libpthread-0.10.so libpthread.so.0。重新执行game时,无错误发生,正常执行。在以后的开发中,当所写的程序中使用到的链接库需要提前放入到/lib下,这样才能确保应用程序在目标机上能够执行。但基于目标机的存储空间有限,不采取将所有的链接库放入/lib下。
(三) Frame Buffer 显示bmp图像
1. bmp图像文件格式简介:
bmp是一种与硬件设备无关的图像文件格式,它采用位映射存储格式,除了图像深度可选以外,不采用其他任何压缩,因此,bmp文件所占用的空间很大。bmp文件的图像深度可选1bit、4bit、8bit及24bit。bmp文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序。典型的bmp图像文件至少有3个部分组成:
(1)位图文件数据结构,包含bmp图像文件的类型、显示内容等信息,其结构体定义如下:typedef struct
{ char cfType[2]; /* 文件类型*/
char cfSize[4]; /* 文件的大小(字节) */
char cfReserved[4]; /* 保留, 必须为 0 */
char cfoffBits[4]; /* 位图阵列相对于文件头的偏移量(字节) */
} BITMAPFILEHEADER; /* 文件头结构 */
(2) 位图信息数据结构,包含有bmp图像的宽、高、压缩方法,以及定义颜色等信息,结构体如下:
typedef struct
{ char ciSize[4]; /* 说明BITMAPINFOHEADER 结构所需的字节数*/
char ciWidth[4]; /* 位图宽度(像素) */
char ciHeight[4]; /* 位图高度(像素) */
char ciPlanes[2]; /* 目标设备的位平面数, 必须置为1 */
char ciBitCount[2]; /* 每个像素的位数*/
char ciCompress[4]; /* 位图阵列的压缩方法*/
char ciSizeImage[4]; /* 图像大小(字节) */
char ciXPelsPerMeter[4];/* 目标设备水平每米像素个数 */
char ciYPelsPerMeter[4];/* 目标设备垂直每米像素个数 */
char ciClrUsed[4]; /* 位图实际使用的颜色表的颜色数 */
char ciClrImportant[4]; /* 重要颜色索引的个数 */
} BITMAPINFOHEADER; /* 位图信息头结构 */
(3) 调色板,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24位的bmp)就不需要调色板;
(4) 位图数据。这部分的内容根据bmp位图使用的位数不同而不同,在24位图中直接使用RGB,而其他的小于24位的使用调色板中颜色索引值,结构体如下:
typedef struct
{ char rgbBlue;
char rgbGreen;
char rgbRed;
char rgbReserved;
} RGBQUAD; /* 颜色模式 */
2. bmp图片数据的获取
使用fopen()函数打开位图文件,使用fread()读取位图头文件,并判断位图
的类型,接着读取位图信息头。利用memcpy()函数将InfoHead.ciWidth、
InfoHead.ciHeight、FileHead.cfoffBits转成整形。获取每一个像素点的颜色
后调用画点函数draw_point()进行图像每一个点的显示。
图片数据的获取:
memcpy( &ciWidth, InfoHead.ciWidth, 4 );
memcpy( &ciHeight, InfoHead.ciHeight, 4 );
memcpy( &cfoffBits, FileHead.cfoffBits, 4 );
fseek(fp, cfoffBits, SEEK_SET); /*从文件开始偏移bfoffbits个字节的位置*/
int i,j;
for(j=y0+ciHeight;j>y0;j--)
{
for(i=x0;i<x0+ciWidth;i++)
{
fread( (char *)&rgbquad, 1, sizeof(RGBQUAD), fp );
color=((rgbquad.rgbRed>>3)<<11)|(((rgbquad.rgbGreen>>2)<<5)&0x07e0)| ((rgbquad.rgbBlue>>3)&0x1f);
draw_point(i,j,color);
}
}
问题:
(1) 实验中采用的是24位bmp格式的图片,因此需要将RGB24位压缩成
16位后显示。若采用书中的做法:
color=(rgbquad.rgbRed<<11)|((rgbquad.rgbGreen<<5)&0x07e0)|(rgbquad.rgbBlue&0x1f);
显示图片的颜色有很严重的失真。压缩时,应先将Red向右移动3位,舍弃低位的3位,同理,将Green、Blue分别舍弃低位的2位和3位,从而实现数据的压缩。编译执行后,再观察显示图片,与原图颜色符合。
(2) 对于第一个循环,刚开始时是: for(j=y0;j<y0+ciHeight;j++)
但发现此时显示的图片是倒置的,改用for(j=y0+ciHeight;j>y0;j--) 后图片显示正常。因为在每次循环时,首先获取的图片最后一行的数据,这是由于bmp文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序,这一点需要特别关注。
1. 程序的编译调试:
编译生成show_bmp.o文件,并重新生成静态链接库:
$ ar cq libxc.adraw_point.o draw_line.o draw_circle.o draw_star.o draw_rectangle.o draw_rhombus.o draw_triangle.o show_char.o show_bmp.o
$ arm-linux-gcc –o game game.c -lxc -lm -lpthread -L ./
执行后显示效果:
游戏结束后,当显示字符“Excellent”时,调用图片显示程序在其上方显示一张笑脸图;当显示字符“Game Over”时,在其上方显示一张哭脸图。五、实验总结:
本次实验花费的时间较长,主要内容有3个方面:图形库制作、字符显示、图片显示。其中在图形库的制作上花的精力较多,掌握并理解静态库的生成方式,并能够对静态库的调用。Frame Buffer总体上是对数据的操作,如何获取简单图形、字符及图片的数据显得很重要,再者便是程序中算法的运用上,好的算法不仅能简单地得到数据,还能显示出更清晰的图形。
实验的过程虽然很曲折,由于自己的计算机是32位,打开Frame Buffer设备后,显示的图片的颜色与实验室目标机上的不一样,程序在目标机上运行时需要重新调整,也花费了一些时间;对于实验中遇到一些新的概念或者是自己并未接触过的知识时,通过书籍的查阅或者是网上的搜索还是能够及时了解并掌握;C语言的使用上,可能是长时间的没有使用,很多函数都已经忘记,也犯了很多低级的错误,在以后的编程开发中,相信多练习C语言还是能够熟练掌握使用的。
链接:源程序