GBA程序开发入门

1.GBA简介

Game Boy Advanced(GBA)是日本任天堂公司于2001年推出的一款32位掌上游戏机.它着重于游戏机的便携式,并且以2D游戏为主(3D游戏依然不错).GBA有十分高效硬件图像处理加速,GBA基本上是现在全球公认的最佳的掌上游戏机.

GBA的硬件功能十分多.它的CPU是以RISC为基础的32位ARM CPU,主频是16.78MHz.屏幕最大支持240x160的16位真彩色显示.在图形处理方面GBA还另外提供了硬件加速,支持硬件上的图形旋转,缩放,alpha混合,face in/out淡入淡出等.

GBA的软件开发主要是以C语言为主,程序设计简单而且十分自由,也正是这一点吸引了许多爱好者在GBA上做东西(不一定是游戏).你完全可以把它做成PDA,做成Mp3播放器(它的声音处理方面能力也不错哦),做成随身电影播放齐,做成电子词典等,只要你有能力设计它的软件.

比如azhong的主页http://huangyz2002abcd.nease.net/上就有他写的WORDBOY字典软件.还有MUSIC PLAYER GBA的音乐播放器,能让GBA变身Mp3播放器.VideoGBA,能把图片转换成GBA影片的工具.不过要制做它的电影有点困难.这里提供一部已经做来的,<<黑客帝国>>的片断下载: http://213.30.21.181/web/down/show.asp?id=40&down=1

在http://readboy.nease.net/上还可以找到READBOY,GBA上的读书软件,这个软件做得很不错,除了看书,甚至还可以浏览图片.

不说多了,我们还是主要来看看如何开发GBA的软件.应该说,你只要会点C语言,就可以进入GBA的开发行列中了.在GBA上写程序比在电脑上写程序更简单.GBA是单总线的设计,也就是说通过简单的读写命令就能完成所有的内部硬件操作.访问GBA内部的寄存器就犹如访问一块内存一样简单,而GBA所有的功能都是通过读写寄存器而完成的.

2.GBA程序开发准备

GBA开发所需要的东西除了一台可以用的电脑外,主要就是一个编译器和一个GBA模拟器.

有了GBA模拟器,你就可以在你的电脑上运行GBA程序了.GBA模拟器应该是到处可以找得到的.我推荐的是VisualBoyAdvance.你可以在www.gbadev.org上下载最新的版本.如果你还想把你的程序弄到GBA机器上玩,那么除了应该有个GBA外,还应该有个ROM烧录器.比如EZ-Flash.通过它可以把电脑上的ROM烧录到GBA卡带上,然后放到GBA上运行.(总共一个GBA加一个128MB的EZ-Flash大概是1000元人民币)

GBA程序开发可以用汇编语言和C/C++语言来写程序,不过汇编语言比较复杂,而且不是我们平常使用8086汇编,而是Arm汇编.任天堂公司可能觉得C++程序运行消耗太大,不推荐C++语言来开发,所以我们都一直将C语言作为首选.

GBA开发所用的C语言编译器主要有两种,一种是免费的gcc,一种是Arm公司提供的ArmSDT.

这里的gcc当然不是简单的linux下的gcc,而是Arm Gcc.简单地说就是把我们平时常见的gcc改造成生成ARM CPU代码的gcc.你可能会问难道我们要使用linux操作系统了吗?其实我们还是在windows下做GBA开发,虽然gcc主要是在linux下的东西,但是我们也可以从cygwin(在windows下模拟UNIX的工具)中得到在windows下运行的gcc.你可以根据这个cygwin下的gcc,自己去配置Arm Gcc,也可以直接去下载个配置好了的Arm Gcc.比如DevKitAdv,你可以到http://www.gbadev.org 去下载这个软件.DevKitAdv大概10多MB.有了它,你用不着去找什么cygwin(200-300MB)和配置什么Arm Gcc了,DevKitAdv已经帮你做好了一切,你只需安装它就OK了.

ArmSDT是Arm公司提供的专门为ARM CPU的开发程序的C语言编译器.前面说过,GBA是采用32位的ARM CPU,所以这个ArmSDT是可以用来开发GBA程序的.而且由于ArmSDT是ARM公司官方的开发包,所以它的编译速度和质量上都比免费的gcc要高,但对C语言的支持也不及gcc好(gcc毕竟已经成了C语言编译器的标准),最关键的问题它不是免费的,但是它的免费测试版还是可以用的.你可以到Arm公司的网站去下载ARMSDT2.51. http://www.arm.com

我以DevkitAdv配置的gcc为主还讲解GBA的程序开发.

安装DevKitAdv很简单,用WinZip或WinRAR软件解压下载文件后,双击DKA_Setup.exe,出现下面的对话框,选择安装目录,然后点击"Install"就进行安装了.整个DevKitAdv共49MB,应该是十分小的.

编译器和模拟器都装好后,下面我们需要一批小工具,主要就是帮我们转换图片,声音等数据.GBA程序编译后通常都是一个rom文件,图像和声音等数据都是包含在主程序里的.这些工具能把我们电脑上的图片或声音文件转换成一个*.h头文件,然后包含在主程序里编译.关于这种转换工具也是有许多,我推荐的图形转换工具是kaleid_1-2-3,在http://www.gbadev.org上下载,它可以转换我们平时用的BMP文件.声音转换工具是wav2gbac.exe,也是在http://www.gbadev.org下载,它可以转换我们平时用的WAV文件.

好了,下载完后这两个小工具后,准备工作就算做完了.

3.第一个GBA程序

现在我们开始我们的第一个GBA程序.在这个程序中我们将在屏幕上显示一张的图片.

首先你建立一张240*160大小,256色的bmp图片,比如这样:

名字就叫helloworld.bmp.打开刚才下载的Kaleid_1-2-3程序.选中菜单File中Open,打开这个helloworld.bmp文件,再选中菜单File中Convert and Save.出现对话框:

如图一样选择各项选择,特别注意的是一定要选择"BitMap Graphics(Mode 4)"和"Save As C Source",并且去掉"#include <agbtypes.h>的选择.最后单击"Convert and Save",保存为helloworld.h

图像数据已经转换好了,接着我们开始写代码.建立个main.c的文件,并把helloworld.h放在一个目录下.然后在main.c中写下代码

typedef unsigned char u8;

typedef unsigned short u16;

typedef unsigned long u32;

#define REG_DISPCNT *(u16*)0x04000000 // 显示寄存器地址

#define VRAM 0x06000000 // 图像缓冲区地址

#define PALETTE 0x5000000 // 调色板地址

#define MODE_4 0x04 // mode4标志

#define BG2_ENABLE 0x0400 // BG_2标志

#define
SetMode(Mode)
REG_DISPCNT=(Mode)                 // 设置显示模式的宏定义

#include "helloworld.h" // 包含图像调色板和数据的头文件

u16* palette_mem=(u16*)PALETTE; // 系统调色板

u16* video_buffer=(u16*)VRAM; // 图像缓冲区

void Draw(u16* src_palette,u16* src_data,u16* dst_palette,u16* dst_data);

int main()

{

// 设置屏幕模式,这里使用MODE_4

SetMode (MODE_4 | BG2_ENABLE);

// helloworld_pal和helloworld_gfx是在"helloworld.h"定义的调色板和图像数据数组名

Draw((u16*)helloworld_pal,(u16*)helloworld_gfx,palette_mem,video_buffer);

// 死循环

while(1)

{ ;}

}

// MODE_4绘图函数

void Draw(u16* src_palette,u16* src_data,u16* dst_palette,u16* dst_data)

{

int loop,x,y;

// 写入目的调色板

for(loop = 0; loop < 256; loop++)

dst_palette[loop] = src_palette[loop];

// 写入图像缓冲区

for(x = 0; x < 120; x++)

for(y = 0; y < 160; y++)

dst_data[(y) *120 + (x)]=src_data[(y) *120 + (x)];

}

最后建立一个编译批处理命令make.bat来帮我们编译生成目标代码.

写入下面的命令到一个新建的make.bat文件里面

PATH=C:\devkitadv\bin;%PATH%

gcc -lm -o main.elf main.c

objcopy -v -O binary main.elf main.bin

pause

如果你的devkitadv是在D盘,那么就应该是PATH=D:\devkitadv\bin;%PATH%,我这里的DevKitAdv是安装在C盘的.执行make.bat,如果没有错误的话就可以得到编译出来的main.bin和main.elf. main.bin就是可以运行的GBA的rom.你可以用的模拟器VisualBoyAdvance来打开运行它,或者烧录到GBA卡带上运行.

这是在模拟器VisualBoyAdvance中运行的结果.

下面我将一一讲解上面的程序

前三行:

typedef unsigned char u8;

typedef unsigned short u16;

typedef unsigned long u32;

定义GBA程序自己的数据类型,因为这样我们就很容易知道每个数据在GBA中占的字节数.

#define REG_DISPCNT *(u16*)0x04000000 // 显示寄存器地址

REG_DISPCNT是设置显示模式的寄存器.GBA中有mode0-mode5六个显示模式.我们这个程序的显示模式是mode4,也就是240x160的8位色(256色)的双缓冲显示模式.比如mode3,就是240x160的16位真彩色的单缓冲显示模式.

#define VRAM 0x06000000 // 图像缓冲区地址

#define PALETTE 0x5000000 // 调色板地址

VRAM就是GBA的显示内存,简单地说VRAM中的数据就是屏幕上的图象.跟我们的平常用的计算机一样的.PALETTE就是调色板地址,简单地说就是装图像调色板的地方.

#define MODE_4 0x04 // mode4标志

#define BG2_ENABLE 0x0400 // BG_2标志

MODE_4和BG2_ENABLE都是设置REG_DISPCNT的值.

我们再看看main函数主要做了些什么.

首先是SetMode (MODE_4 | BG2_ENABLE); 把显示模式设置成mode_4.然后是执行Draw函数,把我们刚刚转换的helloworld.h里面的图像数据和调色板数据复制指定的内存去.在Draw这个函数里面,都是使用了for循环一个一个地把调色板数据和图象数据复制到内存中去的.

前面提到过,GBA中MODE4支持的是240*160并且256色的显示方式,那么显示内存VRAM中一个字节就对应屏幕上的一点的颜色.这么算来,整张240*160大小的屏幕就应该有240*160=38400个字节.所以我们在helloworld.h中的helloworld_gfx有38400/2=19200个元素(一个u16是占两个字节).而且我们还可以通过公式来计算得到屏幕中一点对应在VRAM的地址.这样就可以得到下面的写点函数.特别指出的是,对于VRAM的访问只能是u16大小的数据类型,而不能是u8(包括char).而一个u16类型的数据表示着两个点的信息,低8位对应的是前面的点,高8位对应的是后面的点. 所以我们需要一些算法来实现对屏幕中单独一个点的读写,请看下面的写点函数

void SetPixel(int x,int y,u8 color)

{

register u16 *tc;

tc= video_buffer+y*120+x/2;

if(x&1) // 如果x是奇数

*tc=((*tc&0x00FF)+(color<<8));

else

*tc=(*tc&0xFF00)+color;

}

其中video_buffer就是显示内存VRAM的地址.屏幕上的点与VRAM中的值是按线性一一对应的.VRAM中第1个字节的信息就是屏幕中(0,0)这个点的颜色在调色板上的引索,第2个字节就是屏幕(1,0)的颜色信息,第3个字节就是(2,0)的颜色信息,一次类推.到第241个字节就是屏幕(0,1)的颜色信息.然后第242就是(1,1)的信息...由此关系,我们可以找出公式,在屏幕上(x,y)点的信息应该是储存于VRAM中第 y*240+x 个字节里,那么我们如果要画(x,y)这一点,就只需要修改VRAM中第y*240+x个字节的信息就OK了.可是这里又有个问题困扰着我们,那就是上面黑体字写的,GBA中对于VRAM的访问不能是单单的一个字节的访问,而必须是16位两个字节的访问.这也就是说你每次写点的同时会影响到旁边的点的信息.还好,我们可以通过简单的位运算可以在避开影响旁边点的同时,完成对(x,y)的写点工作.代码已经在上面了.由于*tc是16位的指针,所以tc加1就等于移动了16位两个字节的距离.所以在代码中,tc=video_buffer+ y*120+ x/2 而不是前面所说的tc=video_buffer+ y*240 +x.至于后面的代码我也就不多讲了,如果你觉得有问题,那么应该去查看一下C语言书籍.

通过这个函数,你就可以在屏幕上画线,画圆,画矩型等,甚至还可以写汉字.

4.更多的资料和信息

GBA上的mode4下第一个程序虽然做出来了,但是想要真正地做GBA开发那还远远不够的.GBA里面支持Bg(Background)背景,OBJ(Object)精灵等东西,那些才是GBA游戏中常用的技术.通过Bg和OBj才能进一步使用GBA硬件上的Alpha混合,Face in/out,马塞克等特效支持.可惜我不可能在这里把这些东西都详细讲解出来.你可以到www.gpgame.net网站上找到我写的十分详细的GBA资料,名字叫做GBA探索日记.同时也可以到www.gbadev.org去查看许多国外的教程.www.gbadev.org是个十分齐全的GBA开发网站,你可以在上面找很多国外的朋友写的demo,开发工具,源代码,文章等等.虽然目前国内还没有正式的GBA开发网站,但是可以到www.cgdn.net论坛上找到很多已经在做GBA开发的朋友.在这里我要提醒大家的是,开发GBA程序其实是很简单的事情,困难的是资料的缺乏,尤其是中文资料.最后我把我的联系方式告诉大家,如果大家在做GBA过程有什么问题,希望我能帮上你.QQ:8664220,email:tangl_99@sohu.com,msn:tangl_99@sohu.com


制作技术

::金点时空::
文档

GBA探索日记
----tangl_99

    GBA开发入门：关于GBA的基本知识

    GBA探索日记(一)：MODE0-2的BG与VRAM

    GBA探索日记(二)：OBJ及OAM

    GBA探索日记(二)：外篇-关于编译器

    GBA探索日记(三)：Color Special Effects

    GBA探索日记(四)：编译器

    GBA探索日记(五)：DMA

    GBA探索日记(六)：Timer

    GBA探索日记(七)：Interrupt

    GBA探索日记(八)：声音播放1

    GBA探索日记(九)：声音播放2

    GBA探索日记(十)：回头

to be continue..
www.gpgame.net


GBA探索日记(-)

mode0-2的BG与VRAM

GBA 的VRAM总共96KB,在mode3-5中,VRAM跟计算机的显示内存一样.屏幕上一个点对应一个显示内存地址.

可以看看下面两个函数,都是在mode4下的写点函数.

void PlotPixel(int x,int y, unsigned short int c)

{

m_VideoBuffer[(y) * 120 + (x)] = (c);

}

void SetPixel(int x,int y,u8 color)

{

register u16 *tc;

tc=m_VideoBuffer+y*120+x/2;

if(x&1)

*tc=((*tc&0x00FF)+(color<<8));

else

*tc=(*tc&0xFF00)+color;

}

由于GBA支持的最小数据传输单位为16位,而16位在mode4(256色)下为两个点的信息.

所以上面的PlotPixel是对两个点的写法.而SetPixel是通过一些简单的位移运算来实现对单个点的写入.

特别指出的是一个VRAM里的u16的值低8位对应的是前面的点,高8位对应的是后面的点.

说了这么多mode4的,下面看看GBA最精彩的部分mode0-2

设置REG_DISPCNT这个寄存器

不说多了,它就是指定你要显示的模式和支持的BG.

比如看这个的语句:

*(vu16 *)REG_DISPCNT = DISP_MODE_1 | DISP_BG0_ON ;

这段语句就是说让GBA支持MODE1的显示模式,并支持BG0

当然你也可以

*(vu16 *)REG_DISPCNT = DISP_MODE_1 | DISP_BG0_ON | DISP_BG1_ON |DISP_BG2_ON;

但是你不能把BG3也加进来.因为mode1只能支持BG0,BG1,BG2.

mode0-2都是运用tile(图块)先来组成整个屏幕的.

而mode0-2每个都有不同的BG

一个屏幕可以由多个BG组合显示出来.比如你可以安排地面为一个BG,天空为一个BG等等.

每个BG对应一个寄存器

Text BG:就是不能旋转的,似乎只能使用16色.当然,如果使用16色的调色板,当然可以使用最多到16个调色板.

Rotation BG:就是可以旋转的,放大的.只能使用256色.如果使用了256色的调色板,就不能使用16色的调色板了.因为GBA的BG调色板就只有256x2=512那么大.

再看看不同模式下的BG分配

mode 0: 4个Text BG

mode 1: Text BG: BG0 BG1

Rotation BG: BG2

mode 2: Raotation BG: BG2,BG3

接下来就看看控制这些BG的寄存器:

1.Text BG

在AgbLib任天堂提供的官方开发包里面是这么写出它的寄存器

#define REG_BASE 0x04000000 // Registers

#define REG_BG0CNT (REG_BASE + 0x8) // BG 0 Control

#define REG_BG1CNT (REG_BASE + 0x8) // BG 1 Control

2.Rotation BG

在AgbLib任天堂提供的官方开发包里面是这么写出它的寄存器

#define REG_BASE 0x04000000 // Registers

#define REG_BG2CNT (REG_BASE + 0x8) // BG 0 Control

#define REG_BG3CNT (REG_BASE + 0x8) // BG 1 Control

Poriority是指的是显示的优先权.

0最高,4最低.

再看看关于BG大小的问题.

Screen Size Setting Text Screen Rotation/Scaling Screen

Screen Size Screen Data Screen Size Screen Data

00 256×256 2 Kbytes 128×128 256 Bytes

01 512×256 4 Kbytes 256×256 1 Kbyte

10 256×512 4 Kbytes 512×512 4 Kbytes

11 512×512 8 Kbytes 1024×1024 16 Kbytes

为了显示一张BG.

你可以通过AgbLib中提供的bmp2map.exe来把一张bmp文件转换成GBA可以用的BG数据.

BG数据分三块.

1.Character Data

前面已经说了.mode0-2下的GBA屏幕是由图块组成的.这些图块都是8x8大的.

这些图块有些人把它叫做Tile,也有些人把它叫做Character(AgbLib里面就是这么叫的).

这些数据就是保存这些8x8图块的图象数据的.

2.Map Data

Map Data可以说是图块的引索.它是记录Character Data的在屏幕中的排布的数据.

3.Palatte Data

这个不用说了,就是调色板

在显示一张BG前,应该把上面的上部分数据传输到GBA内存中.

看看下面的代码吧:

DmaArrayCopy(3,e1_Character, BG_VRAM, 32);

DmaArrayCopy(3,e1_Map, BG_VRAM+0x8000, 32);

DmaArrayCopy(3,e1_Palette, BG_PLTT+32*2, 32);

DmaArrayCopy 是个数据传输的函数.当然,你也可以使用for(...)一个字节一个字节地写.但是GBA里有3个DMA通道(就是不经过CPU,专门用来传输大量数据的专线),它应该是就像memcpy那样的功能.

上面的代码就是将e1图像文件Character,Map,Palette传输到GBA中对应的内存.

特别指出的是在使用16色的调色板的时候,你如果用bmp2map.exe来转换.那么就要注意这个程序中-p这个参数.它指出了这个Map用的哪个Paletee.比如我上面的那个e1的BG就是使用了-p2,即使用的第2个调色板.那么它在GBA中对应的内存地址应该是BG_PLTT+32*2(16色的调色板总共占32个字节).

你可能要问BG_VRAM,BG_PLTT是什么.

#define PLTT 0x05000000

#define BG_PLTT (PLTT + 0x0)

#define VRAM 0x06000000

#define BG_VRAM (VRAM + 0x0)

它们其实就是VRAM,Palette在内存中的指定位置.

Map Data,Character Data都是放在VRAM中的.

在mode0-2中,VRAM被分成许多Block,每个2KB(0x800).Character Data比较大一点,它一个要占8个Block(16KB).Map Data比较小一点,它一个占一个Block.

还是看看这张十分有用图片

Character Base Block的值为0-3

Screen Base Block(就是装Map Data的)的值为0-31

当然,不能让他们重复,否则先写的数据就被覆盖了.

再看看设置BG的寄存器的代码吧:

#define TEXTBG_SIZE_256x256 0x0

#define TEXTBG_SIZE_256x512 0x8000

#define TEXTBG_SIZE_512x256 0x4000

#define TEXTBG_SIZE_512x512 0xC000

#define ROTBG_SIZE_128x128 0x0

#define ROTBG_SIZE_256x256 0x4000

#define ROTBG_SIZE_512x512 0x8000

#define ROTBG_SIZE_1024x1024 0xC000

DmaArrayCopy(3,e1_Character, BG_VRAM, 32);

DmaArrayCopy(3,e1_Map, BG_VRAM+0x8000, 32);

DmaArrayCopy(3,e1_Palette, BG_PLTT+32*2, 32);

*(vu16 *)REG_BG0CNT =

BG_COLOR_16 | TEXTBG_SIZE_256x256 | BG_PRIORITY_0

| 16 << BG_SCREEN_BASE_SHIFT | 0 << BG_CHAR_BASE_SHIFT;

你对应前面的图看看.

16是Screen Base Block的值,0是Character Base Block的值.

0x8000就是Base Block 16的对应偏移址,0是Base Block 0的对应偏移址

GBA硬件上对有很多对BG处理的支持,比如旋转,移动,放大,Alpha,马塞克.

我们先看看BG的移动.

这同样也是通过对GBA的寄存器的写入来实现的.

在BG2(Rotation BG)中有两个寄存器控制它的位置.REG_BG2X,REG_BG2Y

还是看看它们的定义

#define REG_BASE 0x04000000 // Registers

#define REG_BG2X (REG_BASE + 0x28) // BG 2 Start X Coordinate

#define REG_BG2Y (REG_BASE + 0x2c) // BG 2 Start Y Coordinate

它们是只能写的寄存器,你可不要去读它们,否则读不出来什么有用的数据的.

但是你可以去写它们:

s32 x=10<<8;

s32 y=10<<8;

*(s32 *)REG_BG2X=x;

*(s32 *)REG_BG2Y=y;

需要记住的它们都是32位的数据,而且是有符号的.

x增大,BG向左移,x减小,BG向右移.

y增大,BG向上移,y减小,BG想下移.

当x或y每改变256,BG移动1个像素.

所以上面的x,y都使用了<<8

金点 tangl_99

2002/12/14


GBA探索日记(二)

OBJ及OAM

OBJ就是指的一个个的精灵,或者说就是GBA中的小块小块的图片.它其实跟BG差不多.只是它要小一些.它也算个图层.比如GBA中的RPG游戏就喜欢把一个个的人物用OBJ来实现.这样方便控制他们的位置.当然,从 GBA探索日记(-)中也可以看到BG的控制其实也是很方便的.而OBJ和BG的差别就是大小.

设置显示模式

如果你的程序里要用OBJ那么,

在设置显示模式的时候需要把DISP_OBJ_ON和DISP_OBJ_CHAR_1D_MAP加进去

*(vu16 *)REG_DISPCNT = DISP_MODE_1 | DISP_OBJ_ON | DISP_OBJ_CHAR_1D_MAP;

#define DISP_OBJ_CHAR_1D_MAP 0x0040 // OBJ Character Data 1D Mapping

#define DISP_OBJ_ON 0x1000 // OBJ ON

下面就直接来看看OBJ在GBA中怎么实现的.

首先要说明的就是OBJ在GBA中显示也是由一个个8x8的小图块拼成,跟BG一样.

共有下面12种大小.

0000: 8 x 8 1000: 8 x 16

0001: 16 x 16 1001: 8 x 32

0010: 32 x 32 1010: 16 x 32

0011: 64 x 64 1011: 32 x 64

0100: 16 x 8

0101: 32 x 8

0110: 32 x 16

0111: 64 x 32

OBJ的数据分三部分.

1.Character

它跟BG的数据格式是一样的,都是由8x8的小图块的图像数据.而且你也可以通过转换BG的工具来转换OBJ的Character.所以说BG和OBJ的数据结构基本是一样的.不过所有的 OBJ总共的Character不能超过32K.

256色的OBJ能有512个Characters,每个Characters占8*8=64bytes.而16色的Characters能有1024个.每个Character占8*8/2=32Bytes.似乎比一个BG的256个Character多?但是BG可以有多个,不同的BG可以有不同的Character,而所有OBJ只能共用这512个Charater.

2.Palette

调色板就不用说了,它跟BG一样,支持一个256色调色板或16个16色的调色板.

3.OAM

OAM就是OBJ Attribure Memory的缩写.它就是控制OBJ属性的数据部分.比如OBJ的位置,大小等数据部分就放在这里.每个OBJ的OAM占8个字节,OAM是从内存中07000000h到070003FFh,共1024=8*128,所以OAM总共能有128个.

如何创建一个OBJ呢?

前面我就说过通过转换BG的工具也可以用于转换OBJ.

以前转换BG我建议使用的是AgbLib里的bmp2map.exe,同样,转换OBJ也同样用bmp2map.exe这个工具.

比如我把Character Data放到sprite_gfx这个数组里,把Palette放到sprite_pal里面.(OBJ中没有Map Data,你的转换时可以把Map data也转换进来,只不过后面用不着)

然后在程序里写入:

DmaArrayCopy(3, sprite_gfx,OBJ_MODE1_VRAM,32);

DmaArrayCopy(3, sprite_pal,OBJ_PLTT,32);

把sprite_gfx,sprite_pal拷贝到指定内存中去.

这里又设计到OBJ_MODE1_VRAM和OBJ_PLTT两个内存地址

还是看看AgbLib中是怎么定义它们的吧.

#define PLTT 0x05000000 // Palette RAM

#define PLTT_END (PLTT + 0x400)

#define BG_PLTT (PLTT + 0x0) // BG Palette RAM

#define BG_PLTT_END (PLTT + 0x200)

#define OBJ_PLTT (PLTT + 0x200) // OBJ Palette RAM

#define OBJ_PLTT_END (PLTT + 0x400)

#define VRAM 0x06000000 // VRAM

#define VRAM_END (VRAM + 0x18000)

#define BG_VRAM (VRAM + 0x0) // BG Character/Screen RAM

#define OBJ_MODE0_VRAM (VRAM + 0x10000) // OBJ Character RAM

#define OBJ_MODE1_VRAM (VRAM + 0x10000)

#define OBJ_MODE2_VRAM (VRAM + 0x10000)

#define OBJ_MODE3_VRAM (VRAM + 0x14000)

#define OBJ_MODE4_VRAM (VRAM + 0x14000)

#define OBJ_MODE5_VRAM (VRAM + 0x14000)

#define OBJ_VRAM_END (VRAM + 0x18000)

这里我把BG中的地址也包含近来了,你可以对比一下BG和OBJ中的地址.

然后就是设置OAM(OBJ Attribure Memory).

这里先看看官方文档中是怎么说这个OAM的数据结构的.

特别指出的是上面表格中Character Name是指这个OBJ的起始Character的引索

我将介绍两中方法来设置.

第一种方法很直观,也很简单.这是我在网上看到最多的.

首先定义了数据结构

typedef struct tagOAMEntry

{

u16 attribute0;

u16 attribute1;

u16 attribute2;

u16 attribute3;

}OAMEntry, *pOAMEntry;

//attribute0 #defines

#define ROTATION_FLAG 0x100

#define SIZE_DOUBLE 0x200

#define MODE_NORMAL 0x0

#define MODE_TRANSPARENT 0x400

#define MODE_WINDOWED 0x800

#define MOSAIC 0x1000

#define COLOR_16 0x0000

#define COLOR_256 0x2000

#define SQUARE 0x0

#define TALL 0x4000

#define WIDE 0x8000

//attribute1 #defines

#define ROTDATA(n) ((n)<<9)

#define HORIZONTAL_FLIP 0x1000

#define VERTICAL_FLIP 0x2000

#define SIZE_8 0x0

#define SIZE_16 0x4000

#define SIZE_32 0x8000

#define SIZE_64 0xC000

//atrribute2 #defines

#define PRIORITY(n) ((n)<<10)

#define PALETTE(n) ((n)<<12)

然后写入代码:

OAMEntry sprites[128]; // 这里把总共128个OBJ都定义了,不过我们现在只使用第一个

void InitializeSprites()

{

u16 loop;

for(loop = 0; loop < 128; loop++)

{

sprites[loop].attribute0 = 160; //y to > 159

sprites[loop].attribute1 = 240; //x to > 239

}

}

void CopyOAM()

{

u16 loop;

u16* temp;

u16* _OAM = (u16*)0x7000000;

temp = (u16*)sprites;

for(loop = 0; loop < 128*4; loop++)

{

_OAM[loop] = temp[loop];

}

}

u16 spriteY=100;

u16 spriteX=100;

InitializeSprites();

sprites[0].attribute0 = COLOR_256 | SQUARE | spriteY;

sprites[0].attribute1 = SIZE_16 | spriteX;

sprites[0].attribute2 = 0 | PRIORITY(0);

由于这个OBJ的Character是从OBJ_MODE1_VRAM的首地址开始写入的,所以它的Character Name就应该是 0.所以sprites[0].attribute2 = 0 | PRIORITY(0);

由于GBA中16色的OBJ和256色的OBJ中对于Characters的寻址方式是一样的.那么无论是16色的还是256色的OBJ,它们的起始Characters都应该是OBJ的VRAM+Character Name*32(虽然256色下的Character占64Bytes).

比如看看下面的例子:

如果传输256色的sprite的Character Data到OBJ_MODE1_VRAM+N*32的地址

DmaArrayCopy(3,sprite_gfx,OBJ_MODE1_VRAM+N*32,32);

那么它对应的的attribute2中的Character Name应该是N.

sprites[0].attribute2 = N;

N的范围是0-1023

对于attribute3,我们现在暂时不用.

接下来就是把sprites写到OAM中

要在程序的主循环中不断更新OAM

while(1)

{

// 等待图像缓冲同步

WaitSync();

CopyOAM();

}

第二种方法是AgbLib里面使用的方法.

比如我要显示一个是OBJ

就直接定义个

vu32 bg_oam_buffer[2];

然后同样要写Character和Palette数据进去

DmaArrayCopy(3, sprite_gfx,OBJ_MODE1_VRAM,32);

DmaArrayCopy(3, sprite_pal,OBJ_PLTT,32);

设置OAM的时候就这样写

bg_oam_buffer[0] = OAM_COLOR_256 | OAM_SQUARE |OAM_SIZE_16x16

| (100) << OAM_H_POS_SHIFT

| (72) << OAM_V_POS_SHIFT;

bg_oam_buffer[1] = 0x0400;

下面给出AgbLib中对于上面的宏的定义:

#define OAM_V_POS_SHIFT 0

#define OAM_H_POS_SHIFT 16

#define OAM_COLOR_16 0x00000000 // Select 16

#define OAM_COLOR_256 0x00002000 // Select 256 colors

#define OAM_SIZE_8x8 0x00000000 // OBJ 8 x 8 dot

#define OAM_SIZE_16x16 0x40000000 // OBJ 16 x 16 dot

#define OAM_SIZE_32x32 0x80000000 // OBJ 32 x 32 dot

#define OAM_SIZE_64x64 0xc0000000 // OBJ 64 x 64 dot

#define OAM_SIZE_16x8 0x00004000 // OBJ 16 x 8 dot

#define OAM_SIZE_32x8 0x40004000 // OBJ 32 x 8 dot

#define OAM_SIZE_32x16 0x80004000 // OBJ 32 x 16 dot

#define OAM_SIZE_64x32 0xc0004000 // OBJ 64 x 32 dot

#define OAM_SIZE_8x16 0x00008000 // OBJ 8 x 16 dot

#define OAM_SIZE_8x32 0x40008000 // OBJ 8 x 32 dot

#define OAM_SIZE_16x32 0x80008000 // OBJ 16 x 32 dot

#define OAM_SIZE_32x64 0xc0008000 // OBJ 32 x 64 dot

你可以看出.

bg_oam_buffer[0]其实就是第一种方法的attribute0,attribute1的集合.

bg_oam_buffer[1]就是第一种方法的attribute2,attribute3的集合.

然后同样地在程序住循环里不断更新写入OAM

while(1)

{

DmaCopy(3, bg_oam_buffer, OAM, 8 , 32);

}

这是OAM的定义:

#define OAM 0x07000000 // OAM

#define OAM_END (OAM + 0x400)

金点 tangl_99

2002/12/21


GBA探索日记(二)(外篇 关于编译器)

现在网上有两种编译GBA程序的编译器.

1.gcc

由于GBA采用的是Arm公司的CPU,所以也需要去网上找到个armgcc覆盖你现有的gcc

gcc本身是个不错的c语言编译器.但是比起后面的讲的ArmSDT就有几个致命的缺点.

关于GBA所用的gcc有好几个不错的选择.

(1)devkitadv.你下载后就可以直接用了.它里面的gcc是已经装了armgcc的.

(2)HAM.它也是已经把gcc转换成armgcc的了.

(3)当然,你也可以自己配置armgcc

首先你应该有个常规的gcc.

可以在cygwin这个软件里找到完整的gcc.不过cygwin这个工具很大.有几百MB.

然后再找到armgcc以及一些补丁.具体的补丁我也不是很清楚.

但是你应该可以在Romandoo发布在cgdn论坛的贴子可以找到他提供的完整的补定包和AgbLib3.0

他在贴子中这么写到.

一： 安装CYGWIN工具

运行CYGWIN_SETUP目录下setup.exe进行cygwin的安装

选INSTALL FROM LOCAL DIRECTORY

在SELECT INSTALL ROOT diretory 选择安装目录 next>

再NEXT

在弹出的窗口中，在Perv Curr Exp 中选Curr

点开列表栏中各项，

选 base 的 cygwin ash bash login sh-utils

选 devel 的 autoconf automake binutils gcc gdb make

选 shell下的 ash bash sh-utils

置NEW栏显示版本号，表示将其安装，而其它的工具选为skip表示跳过不安装

最后选next开始安装！

（注意，我为了节省空间，我删除了许多不重要的东西，其实cygwin下有高达几百m的各式各样的工具，今后你可根据自已需要到网上下载安装去）

二、安装任天堂官方开发包

在 AGB LIBRARY3.0中点 agbsetup1安装

注意，一定要安装到c:\agb目录下，因为这是官方的默认目录，有一些例子与这个目录关连了

所以最好选这个目录 ，不然可能编不过去。否则自已设置会麻烦一点的。

修改c:\autoexec.bat 文件增加如下代码行

SET AGBDIR=C:\AGB

三、安装CYGWIN补丁

（由于刚安装完的cygwin版本并不支持arm cpu,所以需要将某些程序、库覆盖成arm相关的程序库）

将arm-thumb-elf连目录一起copy到安装好的cygwin目录的\lib\gcc-lib下

将bin-patch目录下的所有文件copy到cygwin目录的bin下面覆盖

将include patch目录下文件copy到cygwin目录的include下面

下面应该看看gcc编译GBA的命令:

path=C:\devkitadv\bin;

gcc -o Game.elf Game.cpp -lm

objcopy -O binary Game.elf Game.bin

这个应该是很简单的.

2.ArmSDT

ArmSDT是Arm公司官方提供的为Arm CPU写程序的编译器.

它在编译速度和编译质量上是上面的gcc无法相比的.

如果你有很大的头文件的,或者很大的程序要编译,那么你就应该使用ArmSDT

不过ArmSDT不是免费软件,但是网上还是有人把它盗了.所以你也可以去cgdn上找.

还有,www.arm.com提供ArmSDT的测试版,其实也可以用的.

不过用ArmSDT编译GBA程序需要个boot.s汇编程序.

下面我就把乐水提供的boot.s写在下面:

AREA my_1st_start, CODE, READONLY

IMPORT |Image$$RO$$Limit| ; End of ROM code (=start of ROM data)

IMPORT |Image$$RW$$Base| ; Base of RAM to initialize

IMPORT |Image$$ZI$$Base| ; Base and limit of area

IMPORT |Image$$ZI$$Limit| ; to zero initialize

IMPORT C_Entry

PSR_CPU_MODE_MASK EQU 0x1f ; CPU Mode

PSR_USER_MODE EQU 0x10 ; User

PSR_FIQ_MODE EQU 0x11 ; FIQ

PSR_IRQ_MODE EQU 0x12 ; IRQ

PSR_SVC_MODE EQU 0x13 ; Supervisor

PSR_ABORT_MODE EQU 0x17 ; Abort (Prefetch/Data)

PSR_UNDEF_MODE EQU 0x1b ; Undefined Command

PSR_SYS_MODE EQU 0x1f ; System

EX_WRAM EQU 0x02000000 ; CPU External Work RAM

EX_WRAM_END EQU (EX_WRAM + 0x40000)

CPU_WRAM EQU 0x03000000 ; CPU Internal Work RAM

CPU_WRAM_END EQU (CPU_WRAM + 0x8000)

WRAM EQU EX_WRAM ; Entire Work RAM

WRAM_END EQU CPU_WRAM_END

SOUND_AREA_ADDR_BUF EQU (CPU_WRAM_END - 0x10); Sound Driver Work Address

INTR_CHECK_BUF EQU (CPU_WRAM_END - 0x8); Interrupt Check

SOFT_RESET_DIRECT_BUF EQU (CPU_WRAM_END - 0x6); SoftReset() Specify Return to

INTR_VECTOR_BUF EQU (CPU_WRAM_END - 0x4); Interrupt Branch Address

Cpu_Mode_USR EQU 0x10

Cpu_Mode_IRQ EQU 0x12

Cpu_Mode_SVC EQU 0x13

Cpu_Stack_USR EQU 0x03007F00 ; GBA USR stack adress

Cpu_Stack_IRQ EQU 0x03007FA0 ; GBA IRQ stack adress

Cpu_Stack_SVC EQU 0x03007FE0 ; GBA SVC stack adress

ENTRY

b start

DCD 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

DCD 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

DCD 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

start

mov r0, #Cpu_Mode_IRQ:OR:0x80:OR:0x40 ; Disable interrupts

msr CPSR_c, r0 ; Enable IRQ mode

ldr r13, =Cpu_Stack_IRQ ; Setup IRQ stack pointer

mov r0, #Cpu_Mode_SVC:OR:0x80:OR:0x40 ; Disable interrupts

msr CPSR_c, r0 ; Enable SVC mode

ldr r13, =Cpu_Stack_SVC ; Setup SVC stack pointer

ldr r0, =|Image$$RO$$Limit| ; Get pointer to ROM data

ldr r1, =|Image$$RW$$Base| ; and RAM copy

ldr r3, =|Image$$ZI$$Base| ; Zero init base => top of initialized data

cmp r0, r1 ; Check that they are different

beq %F1

0 cmp r1, r3 ; Copy init data

ldrcc r2, [r0], #4

strcc r2, [r1], #4

bcc %B0

1 ldr r1, =|Image$$ZI$$Limit| ; Top of zero init segment

mov r2, #0

2 cmp r3, r1 ; Zero init

strcc r2, [r3], #4

bcc %B2

mov r0, #Cpu_Mode_SVC:OR:0x40 ; Only IRQ enabled

msr CPSR_c, r0

mov r0, #Cpu_Mode_USR:OR:0x40 ;

msr CPSR_c, r0 ; Switch to USR mode

ldr sp, =Cpu_Stack_USR ; Setup USR stack pointer

; b MyMain__Fv ; Now branch to start of C code

;final_return_loop

; b final_return_loop ; In case of a return...

; END

start_vector

msr cpsr_c,#PSR_IRQ_MODE ; 切换到IRQ管理模式

ldr sp, sp_irq ; 设置堆栈,为IRQ中断作准备

msr cpsr_c, #PSR_SYS_MODE ; 切换到系统模式

ldr sp, sp_usr ; 设置堆栈用户模式

ldr r1, =INTR_VECTOR_BUF ; 设置中断向量缓冲

adr r0, intr_main ; 设置中断控制函数

str r0, [r1]

ldr r1, =C_Entry ; 进入C语言主循环体

mov lr, pc

bx r1

b start_vector ; Reset

;final_return_loop

; b final_return_loop ; In case of a return...

ALIGN

sp_usr DCD WRAM_END - 0x100

sp_irq DCD WRAM_END - 0x60

EXTERN IntrTable ;中断向量表

GLOBAL intr_main ;中断控制

ALIGN

CODE32

intr_main

mov r3, #0x4000000 ; Check IE/IF

add r3, r3, #0x200 ; r3: REG_IE

ldr r2, [r3]

and r1, r2, r2, lsr #16 ; r1: IE & IF

ands r0, r1, #0x2000 ; Game Pak Interrupt

loop bne loop

mov r2, #0

ands r0, r1, #0x0001 ; V-blank Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0002 ; H-blank Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0004 ; V-counter Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0008 ; Timer 0 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0010 ; Timer 1 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0020 ; Timer 2 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0040 ; Timer 3 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0080 ; Serial Communication Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0100 ; DMA0 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0200 ; DMA1 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0400 ; DMA2 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x0800 ; DMA3 Interrupt

bne jump_intr

add r2, r2, #4

ands r0, r1, #0x1000 ; Key Interrupt

jump_intr

strh r0, [r3, #2] ; IF Clear 11c

ldr r1, =IntrTable ; Jump to user IRQ process

add r1, r1, r2

ldr r0, [r1]

bx r0

END

需要注意的一点就是在你的GBA程序里,main主函数需要变成C_Entry.

还有,这个boot.s也初始了中断表

那么在你里的程序里也定义全局常量

如下:

const IntrFuncp IntrTable[13] = {

VBlankIntr, // V Blank interrupt

IntrDummy, // H Blank interrupt

IntrDummy, // V Counter interrupt

IntrDummy, // Timer 0 interrupt

IntrDummy, // Timer 1 interrupt

IntrDummy, // Timer 2 interrupt

IntrDummy, // Timer 3 interrupt

IntrDummy, // Serial communication interrupt

IntrDummy, // DMA 0 interrupt

IntrDummy, // DMA 1 interrupt

IntrDummy, // DMA 2 interrupt

IntrDummy, // DMA 3 interrupt

IntrDummy, // Key interrupt

};

最后就是编译命令.

不费话了,直接看下面的命令吧:

set PATH=f:\arm250\bin;%PATH%

zarmasm -Littleend -cpu ARM7TDMI boot.s

zarmcc -c -W -Wall -Otime -fpu none -Littleend -cpu ARM7TDMI -apcs /narrow/noswst main.c -o main.o -errors log.txt

zarmlink -bin -first boot.o -map -ro-base 0x08000000 -rw-base 0x3000000 boot.o main.o -o main.gba

del *.o

pause

你可以建立个make.bat

或者写个makefile.

不过有一点要提示你的是:

ArmSDT中的zarmmake.exe的功能很弱.建议你使用MS的nmake.exe

下面就是我为nmake.exe写的makefile:

TARGET = main.bin

CFILES = main.c

OFILES = $(CFILES:.c=.o)

SFILES = boot.s

BOOTFILES = boot.o

LIBFILES = *.a

ASFLAGS = -CPU ARM7TDMI -Littleend

CFLAGS = -c -Wall -cpu ARM7TDMI -fpu none -W -apcs /narrow/noswst -Otime

LDFLAGS = -bin -errors log.txt -map -ro-base 0x08000000 -rw-base 0x03000000

CC = zarmcc

LD = zarmlink

AS = zarmasm

$(TARGET) :$(BOOTFILES) $(OFILES)

$(LD) $(LDFLAGS) -first $(BOOTFILES) $(BOOTFILES) $(OFILES) -o $(TARGET)

$(BOOTFILES):$(SFILES)

$(AS) $(ASFLAGS) $(SFILES) -o $(BOOTFILES)

$(OFILES):$(CFILES)

$(CC) $(CFLAGS) $(CFILES) -o $@

clean:

@-if exist *.bin del *.bin

@-if exist log.txt del log.txt

@-if exist *.o del *.o

@echo All generated files have been cleaned!!!

关于makefile中这些语法我就不说了.

你可以去看看关于GNU MAKE指南.

最后需要强调一点的就是一个使用AgbLib里面的代码编译要注意的问题:

如果你使用了下面的代码,那么你就无法使用程序中的全局变量,而只能使用全局常量.

DmaClear(3, 0, EX_WRAM, EX_WRAM_SIZE, 32);

DmaClear(3, 0, CPU_WRAM, CPU_WRAM_SIZE - 0x200,32);

因为程序的全局变量是放在EX_WRAM中的,而全局常量是放在ROM里的.

金点 tangl_99

2002/12/24


GBA探索日记(三)(Color Special Effects)

我们知道在游戏中做Alpha混合和Fade-in/Fade-out等颜色特效是十分消耗CPU的.对于GBA这样的16.78MHz的CPU就更显得费劲.不过还好,GBA提供了硬件上对这两种颜色特效(Color Special Effects)的支持.所以你就不用担心这些问题了.

1.Alpha混合

首先你应该知道什么是Alpha混合.其实就是透明处理.

其处理的算法就是

R=R1*alpha+R2*(1-alpha);

G=G1*alpha+G2*(1-alpha);

B=B1*alpha+B2*(1-alpha);

其中alpha是个小于等于1的小数.而R,G,B是混合后的点的RGB值,R1,G1,B1和R2,G2,B2分别是两个要混合的点的RGB值.

由于一张图片有好几千到好几百万个点,而每个点又包含三个浮点运算,所以这中Alpha混合的计算量是很巨大的,计算速度就是个很严重的问题.

GBA中有对Alpha混合的硬件支持,你只需对相关的寄存器设置好参数,然后它就会自己来帮你计算,由于是硬件上的计算,所以速度问题就解决了.

下面我们就直接看看如何设置Alpha混合的寄存器.

在AgbLib中定义了下面两个寄存器

#define REG_BLDCNT (REG_BASE + 0x50) // Blend Mode Control

#define REG_BLDALPHA (REG_BASE + 0x52) // Semi-transparent Parameter

其中涉及到REG_BASE,它是寄存的开始地址,你可以看看它的总线地址

#define REG_BASE 0x04000000 // Registers

REG_BLDCNT是设置颜色特效(Color Special Effects)参数的寄存器.它不仅控制Alpha混合,还控制后面讲的Fade-in/Fade-out的设置.

在AgbLib中的官方文档中是这么说的这个寄存器的

The types of color special effects and the target pixels, are determined by the BLDMOD register.

可以看出,REG_BLDCNT的0-5位是设置第一个混合目标(1st Target Pixel)的范围,可以是BG0-BG3,OBJ,BD中的一个或多个.

9-13位是设置第二个混合目标(2nd Targer Pixel)的范围,同样的是BG0-BG3,OBJ,BD中的一个或多个.

而中间的06-07是设置颜色特效的种类.

下面我们再来看另一个控制Alpha混合的寄存器REG_BLDALPHA的设置

这个寄存器简单地说就是设置Alpha值的寄存器.我们还是看看官方文档中怎么讲解它的

其中EVB,EVA就是Alpha值.只能是0-16

它们在寄存器中设置的值和参与计算的值对应如下:

其中讲到EVY,它是关于Fade-in/Fade-out的,现在可以不管它.

实际GBA硬件内部进行Alpha混合的计算的方式是这样的.

Alpha Blending (16 levels of semi-transparency) Operations

Display color (R) = 1st pixel color (R) ×EVA + 2nd pixel color (R)×EVB

Display color (G) = 1st pixel color (G) ×EVA + 2nd pixel color (G) ×EVB

Display color (B) = 1st pixel color (B) ×EVA + 2nd pixel color (B) ×EVB

好,说了这么多,让我们来看一个例子.

先看看这张经过Alpha混合的图片

其中地面是放在BG0,海洋是放在BG1,白云是放在BG2,中间白色的球是放在OBJ中的.

其中Alpha混合的目的是要让地面,海洋,百球都和白云混合.

它的寄存器是这样设置的.

// Semi-transparent setting for cloud

*(vu16 *)REG_BLDCNT = 0x3b44; // 11101101000100

*(vu16 *)REG_BLDALPHA = 0x080c; // 10000000001100

然后你就自己对照着前面的图表去看吧.

2 Fade-in/Fade-out

Fade-in/Fade-out就是淡入淡出效果.GBA同样提供了硬件支持.而且它的设置跟上面讲的Alpha混合几乎是完全一样的.

设置它的同样是两个寄存器.

#define REG_BLDCNT (REG_BASE + 0x50) // Blend Mode Control

#define REG_BLDY (REG_BASE + 0x54) // Brightness Change Parameter

关于REG_BLDCNT,前面讲的Alpha混合中已经消息讲解过了.

下面我们就直接看看REG_BLDY这个寄存器

似乎有点浪费,这个16位的寄存器只有前5位有用.

这5位就是表示明暗侄EVY.

同样请看看前面关于EVA,EVB,EVY的图表.它记录了EVY对应的实际计算值.

然后看看GBA硬件是怎么样计算Fade-in/Fade-out效果的.

Brightness Increase Operations

Display color (R) = 1st pixel (R) + (31 - 1st pixel (R) ) ×EVY

Display color (G) = 1st pixel (G) + (63 - 1st pixel (G) ) ×EVY

Display color (B) = 1st pixel (B) + (31 - 1st pixel (B) ) ×EVY

Brightness Decrease Operations

Display color (R) = 1st pixel (R) - 1st pixel (R) ×EVY

Display color (G) = 1st pixel (G) - 1st pixel (G) ×EVY

Display color (B) = 1st pixel (B) - 1st pixel (B) ×EVY

讲到这里基本就结束了吧.这里也不用再举例子了,Fade-in/Fade-out的处理和前面的Alpha混合几乎是一样的.所以这里就没有这个必要了.不过要说明的是 Brightness Increase Operations 是逐渐变白,Brightness Decrease Operations 是逐渐变黑

金点 tangl_99

2003/1/9


 GBA探索日记(4)(编译器)

又是一篇关于编译器的日记.以前已经讨论过编译器的问题,现在又拿出来的谈,我觉得是有很必要的.工欲善其事,必先利其器.在我做GBA过程中,遇到最困难的问题并非写代码,而是配置编译器.这篇日记的内容很零散,记录我遇到的问题以及解决问题的方法,希望对现在正在做GBA的朋友有所帮助.

探索日记(2)外篇中说到两个编译器,gcc,ArmSDT

ArmSDT编译速度比gcc快许多,但是任天堂提供的官方开发包是用的gcc.而且gcc是个很流行的编译器,所以我打算以gcc为主.同时这些编译器的命令选项的用法也通常适合现在大多数编译的c语言编译(包括ArmSDT).

1.gcc的编译命令

gcc是国际上很流行的c,c++编译器.它是免费的,而且可以被分割成许多部分,因此许多开发商把自己的产品编译器做到gcc上了.除了GBA,我知道还有Palm OS也是如此.可见这些开发商都是十分信任gcc的编译能力的.

不再费话了,下面先看看怎样使用gcc把c源代码编译成可执行文件.

先举个例子.我要编译一个a.c文件

那么我一直接在dos或linux,unix下打入命令:

gcc a.c

就可以得到a.exe或a.out(linux下的执行文件,相当于dos下的a.exe)

这是最简单的编译方式.

但是通常的方法是将*.c文件编译成*.o中间文件,然后再编译成可执行文件

gcc -c a.c -o a.o

gcc a.o -o a

然后就会得到a.exe或a.out

第一行命令是编译,-c后面跟的是源代码文件,-o后面跟的是编译出来的中间文件的文件名.整个命令也可以这么写:gcc -o a.o -c a.c

第二行命令是连接,-o后面跟的是目标执行文件的文件名,前面跟的是要连接的*.o文件名

整个两行命令也可以写成:gcc -o a.exe a.c

接下来来就讲一些你使用AgbLib时需要用的一些选项

-c

编译*.c文件的时候使用

Compile or assemble the source files, but do not link. The linking stage simply is not done. The ultimate output is in the form of an object file for each source file.

编译或者集合这些源代码文件,但是不连接.连接过程将不进行.最终输出文件是以一种object文件(对于每一个源文件)

-S

编译*.c文件的时候使用

把*.c文件翻译成*.s汇编代码

Stop after the stage of compilation proper; do not assemble. The output is in the form of an assembler code file for each non-assembler input file specified.

在编辑过程后停止,不进行装配.输出文件是汇编代码

-I

无论是在编译还是连接都可以使用(需要注意的是gcc的选项是区分大小写的).

它是指定include目录的.

比如你在使用cygwin中的gcc,那么include目录是c:\cygwin\include,所以你在编译的时候就应该这么写

gcc -c a.c -o a.o -I"C:\cygwin\include"

否则很可能在#include一些文件的时候找不到头文件,不过在一般安装好cygwin后,include目录是自动给你配置好了的,所以你即使不用-I"c:\cygwin\include"它也找得到需要的头文件.但是麻烦的问题是当你把你好好的gcc变成armgcc后,你就需要重新去配置这些东西了.

你可以同时给出多个include目录,比如c:\agb\include

gcc -c a.c -o a.o -I"C:\cygwin\include" -I"C:\agb\include"

-w

禁止所有警告信息

Inhibit all warning messages.

这个选项在ArmSDT变成大写的-W

-L

这是个连接*.o文件时使用的参数.

它是指定lib目录的.

例如

gcc -o a.exe a.o -L"C:\cygwin\lib"

同样,它依然可以指定多个lib目录

-l

依然是连接*.o文件时所使用的参数

它是指定所需的lib文件

例如,我在c:\agb\lib中有个叫libxxxx.a的库文件,那么我输入:

gcc -o a.exe a.o -I"C:\agb\include" -L"C:\agb\lib" -lxxxx

在gcc中对于lib文件的名字有些特殊的要求.

第一,文件名必须以lib三个字母开头.

第二,文件名的扩展名必须是a,及文件必须是*.a文件

在AgbLib中,如果你想使用一些c语言常规库函数,就必须把libm.a包含进去.

所以应该有-lm这个参数.

-nostartfiles

连接*.o时的选项

Do not use the standard system startup files when linking. The standard system libraries are used normally, unless -nostdlib or -nodefaultlibs is used.

不使用标准开始连接文件.

这里主要指的是C语言的标准库文件.这些库文件通常都普遍地在使用,除非你用了-nostdlib 或 -nodefaultlibs这些选项.

AgbLib编译的samples里都使用了这个选项.

-nostdlib

连接*.o时的选项

不是使用stdlib等标准库文件

Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify will be passed to the linker. The compiler may generate calls to memcmp, memset, and memcpy for System V (and ISO C) environments or to bcopy and bzero for BSD environments. These entries are usually resolved by entries in libc. These entry points should be supplied through some other mechanism when this option is specified.

AgbLib的samples里也使用了该选项

2.ARM compiler options

这些都是针对Armgcc的选项,它并非属于GNU C的部分.

但是AgbLib中使用其中的一些选项,所以还是那它一部分出来讲一讲

-mthumb

Generate code for the 16-bit Thumb instruction set. The default is to use the 32-bit ARM instruction set.

产生的代码是16位的Thumb模式.通常缺省的都是用的32位Arm模式

-mthumb-interwork

Produce assembly code which supports calls between the ARM instruction set and the THUMB instruction set.

提供支持Arm模式和THUMB模式的汇编代码

至于Arm和THUMB模式是什么,其实我也不清楚,但是现在市面上有一本叫Arm Soc的书籍,它专门讲解了Arm CPU已经基于Arm CPU的System等原理.那些东西似乎有点扯远了,我们的目的不是设计Arm Soc,而只是做GBA游戏而已, 所以你在AgbLib看到他们使用了这个选项,我们也就跟着使用就是了

-mcpu=XXXX

Produce assembly code specifically for the indicated processor. The ‘XXXX’ variable can be one of the following processors.

提供汇编代码明确CPU,'XXXX'就是下面的CPU名字

arm2

arm250

arm3

arm6

arm600

arm610

arm620

arm7

arm7m (the default setting)

arm7d

arm7dm

arm7di

arm7dmi

arm70

arm700

arm700i

arm710

arm710c

arm7100

arm7500

arm7500fe

arm7tdmi

arm8

strongarm

strongarm110

这个选项在AgbLib使用gcc环境下很少见,但是如果你使用的ArmSDT.那么你就肯定要用到这个了. 不过需要注意的是ArmSDT中的zarmcc中不再是用-mcpu,而变成了-cpu

比如下面的命令:

zarmcc -c -Wall -Otime -fpu none -Littleend -cpu ARM7TDMI -apcs /narrow/noswst main.c -o main.o -errors log.txt

2.makefile

打开AgbLib官方开发包里的samples.可以发现它的代码都是配置了makefile文件的.makefile是个帮助你编译多个程序文件的有用的工具,无论是在Visual C++开发还是GCC开发,这个工具的使用是很常见的.

这里要十分感谢EyeOnMe,关于makefile的配置的帮助是他提供给我的.

下面就是一篇关于makefile配置的文章.

GNU make 指南

翻译： 哈少

译者按： 本文是一篇介绍 GNU Make 的文章，读完后读者应该基本掌握了 make 的用法。而 make 是所有想在 Unix （当然也包括 Linux ）系统上编程的用户必须掌握的工具。如果你写的程序中没有用到 make ，则说明你写的程序只是个人的练习程序，不具有任何实用的价值。也许这么说有点 儿偏激，但 make 实在是应该用在任何稍具规模的程序中的。希望本文可以为中国的 Unix 编程初学者提供一点儿有用的资料。中国的 Linux 用户除了学会安装红帽子以外， 实在应该尝试写一些有用的程序。个人想法，大家参考。

0) 介绍

~~~~~~~~~~~~~~~

本文将首先介绍为什么要将你的Ｃ源代码分离成几个合理的独立档案，什么时 候需要分，怎么才能分的好。然后将会告诉你 GNU Make 怎样使你的编译和连 接步骤自动化。对于其它 Make 工具的用户来说，虽然在用其它类似工具时要 做适当的调整，本文的内容仍然是非常有用的。如果对你自己的编程工具有怀 疑，可以实际的试一试，但请先阅读用户手册。

1) 多文件项目

~~~~~~~~~~~~~~~~~~~~~~

1.1为什么使用它们?

首先，多文件项目的好处在那里呢？

它们看起来把事情弄的复杂无比。又要 header 文件，又要 extern 声明，而且如果需要查找一个文件，你要在更多的文件里搜索。

但其实我们有很有力的理由支持我们把一个项目分解成小块。当你改动一行代码，编译器需要全部重新编译来生成一个新的可执行文件。但如果你的项目是分开在几个小文件里，当你改动其中一个文件的时候，别的源文件的目标文件(object files)已经存在，所以没有什么原因去重新编译它们。你所需要做的只是重现编译被改动过的那个文件，然后重新连接所有的目标文件罢了。在大型的项目中，这意味着从很长的（几分钟到几小时）重新编译缩短为十几，二十几秒的简单调整。

只要通过基本的规划，将一个项目分解成多个小文件可使你更加容易的找到一段代码。很简单，你根据代码的作用把你的代码分解到不同的文件里。当你要看一段代码时，你可以准确的知道在那个文件中去寻找它。

从很多目标文件生成一个程序包 (Library)比从一个单一的大目标文件生成要好的多。当然实际上这是否真是一个优势则是由你所用的系统来决定的。但是当使用 gcc/ld (一个 GNU C 编译／连接器) 把一个程序包连接到一个程序时，在连接的过程中，它会尝试不去连接没有使用到的部分。但它每次只能从程序包中把一个完整的目标文件排除在外。因此如果你参考一个程序包中某一个目标档中任何一个符号的话，那么这个目标文件整个都会被连接进来。要是一个程序包被非常充分的分解了的话，那么经连接后，得到的可执行文件会比从一个大目标文件组成的程序包连接得到的文件小得多。

又因为你的程序是很模块化的，文件之间的共享部分被减到最少，那就有很多好处--可以很容易的追踪到臭虫，这些模块经常是可以用在其它的项目里的，同时别人也可以更容易的理解你的一段代码是干 什么的。当然此外还有许多别的好处……

1.2 何时分解你的项目

很明显，把任何东西都分解是不合理的。象“世界，你们好”这样的简单程序根本就不能分，因为实在也没什么可分的。把用于测试用的小程序分解也是没什么意思的。但一般来说，当分解项目有助于布局、发展和易读性的时候，我都会采取它。在大多数的情况下，这都是适用的。（所谓“世界，你们好”，既 'hello world' ，只是一个介绍一种编程语言时惯用的范例程序，它会在屏幕上显示一行 'hello world' 。是最简单的程序。）

如果你需要开发一个相当大的项目，在开始前，应该考虑一下你将如何实现它，并且生成几个文件（用适当的名字）来放你的代码。当然，在你的项目开发的过程中，你可以建立新的文件，但如果你这么做的话，说明你可能改变了当初的想法，你应该想想是否需要对整体结构也进行相应的调整。

对于中型的项目，你当然也可以采用上述技巧，但你也可以就那么开始输入你的代码，当你的码多到难以管理的时候再把它们分解成不同的档案。但以我的经验来说，开始时在脑子里形成一个大概的方案，并且尽量遵从它，或在开发过程中，随着程序的需要而修改，会使开发变得更加容易。

1.3 怎样分解项目

先说明，这完全是我个人的意见，你可以（也许你真的会？）用别的方式来做。这会触动到有关编码风格的问题，而大家从来就没有停止过在这个问题上的争论。在这里我只是给出我自己喜欢的做法（同时也给出这么做的原因）：

i) 不要用一个 header 文件指向多个源码文件（例外：程序包 的 header 文件）。用一个 header定义一个源码文件的方式 会更有效，也更容易查寻。否则改变一个源文件的结构（并且 它的 header 文件）就必须重新编译好几个文件。

ii) 如果可以的话，完全可以用超过一个的 header 文件来指向同 一个源码文件。有时将不可公开调用的函数原型，类型定义 等等，从它们的Ｃ源码文件中分离出来是非常有用的。使用一 个 header 文件装公开符号，用另一个装私人符号意味着如果 你改变了这个源码文件的内部结构，你可以只是重新编译它而 不需要重新编译那些使用它的公开 header 文件的其它的源文 件。

iii) 不要在多个 header 文件中重复定义信息。 如果需要， 在其中一个 header 文件里 #include 另一个，但 是不要重复输入相同的 header 信息两次。原因是如果你以后改 变了这个信息，你只需要把它改变一次，不用搜索并改变另外一 个重复的信息。

iv) 在每一个源码文件里， #include 那些声明了源码文件中的符 号的所有 header 文件。这样一来，你在源码文件和 header 文件对某些函数做出的矛盾声明可以比较容易的被编译器发现。

1.4 对于常见错误的注释

a) 定义符 (Identifier) 在源码文件中的矛盾：在Ｃ里，变量和函数的缺 省状态是公用的。因此，任何Ｃ源码档案都可以引用存在于其它源 码档中的通用 (global) 函数和通用变量，既使这个档案没有那个变 量或函数的声明或原型。因此你必须保证在不同的两个档案里不能 用同一个符号名称，否则会有连接错误或者在编译时会有警告。

一种避免这种错误的方法是在公用的符号前加上跟其所在源文件有 关的前缀。比如：所有在 gfx.c 里的函数都加上前缀“gfx_”。如果 你很小心的分解你的程序，使用有意义的函数名称，并且不是过分 使用通用变量，当然这根本就不是问题。

要防止一个符号在它被定义的源文件以外被看到，可在它的定义前 加上关键字“static”。这对只在一个档案内部使用，其它档案都 都不会用到的简单函数是很有用的。

b) 多次定义的符号： header 档会被逐字的替换到你源文件里 #include 的位置的。因此，如果 header 档被 #include 到一个以上的源文件 里，这个 header 档中所有的定义就会出现在每一个有关的源码文件 里。这会使它们里的符号被定义一次以上，从而出现连接错误（见 上）。

解决方法： 不要在 header 档里定义变量。你只需要在 header 档里声明它们然后在适当的Ｃ源码文件（应该 #include 那个 header 档的那个）里定义它们（一次）。对于初学者来说，定义和声明是 很容易混淆的。声明的作用是告诉编译器其所声明的符号应该存在， 并且要有所指定的类型。但是，它并不会使编译器分配贮存空间。 而定义的做用是要求编译器分配贮存空间。当做一个声明而不是做 定义的时候，在声明前放一个关键字“extern”。

例如，我们有一个叫“counter”的变量，如果想让它成为公用的， 我们在一个源码程序（只在一个里面）的开始定义它：“int counter;”，再在相关的 header 档里声明它：“extern int counter;”。

函数原型里隐含着 extern 的意思，所以不需顾虑这个问题。

c) 重复定义，重复声明，矛盾类型：

请考虑如果在一个Ｃ源码文件中 #include 两个档 a.h 和 b.h， 而 a.h 又 #include 了 b.h 档（原因是 b.h 档定义了一些 a.h 需要的类型），会发生什么事呢？这时该Ｃ源码文件 #include 了 b.h 两次。因此每一个在 b.h 中的 #define 都发生了两次，每一 个声明发生了两次，等等。理论上，因为它们是完全一样的拷贝， 所以应该不会有什么问题，但在实际应用上，这是不符合Ｃ的语法 的，可能在编译时出现错误，或至少是警告。

解决的方法是要确定每一个 header 档在任一个源码文件中只被包 含了一次。我们一般是用预处理器来达到这个目的的。当我们进入 每一个 header 档时，我们为这个 header 档 #define 一个巨集 指令。只有在这个巨集指令没有被定义的前提下，我们才真正使用 该 header 档的主体。在实际应用上，我们只要简单的把下面一段 码放在每一个 header 档的开始部分：

#ifndef FILENAME_H

#define FILENAME_H

然后把下面一行码放在最后：

#endif

用 header 档的档名（大写的）代替上面的 FILENAME_H，用底线 代替档名中的点。有些人喜欢在 #endif 加上注释来提醒他们这个 #endif 指的是什么。例如：

#endif /* #ifndef FILENAME_H */

我个人没有这个习惯，因为这其实是很明显的。当然这只是各人的 风格不同，无伤大雅。

你只需要在那些有编译错误的 header 档中加入这个技巧，但在所 有的 header 档中都加入也没什么损失，到底这是个好习惯。

1.5 重新编译一个多文件项目

清楚的区别编译和连接是很重要的。编译器使用源码文件来产生某种 形式的目标文件(object files)。在这个过程中，外部的符号参考并 没有被解释或替换。然后我们使用连接器来连接这些目标文件和一些 标准的程序包再加你指定的程序包，最后连接生成一个可执行程序。 在这个阶段，一个目标文件中对别的文件中的符号的参考被解释，并 报告不能被解释的参考，一般是以错误信息的形式报告出来。

基本的步骤就应该是，把你的源码文件一个一个的编译成目标文件的格 式，最后把所有的目标文件加上需要的程序包连接成一个可执行文件。 具体怎么做是由你的编译器决定的。这里我只给出 gcc （GNU C 编译 器）的有关命令，这些有可能对你的非 gcc 编译器也适用。

gcc 是一个多目标的工具。它在需要的时候呼叫其它的元件（预处理 程序，编译器，组合程序，连接器）。具体的哪些元件被呼叫取决于 输入文件的类型和你传递给它的开关。

一般来说，如果你只给它Ｃ源码文件，它将预处理，编译，组合所有 的文件，然后把所得的目标文件连接成一个可执行文件（一般生成的 文件被命名为 a.out ）。你当然可以这么做，但这会破坏很多我们 把一个项目分解成多个文件所得到的好处。

如果你给它一个 -c 开关，gcc 只把给它的文件编译成目标文件， 用源码文件的文件名命名但把其后缀由“.c”或“.cc”变成“.o”。 如果你给它的是一列目标文件， gcc 会把它们连接成可执行文件， 缺省文件名是 a.out 。你可以改变缺省名，用开关 -o 后跟你指定 的文件名。

因此，当你改变了一个源码文件后，你需要重新编译它： 'gcc -c filename.c' 然后重新连接你的项目： 'gcc -o exec_filename *.o'。 如果你改变了一个 header 档，你需要重新编译所有 #include 过 这个档的源码文件，你可以用 'gcc -c file1.c file2.c file3.c' 然后象上边一样连接。

当然这么做是很繁琐的，幸亏我们有些工具使这个步骤变得简单。 本文的第二部分就是介绍其中的一件工具：GNU Make 工具。

（好家伙，现在才开始见真章。您学到点儿东西没？）

2) GNU Make 工具

~~~~~~~~~~~~~~~~

2.1 基本 makefile 结构

GNU Make 的主要工作是读进一个文本文件， makefile 。这个文 件里主要是有关哪些文件（‘target’目的文件）是从哪些别的 文件（‘dependencies’依靠文件）中产生的，用什么命令来进行 这个产生过程。有了这些信息， make 会检查磁碟上的文件，如果 目的文件的时间戳（该文件生成或被改动时的时间）比至少它的一 个依靠文件旧的话， make 就执行相应的命令，以便更新目的文件。 （目的文件不一定是最后的可执行档，它可以是任何一个文件。）

makefile 一般被叫做“makefile”或“Makefile”。当然你可以 在 make 的命令行指定别的文件名。如果你不特别指定，它会寻 找“makefile”或“Makefile”，因此使用这两个名字是最简单 的。

一个 makefile 主要含有一系列的规则，如下：

: ...

(tab)<command>

(tab)<command>

.

.

.

例如，考虑以下的 makefile ：

=== makefile 开始 ===

myprog : foo.o bar.o

gcc foo.o bar.o -o myprog

foo.o : foo.c foo.h bar.h

gcc -c foo.c -o foo.o

bar.o : bar.c bar.h

gcc -c bar.c -o bar.o

=== makefile 结束 ===

这是一个非常基本的 makefile -- make 从最上面开始，把上 面第一个目的，‘myprog’，做为它的主要目标（一个它需要保 证其总是最新的最终目标）。给出的规则说明只要文件‘myprog’ 比文件‘foo.o’或‘bar.o’中的任何一个旧，下一行的命令将 会被执行。

但是，在检查文件 foo.o 和 bar.o 的时间戳之前，它会往下查 找那些把 foo.o 或 bar.o 做为目标文件的规则。它找到的关于 foo.o 的规则，该文件的依靠文件是 foo.c, foo.h 和 bar.h 。 它从下面再找不到生成这些依靠文件的规则，它就开始检查磁碟 上这些依靠文件的时间戳。如果这些文件中任何一个的时间戳比 foo.o 的新，命令 'gcc -o foo.o foo.c' 将会执行，从而更新 文件 foo.o 。

接下来对文件 bar.o 做类似的检查，依靠文件在这里是文件 bar.c 和 bar.h 。

现在， make 回到‘myprog’的规则。如果刚才两个规则中的任 何一个被执行，myprog 就需要重建（因为其中一个 .o 档就会比 ‘myprog’新），因此连接命令将被执行。

希望到此，你可以看出使用 make 工具来建立程序的好处--前 一章中所有繁琐的检查步骤都由 make 替你做了：检查时间戳。 你的源码文件里一个简单改变都会造成那个文件被重新编译（因 为 .o 文件依靠 .c 文件），进而可执行文件被重新连接（因为 .o 文件被改变了）。其实真正的得益是在当你改变一个 header 档的时候--你不再需要记住那个源码文件依靠它，因为所有的 资料都在 makefile 里。 make 会很轻松的替你重新编译所有那 些因依靠这个 header 文件而改变了的源码文件，如有需要，再 进行重新连接。

当然，你要确定你在 makefile 中所写的规则是正确无误的，只 列出那些在源码文件中被 #include 的 header 档……

2.2 编写 make 规则 (Rules)

最明显的（也是最简单的）编写规则的方法是一个一个的查 看源码文件，把它们的目标文件做为目的，而Ｃ源码文件和被它 #include 的 header 档做为依靠文件。但是你也要把其它被这些 header 档 #include 的 header 档也列为依靠文件，还有那些被 包括的文件所包括的文件……然后你会发现要对越来越多的文件 进行管理，然后你的头发开始脱落，你的脾气开始变坏，你的脸 色变成菜色，你走在路上开始跟电线杆子碰撞，终于你捣毁你的 电脑显示器，停止编程。到低有没有些容易点儿的方法呢？

当然有！向编译器要！在编译每一个源码文件的时候，它实在应 该知道应该包括什么样的 header 档。使用 gcc 的时候，用 -M 开关，它会为每一个你给它的Ｃ文件输出一个规则，把目标文件 做为目的，而这个Ｃ文件和所有应该被 #include 的 header 文 件将做为依靠文件。注意这个规则会加入所有 header 文件，包 括被角括号(`<', `>')和双引号(`"')所包围的文件。其实我们可以 相当肯定系统 header 档（比如 stdio.h, stdlib.h 等等）不会 被我们更改，如果你用 -MM 来代替 -M 传递给 gcc，那些用角括 号包围的 header 档将不会被包括。（这会节省一些编译时间）

由 gcc 输出的规则不会含有命令部分；你可以自己写入你的命令 或者什么也不写，而让 make 使用它的隐含的规则（参考下面的 2.4 节）。

2.3 Makefile 变量

上面提到 makefiles 里主要包含一些规则。它们包含的其它的东 西是变量定义。

makefile 里的变量就像一个环境变量(environment variable)。 事实上，环境变量在 make 过程中被解释成 make 的变量。这些 变量是大小写敏感的，一般使用大写字母。它们可以从几乎任何 地方被引用，也可以被用来做很多事情，比如：

i) 贮存一个文件名列表。在上面的例子里，生成可执行文件的 规则包含一些目标文件名做为依靠。在这个规则的命令行 里同样的那些文件被输送给 gcc 做为命令参数。如果在这 里使用一个变数来贮存所有的目标文件名，加入新的目标 文件会变的简单而且较不易出错。

ii) 贮存可执行文件名。如果你的项目被用在一个非 gcc 的系 统里，或者如果你想使用一个不同的编译器，你必须将所 有使用编译器的地方改成用新的编译器名。但是如果使用一 个变量来代替编译器名，那么你只需要改变一个地方，其 它所有地方的命令名就都改变了。

iii) 贮存编译器旗标。假设你想给你所有的编译命令传递一组 相同的选项（例如 -Wall -O -g）；如果你把这组选项存 入一个变量，那么你可以把这个变量放在所有呼叫编译器 的地方。而当你要改变选项的时候，你只需在一个地方改 变这个变量的内容。

要设定一个变量，你只要在一行的开始写下这个变量的名字，后 面跟一个 = 号，后面跟你要设定的这个变量的值。以后你要引用 这个变量，写一个 $ 符号，后面是围在括号里的变量名。比如在 下面，我们把前面的 makefile 利用变量重写一遍：

=== makefile 开始 ===

OBJS = foo.o bar.o

CC = gcc

CFLAGS = -Wall -O -g

myprog : $(OBJS)

$(CC) $(OBJS) -o myprog

foo.o : foo.c foo.h bar.h

$(CC) $(CFLAGS) -c foo.c -o foo.o

bar.o : bar.c bar.h

$(CC) $(CFLAGS) -c bar.c -o bar.o

=== makefile 结束 ===

还有一些设定好的内部变量，它们根据每一个规则内容定义。三个 比较有用的变量是 $@, $< 和 $^ （这些变量不需要括号括住）。 $@ 扩展成当前规则的目的文件名， $< 扩展成依靠列表中的第 一个依靠文件，而 $^ 扩展成整个依靠的列表（除掉了里面所有重 复的文件名）。利用这些变量，我们可以把上面的 makefile 写成：

=== makefile 开始 ===

OBJS = foo.o bar.o

CC = gcc

CFLAGS = -Wall -O -g

myprog : $(OBJS)

$(CC) $^ -o $@

foo.o : foo.c foo.h bar.h

$(CC) $(CFLAGS) -c $< -o $@

bar.o : bar.c bar.h

$(CC) $(CFLAGS) -c $< -o $@

=== makefile 结束 ===

你可以用变量做许多其它的事情，特别是当你把它们和函数混合 使用的时候。如果需要更进一步的了解，请参考 GNU Make 手册。 ('man make', 'man makefile')

2.4 隐含规则 (Implicit Rules)

请注意，在上面的例子里，几个产生 .o 文件的命令都是一样的。 都是从 .c 文件和相关文件里产生 .o 文件，这是一个标准的步 骤。其实 make 已经知道怎么做--它有一些叫做隐含规则的内 置的规则，这些规则告诉它当你没有给出某些命令的时候，应该 怎么办。

如果你把生成 foo.o 和 bar.o 的命令从它们的规则中删除， make 将会查找它的隐含规则，然后会找到一个适当的命令。它的命令会 使用一些变量，因此你可以按照你的想法来设定它：它使用变量 CC 做为编译器（象我们在前面的例子），并且传递变量 CFLAGS （给 C 编译器，C++ 编译器用 CXXFLAGS ），CPPFLAGS （ C 预 处理器旗标）， TARGET_ARCH （现在不用考虑这个），然后它加 入旗标 '-c' ，后面跟变量 $< （第一个依靠名），然后是旗 标 '-o' 跟变量 $@ （目的文件名）。一个Ｃ编译的具体命令将 会是：

$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@

当然你可以按照你自己的需要来定义这些变量。这就是为什么用 gcc 的 -M 或 -MM 开关输出的码可以直接用在一个 makefile 里。

2.5 假象目的 (Phony Targets)

假设你的一个项目最后需要产生两个可执行文件。你的主要目标 是产生两个可执行文件，但这两个文件是相互独立的--如果一 个文件需要重建，并不影响另一个。你可以使用“假象目的”来 达到这种效果。一个假象目的跟一个正常的目的几乎是一样的， 只是这个目的文件是不存在的。因此， make 总是会假设它需要 被生成，当把它的依赖文件更新后，就会执行它的规则里的命令 行。

如果在我们的 makefile 开始处输入：

all : exec1 exec2

其中 exec1 和 exec2 是我们做为目的的两个可执行文件。 make 把这个 'all' 做为它的主要目的，每次执行时都会尝试把 'all' 更新。但既然这行规则里没有哪个命令来作用在一个叫 'all' 的 实际文件（事实上 all 并不会在磁碟上实际产生），所以这个规 则并不真的改变 'all' 的状态。可既然这个文件并不存在，所以 make 会尝试更新 all 规则，因此就检查它的依靠 exec1, exec2 是否需要更新，如果需要，就把它们更新，从而达到我们的目的。

假象目的也可以用来描述一组非预设的动作。例如，你想把所有由 make 产生的文件删除，你可以在 makefile 里设立这样一个规则：

veryclean :

rm *.o

rm myprog

前提是没有其它的规则依靠这个 'veryclean' 目的，它将永远 不会被执行。但是，如果你明确的使用命令 'make veryclean' ， make 会把这个目的做为它的主要目标，执行那些 rm 命令。

如果你的磁碟上存在一个叫 veryclean 文件，会发生什么事？这 时因为在这个规则里没有任何依靠文件，所以这个目的文件一定是 最新的了（所有的依靠文件都已经是最新的了），所以既使用户明 确命令 make 重新产生它，也不会有任何事情发生。解决方法是标 明所有的假象目的（用 .PHONY），这就告诉 make 不用检查它们 是否存在于磁碟上，也不用查找任何隐含规则，直接假设指定的目 的需要被更新。在 makefile 里加入下面这行包含上面规则的规则：

.PHONY : veryclean

就可以了。注意，这是一个特殊的 make 规则，make 知道 .PHONY 是一个特殊目的，当然你可以在它的依靠里加入你想用的任何假象 目的，而 make 知道它们都是假象目的。

2.6 函数 (Functions)

makefile 里的函数跟它的变量很相似--使用的时候，你用一个 $ 符号跟开括号，函数名，空格后跟一列由逗号分隔的参数，最后 用关括号结束。例如，在 GNU Make 里有一个叫 'wildcard' 的函 数，它有一个参数，功能是展开成一列所有符合由其参数描述的文 件名，文件间以空格间隔。你可以像下面所示使用这个命令：

SOURCES = $(wildcard *.c)

这行会产生一个所有以 '.c' 结尾的文件的列表，然后存入变量 SOURCES 里。当然你不需要一定要把结果存入一个变量。

另一个有用的函数是 patsubst （ patten substitude, 匹配替 换的缩写）函数。它需要３个参数--第一个是一个需要匹配的 式样，第二个表示用什么来替换它，第三个是一个需要被处理的 由空格分隔的字列。例如，处理那个经过上面定义后的变量，

OBJS = $(patsubst %.c,%.o,$(SOURCES))

这行将处理所有在 SOURCES 字列中的字（一列文件名），如果它的 结尾是 '.c' ，就用 '.o' 把 '.c' 取代。注意这里的 % 符号将匹 配一个或多个字符，而它每次所匹配的字串叫做一个‘柄’(stem) 。 在第二个参数里， % 被解读成用第一参数所匹配的那个柄。

2.7 一个比较有效的 makefile

利用我们现在所学的，我们可以建立一个相当有效的 makefile 。 这个 makefile 可以完成大部分我们需要的依靠检查，不用做太大 的改变就可直接用在大多数的项目里。

首先我们需要一个基本的 makefile 来建我们的程序。我们可以让 它搜索当前目录，找到源码文件，并且假设它们都是属于我们的项 目的，放进一个叫 SOURCES 的变量。这里如果也包含所有的 *.cc 文件，也许会更保险，因为源码文件可能是 C++ 码的。

SOURCES = $(wildcard *.c *.cc)

利用 patsubst ，我们可以由源码文件名产生目标文件名，我们需 要编译出这些目标文件。如果我们的源码文件既有 .c 文件，也有 .cc 文件，我们需要使用相嵌的 patsubst 函数呼叫：

OBJS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES)))

最里面一层 patsubst 的呼叫会对 .cc 文件进行后缀替代，产生的结 果被外层的 patsubst 呼叫处理，进行对 .c 文件后缀的替代。

现在我们可以设立一个规则来建可执行文件：

myprog : $(OBJS)

gcc -o myprog $(OBJS)

进一步的规则不一定需要， gcc 已经知道怎么去生成目标文件 (object files) 。下面我们可以设定产生依靠信息的规则：

depends : $(SOURCES)

gcc -M $(SOURCES) > depends

在这里如果一个叫 'depends' 的文件不存在，或任何一个源码文件 比一个已存在的 depends 文件新，那么一个 depends 文件会被生 成。depends 文件将会含有由 gcc 产生的关于源码文件的规则（注 意 -M 开关）。现在我们要让 make 把这些规则当做 makefile 档 的一部分。这里使用的技巧很像 C 语言中的 #include 系统--我 们要求 make 把这个文件 include 到 makefile 里，如下：

include depends

GNU Make 看到这个，检查 'depends' 目的是否更新了，如果没有， 它用我们给它的命令重新产生 depends 档。然后它会把这组（新） 规则包含进来，继续处理最终目标 'myprog' 。当看到有关 myprog 的规则，它会检查所有的目标文件是否更新--利用 depends 文件 里的规则，当然这些规则现在已经是更新过的了。

这个系统其实效率很低，因为每当一个源码文件被改动，所有的源码 文件都要被预处理以产生一个新的 'depends' 文件。而且它也不是 100% 的安全，这是因为当一个 header 档被改动，依靠信息并不会 被更新。但就基本工作来说，它也算相当有用的了。

2.8 一个更好的 makefile

这是一个我为我大多数项目设计的 makefile 。它应该可以不需要修 改的用在大部分项目里。我主要把它用在 djgpp 上，那是一个 DOS 版的 gcc 编译器。因此你可以看到执行的命令名、 'alleg' 程序包、 和 RM -F 变量都反映了这一点。

=== makefile 开始 ===

######################################

#

# Generic makefile

#

# by George Foot

# email: george.foot@merton.ox.ac.uk

#

# Copyright (c) 1997 George Foot

# All rights reserved.

# 保留所有版权

#

# No warranty, no liability;

# you use this at your own risk.

# 没保险，不负责

# 你要用这个，你自己担风险

#

# You are free to modify and

# distribute this without giving

# credit to the original author.

# 你可以随便更改和散发这个文件

# 而不需要给原作者什么荣誉。

# （你好意思？）

#

######################################

### Customising

# 用户设定

#

# Adjust the following if necessary; EXECUTABLE is the target

# executable's filename, and LIBS is a list of libraries to link in

# (e.g. alleg, stdcx, iostr, etc). You can override these on make's

# command line of course, if you prefer to do it that way.

#

# 如果需要，调整下面的东西。 EXECUTABLE 是目标的可执行文件名， LIBS

# 是一个需要连接的程序包列表（例如 alleg, stdcx, iostr 等等）。当然你

# 可以在 make 的命令行覆盖它们，你愿意就没问题。

#

EXECUTABLE := mushroom.exe

LIBS := alleg

# Now alter any implicit rules' variables if you like, e.g.:

#

# 现在来改变任何你想改动的隐含规则中的变量，例如

CFLAGS := -g -Wall -O3 -m486

CXXFLAGS := $(CFLAGS)

# The next bit checks to see whether rm is in your djgpp bin

# directory; if not it uses del instead, but this can cause (harmless)

# `File not found' error messages. If you are not using DOS at all,

# set the variable to something which will unquestioningly remove

# files.

#

# 下面先检查你的 djgpp 命令目录下有没有 rm 命令，如果没有，我们使用

# del 命令来代替，但有可能给我们 'File not found' 这个错误信息，这没

# 什么大碍。如果你不是用 DOS ，把它设定成一个删文件而不废话的命令。

# （其实这一步在 UNIX 类的系统上是多余的，只是方便 DOS 用户。 UNIX

# 用户可以删除这５行命令。）

ifneq ($(wildcard $(DJDIR)/bin/rm.exe),)

RM-F := rm -f

else

RM-F := del

endif

# You shouldn't need to change anything below this point.

#

# 从这里开始，你应该不需要改动任何东西。（我是不太相信，太ＮＢ了！）

SOURCE := $(wildcard *.c) $(wildcard *.cc)

OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE)))

DEPS := $(patsubst %.o,%.d,$(OBJS))

MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))

MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \

$(patsubst %.d,%.cc,$(MISSING_DEPS)))

CPPFLAGS += -MD

.PHONY : everything deps objs clean veryclean rebuild

everything : $(EXECUTABLE)

deps : $(DEPS)

objs : $(OBJS)

clean :

@$(RM-F) *.o

@$(RM-F) *.d

veryclean: clean

@$(RM-F) $(EXECUTABLE)

rebuild: veryclean everything

ifneq ($(MISSING_DEPS),)

$(MISSING_DEPS) :

@$(RM-F) $(patsubst %.d,%.o,$@)

endif

-include $(DEPS)

$(EXECUTABLE) : $(OBJS)

gcc -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS))

=== makefile 结束 ===

有几个地方值得解释一下的。首先，我在定义大部分变量的时候使 用的是 := 而不是 = 符号。它的作用是立即把定义中参考到的函 数和变量都展开了。如果使用 = 的话，函数和变量参考会留在那 儿，就是说改变一个变量的值会导致其它变量的值也被改变。例 如：

A = foo

B = $(A)

# 现在 B 是 $(A) ，而 $(A) 是 'foo' 。

A = bar

# 现在 B 仍然是 $(A) ，但它的值已随着变成 'bar' 了。

B := $(A)

# 现在 B 的值是 'bar' 。

A = foo

# B 的值仍然是 'bar' 。

make 会忽略在 # 符号后面直到那一行结束的所有文字。

ifneg...else...endif 系统是 makefile 里让某一部分码有条件的 失效／有效的工具。 ifeq 使用两个参数，如果它们相同，它把直 到 else （或者 endif ，如果没有 else 的话）的一段码加进 makefile 里；如果不同，把 else 到 endif 间的一段码加入 makefile （如果有 else ）。 ifneq 的用法刚好相反。

'filter-out' 函数使用两个用空格分开的列表，它把第二列表中所 有的存在于第一列表中的项目删除。我用它来处理 DEPS 列表，把所 有已经存在的项目都删除，而只保留缺少的那些。

我前面说过， CPPFLAGS 存有用于隐含规则中传给预处理器的一些 旗标。而 -MD 开关类似 -M 开关，但是从源码文件 .c 或 .cc 中 形成的文件名是使用后缀 .d 的（这就解释了我形成 DEPS 变量的 步骤）。DEPS 里提到的文件后来用 '-include' 加进了 makefile 里，它隐藏了所有因文件不存在而产生的错误信息。

如果任何依靠文件不存在， makefile 会把相应的 .o 文件从磁碟 上删除，从而使得 make 重建它。因为 CPPFLAGS 指定了 -MD ， 它的 .d 文件也被重新产生。

最后， 'addprefix' 函数把第二个参数列表的每一项前缀上第一 个参数值。

这个 makefile 的那些目的是（这些目的可以传给 make 的命令行 来直接选用）：

everything:（预设） 更新主要的可执行程序，并且为每一个 源码文件生成或更新一个 '.d' 文件和一个 '.o' 文件。

deps: 只是为每一个源码程序产生或更新一个 '.d' 文件。

objs: 为每一个源码程序生成或更新 '.d' 文件和目标文件。

clean: 删除所有中介／依靠文件（ *.d 和 *.o ）。

veryclean: 做 `clean' 和删除可执行文件。

rebuild: 先做 `veryclean' 然后 `everything' ；既完全重建。

除了预设的 everything 以外，这里头只有 clean ， veryclean ， 和 rebuild 对用户是有意义的。

我还没有发现当给出一个源码文件的目录，这个 makefile 会失败的 情况，除非依靠文件被弄乱。如果这种弄乱的情况发生了，只要输入 `make clean' ，所有的目标文件和依靠文件会被删除，问题就应该 被解决了。当然，最好不要把它们弄乱。如果你发现在某种情况下这 个 makefile 文件不能完成它的工作，请告诉我，我会把它整好的。

3 总结

~~~~~~~~~~~~~~~

我希望这篇文章足够详细的解释了多文件项目是怎么运作的，也说明了 怎样安全而合理的使用它。到此，你应该可以轻松的利用 GNU Make 工 具来管理小型的项目，如果你完全理解了后面几个部分的话，这些对于 你来说应该没什么困难。

GNU Make 是一件强大的工具，虽然它主要是用来建立程序，它还有很多 别的用处。如果想要知道更多有关这个工具的知识，它的句法，函数， 和许多别的特点，你应该参看它的参考文件 (info pages, 别的 GNU 工具也一样，看它们的 info pages. )。

4.配置IDE开发环境

我通常都是用的Microsoft Visual Studio作为我写程序的IDE.所以,我在这里也是通过Visual Studio.NET作为例子来讲解如何使用VC7.0来配置你开发GBA程序的平台.

通过上面的资料,你应该有能力为自己的GBA项目写makefile了.即使不会写,也可以抄写AgbLib中许多samples的makefile.有了makefile,下面的工作就很简单了.

第一步,从菜单"文件"->"新建"->"项目"得到的"新建项目"对话框

选中对话框中的"生成文件项目",再自己设置好你项目的路径.确定后

然后出现"生成文件应用程序向导"对话框,先不用管上面的设置.单击确定

建立好项目后,鼠标右键单击"解决方法资源管理器"中项目的名称,再选中"属性".弹出属性对话框.

选中"NMAKE"

在"生成命令行"中输入path=c:\cygwin\bin;%path%

make

注意你的编译器的路径,上面设置的是c:\cygwin\bin,可能你的cygwin不在c:盘,也可能你用的不是cygwin中的gcc,但是只要设置好你的编译器路径就好.

确定后就大工告成了.你就可以在VC.NET下做你的GBA游戏了.

金点 tangl_99

2003/1/18


GBA探索日记(5)(DMA)

这一篇的内容其实是为后一篇播放声音的部分作预备.播放声音的部分一共有三个预备篇DMA,Timer,V-Blank中断.这是第一个预备篇(DMA).

声音和中断一直是困惑GBA开发者的两大难题.GBA中播放声音需要使用中断,而GBA的中断设置又很复杂.这篇日记我从简单的几个方面来讲解播放声音的时候需要的预备知识.播放声音的方法其实是很简单的,但是里面涉及到另外的一些东西,如果不把这一节的东西搞清楚,那么后面播放声音的时候就会感到迷惑.

在讲解播放声音之前我需要讲解三个预备的东西:

DMA传输,Timer定时器,V-Blank中断.

最后我还会给出使用中断服务程序的方法

DMA传输

前面的日记里已经提到过一个AgbLib里函数(更确切地说应该是宏)DmaArrayCopy.

它就是使用GBA里面的DMA通道来传输数据的.

DMA通道不仅在GBA里面有,在我们平时使用的计算机里面更是普遍.

它是一种专门传输数据的线路,它传输的数据可以不经过CPU的访问,所以它传输数据的速度是比一般的传输方式快得多.但对于一般的程序员来说,DmaArrayCopy的使用方式和memcpy没有多少区别,只是效率更高.

现在让我们具体来看看GBA里面的DMA.

GBA里面有4条DMA通道

其优先权从高到底如下:DMA0,DMA1和DMA2,DMA3

如果较高优先级的DMA通道开始运行的时候,有较低优先级的DMA已经在运行,那么较地的DMA就回停止运行,而先运行较高的优先权的DMA.当优先级较高的DMA运行完后,原先优先级较低的DMA从它停止的地方的继续开发运行.

DMA0

通常我们并不使用它,它有最高的优先级,不会被其它DMA通道打断,所以它通常是用来传输一些可靠性很高的数据,而当成horizontal-blanking DMA来用.

DMA1,DMA2

就是我们播放声音的时候需要用到的两个DMA通道.

DMA3

可以用来传输任何数据,完全可以被当成memcpy一样的函数来用,而AgbLib中的DmaArrayCopy中就是使用DMA3.

我们以使用最多的DMA3为例,来看看如何使用DMA传输数据.

我们大致需要做4件事情就可以使用DMA通道了

1.设置要传输的数据的地址到DMA的数据源地址寄存器

2.设置数据要传输到目的地址到DMA的数据目的地址寄存器

3.设置数据量到DMA的数据量寄存器

4.设置DMA传输属性的寄存器

如果你看过前面几篇探索日记,你会发现GBA里面很多硬件方法的使用都是通过设置寄存器来完成的,通过简单的写内存(有些地方不应该叫内存)就可以完成了.

让我们来看看 DMA3 的寄存器

1) Source Address 数据源地址寄存器

这里一共用了28位来保存数据的源地址,源地址的范围是00000000h-0FFFFFFFh(几乎是全部)

2) Destination Address 数据目的地址寄存器

同样,一共使用了28位来记录数据传输的目的地址,目的地址的范围是00000000h-0FFFFFFFh.

3) Word Count 数据量寄存器

一共用了16位,数据量的范围是1-10000h 字节.

单位是字,两个字节.

如果设置的是16位传输,总共有10000h * 2 =20000h字节传输

如果设置的是32位传输,总共有1000h * 4 =40000h字节可以传输

特别指出的是,通常这个寄存器的值就是要传输数据的字节数.

但是当这个寄存器的值是0的时候,表示要传输的数据为10000h或40000h字节数.

4) DMA Control DMA属性寄存器

下面的属性说明是从官方开发包里翻译而来,其实也没有多大必要仔细看.

DM3CNT_H 第15位: DMA开关

设置成0,表示不能使用DMA

设置成1,表示开始使用DMA,并且当DMA传输完毕数据后,数据源地址寄存器和数据目的寄存器回复到上次的设置.

需要注意的是,一旦设置成1后,DMA马上就开始根据前面三个寄存器的设置开始传输数据.所以你应该想设置好上面三个寄存器和其它DMA属性后,最后设置这个开关.

DM3CNT_H 第14位: 中断要求开关

设置成0,表示不使用中断

设置成1,表示使用中断

这里的中断是在数据全部传输完毕后产生.

DM3CNT_H 第13-12位 传输时间控制

DM3CNT_H 第10位 传输方式

设置成0,表示以16位的方式传输

设置成1,表示以32位的方式传输

好!理论部分就讲完了,下面让我们看看实例代码

首先是寄存器的定义

#define REG_DMA0SAD *(u32*)0x40000B0

#define REG_DMA0DAD *(u32*)0x40000B4

#define REG_DMA0CNT *(u32*)0x40000B8

#define REG_DMA1SAD *(u32*)0x40000BC

#define REG_DMA1DAD *(u32*)0x40000C0

#define REG_DMA1CNT *(u32*)0x40000C4

#define REG_DMA2SAD *(u32*)0x40000C8

#define REG_DMA2DAD *(u32*)0x40000CC

#define REG_DMA2CNT *(u32*)0x40000D0

#define REG_DMA3SAD *(u32*)0x40000D4

#define REG_DMA3DAD *(u32*)0x40000D8

#define REG_DMA3CNT *(u32*)0x40000DC

然后属性的定义

#define DMA_ENABLE 0x80000000

#define DMA_INTERUPT_ENABLE 0x40000000

#define DMA_TIMEING_IMMEDIATE 0x00000000

#define DMA_TIMEING_VBLANK 0x10000000

#define DMA_TIMEING_HBLANK 0x20000000

#define DMA_TIMEING_SYNC_TO_DISPLAY 0x30000000

#define DMA_16 0x00000000

#define DMA_32 0x04000000

#define DMA_REPEATE 0x02000000

#define DMA_SOURCE_INCREMENT 0x00000000

#define DMA_SOURCE_DECREMENT 0x00800000

#define DMA_SOURCE_FIXED 0x01000000

#define DMA_DEST_INCREMENT 0x00000000

#define DMA_DEST_DECREMENT 0x00200000

#define DMA_DEST_FIXED 0x00400000

#define DMA_DEST_RELOAD 0x00600000

#define DMA_32NOW DMA_ENABLE | DMA_TIMEING_IMMEDIATE |DMA_32

#define DMA_16NOW DMA_ENABLE | DMA_TIMEING_IMMEDIATE |DMA_16

里面有很多我们用不着.

下面直接看看这个函数就知道了

void DMA_Copy(u8 channel, void* source, void* dest, u32 WordCount, u32 mode)

{

switch (channel)

{

case 0:

REG_DMA0SAD = (u32)source;

REG_DMA0DAD = (u32)dest;

REG_DMA0CNT = WordCount | mode;

break;

case 1:

REG_DMA1SAD = (u32)source;

REG_DMA1DAD = (u32)dest;

REG_DMA1CNT = WordCount | mode;

break;

case 2:

REG_DMA2SAD = (u32)source;

REG_DMA2DAD = (u32)dest;

REG_DMA2CNT = WordCount | mode;

break;

case 3:

REG_DMA3SAD = (u32)source;

REG_DMA3DAD = (u32)dest;

REG_DMA3CNT = WordCount | mode;

break;

}

}

看起来很简单吧.

我们使用的时候也很简单,比如我要把的图片数据传输到VRAM中显示:

DMA_Copy(3,(void*)landbmp_gfx,(void*)VideoBuffer,19200/2,DMA_32NOW);

金点 tangl_99

2003/1/24


GBA探索日记(6)(Timer)

这一节也是将来实现声音播放的基础部分.这一部分的东西十分简单,不过后一节关于中断的部分就有点困难,我们只需要知道怎么用就可以了,所以这一节总体是十分简单的.

首先我要自我介绍一下.写了这么多日记,大家可能还不知道我是谁.

我叫tangl_99,email:tangl_99@sohu.com,QQ:8664220(Min-Kyeong)

如果你在做GBA过程中有什么问题,可以联系我,我会尽可能地帮助你(只要你不是日本人).

GBA中的计时器有4个.就是Timer0,Timer1,Timer2,Timer3.

也是很简单地通过读写寄存器就可以实现对它们的使用了.你可以对它们任意使用.不过需要提醒的是Timer0,Timer1通常用来控制声音播放的速度.

每个Timer有两个寄存器来实现使用和控制.

我们先来看看Timer的计数器寄存器.

TMXD就是TimerX的计数器的寄存器.它们都是16位的寄存器,从Address可以看到它们的总线地址,从Attributes可以看出它们都是可读可写的寄存器.

很奇怪的第一点,在官方开发包里对应它们的定义的名字却不是TMXD,而是REG_TMXCNT_L,比如看看Timer0的计数器的定义.

#define REG_TM0CNT_L (REG_BASE + 0x100)

有了计数器,你通过对它的读写,就可以知道现在时间走了多少.

然后我们来看看Timer的属性设置寄存器

TMXCNT就是TimerX的属性寄存器.它们的属性很简单,我们大致来看看吧.

第7位的Timer Operation Flag就是Timer的开关.

如果设置成0,就是关于这个Timer;如果设置成1,就是开始使用这个Timer.

第6位是Timer中断的开关.

Timer的中断是当Timer的计数器产生一个溢出,就产生一个中断.溢出简单地说就是前面的Timer的计数器的值超过了最大值(0xFFFF).至于如何编写中断服务程序我将在后面说使用的方法.

第2位是Count-up Timing表明是否需要使用Prescalar Selection的协调

设置成0,表示使用后面的Prescalar Selection.

设置成1的时候我就不知道是什么意思了,我也从来没有用过,如果你有兴趣,你可以研究一下.AgbLib中这么描述它的.

With a setting of 0, count-up is performed in accordance with the prescalar specification in [d01-00]. With a setting of 1, overflow of the timer channel one number lower starts a count-up regardless of the prescalar specification.

This mode is suitable for purposes such as time measurement over relatively long periods.

The count-up timing specification is disabled for Timer 0, which counts up in accordance with the prescalar specification

我们一般都不管它,一般都设置0,使用.

不过对于Timer0这个选项是无效的.

第0,1位是Prescalar Selection,就是Timer的时间标准.

所有的Prescalar都是以GBA的16.78MHz的CPU时钟频率为基础

Setting Prescalar (Count-Up Interval)

00 System clock (59.595 ns)

01 64 cycles of system clock ( 3.814 us)

10 256 cycles of system clock (15.256 us)

11 1024 cycles of system clock (61.025 us)

其中ns是1/10^9秒,us是1/10^6秒.

可以说都是很快的了.

比如我把它设置成11,那么每61.025 us,Timer的计数器就自加1.同理设置成10那么就是15.256 us,Timer的计数器就自加1.

好了,我们来看看一个例子,写个延时的函数(相当于Windows里的Sleep函数)

#define REG_TM0CNT (REG_BASE + 0x100) // Timer 0

#define REG_TM0CNT_L (REG_BASE + 0x100)

#define REG_TM0CNT_H (REG_BASE + 0x102)

#define REG_TM1CNT (REG_BASE + 0x104) // Timer 1

#define REG_TM1CNT_L (REG_BASE + 0x104)

#define REG_TM1CNT_H (REG_BASE + 0x106)

#define REG_TM2CNT (REG_BASE + 0x108) // Timer 2

#define REG_TM2CNT_L (REG_BASE + 0x108)

#define REG_TM2CNT_H (REG_BASE + 0x10a)

#define REG_TM3CNT (REG_BASE + 0x10c) // Timer 3

#define REG_TM3CNT_L (REG_BASE + 0x10c)

#define REG_TM3CNT_H (REG_BASE + 0x10e)

再次提醒大家,REG_TMXCNT_H是属性控制寄存器,REG_TMXCNT_L是计数器.

void wait(u16 ms)

{

//Start the timer

*(vu16 *)REG_TM3CNT_H = 1|2 | 128;

//zero the timer

*(vu16 *)REG_TM3CNT_L = 0;

while(ms--)

{

while(*(vu16 *)REG_TM3CNT_L <= 16.386){} //wait

*(vu16 *)REG_TM3CNT_L = 0; //reset the timmer

}

}

这个函数很简单,参数ms是需要延时的毫秒数.

至于里面的16.386是因为16.386*61.025us近似等于1ms.

你可以把这个函数写到你的程序里面,然后用wait(1000),就可以延长一秒钟了.很简单吧.

金点 tangl_99

2003/2/9


GBA探索日记(7)(Interrupt)

这一节是简单地讲讲关于中断的使用.声音的播放就必修使用有关时间的中断.GBA里面有14个硬件中断可以使用,但其实我们一般都只使用其中的一个就够了.我在这里将介绍两种使用中断的方法,希望对你能有所帮助.

首先我介绍一个十分简单的使用中断的方法.

先看看这张图片

图中看到Work RAM的顶头03007ffc的地址就是保存中断处理函数地址的地方.

在GBA程序里,你只需要简单地把你的中断处理函数的地址传输到这个32位内存块中就行了.

然后我们再看看有关中断控制的寄存器

1.REG_IME 硬件中断总开关

When this flag is 0, all interrupts are disabled.

当REG_IME设置成0,所有中断都禁止

When 1, the setting for interrupt enable register IE is enabled

当REG_IME设置成1,允许使用中断.

2.REG_IE 具体中断开关

With the interrupt enable register, each hardware interrupt can be individually masked

我想图中已经很清楚地显示出来它的设置方法了.

当对应位设置成1,表示开起该中断,当对应位设置成0,表示关闭改中断.

除了Timer和V -Blank中断,其它中断我们一般不用,所以也不必太在意具体的设置.

Timer中断就是简单的时间中断.前一篇日记中具体讲解了Timer的使用.当一个Timer计数器自增到溢出时,系统就会产生个Timer中断.

V-Blank中断是个关于显示扫描的中断.具体时间是频率是59.727 Hz,每16.743 ms一次.由于它的频率跟60Hz很接近,而且很稳定,所以AgbLib官方开发包里面也把它当成了控制声音播放的时间中断来使用. 再后面关于声音的播放部分,我也将使用它作为声音播放的时间控制. 当然,你也可以使用标准的Timer来控制时间,不过对于声音播放这种精度要求不高的控制来所,V-Blank中断完全足够了.

3.REG_IF

When an interrupt request signal is generated from each hardware device, the corresponding interrupt request flag is set in the IF Register

当一个中断信号由硬件产生,相应的中断标志就会出现在REG_IF寄存器里面.这样你就知道该中断是从那里产生的.

首先要说明的是,REG_IF是个只读寄存器.通过它你可以知道你的中断服务函数是由什么中断引起的.

好了,理论部分就讲完了.下面我们看看实例代码.

#include <agb.h>

void Interrupts(void);

int main()

{

REG_IME = 0x00; // Disable interrupts

REG_INTERUPT = (u32)Interrupts;// Set interrupt proc address (IRQHandler)

REG_IE |= 1; // Enable V-Blank IRQ.

REG_DISPSTAT |= 1<<3; // Enable Display V-Blank IRQ also.

REG_IME = 1; // Enable interrupts

while(1)

{

}

}

void Interrupts(void)

{

REG_IME = 0x00; // Disable interrupts

if((REG_IF & 1) == 1) // 如果是V-Blank中断

{

// 处理V-Blank中断的代码......

}

REG_IME = 1; // Re-Enable interrups

}

上面中interrupts()函数首先必须将REG_IME=0x00中断总开关关掉,然后是处理中断响应的代码,最后再重新开起中断开关.当重新开起中断开关后,REG_IF就自动回复到0,也就是说,你对REG_IF的访问必须是在重新开起中断总开关REG_IME前.

在处理V-Blank中断的时候设计到另一个寄存器REG_DISPSTAT.通常我们如果要使用V-BLANK中断,需要把它设置成REG_DISPSTAT |= 1<<3; // Enable Display V-Blank IRQ also.

下面我将简单地讲一下官方开发包里面使用中断的方法:

AgbLib的demo,sample的代码里面都有个crt0.s的汇编程序.里面的代码除了设置GBA ROM的版本信息等,主要的就是设置中断向量表.具体的设置方法其实我不清楚,我没有学过Arm汇编,对于这个crt0.s也是无能为力.但正是这个crt0.s给我们的编译器带来了很多麻烦.比如AgbLib的crt0.s覆盖了常规C语言的入口函数.一般C语言都是从main函数入口,而AgbLib的demo,sample都是从AgbMain函数入口.而且AgbLib的代码在编译的时候去掉了一些常规C语言库函数,弄得我们不得不自己再去配置我们的gcc.所以我的建议是不要使用这种设置中断的方法,而使用前面讲解的简单使用中断的方法,那样你就可以避开使用crt0.s.编译器最好是DevKitAdv.它是个为开发GBA而已经配置好了的gcc.拿来就可以用.

本来我想把官方开发包里面使用中断的方法详细在这里讲解出来的,但是很不好意思地告诉大家,其实我本人对于官方开发包里crt0.s处理中断的代码都看不懂,所以无法详细说明了.

AgbLib里面的crt0.s加入了处理中断向量表的代码.C程序里面就只需定义一个中断向量表,每个中断的产生就会自动地启动处理该种中断的函数,而不是通过对REG_IF寄存器的访问.应该说速度更快些.如果你有兴趣,我可以告诉你具体的使用方法.我的QQ是8664220,Email是tangl_99@sohu.com.

好!就到这里,下一部分我们就开始声音播放的讲解了.有了这3篇日记的基础后,声音播放应该就是很简单了.等声音播放的讲解完后,我们对GBA的基础介绍大致就告一段落了.不过我们对于GBA的探索还没有完,后面我将介绍一些开发中常遇到的问题和解决方法,还包括对前面日记没有讲解到的补充以及扩充.

金点 tangl_99

2003/2/19


GBA探索日记(8)(声音播放1)

有了前几篇关于中断和Timer,DMA传输的介绍后,现在我们可以进入GBA声音播放的部分了.GBA一共有6个声道,从Sound 1-4加上两个Direct Sound A(and)B.GBA中对声音的播放应该是最困难的部分.至少我是这么认为,因为这里面涉及到太多的技术细节.所以我在这里只能告诉大家怎么在GBA中播放声音,至于内部的细节,我就无可奉告了.

如果你手上有任天堂的官方开发包AgbLib3.0,那么看看AgbLib3.0中有关声音的播放部分.任天堂公司在官方开发包里提供了个mediaPlayer2000来实现GBA声音的播放.那个mediaPlayer2000是个十分复杂的东西,需要配置很复杂的ini配置文件.光是看配置ini文件的帮助文件都够我们看几天了.为此,在官方开发包里面还专门附加了mediaPlyaer2000的帮助部分.不过最大的问题是,它的mediaPlayer2000播放声音的库文件无法在一般的GCC下在编译.或许是我所得到的官方开发包不完整(只有个AgbLib,GCC是另外自己搭配的),总之,那个mediaPlayer2000是不能用了.

前面的<<GBA探索日记>>都是以官方开发包为基础而写的,内容是十分贴近AgbLib.但是这一节由于官方开发包里的mediaPlayer2000无法使用,所以我们就必须另开辟一条道路来走了.还好,这条路我已经走通了,大家也可以顺着这个方向前进.

这一部分关于声音播放的内容你也可以跳过前面的理论讲解.在后面我会直接把所有的声音播放的代码完整得公布在下一篇的探索日记中.如果你觉得麻烦,也可以直接看后面怎么使用这些播放库函数就可以了.

我们首先看看这张有关声道的内部结构图:

这里我们看到,Sound 1-4跟Direct Sound A和B有着不同的播放渠道.Direct Sound播放是需要通过DMA实现声音数据的传输.而传输速度就必须由Timer来控制.这也就是为什么我要在前面几节讲解DMA和Timer了.

我们这里主要看Direct Sound的声音播放.因为它的功能是最强大的.而Sound 1-4各个播放的声音数据都不同,而且使用起来很复杂,所以这里不讲了,如果你有兴趣,可以参考官方开发包.

Direct Sounds A and B 播放的是8位线性的音频数据

它们的数据入口就是两个寄存器SGFIFOA(B)_L和SGFIFOA(B)_H

每个Direct Sound一次可以接受4个字节的数据.

然后我们看看声音播放的控制寄存器

很复杂吧.其实我已经不只一次地给出这样的寄存器图表了.这些图片都是官方开发包里面的.它的说明都是很齐全的.不过我还是要说几句.

Direct Sound的播放一定要和Timer0或者1,DMA联系在一起.其中

Timer Selection for Direct Sound A或者B

就是指定该Direct Sound所使用的Timer了.而Timer只能是Timer 1或者Timer 0.

Output of Direct Sound A或者B

是选择每个Direct Sound左右声道的使用.其实这个选择没有多大用处.我试过,无论你选哪个,在GBA上播放的效果都差不多.所以我们一般都是把每个Direct Sound左右声道都开起.

Direct Sound FIFO A(B) Clear and Sequencer Reset

一般是初始化Direct Sound时候使用.FIFO是指先进先出接口.

下面让我们来做个实例来看看具体要做的步骤.

首先,我们要有个转换工具wav2gbac.exe. 它可以把我们电脑上的*.wav文件转换成一个*.h头文件.但是得注意,你的*.wav文件最好是8位音频,因为GBA上的Direct Sound只能播放8位的音频.即使8位以上的wav文件,但是播放出来还是只有8位的效果.

wav2gbac.exe这个工具可以在www.gbadev.org上下载.是个DOS下的程序.

我们在dos提示符下输入:

wav2gbac xxx.wav xxx.h

这样它就把xxx.wav音频数据转换到xxx.h里的一个大数组里了.

好了,我们已经得到转换后的音频数据.下面我们来看看如何写我们的声音播放函数.

void PlayDirectSoundA(u8 *sound, u16 sampleRate, u32 length)

{

//Stop any previous sample

*(vu16 *)REG_TM0CNT_H = 0;

*(vu16 *)REG_TM0CNT_L = 0;

*(vu32 *)REG_DMA1SAD = 0;

*(vu16 *)REG_DMA1CNT_H = 0;

*(vu32 *)REG_DMA1DAD = 0;

//Output DirectSound A to right channel

*(vu16 *)REG_SOUNDCNT_H |= DSOUND_A_RIGHT_CHANNEL| DSOUND_A_TIMER_0 | DSOUND_A_LEFT_CHANNEL | DSOUND_A_FIFO_RESET | DSOUND_A_OUTPUT_FULL;

//Enable all sound

*(vu16 *)REG_SOUNDCNT_X |= SOUND_MASTER_ENABLE;

//DMA1 Source Addresss

*(vu32 *)REG_DMA1SAD = (u32)sound;

//DMA1 Destination Address (REG_SGFIFOA)

*(vu32 *)REG_DMA1DAD = 0x40000A0;

//Write 32 bits into 0x040000A0 (REG_SGFIF0A) every VSync

*(vu32 *)REG_DMA1CNT = DMA_DEST_FIXED | DMA_REPEATE | DMA_32 | DMA_TIMEING_SYNC_TO_DISPLAY | DMA_ENABLE;

//Sample Rate

*(vu16 *)REG_TM0CNT_L = 65536 - (16777216/sampleRate);

//Enable the timer

*(vu16 *)REG_TM0CNT_H = TIMER_ENABLE | TIMER_IRQ;

}

这是个播放Direct Sound A声道的播放函数.

首先我们来看函数的参数,u8 *sound,u16 sampleRate,u32 length这些都是我们的声音转换工具wav2gbac.exe自动创建了的.你只要简单地把声音文件xxx.h的参数传递过来就可以了.

在播放声音之前,我们得先重至一次所需要的一切资源.这就是下面的代码完成的功能:

//Stop any previous sample

*(vu16 *)REG_TM0CNT_H = 0;

*(vu16 *)REG_TM0CNT_L = 0;

*(vu32 *)REG_DMA1SAD = 0;

*(vu16 *)REG_DMA1CNT_H = 0;

*(vu32 *)REG_DMA1DAD = 0;

我使用了Timer0,DMA1作为我们播放Direct Sound A的辅助工具.这里的Timer,DMA都是可以自己选择的.但是需要注意的是Timer只能是Timer0或者Timer1,DMA也只能是DMA1或者DMA2.我们一般让Timer0,DMA1作为Direct Sound A的辅助控制,Timer1,DMA2作为Direct Sound B的辅助控制.

//Output DirectSound A to right channel

*(vu16 *)REG_SOUNDCNT_H |= DSOUND_A_RIGHT_CHANNEL| DSOUND_A_TIMER_0 | DSOUND_A_LEFT_CHANNEL | DSOUND_A_FIFO_RESET | DSOUND_A_OUTPUT_FULL;

这是设置前面已经讲到的声音播放的控制寄存器.你可以根据前面的图表来自己设置.不过一般我们都使用上面代码的设置.如果你搞不清楚,在后面我会把完整的Direct Sound库函数给出来.下面是上面使用到的参数定义(其实就是C语言中的宏)

#define BIT00 1

#define BIT01 2

#define BIT02 4

#define BIT03 8

#define BIT04 16

#define BIT05 32

#define BIT06 64

#define BIT07 128

#define BIT08 256

#define BIT09 512

#define BIT10 1024

#define BIT11 2048

#define BIT12 4096

#define BIT13 8192

#define BIT14 16384

#define BIT15 32768

//SGCNT0_H

#define SOUND_OUTPUT_1_4 0

#define SOUND_OUTPUT_1_2 BIT00

#define SOUND_OUTPUT_FULL BIT01

#define DSOUND_A_OUTPUT_HALF 0

#define DSOUND_A_OUTPUT_FULL BIT02

#define DSOUND_B_OUTPUT_HALF 0

#define DSOUND_B_OUTPUT_FULL BIT03

#define DSOUND_A_RIGHT_CHANNEL BIT08

#define DSOUND_A_LEFT_CHANNEL BIT09

#define DSOUND_A_TIMER_0 0

#define DSOUND_A_TIMER_1 BIT10

#define DSOUND_A_FIFO_RESET BIT11

#define DSOUND_B_RIGHT_CHANNEL BIT12

#define DSOUND_B_LEFT_CHANNEL BIT13

#define DSOUND_B_TIMER_0 0

#define DSOUND_B_TIMER_1 BIT14

#define DSOUND_B_FIFO_RESET BIT15

接下来,我们开起声音播放的总开关

#define SOUND_MASTER_ENABLE BIT07

//Enable all sound

*(vu16 *)REG_SOUNDCNT_X |= SOUND_MASTER_ENABLE;

下面就是设置好DMA

//DMA1 Source Addresss

*(vu32 *)REG_DMA1SAD = (u32)sound;

//DMA1 Destination Address (REG_SGFIFOA)

*(vu32 *)REG_DMA1DAD = 0x40000A0;

//Write 32 bits into 0x040000A0 (REG_SGFIF0A) every VSync

*(vu32 *)REG_DMA1CNT = DMA_DEST_FIXED | DMA_REPEATE | DMA_32 | DMA_TIMEING_SYNC_TO_DISPLAY | DMA_ENABLE;

关于这个DMA的设置我就不再讲解了,你可以直接看看前面一篇关于DMA的日记.不过我建议你最好还是参考一下官方开发包里面的文档,那是才是最完整的资料.

这里对于DMA的属性设置跟我们平常简单用来快速传输数据的DMA有点不同.首先你肯定会发觉,我们这里没有设置DMA传输的数据的字节数(Word Count).这是声音播放所使用的DMA的特色,我们这里不能像快速传输数据那样一口气把所有声音数据传输完毕.所以,这里的DMA是不能产生DMA中断的(也就是平常的DMA传输结束后产生的中断),所以即使你在中断设置中设置了DMA1和DMA2,它也不能为你产生中断.正因为这一点,我在后面需要用到另一个时间有关的中断来控制声音的结束(下一篇再讲).

0x40000A0就是前面讲解的Direct Sound A的音频采样数据入口寄存器地址.

//Sample Rate

*(vu16 *)REG_TM0CNT_L = 65536 - (16777216/sampleRate);

//Enable the timer

*(vu16 *)REG_TM0CNT_H = TIMER_ENABLE | TIMER_IRQ;

这个是设置的Timer的代码.前面关于Timer的日记已经说过,当Timer的计数器溢出的时候产生个中断.这里控制音频数据传输也是一样的.当Timer溢出,DMA就传输4个字节数据到Direct Sound A的数据入口.这样,我们就能有办法控制声音播放的速度了.sampleRete是关于音频传输速度的参数.65536就是Timer溢出时候的值.至于16777216这个数值我就不清楚了.可能得问问wav2gbac.exe的作者才行.

好了,我们的声音播放的代码就完成了.同样的方法可以用来设置Direct Sound B的播放.但是现在我还不能结束声音的内容.因为这里还有太多的问题要解决.比如我们现在无法正常控制声音结束的问题.在模拟器上运行可能不会察觉出来问题,但是一旦把程序烧录到GBA机器上玩,声音播发结束后就会发出十分刺耳的怪声.主要原因就是我们在声音播放结束后没有关于声音播放.今天就到这里,这个问题我们在下一篇中再解决吧.

金点 tangl_99

2003/3/17


GBA探索日记(9)(声音播放2)

这一节我们讨论如何正常结束声音播放的问题.不要觉得这是个小问题,当初这个小问题难倒了我们所有研究GBA的爱好者.我总觉得这一点似乎是GBA设计上的缺陷,但是最后我们可以通过使用VBlank中断还解决这个问题.本篇结束的时候,我还会给出完整声音播放的代码.这样,你甚至不必了解声音播放的细节就可以实现声音的播放功能了.

由于声音播放使用的DMA不能根据WORD COUNT来控制传输的数据量,同时又不能在传输完毕的时候产生DMA中断,所以我们无法知道什么时候DMA把我们的音频数据传输完毕.这样,我们必须自己另外加一计时器,来计算什么时候声音播放结束.当然,你可以使用Timer来做,但是另外再设置个Timer似乎就麻烦了点,而且Timer的精度过高,也不好计算长时间的声音播放.依照官方开发包中的做法,我们就拿VBlank中断作为声音播放的计时器.

VBlank中断是在硬件扫描屏幕V方向扫描线的时候产生的硬件中断.它是严格的每秒59.78次(接近60次/s)

下面我们来看看这段修改后的播放声音的代码.需要注意拿他和前一节的播放代码来比较一下,看看他们之间有什么不同.

这个PlayDirectSoundA的播发函数多了一个参数u32 time.它就是用来控制声音结束的时间参数.它的单位是秒(对于一般的声音播放来说,秒单位应该是足够精确了).

soundATime = time * 59.727;

这条代码是将实际播放时间的秒数转换成一共要产生VBlank的中断数.前面说过,我们将使用VBlank来判断我们的声音是否播放结束.而VBlank中断的频率是59.727Hz,也就是说一秒中要产生59.727次.比如说如果我们的这段声音要播放5秒钟,那么播放过程中一共要产生5*59.727次VBlank中断.这样,我们只要使用一个计数器,每次VBlank中断产生的时候计数器自加1,当它等于5*59.727的时候,那么声音也就应该结束了.

其它的代码就差不多和前一节的PlayDirectSoundA差不多了.

////////////////////////////////////PlayDirectSoundA/////////////////////////////////////

// time是播发时间,单位是秒数

void PlayDirectSoundA(u8 *sound, u16 sampleRate, u32 length,u32 time)

{

//Stop any previous sample

soundAPlaying = 0;

*(vu16 *)REG_TM0CNT_H = 0;

*(vu16 *)REG_TM0CNT_L = 0;

*(vu32 *)REG_DMA1SAD = 0;

*(vu16 *)REG_DMA1CNT_H = 0;

*(vu32 *)REG_DMA1DAD = 0;

//Output DirectSound A to right channel

*(vu16 *)REG_SOUNDCNT_H |= DSOUND_A_RIGHT_CHANNEL| DSOUND_A_TIMER_0 | DSOUND_A_LEFT_CHANNEL | DSOUND_A_FIFO_RESET | DSOUND_A_OUTPUT_FULL;

//Enable all sound

*(vu16 *)REG_SOUNDCNT_X |= SOUND_MASTER_ENABLE;

//DMA1 Source Addresss

*(vu32 *)REG_DMA1SAD = (u32)sound;

//Set sound A's current sound

soundA = sound;

//Set the length, looping, etc

soundALength = length;

// 讲time秒转换成V-Blank中断次数,V-Blank中断是59.727Hz

soundATime = time * 59.727;

soundACurrent = 0;

soundASampleRate = sampleRate;

//DMA1 Destination Address (REG_SGFIFOA)

*(vu32 *)REG_DMA1DAD = 0x40000A0;

//Write 32 bits into 0x040000A0 (REG_SGFIF0A) every VSync

*(vu32 *)REG_DMA1CNT = DMA_DEST_FIXED | DMA_REPEATE | DMA_32 | DMA_TIMEING_SYNC_TO_DISPLAY | DMA_ENABLE;

//Sample Rate

*(vu16 *)REG_TM0CNT_L = 65536 - (16777216/sampleRate);

//Enable the timer

*(vu16 *)REG_TM0CNT_H = TIMER_ENABLE | TIMER_IRQ;

//The sound is playing

soundAPlaying = 1;

}

在这里我们增加了一个函数UpdateDirectSoundA.它的作用是判断Direct Sound A是否播放完毕的函数.它的代码很简单.每调用它一次,当前计数器soundACurrent自加1,同时判断是否播放完毕.

/////////////////////////////////////UpdateDirectSoundA////////////////////////////////

void UpdateDirectSoundA(void)

{

// if now is not playing sound A,return

if(!soundAPlaying)

return;

// Increase the Current timer

soundACurrent +=1;

// If sound A play time is up,close the sound A

if(soundACurrent >= soundATime)

CloseDirectSoundA();

}

需要注意的是,这个函数的调用必须是放在VBlank中断的响应函数里.比如像下面一样,把它的调用放在Interrupt中处理VBlank的部分里.这样我们才能保证它是每1/59.727秒被调用一次.

关于中断的部分,请看看前面的<<GBA探索日记>>有关中断的部分.

void Interrupts(void)

{

u16 Int_Flag;

REG_IME = 0x00; // Disable interrupts

Int_Flag = REG_IF; // Read the interrupt flags

if((REG_IF & 1) == 1)

{

UpdateDirectSoundA();

}

REG_IF = Int_Flag; // Write back the interrupt flags

REG_IME = 1; // Re-Enable interrups

}

好了.这个如何控制声音播放结束的问题总算解决了.其实我们也可以使用Timer用同样的原理来控制声音播放的结束,不过至少我觉得Timer的中断产生频率太快,完全没有必要.下面我将把我播放声音的全部代码公布出来,你可以将它拷贝到libsound.c和libsound.h,然后就可以使用了.

随便在结束本节之前再说几句,我们关于GBA的基础部分也就结束.但是我们对于GBA开发的探索还没有结束.后面的GBA探索日记将介绍一些实际游戏开发过程中需要的技术,比如汉字显示,Tile模式下写点等问题.不过这些问题就和我们一直走的官方开发包没有关系(无论是日本人还是老美根本不会去考虑汉字显示的问题),但是这些技术在实际开发过程中是很有用的.特别是汉字显示和Tile下写点这两个问题,是很多朋友都在做的,现在应该说是比较成熟的,所以在后面的两篇日记里,我也将它们公布出来,希望对大家有所帮助.

//*************************************

// 文件名: libsound.h

// GBA中Direct Sound处理函数或宏

// tangl_99 创建于2003.2.7

//*************************************

#ifndef _SOUND_H

#define _SOUND_H

//////////////////////////Necessary Includes/////////////////////////////

//////////////////////////////Definitions/////////////////////////////////

#define BIT00 1

#define BIT01 2

#define BIT02 4

#define BIT03 8

#define BIT04 16

#define BIT05 32

#define BIT06 64

#define BIT07 128

#define BIT08 256

#define BIT09 512

#define BIT10 1024

#define BIT11 2048

#define BIT12 4096

#define BIT13 8192

#define BIT14 16384

#define BIT15 32768

#define TIMER_CASCADE BIT02

#define TIMER_IRQ BIT06

#define TIMER_ENABLE BIT07

#define DMA_ENABLE 0x80000000

#define DMA_INTERUPT_ENABLE 0x40000000

#define DMA_TIMEING_IMMEDIATE 0x00000000

#define DMA_TIMEING_VBLANK 0x10000000

#define DMA_TIMEING_HBLANK 0x20000000

#define DMA_TIMEING_SYNC_TO_DISPLAY 0x30000000

#define DMA_16 0x00000000

#define DMA_32 0x04000000

#define DMA_REPEATE 0x02000000

#define DMA_SOURCE_INCREMENT 0x00000000

#define DMA_SOURCE_DECREMENT 0x00800000

#define DMA_SOURCE_FIXED 0x01000000

#define DMA_DEST_INCREMENT 0x00000000

#define DMA_DEST_DECREMENT 0x00200000

#define DMA_DEST_FIXED 0x00400000

#define DMA_DEST_RELOAD 0x00600000

#define RIGHT_VOLUME(n) n

#define LEFT_VOLUME(n) n<<4

#define SOUND_1_RIGHT_CHANNEL BIT08

#define SOUND_2_RIGHT_CHANNEL BIT09

#define SOUND_3_RIGHT_CHANNEL BIT10

#define SOUND_4_RIGHT_CHANNEL BIT11

#define SOUND_1_LEFT_CHANNEL BIT12

#define SOUND_2_LEFT_CHANNEL BIT13

#define SOUND_3_LEFT_CHANNEL BIT14

#define SOUND_4_LEFT_CHANNEL BIT15

//SGCNT0_H

#define SOUND_OUTPUT_1_4 0

#define SOUND_OUTPUT_1_2 BIT00

#define SOUND_OUTPUT_FULL BIT01

#define DSOUND_A_OUTPUT_HALF 0

#define DSOUND_A_OUTPUT_FULL BIT02

#define DSOUND_B_OUTPUT_HALF 0

#define DSOUND_B_OUTPUT_FULL BIT03

#define DSOUND_A_RIGHT_CHANNEL BIT08

#define DSOUND_A_LEFT_CHANNEL BIT09

#define DSOUND_A_TIMER_0 0

#define DSOUND_A_TIMER_1 BIT10

#define DSOUND_A_FIFO_RESET BIT11

#define DSOUND_B_RIGHT_CHANNEL BIT12

#define DSOUND_B_LEFT_CHANNEL BIT13

#define DSOUND_B_TIMER_0 0

#define DSOUND_B_TIMER_1 BIT14

#define DSOUND_B_FIFO_RESET BIT15

//SGCNT1

#define SOUND_1_ENABLE BIT00

#define SOUND_2_ENABLE BIT01

#define SOUND_3_ENABLE BIT02

#define SOUND_4_ENABLE BIT03

#define SOUND_MASTER_ENABLE BIT07

//SG10_L

#define S10L_NUM_SWEEP_SHIFTS(n) n

#define S10L_SWEEP_INCREASE 0

#define S10L_SWEEP_DECREASE BIT03

#define S10L_SWEEP_CHANGE_FREQ(n) n<<4

//SG10_H

#define S10H_SOUND_LENGTH(n) n

#define S10H_WAVEFORM_CYCLE_12 0

#define S10H_WAVEFORM_CYCLE_25 BIT06

#define S10H_WAVEFORM_CYCLE_50 BIT07

#define S10H_WAVEFROM_CYCLE_75 BIT06 | BIT07

#define S10H_ENVELOPE_STEPS(n) n<<8

#define S10H_ENVELOPE_INCREASE 0

#define S10H_ENVELOPE_DECREASE BIT11

#define S10H_ENVELOPE_START_VALUE(n) n<<12

//SG20

#define S20_SOUND_LENGTH(n) n

#define S20_WAVEFORM_CYCLE_12 0

#define S20_WAVEFORM_CYCLE_25 BIT06

#define S20_WAVEFORM_CYCLE_50 BIT07

#define S20_WAVEFROM_CYCLE_75 BIT06 | BIT07

#define S20_ENVELOPE_STEPS(n) n<<8

#define S20_ENVELOPE_INCREASE 0

#define S20_ENVELOPE_DECREASE BIT11

#define S20_ENVELOPE_START_VALUE(n) n<<12

//SG21

#define S21_FREQUENCY(n) n

#define S21_CONTINOUS 0

#define S21_COUNTER BIT14

#define S21_RESTART BIT15

//SG30_L

#define S30L_32_STEP_WAVEFORM 0

#define S30L_64_STEP_WAVEFORM BIT05

#define S30L_RAM_BANK_0 0

#define S30L_RAM_BANK_1 BIT06

#define S30L_ENABLE BIT07

//SG30_H

#define S30H_SOUND_LENGTH(n) n

#define S30H_OUTPUT_MUTE 0

#define S30H_OUTPUT_FULL BIT13

#define S30H_OUTPUT_1_2 BIT14

#define S30H_OUTPUT_1_4 BIT13 | BIT14

#define S30H_OUTPUT_3_4 BIT15

//SG31

#define S31_FREQUENCY(n) n

#define S31_CONTINOUS 0

#define S31_COUNTER BIT14

#define S31_RESTART BIT15

//SG40

#define S40_SOUND_LENGTH(n) n

#define S40_ENVELOPE_STEPS(n) n<<8

#define S40_ENVELOPE_INCREASE 0

#define S40_ENVELOPE_DECREASE BIT11

#define S40_ENVELOPE_START_VALUE(n) n<<12

//SG41

#define S41_DIVIDE_FREQ_2 0

#define S41_DIVIDE_FREQ_1 BIT00

#define S41_DIVIDE_FREQ_1_2 BIT01

#define S41_DIVIDE_FREQ_1_3 BIT00 | BIT01

#define S41_DIVIDE_FREQ_1_4 BIT02

#define S41_DIVIDE_FREQ_1_5 BIT00 | BIT02

#define S41_DIVIDE_FREQ_1_6 BIT01 | BIT02

#define S41_DIVIDE_FREQ_1_7 BIT00 | BIT01 | BIT02

#define S41_POLY_STEPS_15 0

#define S41_POLY_STEPS_7 BIT03

#define S41_POLY_SHIFT_FREQ(n) n<<4

#define S41_CONTINOUS 0

#define S41_COUNTER BIT14

#define S41_RESTART BIT15

#define REG_SGCNT0_L 0x4000080

#define REG_SGCNT0_H 0x4000082

#define REG_SGCNT1 0x4000084

extern u8 *soundA; //Sound A sample

extern u8 soundALoop; //Sound A loop parameter

extern u8 soundAPlaying; //Sound A status

extern u32 soundACurrent; //Sound A current location

extern u32 soundALength; //Sound A sample length

extern u32 soundATime; //Sound A sample time

extern u16 soundASampleRate; //Sound A sample rate

extern u8 *soundB; //Sound B sample

extern u8 soundBLoop; //Sound B loop parameter

extern u8 soundBPlaying; //Sound B status

extern u32 soundBLength; //Sound B sample length

extern u32 soundBCurrent; //Sound B current location

extern u32 soundBTime; //Sound B sample time

extern u16 soundBSampleRate; //Sound B sample rate

////////////////////////////////Function Prototypes//////////////////////////////

void PlaySound(u8 *sound, u16 sampleRate, u32 length,u32 time);

void UpdateDirectSound(void);

void PlayDirectSoundA(u8 *sound, u16 sampleRate, u32 length, u32 time);

void PlayDirectSoundB(u8 *sound, u16 sampleRate, u32 length, u32 time);

void UpdateDirectSoundA(void);

void UpdateDirectSoundB(void);

void CloseDirectSoundA(void);

void CloseDirectSoundB(void);

void StopDirectSoundA(void);

void StopDirectSoundB(void);

void ResumeDirectSoundA(void);

void ResumeDirectSoundB(void);

#endif

//*************************************

// 文件名:libsound.c

// GBA中Direct Sound处理函数或宏

// tangl_99 创建于2003.2.7

//*************************************

#include <agb.h>

#include "libsound.h"

/////////////////////////////////////Globals/////////////////////////////////////

u8 *soundA; //Sound A sample

u8 soundALoop; //Sound A loop parameter

u8 soundAPlaying; //Sound A status

u32 soundACurrent; //Sound A current location

u32 soundALength; //Sound A sample length

u32 soundATime; //Sound A sample time

u16 soundASampleRate; //Sound A sample rate

u8 *soundB; //Sound B sample

u8 soundBLoop; //Sound B loop parameter

u8 soundBPlaying; //Sound B status

u32 soundBLength; //Sound B sample length

u32 soundBCurrent; //Sound B current location

u32 soundBTime; //Sound B sample time

u16 soundBSampleRate; //Sound B sample rate

////////////////////////////////////PlayDirectSoundA/////////////////////////////////////

// time是播发时间,单位是秒数

void PlayDirectSoundA(u8 *sound, u16 sampleRate, u32 length,u32 time)

{

//Stop any previous sample

soundAPlaying = 0;

*(vu16 *)REG_TM0CNT_H = 0;

*(vu16 *)REG_TM0CNT_L = 0;

*(vu32 *)REG_DMA1SAD = 0;

*(vu16 *)REG_DMA1CNT_H = 0;

*(vu32 *)REG_DMA1DAD = 0;

//Output DirectSound A to right channel

*(vu16 *)REG_SOUNDCNT_H |= DSOUND_A_RIGHT_CHANNEL| DSOUND_A_TIMER_0 | DSOUND_A_LEFT_CHANNEL | DSOUND_A_FIFO_RESET | DSOUND_A_OUTPUT_FULL;

//Enable all sound

*(vu16 *)REG_SOUNDCNT_X |= SOUND_MASTER_ENABLE;

//DMA1 Source Addresss

*(vu32 *)REG_DMA1SAD = (u32)sound;

//Set sound A's current sound

soundA = sound;

//Set the length, looping, etc

soundALength = length;

// 讲time秒转换成V-Blank中断次数,V-Blank中断是59.727Hz

soundATime = time * 59.727;

soundACurrent = 0;

soundASampleRate = sampleRate;

//DMA1 Destination Address (REG_SGFIFOA)

*(vu32 *)REG_DMA1DAD = 0x40000A0;

//Write 32 bits into 0x040000A0 (REG_SGFIF0A) every VSync

*(vu32 *)REG_DMA1CNT = DMA_DEST_FIXED | DMA_REPEATE | DMA_32 | DMA_TIMEING_SYNC_TO_DISPLAY | DMA_ENABLE;

//Sample Rate

*(vu16 *)REG_TM0CNT_L = 65536 - (16777216/sampleRate);

//Enable the timer

*(vu16 *)REG_TM0CNT_H = TIMER_ENABLE | TIMER_IRQ;

//The sound is playing

soundAPlaying = 1;

}

////////////////////////////////////PlayDirectSoundB/////////////////////////////////////

void PlayDirectSoundB(u8 *sound, u16 sampleRate, u32 length,u32 time)

{

//Stop any previous sample

soundBPlaying = 0;

*(vu16 *)REG_TM1CNT_H = 0;

*(vu16 *)REG_TM1CNT_L = 0;

*(vu32 *)REG_DMA2SAD = 0;

*(vu16 *)REG_DMA2CNT_H = 0;

*(vu32 *)REG_DMA2DAD = 0;

//Output DirectSound B to right channel

*(vu16 *)REG_SOUNDCNT_H |= DSOUND_B_RIGHT_CHANNEL | DSOUND_B_TIMER_1| DSOUND_B_LEFT_CHANNEL | DSOUND_B_FIFO_RESET | DSOUND_B_OUTPUT_FULL;

//Enable all sound

*(vu16 *)REG_SOUNDCNT_X |= SOUND_MASTER_ENABLE;

//DMA1 Source Addresss

*(vu32 *)REG_DMA2SAD = (u32)sound;

//Set sound B's current sound

soundB = sound;

//Set the length and looping

soundBLength = length;

// 讲time秒转换成V-Blank中断次数,V-Blank中断是59.727Hz

soundBTime = time * 59.727;

soundBCurrent = 0;

soundBSampleRate = sampleRate;

soundBPlaying = 1;

//DMA1 Destination Address (REG_SGFIFOB)

*(vu32 *)REG_DMA2DAD = 0x40000A4;

//Write 32 bits into 0x040000A4 (REG_SGFIF0B) every VSync

*(vu32 *)REG_DMA2CNT = DMA_DEST_FIXED | DMA_REPEATE | DMA_32 | DMA_TIMEING_SYNC_TO_DISPLAY | DMA_ENABLE;

//Sample Rate

*(vu16 *)REG_TM1CNT_L = 65536 - (16777216/sampleRate);

//Enable the timer

*(vu16 *)REG_TM1CNT_H = TIMER_ENABLE | TIMER_IRQ;

}

/////////////////////////////////////UpdateDirectSoundA////////////////////////////////

void UpdateDirectSoundA(void)

{

// if now is not playing sound A,return

if(!soundAPlaying)

return;

// Increase the Current timer

soundACurrent +=1;

// If sound A play time is up,close the sound A

if(soundACurrent >= soundATime)

CloseDirectSoundA();

}

/////////////////////////////////////UpdateDirectSoundB////////////////////////////////

void UpdateDirectSoundB(void)

{

// if now is not playing sound B,return

if(!soundBPlaying)

return;

soundBCurrent +=1;

// If sound B play time is up,close the sound B

if(soundBCurrent >= soundBTime)

CloseDirectSoundB();

}

void PlaySound(u8 *sound, u16 sampleRate, u32 length, u32 time)

{

PlayDirectSoundA(sound,sampleRate,length,time);

PlayDirectSoundB(sound,sampleRate,length,time);

}

void UpdateDirectSound()

{

UpdateDirectSoundA();

UpdateDirectSoundB();

}

void CloseDirectSoundA(void)

{

soundAPlaying = 0;

*(vu16 *)REG_TM0CNT_H = 0;

*(vu16 *)REG_TM0CNT_L = 0;

*(vu32 *)REG_DMA1SAD = 0;

*(vu16 *)REG_DMA1CNT_H = 0;

*(vu32 *)REG_DMA1DAD = 0;

}

void CloseDirectSoundB(void)

{

soundBPlaying = 0;

*(vu16 *)REG_TM1CNT_H = 0;

*(vu16 *)REG_TM1CNT_L = 0;

*(vu32 *)REG_DMA2SAD = 0;

*(vu16 *)REG_DMA2CNT_H = 0;

*(vu32 *)REG_DMA2DAD = 0;

}

void StopDirectSoundB(void)

{

CloseDirectSoundB();

// 用soundBCurrent来记录播放的时间,Resume的时候根据soundBCurrent/soundBtime,来判断sound data的入口

}

void StopDirectSoundA(void)

{

CloseDirectSoundA();

// 用soundACurrent来记录播放的时间,Resume的时候根据soundACurrent/soundAtime,来判断sound data的入口

}

void ResumeDirectSoundA(void)

{

//Output DirectSound A to right channel

*(vu16 *)REG_SOUNDCNT_H |= DSOUND_A_RIGHT_CHANNEL| DSOUND_A_TIMER_0 | DSOUND_A_LEFT_CHANNEL | DSOUND_A_FIFO_RESET | DSOUND_A_OUTPUT_FULL;

//Enable all sound

*(vu16 *)REG_SOUNDCNT_X |= SOUND_MASTER_ENABLE;

//DMA1 Source Addresss

*(vu32 *)REG_DMA1SAD = (u32)(soundA+soundACurrent*soundALength/soundATime);

//DMA1 Destination Address (REG_SGFIFOA)

*(vu32 *)REG_DMA1DAD = 0x40000A0;

//Write 32 bits into 0x040000A0 (REG_SGFIF0A) every VSync

*(vu32 *)REG_DMA1CNT = DMA_DEST_FIXED | DMA_REPEATE | DMA_32 | DMA_TIMEING_SYNC_TO_DISPLAY | DMA_ENABLE;

//Sample Rate

*(vu16 *)REG_TM0CNT_L = 65536 - (16777216/soundASampleRate);

//Enable the timer

*(vu16 *)REG_TM0CNT_H = TIMER_ENABLE | TIMER_IRQ;

//The sound is playing

soundAPlaying = 1;

}

void ResumeDirectSoundB(void)

{

//Output DirectSound B to right channel

*(vu16 *)REG_SOUNDCNT_H |= DSOUND_B_RIGHT_CHANNEL | DSOUND_B_TIMER_1| DSOUND_B_LEFT_CHANNEL | DSOUND_B_FIFO_RESET | DSOUND_B_OUTPUT_FULL;

//Enable all sound

*(vu16 *)REG_SOUNDCNT_X |= SOUND_MASTER_ENABLE;

//DMA1 Source Addresss 需要计算重新开始的地址

*(vu32 *)REG_DMA2SAD = (u32)(soundB+soundBCurrent*soundBLength/soundBTime);

//DMA1 Destination Address (REG_SGFIFOB)

*(vu32 *)REG_DMA2DAD = 0x40000A4;

//Write 32 bits into 0x040000A4 (REG_SGFIF0B) every VSync

*(vu32 *)REG_DMA2CNT = DMA_DEST_FIXED | DMA_REPEATE | DMA_32 | DMA_TIMEING_SYNC_TO_DISPLAY | DMA_ENABLE;

//Sample Rate

*(vu16 *)REG_TM1CNT_L = 65536 - (16777216/soundBSampleRate);

//Enable the timer

*(vu16 *)REG_TM1CNT_H = TIMER_ENABLE | TIMER_IRQ;

soundBPlaying = 1;

}

金点 tangl_99

2003/4/19


GBA探索日记(10外篇)回头

今天是2003年5月16号.周五的晚上应该是陪女朋友的时候,但是她似乎不想见我,叫了一大帮朋友出学校吃串串.本来我也想去的,但是她的那一大帮朋友都是女的,我一个男的怎么好插进去?所以晚上只有呆在寝室里写代码了.这个下午有点闷,似乎又要下雨,可始终就不下.寝室的配电器不知道怎么回事,老是出问题,一下午就停了5次电,搞得我都没有办法安心写代码了.晚上吃完泡面后终于终于供电稳定了,我再次打开了我的电脑.现在我的电脑已经装满了各种各样的软件,程序,运行速度已经变得超级慢,而且还染上了各种不知名的病毒.但是没有关系,只要能用,我就不想重新装系统了.

研究GBA已经有半年了,GBA上能做的东西我都研究过.什么BG,OBJ,OAM,SOUND,Interrupt都搞得很透,应该说我对GBA的认识比对PC的认识还深.现在打算真正做个像样的GBA游戏了.还好,我的朋友中有能搞美工的,所以拉来他,一起干.可是似乎沟通有点问题,我叫他做的图片老是不成功,改了又改,已经重新做了三次了,但是还需要再重新做.不过他倒是有点耐心.但是我已经等不及了,所以马上喊出睡在我下铺的同学,叫他用最笨的方式给我加工人物图片.还好,人物的图片基本上已经做好了一部分,加进程序里测试,效果还不错,挺逼真的.有关人物动画的程序就这样基本没有问题.

下面却遇到一个大难题.就是需要把一张超大的地图装载到一个BG中去.这个图片一共有上万的Characters,而GBA的Text BG最大也只能支持1024个.似乎不太可能.我想到把图片缩小到1024x1024,然后装进BG中,再通过BG来放大出来.不过很快我就打消了这个想法.首先即使我把图片缩小到1024x1024,还是有好几千的Characters,根本无法装载.其次这样做也会把图片的分辨率降低,图片看起来就很不清晰了.

当天晚上我上QQ,我问azhong.这里随便再说一下,azhong搞GBA的技术很不错的,而且还提供了我很多开发工具.azhong说有办法解决我的问题,但是很难用语言说清楚,他就告诉我用动态的装载方法来实现.然后写了个小程序给我看看他把他电脑上的墙纸装进GBA的BG的方法.可是这个程序里面用了他的GsLib,所以具体实现的方法就无法得知了.但是我从它的运行效果猜出了它的原理.它的方法就是把好转换的图片全部转换成没有重复的Characters(就像OBJ的那样).然后自己用个for循环生成一个0,1,2,...,1024这样的Map数据.

好了,有了思路,下面就开始干了.首先是解除了自己长期以来的两个误解.一是原来一直以为Text BG只能使用16色的BG,但是实际情况它还是可以使用256色的,azhong给的我那个demo就是做在mode0下的text bg0下的256色的.二是原来一直以为一个BG最多只能使用一个Characters Base(16KB),但实际情况并非如此.BGCNT属性中的Characters Base只是说明BG的Characters数据的起始地址,但并没有限制Characters的大小.如果可以,你甚至可以把4个Characters Base都用上(16 * 4 =64 KB),不过这样就没有地方装map数据了.

azhong给我的那个bg2demo里面就是使用3个Characters Base,BG是mode0下的bg0,大小是最小的256x256大.不过256x256的尺寸足以包括整个屏幕的240x160大小了.同时由于BG的Characters没有重复,所以这样一共就要使用256*256/64=1024.如果真的这样做的话,那么就需要占用VRAM一共1024*64=64KB,那么BG的Map数据就没有地方放了.不过还好,我们连256x256的BG的Characters都不必全部装进VRAM.我们需要的仅仅是恰好屏幕240x160大小的Characters.但是我们不可能恰好同时裁剪长和宽,所以我需要装载的就是256x160大小的Characters.一共256x160/64=640个Characters.

显然,这里我们也只能使用Text Bg.因为Rotation BG最多只能使用256,我们无论怎么算,最少都要使用640个Characters,所以我们别无选择,只能使用Text BG.好了.下面我来看看程序代码是怎么做的.

看起来代码其实很简单的

void LoadScreenTiles(u16 x,u16 y)

{

u16 i,j;

u16 x_begin,y_begin;

x_begin=x/8;

y_begin=y/8;

for(i=0;i<20;i++)

DmaCopy(3,(void*)land_gfx+(i+y_begin)*303*64+x_begin*64,BG_VRAM+i*32*64,64*30,16);

}

其中x,y是在裁剪原始大地图中坐上角的坐标.通过对x,y处理,我们就知道屏幕应该从哪里开始装载Tiles了.

rect.x1=0;

rect.y1=0;

rect.x2=255;

rect.y2=255;

bg.number=0;

bg.colorMode=BG_COLOR_256;

bg.size=TEXTBG_SIZE_256x256;

bg.mosaic=0;

bg.charBaseBlock=0;

bg.screenBaseBlock=28;

bg.Palette_number=0;

bg.x_scroll=0;

bg.y_scroll=0;

CreateMap(&bg,MAP_STYLE_DIFFER,&rect);

screen_x=0;

screen_y=0;

LoadBackground256Palette(land_pal);

LoadBackground(&bg);

LoadScreenTiles(screen_x,screen_y);

EnableBackground(&bg);

UpdateBackground(&bg);

从上面的代码我们可以看出装载BG的步骤.其中创建0,1,2,3,4,...,1024那样的Map的函数CreateMap代码比较复杂,就不在这里列出来了.或者用简单的for循环就可以自己实现了.

最后提示一下创建全部不同的Characters的方法.在kaleid_1-2-3工具中转换图片的时候如果你不选择Create Map,那么生成的Characters就像OBJ的Characters那样,全部都不同,这样图片中每个Tile都会相应生成一个Characters.也只有这样的Characters能满足我们那种0,1,2,3,..,1024的Map数据.

金点 tangl_99

2003/5/17