1. LCD模拟的构思和设计概述
LCD模拟模块的设计思路是,使用GTK+图形系统在X Window系统和Win32系统上实现一个LCD屏幕模拟,在SkyEye上运行的嵌入式操作系统中的LCD驱动程序象驱动真正的LCD控制器一样发送控制命令或对LCD显示内存进行访问操作,而SkyEye解释这些控制命令,并根据这些命令对LCD屏幕窗口进行相应的GTK+图形操作,完成对不同灰度或颜色图形的绘制。
在SkyEye模拟器中,如果嵌入式操作系统要执行I/O 地址访问,具体的处理过程由特定CPU 和开发板I/O 模拟模块中的read/write_byte/halfword/word 函数处理。所以LCD模拟模块关注的主要是内存模拟模块模拟出来的LCD显示内存中存储的数据。 LCD的显示内存映射到内存RAM中,代表了要在LCD屏幕上显示的图像。显示内存必须足够大,以处理显示屏幕上所有的象素。应用程序通过直接或间接地存取显示内存中的数据来进行进行图形操作,改变屏幕显示的内容。
LCD模拟模块对GTK+的使用目前仅限于根据分辨率(例如320x240,640x480)创建相应大小的窗口以及根据显示内存中的数据逐点在该窗口进行绘制,因为画点是LCD屏幕最基本的动作,所有其它的相对复杂工作如图形绘制,嵌入式GUI系统的实现都应该由基于LCD驱动程序的应用程序(包括基于FrameBuffer驱动程序的嵌入式GUI系统,例如MiniGUI)通过对LCD显示内存的读写操作来实现,SkyEye"看到"的只是显存中对应于屏幕上各个点的像素值,而不关心这些像素值组成的是什么样的图像。基于MiniGUI的应用程序在SkyEye运行的效果截图如图 0-1所示。
SkyEye中LCD模拟部分的示意图如图 0-2(包括与真实情况的比较):
LCD模拟模块的实现先后采用了两种方案,在第一种方案中,在SkyEye的内存模拟模块中,在每一次的写内存操作之后判断其地址是否属于LCD显示内存的地址范围,如果在该范围之内则调用LCD模拟模块中的GTK+画点函数gdk_draw_point(),根据由像素值查找彩色查找表CLUT得到的RGB值(对于真彩色,颜色深度为16,24,32时,RGB值可以直接由像素值得到),在模拟屏幕窗口的相应位置画一个相应灰度或颜色的点。
该方案的优点在于实现起来简单,且模拟了真实的LCD最基本的画点动作,对于图像随时间流逝而只有小范围变化的情况具有一定的优势,因为对显存有写操作时才有画点操作,但是也有两个方面的缺点,其一,与SkyEye模拟器的内存模拟模块耦合紧密,破坏了模块间的独立性;其二,对于图像随时间流逝而大范围变化的情况,本方案效率低下,在LCD驱动程序连续的每两次写显存操作中,都要经历一个单位延迟时间,其长度等于一次地址范围的判断,一次CLUT查找及一次GTK+画点函数的调用所耗费的时间,对于一次全屏操作,以320x240x8为例,若以字节为单位写显存,则额外的时间延迟将320x240x8/8=76800倍于单位延迟时间。
而第二种方案则直接定时(时间间隔可调,例如设置成200ms) 调用GTK+的绘图函数gdk_draw_rgb_image()将显存中的数据一次性绘制到窗口中。该方案模拟了DMA的定时扫描方式,与真实的DMA方式不同的是,在真实的硬件上,DMA方式无须CPU参与,可与CPU并行工作,而用软件模拟的硬件无法做到这一点,只能串行地定时扫描显示内存,其时间延迟不可避免的比真实硬件大。
第二方案降低了LCD模拟模块与内存模拟模块之间的耦合度,其缺点是不能实时地反映显存的快速变化。如果将定时间隔设置得过大,则增大了窗口内容刷新时的闪烁;如果定时间隔设置得过小,定时扫描过于频繁地发生,对系统资源是一种浪费。
2. SkyEye中的LCD模拟分析
SkyEye的lcd仿真首先是在模拟ep7312时实现的,所以在本文分析lcd仿真时是以ep7312的lcd模块为例,对于模拟其它的开发板时添加lcd模块的方法是一样的。在SkyEye源码中的clps7110.h文件中有如下定义:
//lcd控制寄存器地址
#define LCDCON0x02c0/* LCD Control register */
#define VBUFSIZ0x00001fff
/* Video buffer size (bits/128-1) */
#define LINELEN0x0007e000
/* Line length (pix/16-1) */
#define LINELEN_SHIFT13
#define PIXPSC0x01f80000
/* Pixel prescale (526628/pixels-1) */
#define PIXPSC_SHIFT19
#define ACPSC0x3e000000
/* AC prescale */
#define ACPSC_SHIFT25
//下面两个控制lcd是单色、4级灰度或16级灰度(每个像素点有几位决定灰度级)
#define GSEN0x40000000
/* Grayscale enable (0: monochrome) */
#define GSMD0x80000000
/* Grayscale mode (0: 2 bit, 1: 4 bit) */
//SYSCON寄存器的一位,控制lcd是否enable(SYSCON就是state-io.syscon)
#defineLCDEN0x00001000
/* LCD enable */
在SkyEye源码中的skyeye_lcd. c文件中有如下定义:
#define LCD_BASE 0xC0000000 // lcd显示内存起始地址
在SkyEye源码中的skyeye_mach_ep7312.c文件中有:
state-mach_io.lcdcon=(ARMword *)&io.lcdcon;
// lcd控制寄存器
state-mach_io.lcd_is_enable=(ARMword *)&io.lcd_is_enable;
// 是否打开lcd
state-mach_io.lcd_addr_begin=(ARMword *)&io.lcd_addr_begin; // lcd显示内存起始地址
state-mach_io.lcd_addr_end=(ARMword *)&io.lcd_addr_end;
// lcd显示内存结束地址
在ep7312_io_do_cycle函数中也就是每个时钟后会调用:
skyeye_config.mach-mach_io_do_cycle(state);
SkyEye模拟ep7312时,会在skyeye_mach_ep7312.c中的函数ep7312_mach_init注册
this_mach-mach_io_do_cycle = ep7312_io_do_cycle;
在ep7312_io_do_cycle中调用lcd_cycle,这里
lcd_cycle(state)=gtk_main_iteration_do(FALSE);
检查gtk窗口是否有事件需要处理,没有则立即返回。
ep7312_io_read_word函数中:
如果读LCDCON寄存器的地址
返回 data = state-io.lcdcon;
switch (addr - 0x80000000) {
//如果用户写系统控制寄存器,让lcd的状态从关闭变为打开,则重新初始化lcd。
case SYSCON:
tmp = io.syscon;
io.syscon = data;
//chy 2004-03-11
if ((tmp & LCDEN) != (data & LCDEN)) {
ep7312_update_lcd(state);
}
break;
… …
//如果用户改写lcd控制寄存器,改变lcd的控制参数,则重新初始化lcd
case LCDCON:
tmp = io.lcdcon;
io.lcdcon = data;
//chy 2004-03-11 tmp compare with data
if ((tmp & (VBUFSIZ|LINELEN|GSEN|GSMD)) != (data & (VBUFSIZ|LINELEN|GSEN|GSMD))) {
ep7312_update_lcd(state);
}
break;
… …
}
//在应用程序改写SYSCON或LCDCON时,重新初始化LCD时被调用
static void ep7312_update_lcd(ARMul_State *state)
{
ep7312_lcd_disable(state);
if (io.syscon & LCDEN) {
ARMword lcdcon = io.lcdcon;
ARMword vbufsiz = lcdcon & VBUFSIZ;
ARMword linelen = (lcdcon & LINELEN) LINELEN_SHIFT;
int width, height, depth;
switch (lcdcon & (GSEN|GSMD)) {
case GSEN:
depth = 2;
break;
case GSEN|GSMD:
depth = 4;
break;
default:
depth = 1;
break;
}
width = (linelen + 1) * 16;
height = (vbufsiz + 1) * 128 / depth / width;
//以上是取得lcd的高,宽,像素位数(都从LCDCON寄存器中来)
//使用以上参数重新初始化lcd
ep7312_lcd_enable(state, width, height, depth);
}
}
3. LCD相关函数分析
skyeye_lcd.c中的函数:
//当LCDCON寄存器被改写时,调用lcd_enable重新初始化lcd仿真屏幕窗口
void lcd_enable(ARMul_State *state, int width, int height, int depth)
{
int i;
static int once = 0;
GdkColor tmpColor;
char * title;
char mode[100];
if(skyeye_config.no_lcd){
return;
}//如果不使用lcd,则返回
if (!once) {
once++;
gtk_init(&global_argc, &global_argv);
}//只在第一次运行时初始化一个gtk模拟出的lcd屏幕窗口
lcd_width = width;
//lcd仿真屏幕宽度
lcd_height = height;
//lcd仿真屏幕高度
lcd_depth = depth;
//表示一个象素所用的bit数(决定颜色深度)
*(state-mach_io.lcd_is_enable)=1;
//根据显示模式计算lcd显示内存的结束地址
*(state-mach_io.lcd_addr_end) = *(state-mach_io.lcd_addr_begin) + (width * height * depth / 8);
printf("SKYEYE:
lcd_addr_begin 0x%x,lcd_addr_end 0x%x, width %d, height %d, depth %d\n",*(state-mach_io.lcd_addr_end),
*(state-mach_io.lcd_addr_begin),width, height,depth);
//建立顶层窗口
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);