SkyEye硬件模拟平台,第三部分: 硬件仿真实现之五
LCD/TouchScreen仿真
原文地址:http://www.ibm.com/developerworks/cn/linux/l-skyeye/part3/s5/index.html
陈渝 (yuchen@tsinghua.edu.cn)清华大学
2004 年 11 月 01 日
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模拟模块与内存模拟模块之间的耦合度,其缺点是不能实时地反映显存的快速变化。如果将定时间隔设置得过大,则增大了窗口内容刷新时的闪烁;如果定时间隔设置得过小,定时扫描过于频繁地发生,对系统资源是一种浪费。
SkyEye的lcd仿真首先是在模拟ep7312时实现的,所以在本文分析lcd仿真时是以ep7312的lcd模块为例,对于模拟其它的开发板时添加lcd模块的方法是一样的。在SkyEye源码中的clps7110.h文件中有如下定义:
#define LCDCON 0x02c0 /* LCD Control register */
#define VBUFSIZ 0x00001fff /* Video buffer size (bits/128-1) */
#define LINELEN 0x0007e000 /* Line length (pix/16-1) */
#define LINELEN_SHIFT 13
#define PIXPSC 0x01f80000 /* Pixel prescale (526628/pixels-1) */
#define PIXPSC_SHIFT 19
#define ACPSC 0x3e000000 /* AC prescale */
#define ACPSC_SHIFT 25
// 下面两个控制lcd是单色、4级灰度或16级灰度(每个像素点有几位决定灰度级)
#define GSEN 0x40000000 /* Grayscale enable (0: monochrome) */
#define GSMD 0x80000000 /* Grayscale mode (0: 2 bit, 1: 4 bit) */
// SYSCON寄存器的一位,控制lcd是否enable(SYSCON就是state->io.syscon)
#define LCDEN 0x00001000 /* LCD enable */
在SkyEye源码中的skyeye_lcd. c文件中有如下定义:
#define LCD_BASE 0xC0000000 // lcd显示内存起始地址
在SkyEye源码中的skyeye_mach_ep7312.c文件中有:
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;
// 如果用户写系统控制寄存器,让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);
}
}
skyeye_lcd.c中的函数:
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 " , * (state -> mach_io.lcd_addr_end),
* (state -> mach_io.lcd_addr_begin),width, height,depth);
// 建立顶层窗口
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
// 初始化gtk窗口,包括窗口标题,窗口大小等
title = " SkyEye_LcdScreen_TouchScreen " ;
sprintf(mode, " %s%dx%dx%d " ,title,lcd_width,lcd_height,lcd_depth);
gtk_window_set_title(window, mode);
gtk_widget_set_usize(window, width, height);
// 把"expose_event"和顶层窗口的信号处理器联系起来
gtk_signal_connect(GTK_OBJECT(window), " expose_event " ,
(GtkSignalFunc)expose_event, NULL);
gtk_widget_set_events(window, GDK_EXPOSURE_MASK);
// 建立触摸屏仿真事件盒容器,这在skyeye的触摸屏仿真中会用到,此处不进行详细分析
TouchScreen = gtk_event_box_new ( );
gtk_container_add (GTK_CONTAINER (window), TouchScreen);
gtk_widget_set_events(GTK_OBJECT(TouchScreen), GDK_ENTER_NOTIFY_MASK
| GDK_LEAVE_NOTIFY_MASK
| GDK_BUTTON_PRESS_MASK
| GDK_BUTTON_RELEASE_MASK
| GDK_POINTER_MOTION_HINT_MASK);
gtk_signal_connect (GTK_OBJECT(TouchScreen), " button-press-event " ,
GTK_SIGNAL_FUNC (callback_button_press), NULL);
gtk_signal_connect (GTK_OBJECT(TouchScreen), " button-release-event " ,
GTK_SIGNAL_FUNC (callback_button_release), NULL);
gtk_signal_connect (GTK_OBJECT(TouchScreen), " motion-notify-event " ,
GTK_SIGNAL_FUNC (callback_motion_notify), NULL);
gtk_widget_show (TouchScreen);
gtk_widget_realize (TouchScreen);
gdk_window_set_cursor (TouchScreen -> window,gdk_cursor_new (GDK_HAND2));
// 建立LCD屏幕仿真绘图窗口
LCD = gtk_drawing_area_new ();
gtk_container_add (GTK_CONTAINER (TouchScreen), LCD);
// 显示LCD屏幕仿真绘图窗口
gtk_widget_show (LCD);
// 显示顶层窗口
gtk_widget_show(window);
colormap = gdk_window_get_colormap(LCD -> window);
/* 单色,4色,16色,256色都需要调色板调出RGB颜色 */
switch (lcd_depth) {
case 1 :
for (i = 0 ; i < 2 ; i ++ ){
tmpColor = color2[i];
gdk_color_alloc (colormap, & tmpColor);
gc[i] = gdk_gc_new(LCD -> window);
gdk_gc_set_foreground(gc[i], & tmpColor);
}
break ;
case 2 :
for (i = 0 ; i < 4 ; i ++ ){
tmpColor = color4[i];
gdk_color_alloc (colormap, & tmpColor);
gc[i] = gdk_gc_new(LCD -> window);
gdk_gc_set_foreground(gc[i], & tmpColor);
}
break ;
case 4 :
for (i = 0 ; i < 16 ; i ++ ){
tmpColor = color16[i];
gdk_color_alloc (colormap, & tmpColor);
gc[i] = gdk_gc_new(LCD -> window);
gdk_gc_set_foreground(gc[i], & tmpColor);
}
break ;
case 8 :
for (i = 0 ; i < 256 ; i ++ ){
tmpColor = color256[i];
gdk_color_alloc (colormap, & tmpColor);
gc[i] = gdk_gc_new(LCD -> window);
gdk_gc_set_foreground(gc[i], & tmpColor);
}
break ;
default :
break ;
}
}
// 对lcd屏幕仿真窗口重新初始化时,要先调用lcd_disable释放资源并关闭原来的窗口
void lcd_disable(ARMul_State * state)
{
int i;
if (skyeye_config.no_lcd)
return ;
// chy 2004-03-11
* (state -> mach_io.lcd_is_enable) = 0 ;
if (lcd_depth <= 8 ){
for (i = 0 ; i < 2 ^ lcd_depth ; i ++ ) {
if (gc[i]) {
gdk_gc_destroy(gc[i]);
gc[i] = NULL;
}
}
} // 释放GdkGC(Gdk图形设备上下文)
if (window) {
gtk_widget_destroy(window);
window = NULL;
} // 关闭模拟lcd屏幕的gtk window
}
lcd_cycle(ARMul_State * state)
{
gtk_main_iteration_do(FALSE);
}
/* 无阻塞地检查gtk模拟出的lcd window中是否有事件需要相应的事件回调函数处理(如expose_event, motion_notify_event,button_press_event等),
这些事件是在lcd_enable中注册的。SkyEye模拟ep7312时,本函数在skyeye_mach_ep7312.c的函数ep7312_io_do_cycle中被调用,
最终是被armio.c中的io_do_cycle调用,也就是每个时钟执行一次。
当对lcd显示内存进行写操作时,lcd_write会被调用,将相应的象素画到lcd屏幕仿真窗口上 */
void lcd_write(ARMul_State * state, ARMword addr, ARMword data)
{
ARMword offset;
int pixnum, x, y, bit;
int pix;
GdkColor color;
// 如果不使用lcd,则返回
if (skyeye_config.no_lcd)
return ;
// 根据象素所在的4字节的地址,计算该4字节所包含的第一个象素对应于在lcd仿真屏幕上的坐标
offset = (addr & ~ 3 ) - LCD_BASE;
pixnum = offset * 8 / lcd_depth;
x = pixnum % lcd_width;
y = pixnum / lcd_width;
assert(y < lcd_height);
// 调用gdk画点函数gdk_draw_point画出该4字节所包含的所有象素点
// 当颜色深度等于 1时,要画32个点
// 当颜色深度等于 2时,要画16个点
// 当颜色深度等于 4时,要画8个点
// 当颜色深度等于 8时,要画4个点
// 当颜色深度等于16时,要画2个点
// 当颜色深度等于32时,要画1个点
for (bit = 0 ; bit < 32 ; bit += lcd_depth) {
switch (lcd_depth) {
// 当颜色深度<=8时,已经根据相应的调色板调出RGB值
case 1 :
case 2 :
case 4 :
case 8 :
pix = (data >> bit) % ( 1 << lcd_depth);
gdk_draw_point(LCD -> window, gc[pix], x, y);
x ++ ;
break ;
case 15 :
break ;
// 当颜色深度大于8时,可以直接根据象素值调出RGB值
case 16 :
pix = (data >> bit) % ( 1 << lcd_depth);
color.red = ( short )((pix & 0x00007c00 ) << 1 ); // rgb--->1555
color.green = ( short )((pix & 0x000003e0 ) << 6 );
color.blue = ( short )((pix & 0x0000001f ) << 11 );
gdk_color_alloc (colormap, & color);
mygc = gdk_gc_new(LCD -> window);
gdk_gc_set_foreground(mygc, & color);
gdk_draw_point(LCD -> window, mygc, x, y);
x ++ ;
break ;
case 24 :
break ;
case 32 :
color.red = ((data & 0x00ff0000 ) >> 8 );
color.green = ((data & 0x0000ff00 ));
color.blue = ((data & 0x000000ff ) << 8 );
gdk_color_alloc (colormap, & color);
mygc = gdk_gc_new(LCD -> window);
gdk_gc_set_foreground(mygc, & color);
gdk_draw_point(LCD -> window, mygc, x, y);
break ;
default :
break ;
}
}
}
TouchScreen模拟模块的设计思路将与LCD模拟窗口同样大小的GTK+组件置于LCD组件容器中,并为该组件注册鼠标键按下,释放及移动三种事件,当鼠标在组件窗口有键按下,释放或移动的动作,则在相应的事件回调函数种记录其在窗口上的坐标及键的状态,并产生修改中断寄存器中的相应位置1,在SkyEye上运行的嵌入式OS检测到中断寄存器的数据变化就产生中断,TouchScreen驱动程序中注册了该中断的中断服务程序ISR则复制所记录的数据供应用程序使用,这一思路简单说来就是,完成GTK+的鼠标事件到TouchScreen事件的映射。
因此TouchScreen模拟模块只需要关注GTK+鼠标事件的发生,记录事件数据并在*_io_do_cycle 函数中对I/O模拟模块所模拟的中断状态寄存器进行置数操作,即为嵌入式操作系统内核产生中断信号的条件。
下图就是SkyEye模拟器的TouchScreen模拟的流程图(包括与真实硬件的比较):
TouchScreen模拟模块的实现采用了与模拟采用DragonBall开发板的Xcopilot模拟器相类似的简化方式。在实际的TouchScreen硬件中,为了定位动作发生的坐标,要先先经过一个12位的A/D转换器分别转换X,Y坐标对应12位数字量,然后由驱动程序通过SPI串行总线串行接收。SkyEye作为一个指令级的模拟器,无需保证与真实时钟节拍在时序上的一致,因此允许对TouchScreen这样的外设的模拟进行简化。
本文节自 《源码开放的嵌入式系统软件分析与实践--基于SkyEye和ARM开发平台》一书的第三章,对 SkyEye 开源项目感兴趣的可以阅读本书。
陈渝, 清华大学,通过 yuchen@tsinghua.edu.cn 可以和他联系。