GBA
Development From the Ground Up
Volume 2
by Brian Sowers
What will this article
cover?
This
article will describe how to change the screen mode for the GBA and how to draw
images in the bitmap modes 3,4, and 5.
Let's
begin.
Getting the Compiler Working
I
hope you downloaded ALL the files needed for your operating system from the
DevKitAdv site, as they will all be indispensable (except for maybe the C++
additions, but I like C++ and will probably use it in this article). Unzip all the files to the root directory,
and you should have a
new directory called DevKitAdv.
Congratulations, you just installed DevKitAdv with everything you need.
Now
open your friendly Notepad and type in some code. Save this code as anything you want with a .c
extension (I'll use test.c for this example).
#include<stdio.h>
int main()
{
return 0;
}
Yes,
I know it doesn't do anything. This is
just an example to get the compiler working.
Also, make sure you hit return after the final brace or else your
compiler will whine at you (for some reason there has to be a new line at the
end of every file).
Now
open up your text editor again and type in the following:
path=c:\devkitadv\bin
gcc -o test.elf test.c
objcopy -O binary test.elf test.bin
Save
this file as Make.bat. Note that some of
the information in the file might have to change depending on the name of your
file and what drive DevKitAdvance is installed on.
Now
double click on Make.bat. Wait for the
program to end. Congratulations, you
just wrote your first program. What the
make file does is call gcc to create test.elf from test.c, then it calls
objcopy to create test.bin from test.elf.
You might want to read that a few times if you didn't get it. However, regardless of if you understand that
or not, those are the only three lines you'll need to put in a make file to get
your program to compile (although those lines may have to vary depending on how
many source files you use and the names of these files). For example, if you were using two source
files named test.c and input.c, you'd simply change the second line to:
gcc -o test.elf test.c input.c
I
hope you understand, because things only get more complicated from here. :-)
Now
that we know how to compile a simple program, let's move on.
Using What You Create
When
you compile your program, two files should be created – a
.elf and a .bin. You want to use the
.bin. Simply run this .bin using your
respective emulator to view your creations.
If
you have a linker and a cart, use the linker (instructions are included and
more info can be found at www.visoly.com)
to write the .bin to the cart. Push the
cart in your GBA, turn the GBA on, and viola! Your creations are running on
hardware!
Screen Modes, Among Other Things
As
I said earlier, the GBA has 6 different screen modes to choose between. From the moment I said that, I bet you were
wondering, "How do I change between them?" The answer lies in the REG_DISPCNT that is defined in gba.h.
However,
you may be wondering where this elusive “gba.h” is. Well, the answer is quite simple: you don’t
have it. It’s rather long, and its
contents are going to be divulged throughout the course of all these articles,
so I’m going to do what I don’t like doing and just throw the entire thing at
you. Get it here: prodigygames.8k.com/articles/gba.txt
(you’ll have to copy the contents into a new gba.h or download the file – right
click and hit Save Target As – and name it as gba.h… stupid Freeservers).
This
wonderful file has pointers to sprite data, input data, screen data, and just
about every piece of data you’re ever going to mess with. You’ll need it in every GBA project you create.
REG_DISPCNT is a 16 bit register at memory
address 0x4000000. Each bit within this
register controls something of the GBA.
Here's a description:
Bits 0-2: Control the screen mode and can be any value
between 0 and 5
Bit 3: Automatically set on a GBA
if a GB or GBC cartridge is put in (ignore it).
Bit 4: Used for double buffering in
screen modes 4 and 5.
Bit 5: Allows OAM to be set during
horizontal blank (I'll explain further when we get to sprites).
Bit 6: Determines if we are using
1D or 2D mapping for sprites. Again,
I'll
give
more information when that bridge must be crossed.
Bit 7: Puts the screen into a
forced blank.
Bits 8-11: Enable backgrounds 0-4 (more on this when we get to
tile maps).
Bit 12: Enables hardware rendered sprites.
Bits 13-15: Enables window displays (I don't have much info on
these).
Handy
for one 16 bit number, 'eh? Personally I
probably would have rather remembered a bunch of different variable names than
one variable name and the specifics on every bit in it, but oh well. Now that we know what each bit stands for, we
need a way to change the values. This
can be done by bit masking (the | operator) a series of numbers into the
register. But who wants to remember a
bunch of numbers when we can create a header file and use variable names
instead? Let's do that.
//screenmodes.h
#ifndef __SCREENMODES__
#define __SCREENMODES__
#define SCREENMODE0 0x0 //Enable
screen mode 0
#define SCREENMODE1 0x1 //Enable
screen mode 1
#define SCREENMODE2 0x2 //Enable
screen mode 2
#define SCREENMODE3 0x3 //Enable
screen mode 3
#define SCREENMODE4 0x4 //Enable
screen mode 4
#define SCREENMODE5 0x5 //Enable
screen mode 5
#define BACKBUFFER 0x10 //Determine
backbuffer
#define HBLANKOAM 0x20 //Update
OAM during HBlank?
#define OBJMAP2D 0x0 //2D
object (sprite) mapping
#define OBJMAP1D 0x40 //1D object(sprite) mapping
#define FORCEBLANK 0x80 //Force
a blank
#define BG0ENABLE 0x100 //Enable
background 0
#define BG1ENABLE 0x200 //Enable
background 1
#define BG2ENABLE 0x400 //Enable
background 2
#define BG3ENABLE 0x800 //Enable
background 3
#define OBJENABLE 0x1000 //Enable sprites
#define WIN1ENABLE 0x2000 //Enable
window 1
#define WIN2ENABLE 0x4000 //Enable
window 2
#define WINOBJENABLE 0x8000 //Enable
object window
#define SetMode(mode) (REG_DISPCNT = mode)
#endif
There
we have our header file. Now if we
wanted to set the screen mode to screen mode 3 and support sprites, we'd simply
include this file in our main source file and say:
SetMode( SCREENMODE3 | OBJENABLE );
Naturally,
this header file is terribly important, and I recommend including it in all
your projects.
Drawing to the Screen
Drawing to the screen is remarkably easy now that we
have the screen mode set up. The video
memory, located at 0x6000000, is simply a linear array of numbers indicating color. Thus, all we have to do to put a pixel on the
screen is write to the correct offset off this area of
memory. What’s more, we already have a #define in our gba.h telling us where the video
memory is located (called VideoBuffer). One
minor thing you should take note of is that in the 16bit color modes, colors
are stored in the blue, green, red color format, so you'll want a macro to take
RGB values and convert them to the appropriate 16bit number. Furthermore, we only actually use 15
bytes. Also, when you set the screen
mode to a pixel mode, Background 2 must be turned on because that's where all
the pixels are drawn.
Here's
an example:
#include "gba.h"
#include
"screenmodes.h"
u16* theVideoBuffer = (u16*)VideoBuffer;
#define RGB(r,g,b) (r+(g<<5)+(b<<10)) //Macro to build a color from its parts
int main()
{
SetMode( SCREENMODE3 | BG2ENABLE
); //Set screen mode
int x = 10, y = 10; //Pixel
location
theVideoBuffer[ x + y * 240 ] =
RGB( 31, 31, 31 ); //Plot our pixel
return 0;
}
All
this example does is plot a white pixel at screen position 10,10. Very easy.
Mode
5 is nearly identical. Simply replace
the SCREENMODE3 with SCREENMODE5 in the SetMode macro, and instead of theVideoBuffer[ x + y * 240 ] say theVideoBuffer[
x + y * 160 ] because the screen is only 160 pixels in width. If you want to use the second buffer for Mode
5, read on, because I'll describe double buffering for Mode 4 in just a
bit. Double buffering is identical in
both screen modes - the method of drawing to the buffer is slightly changed,
though.
Mode
4 is slightly more complex since it is only an 8bit color mode and it utilizes
the backbuffer. Naturally, change the SCREENMODE3 to SCREENMODE4 in SetMode
(this should be self-explanatory, and I'm not going to note the change
anymore). However, now we need a few
more things.
Before
we do any drawing with Mode 4, we need a palette. The palette is stored at 0x5000000 and is
simply 256 16bit numbers representing all the possible colors. The memory is pointed to in gba.h, and the
pointer is called BGPaletteMem.
u16* theScreenPalette =
(u16*)BGPaletteMem;
To
put a color in the palette we simply say
theScreenPalette[ position ] = RGB( red, green, blue );
Now,
mind you, I don't recommend you hard code all the palette entries. There are programs on the internet to get the
palette from files and put it in an easy to use format. Furthermore, the color at position 0 should
always be black (0,0,0) – this will be your
transparent color.
Now
we need to point to the area of memory where the backbuffer is stored
(0x600A000). Guess what – this pointer
is already in our gba.h. Behold, BackBuffer!
u16* theBackBuffer = (u16*)BackBuffer;
Now
always write to the backbuffer instead of the video buffer. Then you can call the Flip function to switch between the two. Basically, the Flip function changes which part of video memory is
being viewed and changes the pointers around.
Thus, you will always be viewing the buffer in the front and always be
drawing to the buffer in the back. Here's the function:
void Flip()
{
if (REG_DISPCNT &
BACKBUFFER)
{
REG_DISPCNT &= ~BACKBUFFER;
theVideoBuffer = theBackBuffer;
}
else
{
REG_DISPCNT |= BACKBUFFER;
theVideoBuffer = theBackBuffer;
}
}
However,
you only want to call this function one time in your main loop. That time is during the vertical blank. The vertical blank is a brief period when the
GBA hardware isn't drawing anything. If
you draw any other time, there is a possibility that images will be choppy and
distorted because you're writing data at the same time the drawing is taking
place. This effect is known as
"Shearing."
Luckily,
the function to wait for the vertical blank is very simple. All it does is get a pointer to the area that
stores the position of the vertical drawing.
Once this counter reaches 160 (the y resolution, and thus the end of the
screen), the vertical blank is occurring.
Nothing should happen until that vertical blank happens, so we trap ourselves
in a loop. Note that in Mode 5, the y
resolution is only 128, so you'll want to change your wait accordingly.
void WaitForVblank()
{
#define ScanlineCounter *(volatile u16*)0x4000006;
while(ScanlineCounter<160){}
}
Take
that all in? Good, because there's
another pitfall to Mode 4. Although it
is an 8bit mode, the GBA hardware was set up so you can only write 16 bits at a
time. This basically means that you
either have to do a little extra processing to take into account 2 adjacent
pixels (which is slow and unrecommended) or draw two pixels of the same color
at a time (which cuts your resolution and is also unrecommended).
Here's
a "short" example of drawing a pixel to help you take all this in,
because it's a lot of information all at once.
#include "gba.h"
#include "screenmodes.h"
u16* theVideoBuffer = (u16*)VideoBuffer;
u16* theBackBuffer = (u16*)BackBuffer;
u16* theScreenPalette =
(u16*)BGPaletteMem;
#define RGB(r,g,b) (r+(g<<5)+(b<<10)) //Macro to build a color from its parts
void WaitForVblank()
{
#define ScanlineCounter *(volatile u16*)0x4000006
while(ScanlineCounter<160){}
}
void Flip()
{
if (REG_DISPCNT &
BACKBUFFER)
{
REG_DISPCNT &= ~BACKBUFFER;
theVideoBuffer = theBackBuffer;
}
else
{
REG_DISPCNT |= BACKBUFFER;
theVideoBuffer = theBackBuffer;
}
}
int main()
{
SetMode( SCREENMODE4 | BG2ENABLE
); //Set screen mode
int x = 0, y = 0; //Coordinate
for our left pixel
theScreenPalette[1] = RGB( 31,
31, 31); //Add white to the palette
theScreenPalette[2] = RGB(31,0,0); //Add red to the palette
u16 twoColors = (( 1 << 8
) + 2); //Left
pixel = 0, right pixel = 1
theBackBuffer[ x + y * 240 ] =
twoColors; //Write the two colours
WaitForVblank(); //Wait
for a vertical blank
Flip(); //Flip
the buffers
return 0;
}
There
you have it. What does that all build up
to?
I
don't recommend you use Mode 4 unless you really have to or you plan everything
out meticulously, but I thought you should have the information in case you
needed it.
Drawing a Pre-Made Image
Drawing
an image made in another program is remarkably easy - I've practically given
you all the information already.
First,
get an image converter/creation program.
www.gbadev.org is a great place to
get one. With an image converter, take
the .bmp or .gif or .pcx or whatever (depending on what the program uses), and
convert the image to a standard C .h file.
If your image uses a palette, this .h file should have both palette
information and image information stored in separate arrays.
Now,
all drawing the image consists of is reading from the arrays. Read from the palette array (if there is any)
and put the data into the palette memory.
To draw the image, simply run a loop that goes through your array and
puts the pixels defined in the array at the desired location.
Naturally,
these steps will be slightly different depending on what program you're
using. However, being the nice guy that
I am, I'm going to provide you with a quick example.
The
program: Gfx2Gba v1.03 by Darren (can be found at www.gbadev.org, read the readme
for instructions)
The
image: A 240*160, 8 bit image named gbatest.bmp
The
command line: gfx2gba gbatest.bmp gbatest.h -8 -w 240
The
code:
#include "gba.h"
#include "screenmodes.h"
#include
"gbatest.h"
u16* theVideoBuffer = (u16*)VideoBuffer;
u16* theScreenPalette =
(u16*)BGPaletteMem;
#define RGB(r,g,b) (r+(g<<5)+(b<<10)) //Macro to build a color from its parts
int main()
{
SetMode(SCREENMODE4|BG2ENABLE);
//Copy the palette
u16 i;
for ( i = 0; i < 256; i++ )
theScreenPalette[ i
] = gbatestPalette[ i ];
//Cast a 16 bit pointer to our data so we can read/write
16 bits at a time easily
u16* tempData = (u16*)gbatest;
//Write the data
//Note we’re using 120 instead of 240 because we’re
writing 16 bits
//(2 colors) at a time.
u16 x, y;
for ( x = 0; x < 120; x++ )
for ( y = 0; y <
160; y++ )
theVideoBuffer[
y * 120 + x ] = tempData[ y * 120 + x ];
return 0;
}
And
that's all! I didn't use the backbuffer,
because I was just trying to make a point.
I used Mode 4 so that I could press the fact that you MUST write 16 bits
at a time. If you were using Mode 3/16
bit color, you wouldn't need the tempData pointer. You would also change the 120s back to 240s.
In
fact, that's the end of this article. By
now, you should have bitmapped modes mastered, or at least you should have a
relatively firm grasp on them.
Next Article
In
the next article, I’m going to give you information on a rather easy topic –
input. If you’re lucky, I might even
make a game demo to show off.
Acknowledgements
I
would like to thank dovoto, as his tutorials have been my biggest source of
information on GBA development since I started.
Check out his site at www.thepernproject.com. I'd also like to thank the guys in #gamedev
and #gbadev on EFNet in IRC for all their help, and all the help they'll be
giving me as I write these articles.
Furthermore, I would like to thank www.gbadev.org/. The site is a great resource, and it is
definitely worth your time.
If
you have any questions or comments, you can always e-mail me at
genesisgenocide@yahoo.com or nairb@usa.com.
I can't promise I'll answer your questions, like your comments, or
listen to your suggestions, but I'll at least read the e-mail (as long as it
doesn't look like spam).