这几天试着做一个在 GBA 上看图的软件。上网搜索了几个GBA上的看图软件,绝大多数软件不能直接看大于 GBA 屏幕大小( 240x160 )的图片,如果图片大于屏幕的大小则看图软件要将图片转换为 240x160 的大小(如图 1 )。唯独只有两个软件(一个是国人写的 PictureBoy,另一个是外国人写的 GBAviewer)不需要把图片缩小,而对大于 240x160 的图片采取 1:1 显示并且可以漫游的形式(如图 2 )。
图1
图2
把原来 800x600 的图片缩小成 240x160 来看的话,肯定有很多细节都看不清楚了。因此我试做的这个软件也打算把图片以 1:1 的形式显示出来。
1、GBA简介
Game Boy Advanced(GBA) 是日本任天堂公司前些年推出的一款掌上游戏机,CPU是32位的 ARM(ARM7TDMI) CPU,主频是 16.78MHz。LCD 屏幕最大支持 240x160 的 16 位真彩色显示。显存 96k,有多种显示模式,其中 BG4 模式是使用 15 位的 256 色调色板,从 0x06000000 开始的显存的每一个字节直接对应屏幕上的一个点。有多种开发包可以开发 GBA 程序,这里我选择devkitadv。
2、未压缩的图像的显示
考虑到 GBA 的主频不高,下面的显示图片的方法中运算量也不算小,这里将使用 BG4 模式来显示图像,所以只能显示 256 色的图片。
256 色的图片中有一个 256 色的调色板,图像数据部分储存的是对调色板的索引值,因此图像数据中一个字节就对应屏幕上一个点所显示的颜色。GBA 的屏幕大小是240x160 ,因此一屏的数据大小是 38400 bytes。首先把原图的图像数据全部存入 ROM ,显示时按需要提取 38400 bytes 的数据写入显存。如下图 3 ,假设原图宽度为 width ,高度为 height。把原图看成是一个完整的虚拟的背景,而屏幕是一个窗口,窗口所在的地方就是要显示的内容。
图3
由此,可以得到屏幕左上角第一个点数据在原图像数据中的位置:width * y + x。如下代码既可显示整个 240x160 的屏幕(代码仅用于描述,应用中还要具体修改,下同)。
for ( j = 0; j < LCD_h; j++ )
{
for ( i = 0; i < LCD_w; i++ )
{
*vram = *( width * ( y + j ) + x + i );
vram++;
}
}
在程序中,按下方向键时,程序就根据方向键的方向来给 x 或 y 增减然后在此提取图像数据写入显存,就可以实现漫游的功能。不考虑其他因素,显示一个屏幕的数据要做 240 * 160 = 38400 次循环,对于 GBA 的 CPU 来说负担还不算重。
3、压缩的图像的显示
上面的这种显示图片的方式经实践证明是比较快的,移动过程中过渡平滑。但是,如果图像数据没有压缩过的话,一个字节就表示一个点的内容,那么一幅 4000x4000 的图片就需要 16,000,000 bytes 的空间来储存。而一个普通的 GBA 的卡带就是 16Mbytes 或 32Mbytes。因此,如果图像数据不压缩,一个 16Mbytes 的卡带只能存储 30 多张 800x600 的 256 色图片。
假如图像使用 JPEG 方式压缩,理论上至少能多存储一倍的图片。但是在 GBA 的 16.78MHz 的 CPU 上面做 JPEG 解码不是很快,特别是要想像上面那样实现漫游的功能更加困难。于是我想了另一个既能压缩数据,又便于实现漫游的方法。
如果要实现漫游,就必须能够实时的获得图像数据并写入相应的显存中。在第二节中,由于图像没有压缩,每一个字节都代表一个点的数据,因此可以用上面讲的方法通过 width * ( y + j ) + x + i 来找到到任何一个屏幕上要显示的点在 ROM 中的位置。式子“width * ( y + j ) + x + i”依赖于 width 这一个定值才能计算出 ( x, y ) 点之前的数据长度,同一幅图中的 width 是固定的。但是,如果数据被压缩了,那么图像每一行被压缩之后的数据长度不一定都一样,也就是说,width 是一个变化的值。如图 4 ,假设要原来的“红色 X”那个字节,未压缩的数据很容易得到它的位置:35 * 5 + 17。当数据压缩后,即使“红色 X”仍在第 5 行第 17 个,但是前面的数据由于被压缩长度改变了,所以“红色 X”应该不在 ROM 中的 35 * 5 + 17 位置了。
但是如果能确定压缩后的数据的图像每一行的长度,就同样可以用上面的方法来获得任何一个点的显示数据。如图,“红色 X”的位置就是:18 + 29 + 26 + 8 + 17。
图4
之前使用的图像都是 256 色的,用 RLE 算法来压缩可以达到很好的效果。在压缩原始图像数据的过程中,压缩程序每压缩完一行原始图像的数据,就记下这一行数据压缩后的长度,然后把这个长度写入一个表中。最后,可以得到一个记录着每一行图像被压缩后的数据长度的表(如图 5 )。
图5
3.1、RLE 算法简述
由于在图像数据中存在许多连续的相同的数据,因此,如果对这些连续的相同的字节压缩存放的话就可以减少存储的空间。如图 6。
图6
3.2、压缩原始图像数据
使用普通的 RLE 压缩算法的时候,判断后一个字节与前一个字节是否相同的这个过程是连续的。但是我们现在需要判断到一行图像的结尾时,就不在判断后一个字节是否与前一个字节相同,而是统计前面压缩过的数据长度是多少,然后把相关数据都写入相应位置。然后到下一行的第一个字节开始再重新作 RLE 压缩,然后到行尾时再统计,然后再下一行……由此反复知道文件结尾。
while(1)
{
if ( 当前字节的数据 == 前一个字节的数据 )
{
if (本字节是最后一个字节,或者达到图像边界)
{
标志和缓冲区都写入文件;
计数写入表;
计数归零;
压缩和非压缩数据的长度都归零;
}
else
{
正常 RLE 压缩;
}
}
else
{
if (本字节是最后一个字节,或者达到图像边界)
{
标志和缓冲区都写入文件;
计数写入表;
计数归零;
压缩和非压缩数据的长度都归零;
}
else
{
正常 RLE 压缩;
}
}
到文件结尾则退出循环;
}
3.3、在 GBA 上解压并显示图像
我的程序把BMP文件转换成GBA上使用的文件格式,转换后的数据结构如下:
head:4 bytes固定为 {0xff, 'B', 'M', 'P'} 便于找到图像数据的位置
width:2 bytes图片的宽度
height:2 bytes图片的高度
palt:512 bytes15 位调色板数据
data_table:与宽相等储存 RLE 长度表
data:变化用 RLE 算法压缩过的图像数据
解压程序相对要简单得多。当要显示如图 7 中蓝框部分的图像。
图7
DrawMap( u16 x, u16 y, u16 width, u16 height, u8 *src_data, u16 *table )
{
for( i = 0; i < 160; i++ )
{
pre_data_len = 当前行之前那些行的数据长度;
if ( i <= h )
{
src_pos = 0;
buf_pos = 0;
while (( buf_pos <= ( 240 + x ))
{
*buf = *( src_data + pre_data_len + src_pos );//读取RLE关键标志
if (( *buf & 0x80 ) == 0x80 )
{
*buf = *buf & 0x7f;//是非压缩标志,( & 0x7f )来获得长度
for( data_pos = 1; data_pos <= *buf; data_pos++ )//复制非压缩数据入缓冲区
{
*tmp1 = *( src_data + pre_data_len + src_pos + data_pos );
*( buffer + data_pos - 1 + buf_pos ) = *tmp1;
}
src_pos += ( *buf + 1 );
buf_pos += *buf;
}
else
{
//是压缩标志,本身就是长度
*tmp1 = *( src_data + pre_data_len + 1 + src_pos );
for( data_pos = 1; data_pos <= *buf; data_pos++ )//复制压缩数据入缓冲区
{
*( buffer + data_pos - 1 + buf_pos ) = *tmp1;
}
src_pos += 2;
buf_pos += *buf;
}
}
//把缓冲区中 240 字节长度的数据复制到当前行的显存;
DmaCopy( 3, ( buffer + x ), video_buffer, 240, 16 );
}
}
}
当按下方向键时,程序就根据方向键的方向来给 x 或 y 增减然后在此提取图像数据写入显存,就可以实现漫游的功能。这样,存储图片的数量能增加,同时也可以实现漫游的功能。图像显示程序比原来显示未压缩数据时多了一个“当前行之前那些行的数据长度”的步骤。经实践,显示图像的速度还算满意