Introduction
Welcome to a hands-on journey in C programming for the Game Boy Advance (GBA). In this tutorial, we'll learn the C language basics and immediately apply them to GBA game development. Our goal is to build up the skills needed to create a GBA game using the devkitARM toolchain (part of devkitPro). We will stick strictly to C (not C++) throughout – no classes or object-oriented concepts, just good old procedural C. Along the way, we'll explain core C concepts (variables, control flow, functions, arrays, pointers, structs) in the context of GBA programming. This means every concept will either use GBA-specific examples or lead into how it's useful for making a GBA game.
By the end of this tutorial, you will understand how to write a simple GBA program in C, how to handle input from the GBA buttons, how to draw graphics (pixels, backgrounds, and sprites) on the GBA screen, and how to optimize your code for this resource-constrained handheld. The style of this tutorial is conversational and beginner-friendly. Let's dive in and start our journey toward creating a GBA game in C!
GBA Hardware Overview: CPU, Memory, and I/O
Before coding, it helps to know a bit about the GBA's hardware. The Game Boy Advance is a 32-bit handheld console with a 16.78 MHz ARM7TDMI CPU. It might sound slow by today's standards, but it was sufficient for the 2D games of its era. The GBA has a 240×160 pixel LCD that can display up to 15-bit color (32768 colors) on screen at once. It also features a variety of input buttons (D-pad, A, B, L, R, Start, Select) and dedicated hardware for 2D graphics and sound.
Memory and Memory-Mapped I/O
The GBA does not have an operating system – your program runs directly on the hardware. All of the GBA's hardware components (like the graphics controller, sound, buttons, etc.) are accessed through special memory addresses. This is called memory-mapped I/O. For example, writing to a specific address turns the display on or off, and reading from another address gives you the current state of the buttons. The GBA's memory is organized into distinct regions for different purposes. Table 1 below shows a simplified memory map of the GBA:
Table 1: GBA Memory Map (Key Regions)
Address Range | Size | Purpose |
---|---|---|
0x00000000 – 0x00003FFF | 16 KB | BIOS (System ROM with firmware routines) |
0x02000000 – 0x0203FFFF | 256 KB | EWRAM (External Work RAM – general purpose RAM) |
0x03000000 – 0x03007FFF | 32 KB | IWRAM (Internal Work RAM – faster on-chip RAM) |
0x04000000 – 0x040003FF | 1 KB | I/O Registers (hardware control registers for video, sound, input, etc.) |
0x05000000 – 0x050003FF | 1 KB | Palette RAM (color palettes for backgrounds & sprites) |
0x06000000 – 0x06017FFF | 96 KB | VRAM (Video RAM for graphics data) |
0x07000000 – 0x070003FF | 1 KB | OAM (Object Attribute Memory for sprites' properties) |
0x08000000 – 0x09FFFFFF | up to 32 MB | Game Pak ROM (the cartridge ROM area, where your game code/data resides) |
As you can see, different memory regions have different roles. Notably, VRAM is where graphic data (like tile images or bitmaps) is stored for the display to use, and OAM is a small memory area that holds the attributes for up to 128 sprites (moving objects) on screen. Palette RAM holds color definitions for use in certain video modes. The I/O register area (at 0x04000000
) is how we configure the GBA's hardware – by writing to these addresses in C, we can set video modes, read input, play sounds, and more. For example, there is a display control register at address 0x04000000
that we will use to set the graphics mode, and an input register at 0x04000130
that tells us which buttons are pressed.
One important thing to note is that the GBA's hardware often expects data in specific sizes or alignments. For instance, the screen's mode and status registers are 16-bit wide, so we typically write 16-bit values to them. VRAM is also usually accessed 16 bits at a time (halfwords) – in fact, the GBA hardware doesn't support writing a single byte to VRAM directly (writes must be 16-bit aligned). We'll keep these hardware details in mind as we write C code to interact with the GBA.
Quick Check: Memory
Where is the graphics data (like tile images or bitmaps) typically stored for display?
- IWRAM
- Game Pak ROM
- VRAM
- EWRAM
The GBA Toolchain (devkitARM): From Source Code to ROM
To develop a GBA game in C, we need a toolchain – a set of tools that converts our C source code into a GBA-compatible ROM file. A toolchain typically includes a compiler, assembler, linker, and other utilities. devkitARM is the toolchain we'll use; it's part of the devkitPro project and is essentially a distribution of GCC (the GNU Compiler Collection) targeted for ARM (which is the GBA's CPU). DevkitARM includes:
arm-none-eabi-gcc
(the C/C++ compiler that produces ARM machine code),arm-none-eabi-as
(the assembler, usually invoked by gcc as needed),arm-none-eabi-ld
(the linker, which combines compiled code and libraries into a final binary),arm-none-eabi-gdb
(a debugger, useful for debugging on emulator or hardware),- and some supporting libraries and headers specifically for GBA (and Nintendo DS) development.
When you install devkitARM (via the devkitPro installer or package manager), you also get a set of example projects and a C library (often referred to as libgba) which provides helpful definitions for GBA hardware. For instance, libgba might define constants for video modes and registers (like REG_DISPCNT
for the display control register) so you don't have to memorize numeric addresses. It also provides functions or macros for common tasks. We'll use these definitions in our code examples to make them clearer.
From C Code to GBA ROM
The process of building a GBA program goes like this:
- Writing the C source: You write your game's code in C, using the devkitARM-provided headers for hardware access. For example, you might include
<gba.h>
which in turn includes definitions for registers, etc. - Compiling: The compiler (gcc) compiles each C file into an object file (
.o
), containing machine code for the ARM processor. While compiling, you'll specify that we are targeting the ARM7TDMI. - Linking: The linker then combines your object files with any libraries (like libgba or C standard library portions) into a single binary. For GBA, the output is typically an ELF file which is then turned into a GBA ROM image (
.gba
). Part of this process involves using a special linker script that places the code and data at the correct memory addresses (e.g., code runs from0x08000000
). The linker also inserts a small required header at the start of the ROM. - Assets and conversion: If your game uses graphics or sound assets, you can't just load PNG images or WAV files at runtime. Instead, you use conversion tools to turn assets into C data that you compile into the ROM. One popular tool is grit (GRaphics Information Transmogrifier). Grit can take an image (BMP/PNG etc.) and output C arrays for the pixel data (or tile data and palettes) that you can include in your program. DevkitARM also provides other helper tools and build scripts (Makefiles) to automate this.
- Result – a
.gba
file: The final output is a.gba
file – a binary image of the ROM that can be loaded onto a flash cartridge or run in a GBA emulator like mGBA or VisualBoyAdvance.
What is a toolchain? In summary, a toolchain is just the collection of tools that handle this translation from human-readable code to machine-executable program. DevkitARM is our GBA toolchain; it abstracts a lot of the low-level steps so you can compile with a simple command (or Makefile) and get a working ROM. It also handles things like setting up a C runtime environment on the GBA (e.g., initializing the stack pointer, so your main()
function can run).
Note on devkitARM vs devkitPro: devkitPro is the umbrella project; devkitARM is specifically the toolchain for ARM-based platforms (GBA, DS).
Variables and Data Types in C for GBA
In C, we use variables to store values (like numbers, coordinates). Each variable has a data type that determines what kind of data it holds and how much memory it uses. On the GBA (a 32-bit system), common C data types have these sizes:
char
– 1 byte (8 bits)short
– 2 bytes (16 bits)int
– 4 bytes (32 bits)long
– 4 bytes (32 bits)long long
– 8 bytes (64 bits)float
– 4 bytes (32-bit floating point, *slow on GBA*)double
– 8 bytes (64-bit floating point, *slow on GBA*)
The number of bits determines the range of values a type can hold and how precisely it aligns with GBA hardware. Learn more about common bit depths (1, 4, 8, 16, 32 bits) and their significance here.
For GBA, we often use fixed-width integers from <stdint.h>
like uint8_t
(unsigned 8-bit), uint16_t
, uint32_t
. GBA headers often define shorthands like u8
, u16
, u32
.
Understanding Bits and Bytes (Interactive)
Let's visualize an 8-bit unsigned integer (unsigned char
or uint8_t
). It can hold values from 0 to 255. Click the bits below to toggle them between 0 and 1 and see the resulting decimal and hexadecimal values:
Hexadecimal Value: 0x00
Hexadecimal (base 16, using 0-9 and A-F) is common in low-level programming because it maps neatly to binary (each hex digit represents 4 bits). For example, 0xFF
is 1111 1111
in binary, which is 255 decimal.
// Example variable declarations for GBA:
unsigned int score = 0; // 32-bit unsigned for score
int playerX = 120, playerY = 80; // 32-bit ints for coordinates
unsigned short color = 0x7FFF; // 16-bit value for a color (white)
char initial = 'A'; // 8-bit char
Choosing the Right Type
- Use the smallest type needed (e.g.,
u8
for values 0-255). - Use
unsigned short
(u16
) for 16-bit hardware registers. - Avoid
float
/double
; use integer or fixed-point math.
The `volatile` Keyword
The volatile
keyword tells the compiler that a variable's value might change unexpectedly (e.g., a hardware register). This prevents the compiler from optimizing away reads or writes to that variable. We use it for memory-mapped I/O.
// Example: Accessing a hardware register (hypothetical)
volatile unsigned short* displayControl = (unsigned short*)0x04000000;
*displayControl = 0x0403; // Write to the register
Pointers and Memory Access (Memory-Mapped I/O)
Pointers are variables that hold memory addresses. They are crucial for GBA programming because they let us directly interact with hardware registers and memory regions like VRAM.
Pointer Operators: `&` and `*`
&
(Address-of): Gets the memory address of a variable.*
(Dereference): Accesses the value stored at the address a pointer points to.
int health = 5; // A regular variable
int* healthPtr; // A pointer to an integer
healthPtr = &health; // healthPtr now holds the memory address of 'health'
// Now, *healthPtr is like an alias for 'health'
*healthPtr = 10; // This changes the value of 'health' to 10
int currentValue = *healthPtr; // This reads the value of 'health' (which is 10)
Interactive Pointer Example
Let's visualize this. Imagine memory locations:
Understanding `*` (Dereference) and `&` (Address-Of) in Detail
The syntax for pointer operators can be confusing, especially the dual use of `*` and the role of parentheses. Let's clarify:
1. The `&` (Address-Of) Operator
- Purpose: Gets the memory address where a variable is stored.
- Reads as: "Address of".
- Example:
int score = 100; // score lives at some memory address, e.g., 0x03007F00 int* pointerToScore; // Declares a pointer variable pointerToScore = &score; // &score evaluates to 0x03007F00 // pointerToScore now HOLDS the address 0x03007F00
- Result Type: A pointer to the type of the variable (e.g., `&score` results in an `int*`).
2. The `*` (Dereference / Indirection) Operator
- Purpose: Accesses the value *at the memory address* stored inside a pointer variable. It "follows" the pointer.
- Reads as: "Value pointed to by".
- Usage (Reading):
int score = 100; int* pointerToScore = &score; // pointerToScore holds address of score int currentValue; currentValue = *pointerToScore; // *pointerToScore follows the address in pointerToScore // and retrieves the value stored there (100). // currentValue is now 100.
- Usage (Writing):
int score = 100; int* pointerToScore = &score; *pointerToScore = 250; // *pointerToScore follows the address in pointerToScore // and WRITES the value 250 into that memory location. // The original 'score' variable is now 250.
- Key Idea: Once `pointerToScore = &score;` is done, `*pointerToScore` acts as an **alias** for the original `score` variable. Anything you do to `*pointerToScore` directly affects `score`.
3. Confusion Point: `*` in Declarations vs. Expressions
- Declaration (`int *p;` or `int* p;`): The `*` simply means "p is a pointer". It specifies the variable's type. It does **not** access memory here.
- Expression (`*p`): If `p` is a pointer variable that holds an address, `*p` is the dereference operator. It **does** access the memory at the address stored in `p`.
Think of it like this: `int *p;` tells the compiler what `p` is. Later, `*p = 10;` tells the compiler what to *do* using `p`.
4. Role of Parentheses `()` - Operator Precedence
Parentheses are used to control the order of operations when multiple operators are involved, especially when combined with operators that have higher precedence than unary `*` (dereference).
- Common Case: Incrementing Pointers vs. Values
int numbers[] = {10, 20, 30}; int* p = numbers; // p points to numbers[0] (value 10) int val1 = *p++; // HIGHER PRECEDENCE: ++ (postfix) applies to p first. // 1. *p is evaluated (gets 10). val1 becomes 10. // 2. p is incremented (now points to numbers[1]). // Reset p for next example p = numbers; int val2 = (*p)++; // PARENTHESES: *p is evaluated first. // 1. *p gets the value at p (which is 10). // 2. That value (10) is incremented. numbers[0] becomes 11. // 3. The original value (10) is assigned to val2. // p still points to numbers[0].
- Common Case: Casting and Dereferencing
Parentheses around the cast `*((unsigned short*)0x04000000)` aren't strictly needed here due to precedence but improve clarity.// Writing to a hardware register address *(unsigned short*)0x04000000 = 0x0403; // How it works: // 1. (unsigned short*)0x04000000 : Casts the number to a pointer. // 2. *(...) : Dereferences the resulting pointer. // 3. = 0x0403 : Assigns the value to the dereferenced memory location.
- Advanced Case: Pointers to Arrays vs. Arrays of Pointers
int* ptrArray[5]; // [] higher precedence: An array of 5 pointers to int. int (*arrayPtr)[5]; // () forces * first: A pointer to an array of 5 ints.
Rule of Thumb: When unsure, or if the expression combines dereferences (`*`), increments (`++`), structure members (`.`, `->`), or array indices (`[]`), use parentheses `()` to make your intended order of operations explicit and clear.
Using Pointers for GBA Hardware
We use pointers to access fixed hardware addresses.
// Pointer to the start of VRAM (Video RAM)
// VRAM in mode 3 holds 16-bit color values
unsigned short* videoBuffer = (unsigned short*)0x06000000;
// Set the first pixel (at address 0x06000000) to red (0x001F in 15-bit color)
videoBuffer[0] = 0x001F;
// This is equivalent to: *(videoBuffer + 0) = 0x001F;
// Set the pixel at (x=10, y=5)
// Index = y * width + x = 5 * 240 + 10 = 1210
videoBuffer[1210] = 0x001F;
// Equivalent to: *(videoBuffer + 1210) = 0x001F;
// Pointer to the Display Control Register (16-bit)
volatile unsigned short* REG_DISPCNT = (unsigned short*)0x04000000;
// Set Mode 3 and enable Background 2
*REG_DISPCNT = 3 | (1 << 10); // Write 0x0403 to the register
Note the use of volatile
for hardware registers. The (unsigned short*)
part is a cast, telling the compiler to treat the number 0x06000000
as a memory address pointing to an unsigned short
.
Quick Check: Pointers
If int score = 100;
and int* scorePtr = &score;
, what does *scorePtr
evaluate to?
- The memory address of
score
- The memory address of
scorePtr
- The value
100
- This is invalid C code
Control Flow: Conditionals and Loops in GBA Games
Control flow statements allow your program to make decisions (if
, else
, switch
) and repeat code (while
, for
). This is crucial for games to respond to input and update frame by frame.
If-Else (Branching)
The if
statement executes code only if a condition is true. else if
and else
provide alternative paths.
unsigned int score = 90;
int playerLives = 3;
if (score >= 100) {
// Code here runs only if score is 100 or more
playerLives += 1;
score = 0;
} else {
// Code here runs if score is less than 100
// (Do nothing in this specific case)
}
// Example checking hypothetical button presses
// (We'll cover input properly later)
// Assume isButtonPressed(button) returns true if pressed
/*
if (isButtonPressed(BUTTON_A)) {
playerY -= 5; // Jump
}
if (isButtonPressed(BUTTON_LEFT)) {
playerX -= 1;
} else if (isButtonPressed(BUTTON_RIGHT)) {
playerX += 1;
}
*/
Loops
Loops repeat sections of code. The main game loop is typically an infinite while(1)
loop.
for
: Best when you know the number of iterations (e.g., looping through array elements).while
: Good for loops that run until a condition changes, or indefinitely.do...while
: Executes the loop body *at least once* before checking the condition.
// Example: Filling the screen with green in Mode 3
unsigned short* videoBuffer = (unsigned short*)0x06000000;
unsigned short green = 0x03E0; // 15-bit green color
for (int y = 0; y < 160; y++) { // Loop through 160 rows
for (int x = 0; x < 240; x++) { // Loop through 240 columns
videoBuffer[y * 240 + x] = green; // Set pixel color
}
}
// Example: Basic game loop structure
while (1) { // Loop forever
// Read input
// Update game state
// Draw graphics
// Wait for VBlank (synchronization, covered later)
}
// Example: Using break to exit a loop (less common for main GBA loop)
/*
while (running) {
if (isButtonPressed(BUTTON_START)) {
running = 0; // Set flag to stop
// Alternatively: break; // Immediately exit the loop
}
// ... game logic ...
}
*/
Switch-Case
The switch
statement provides a clean way to handle different actions based on the value of a single variable (often used for game states or menu choices).
#define STATE_MENU 0
#define STATE_PLAYING 1
#define STATE_GAMEOVER 2
int gameState = STATE_MENU;
switch(gameState) {
case STATE_MENU:
// Draw menu graphics
// if (isButtonPressed(BUTTON_START)) gameState = STATE_PLAYING;
break; // IMPORTANT: Prevents falling through to the next case
case STATE_PLAYING:
// Run game logic
// if (playerHealth <= 0) gameState = STATE_GAMEOVER;
break;
case STATE_GAMEOVER:
// Draw game over screen
break;
default:
// Optional: Code to run if gameState matches none of the cases
break;
}
Quick Check: Control Flow
Which loop structure is most commonly used for the main GBA game loop that needs to run indefinitely until the power is turned off?
for (int i = 0; i < 10000; i++)
do { ... } while (playerAlive);
while (1) { ... }
switch (gameState) { ... }
Using Functions to Organize Code
As programs grow, putting all logic in the main
function becomes messy. Functions are named blocks of code that perform specific tasks. They help organize code, make it reusable, and improve readability.
Declaring and Defining Functions
A function definition specifies its return type, name, parameters, and the code it executes.
// Syntax:
return_type functionName(parameter_type1 param1, parameter_type2 param2, ...) {
// Body of the function: code to execute
// Can use param1, param2, etc.
return value; // Optional: return a value of type return_type
}
GBA Examples
Let's create functions for common GBA tasks.
// Function to initialize GBA video Mode 3 (16-bit bitmap)
// Uses a #define for the Display Control Register for clarity
#define REG_DISPCNT (*(volatile unsigned short*)0x04000000)
#define MODE3 3
#define BG2_ENABLE (1 << 10) // Bit 10 enables background 2
void initGraphicsMode3() {
// This function takes no arguments (void) and returns nothing (void)
REG_DISPCNT = MODE3 | BG2_ENABLE; // Set register to 0x0403
}
// Function to set a single pixel color in Mode 3
// Takes x, y coordinates and a 16-bit color; returns nothing
void setPixel(int x, int y, unsigned short color) {
// Check bounds (optional but good practice)
if (x < 0 || x >= 240 || y < 0 || y >= 160) {
return; // Do nothing if pixel is off-screen
}
// Access VRAM directly using pointer arithmetic
volatile unsigned short* videoBuffer = (unsigned short*)0x06000000;
videoBuffer[y * 240 + x] = color;
}
// Function to create a 15-bit GBA color value from R, G, B components
// Takes red, green, blue (0-31) and returns the combined unsigned short color
unsigned short RGB15(int r, int g, int b) {
// Ensure components are within the valid 5-bit range (0-31)
r = r & 31; // Use bitwise AND as a fast way to clamp to 0-31
g = g & 31;
b = b & 31;
// Combine using bit shifts and OR
// Format: 0BBBBBGGGGGRRRRR
return (unsigned short)((b << 10) | (g << 5) | r);
}
// How to use these functions in main:
int main(void) {
initGraphicsMode3(); // Call the initialization function
unsigned short red = RGB15(31, 0, 0); // Create red color using the helper
unsigned short blue = RGB15(0, 0, 31);
setPixel(120, 80, red); // Draw a red pixel near the center
setPixel(121, 80, blue); // Draw a blue pixel next to it
while(1) {
// Game loop would go here
}
return 0;
}
Function Prototypes
In C, you must declare a function before you call it. If you define a function *after* main
(or in another file), you need to provide a prototype (also called a forward declaration) beforehand. A prototype looks like the function header followed by a semicolon.
// Prototypes (usually placed near the top of the file or in a header .h file)
void initGraphicsMode3(void);
void setPixel(int x, int y, unsigned short color);
unsigned short RGB15(int r, int g, int b);
int main(void) {
// Now we can call the functions even if defined later
initGraphicsMode3();
setPixel(10, 10, RGB15(0, 31, 0)); // Green pixel
// ...
return 0;
}
// ... (Function definitions can appear here) ...
void initGraphicsMode3() { /* ... */ }
void setPixel(int x, int y, unsigned short color) { /* ... */ }
unsigned short RGB15(int r, int g, int b) { /* ... */ }
Quick Check: Functions
What is the primary purpose of a function prototype in C?
- To define the actions the function will perform.
- To declare the function's signature (name, return type, parameters) so it can be called before its full definition appears.
- To optimize the function's execution speed.
- To automatically include the function from a library.
Arrays and Strings for Game Data
An array is a collection of variables of the same type stored contiguously (one after another) in memory. They are essential for storing lists of items, graphics data, level maps, etc.
Declaring and Using Arrays
You declare an array by specifying the type, name, and size (number of elements).
// Declare an array of 5 integers to store high scores
int highScores[5];
// Initialize an array at declaration
int initialScores[5] = { 1000, 800, 600, 400, 200 };
// Access elements using an index (starting from 0)
highScores[0] = 950; // Set the first element
int secondScore = initialScores[1]; // Read the second element (800)
// Iterate over an array using a for loop
for (int i = 0; i < 5; i++) {
// Do something with initialScores[i]
}
// Example: Sprite graphics data (8x8 tile, 4 bits per pixel = 32 bytes)
// Using unsigned char (1 byte) to represent the raw data
const unsigned char blueTile[32] = {
0x11, 0x11, 0x11, 0x11, // Row 0 (pixels 0-7, all color 1)
0x11, 0x11, 0x11, 0x11, // Row 1
0x11, 0x11, 0x11, 0x11, // ... and so on for 8 rows ...
0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11 // Row 7
};
// We mark it 'const' because this data won't change;
// it will likely be stored in ROM.
Important: C does not check if your array index is valid! Accessing highScores[5]
or highScores[-1]
is out of bounds and can lead to crashes or unpredictable behavior by reading/writing unintended memory.
Multi-Dimensional Arrays
Arrays can have more than one dimension, useful for grids like tile maps.
// A 2D array representing a small 4x5 tile map
// Each element could be a tile index (e.g., unsigned short)
unsigned short map[4][5]; // 4 rows, 5 columns
// Access element at row 2, column 3
map[2][3] = 15; // Set tile index 15
// In memory, this is stored row-by-row:
// map[0][0], map[0][1], ..., map[0][4], then map[1][0], ...
// Looping through a 2D array
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 5; col++) {
map[row][col] = 0; // Initialize all tiles to 0
}
}
Arrays and Pointers
In C, an array name often behaves like a pointer to its first element. When you pass an array to a function, you are actually passing a pointer.
// Function to zero out an integer array
// It receives a pointer 'arr' and the array's length
void zeroFill(int* arr, int length) {
for(int i = 0; i < length; i++) {
arr[i] = 0; // Access elements using array notation on the pointer
// Equivalent to: *(arr + i) = 0;
}
}
int main() {
int myData[100];
zeroFill(myData, 100); // Pass the array (decays to pointer) and its size
// Now all elements in myData are 0
return 0;
}
Strings (Character Arrays)
A string in C is simply an array of char
terminated by a special null character ('\0'
).
char message[] = "GBA"; // Creates ['G', 'B', 'A', '\0']
char name[10]; // Can hold a name up to 9 characters + null terminator
// strcpy(name, "Player1"); // Need for strcpy
While strings exist, displaying text on the GBA usually involves custom routines to draw characters using tiles or sprites, as there's no standard console output like printf
(though devkitPro provides libraries that can mimic it).
Quick Check: Arrays
If you declare int data[10];
, what is the valid range of indices you can use to access its elements?
0
to10
(inclusive)0
to9
(inclusive)1
to10
(inclusive)1
to9
(inclusive)
Working with GBA Graphics: Video Modes and Backgrounds
Now we move into GBA-specific hardware features, starting with graphics. The GBA has several video modes that determine how graphics are displayed.
Video Modes
The main modes are:
- Bitmap Modes (Mode 3, 4, 5): Treat VRAM as a direct pixel framebuffer. Simple for plotting individual pixels, but less hardware acceleration.
- Mode 3: 240x160, 16-bit color (32,768 colors). VRAM holds color for each pixel.
- Mode 4: 240x160, 8-bit color (256 colors indexed from a palette). Uses less VRAM, allows page flipping.
- Mode 5: 160x128, 16-bit color. Smaller resolution, allows page flipping.
- Tiled Modes (Mode 0, 1, 2): The screen is built from small 8x8 pixel blocks called tiles arranged in background layers. More efficient for scrolling and large worlds, utilizes hardware features.
- Mode 0: Up to 4 regular tiled background layers.
- Mode 1: 2 regular tiled backgrounds, 1 affine (rotatable/scalable) background.
- Mode 2: 2 affine backgrounds.
Sprites (moving objects) can be used in *any* mode.
Setting a Video Mode
This is done by writing to the Display Control Register (REG_DISPCNT
at 0x04000000
).
#define REG_DISPCNT (*(volatile unsigned short*)0x04000000)
// Flags for REG_DISPCNT (common defines)
#define MODE0 0x0000
#define MODE1 0x0001
#define MODE2 0x0002
#define MODE3 0x0003
#define MODE4 0x0004
#define MODE5 0x0005
#define BG0_ENABLE 0x0100 // Bit 8
#define BG1_ENABLE 0x0200 // Bit 9
#define BG2_ENABLE 0x0400 // Bit 10
#define BG3_ENABLE 0x0800 // Bit 11
#define OBJ_ENABLE 0x1000 // Bit 12 (Sprites)
#define OBJ_MAP_1D 0x0040 // Bit 6 (Use linear sprite tile mapping)
// Example: Set Mode 3 (Bitmap) and enable Background 2 (which is the framebuffer in this mode)
REG_DISPCNT = MODE3 | BG2_ENABLE;
// Example: Set Mode 0 (Tiled) and enable Background 0 and Sprites
// Also enable 1D sprite mapping (recommended)
REG_DISPCNT = MODE0 | BG0_ENABLE | OBJ_ENABLE | OBJ_MAP_1D;
Tiled Backgrounds Explained
Tiled modes are powerful but more complex. Here's the concept:
- Tiles: Small 8x8 pixel graphics. Can be 4-bit (16 colors per tile, chosen from a palette bank) or 8-bit (256 colors per tile, using the full background palette). Tile pixel data is stored in VRAM in sections called Charblocks (Character Base Blocks).
- Tile Map: A grid (e.g., 32x32 tiles) that specifies which tile goes where on the background layer. Each map entry contains the tile index, optional horizontal/vertical flip flags, and a palette bank selector (for 4bpp tiles). Map data is stored in VRAM in sections called Screenblocks (Screen Base Blocks).
- Palette: Colors used by the tiles. Stored in Palette RAM (
0x05000000
). Backgrounds use the first 256 colors. - Background Control Register (
REG_BGxCNT
): Each background (BG0-BG3) has a control register (e.g.,REG_BG0CNT
at0x04000008
) to configure its properties: - Which Charblock holds its tile data.
- Which Screenblock holds its map data.
- Color depth (4bpp or 8bpp).
- Size of the tile map (e.g., 256x256 pixels up to 512x512 pixels).
- Priority (which layers appear on top).
- Scrolling Registers (
REG_BGxHOFS
,REG_BGxVOFS
): Each background has horizontal and vertical offset registers to easily scroll the view across the larger tile map.
Workflow:
- Create graphics and map (often using tools like `grit` to convert images).
- Load tile data into a Charblock in VRAM.
- Load map data into a Screenblock in VRAM.
- Load the palette into Palette RAM.
- Configure the `REG_BGxCNT` register for the background.
- Enable the background using `REG_DISPCNT`.
The hardware then automatically draws the background based on the map and tiles, handling scrolling efficiently.
// Simplified Example: Setting up Background 0 in Mode 0
// 1. Assume tile data `myTiles` (e.g., from grit) is loaded to Charblock 0 (0x6000000)
// 2. Assume map data `myMap` (e.g., from grit) is loaded to Screenblock 16 (0x6008000)
// 3. Assume palette `myPalette` is loaded to Background Palette RAM (0x5000000)
// 4. Configure BG0 Control Register
#define REG_BG0CNT (*(volatile unsigned short*)0x04000008)
#define CHAR_BASE(n) ((n) << 2) // Charblock number (0-3) shifted
#define SCREEN_BASE(n) ((n) << 8) // Screenblock number (0-31) shifted
#define BG_4BPP (0 << 7) // 0 for 4bpp, 1 for 8bpp
#define BG_SIZE_0 (0 << 14) // 0: 256x256, 1: 512x256, 2: 256x512, 3: 512x512
#define BG_PRIORITY(n) (n) // Priority 0-3
REG_BG0CNT = CHAR_BASE(0) | SCREEN_BASE(16) | BG_4BPP | BG_SIZE_0 | BG_PRIORITY(1);
// 5. Enable BG0 in Display Control (already shown above)
// REG_DISPCNT = MODE0 | BG0_ENABLE | OBJ_ENABLE | OBJ_MAP_1D;
// Example Scrolling: Move background right by 1 pixel
#define REG_BG0HOFS (*(volatile unsigned short*)0x04000010)
REG_BG0HOFS++; // Increment horizontal offset
Quick Check: Graphics Modes
Which GBA video mode is generally most efficient for creating large, scrolling game worlds with repeating graphical elements?
- Mode 3 (16-bit Bitmap)
- Mode 4 (8-bit Bitmap with Palette)
- Mode 0 (Tiled Mode with 4 Backgrounds)
- Mode 5 (16-bit Bitmap, smaller resolution)
Sprites and Object Attribute Memory (OAM)
Sprites (also called Objects or OBJs) are small, independent graphical elements that move over the backgrounds. They are ideal for player characters, enemies, bullets, UI cursors, etc. The GBA hardware can display up to 128 sprites simultaneously.
Sprite Attributes (OAM)
Each sprite's properties (position, shape, size, graphics tile, etc.) are defined in a special memory area called Object Attribute Memory (OAM), located at 0x07000000
. OAM holds 128 entries, one for each sprite.
Each OAM entry consists of 3 important 16-bit values (Attributes 0, 1, and 2):
Attribute | Key Bits | Description |
---|---|---|
Attribute 0 (16 bits) | 0-7 | Y-Coordinate (0-159 visible, >=160 hides) |
8 | Rotation/Scaling Flag (0=Off, 1=On) | |
9 | OBJ Mode (0=Normal, 1=Semi-Transparent, 2=OBJ Window) - Bit 8 must be 0 for this | |
10-11 | Shape (00=Square, 01=Horizontal, 10=Vertical) | |
12 | Color Mode (0=4bpp/16 colors, 1=8bpp/256 colors) | |
Attribute 1 (16 bits) | 0-8 | X-Coordinate (0-239 visible, >=240 hides, wraps at 512) |
9-11 | Affine Index (If Rot/Scale Flag is ON) | |
12 | Horizontal Flip (If Rot/Scale is OFF) | |
13 | Vertical Flip (If Rot/Scale is OFF) | |
14-15 | Size (Combined with Shape, determines dimensions, e.g., 8x8 to 64x64) | |
Attribute 2 (16 bits) | 0-9 | Base Tile Index (Which tile(s) in VRAM to use for graphics) |
10-11 | Priority (0-3, relative to backgrounds, 0=highest) | |
12-15 | Palette Bank (For 4bpp sprites, selects which 16-color palette to use) |
Note: The exact meaning of some bits (like Attr0 bit 9, Attr1 bits 9-11) changes depending on whether Rotation/Scaling is enabled via Attr0 bit 8. The table above focuses on normal (non-rotated/scaled) sprites.
Sprite Graphics and Palettes
Sprite graphics (tiles) are stored separately from background tiles, typically in the latter part of VRAM (0x06010000 - 0x06017FFF
). Sprite palettes are stored separately from background palettes, in the latter half of Palette RAM (0x05000200 - 0x050003FF
). This allows sprites to have their own distinct colors.
Steps to Display a Sprite
- Prepare Graphics & Palette: Create sprite image(s) and palette (e.g., using `grit`).
- Load Graphics into Sprite VRAM: Copy the tile data to the sprite tile area (e.g., starting at
0x06010000
). Use `memcpy` or DMA. - Load Palette into Sprite Palette RAM: Copy the palette data (e.g., 16 colors for 4bpp) to the sprite palette area (e.g., starting at
0x05000200
for palette bank 0). - Configure OAM Entry: Set the attributes (Attr0, Attr1, Attr2) for a specific sprite index (0-127) in OAM (
0x07000000
). Define its position (X, Y), shape, size, tile index, palette bank, etc. - Enable Sprites in
REG_DISPCNT
: Make sure theOBJ_ENABLE
bit (and usuallyOBJ_MAP_1D
) is set.
Example: Setting up Sprite 0
Let's define a structure to make OAM access easier and set up a simple 8x8 sprite.
// Define a struct matching the OAM entry layout
typedef struct {
unsigned short attr0;
unsigned short attr1;
unsigned short attr2;
unsigned short fill; // Padding for alignment (Attribute 3 is unused)
} OamEntry;
// Define a pointer to the OAM memory region (array of 128 entries)
#define OAM ((volatile OamEntry*)0x07000000)
// --- Assume sprite tile data loaded to VRAM starting at tile index 0 ---
// --- Assume sprite palette loaded to Palette RAM bank 0 ---
// Configure Sprite 0 in OAM
int spriteIndex = 0;
int playerX = 116; // Center X (240/2 - 8/2)
int playerY = 76; // Center Y (160/2 - 8/2)
// Attribute 0: Y-coord, Regular sprite, Normal blending, Square shape, 4bpp
OAM[spriteIndex].attr0 = (playerY & 0xFF) | // Y coordinate (bottom 8 bits)
(0 << 8) | // Rotation/Scaling OFF
(0 << 10) | // Shape: Square
(0 << 12); // Color: 4bpp
// Other bits default to 0 (Normal OBJ, No Mosaic)
// Attribute 1: X-coord, No flipping, Size 0 (8x8 for square shape)
OAM[spriteIndex].attr1 = (playerX & 0x1FF) | // X coordinate (bottom 9 bits)
(0 << 12) | // H Flip OFF
(0 << 13) | // V Flip OFF
(0 << 14); // Size: 0 (8x8)
// Affine index bits (9-11) are 0 when Rot/Scale is OFF
// Attribute 2: Tile index 0, Priority 0 (highest), Palette bank 0
OAM[spriteIndex].attr2 = (0 & 0x3FF) | // Tile Index: 0 (first tile in sprite VRAM)
(0 << 10) | // Priority: 0
(0 << 12); // Palette Bank: 0
// Don't forget to enable sprites in REG_DISPCNT!
// REG_DISPCNT |= OBJ_ENABLE | OBJ_MAP_1D;
// To move the sprite later:
// playerX++;
// OAM[spriteIndex].attr1 = (OAM[spriteIndex].attr1 & ~0x1FF) | (playerX & 0x1FF);
// (Update only the X bits in Attribute 1)
Updating OAM entries (especially position) in the game loop makes sprites move. It's best practice to update OAM during VBlank to avoid visual glitches.
Quick Check: Sprites & OAM
Which memory region holds the attributes (like position, size, tile index) for all 128 possible sprites?
- VRAM (Video RAM)
- EWRAM (External Work RAM)
- OAM (Object Attribute Memory)
- Palette RAM
Handling Input from the GBA Buttons
To make games interactive, we need to read the player's input from the GBA buttons (A, B, Select, Start, D-Pad Right/Left/Up/Down, L Shoulder, R Shoulder).
The Key Input Register (REG_KEYS)
The state of all 10 buttons is stored in a single 16-bit hardware register called REG_KEYS
located at memory address 0x04000130
.
Crucially, each bit in this register corresponds to a button, and a bit value of 0
means the button is pressed, while 1
means it's not pressed (this is called "active low").
The button-to-bit mapping is:
- Bit 0: A
- Bit 1: B
- Bit 2: Select
- Bit 3: Start
- Bit 4: Right
- Bit 5: Left
- Bit 6: Up
- Bit 7: Down
- Bit 8: R (Shoulder)
- Bit 9: L (Shoulder)
- Bits 10-15: Unused (typically read as 1)
Reading Input in C
We read the register using a pointer, just like other hardware registers.
// Define the register address
#define REG_KEYS (*(volatile unsigned short*)0x04000130)
// In the game loop, read the current state
unsigned short currentKeys = REG_KEYS;
// currentKeys now holds the 16-bit value reflecting all button states
Checking Individual Buttons (Bitwise AND)
To check if a specific button is pressed, we use the bitwise AND operator (&
) with a mask. The mask is a value that has a 1
only at the bit position corresponding to the button we care about.
// Define masks for each button (matching the bit positions)
#define KEY_A 0x0001 // Binary ...00000001
#define KEY_B 0x0002 // Binary ...00000010
#define KEY_SELECT 0x0004 // Binary ...00000100
#define KEY_START 0x0008 // Binary ...00001000
#define KEY_RIGHT 0x0010 // Binary ...00010000
#define KEY_LEFT 0x0020 // Binary ...00100000
#define KEY_UP 0x0040 // Binary ...01000000
#define KEY_DOWN 0x0080 // Binary ...10000000
#define KEY_R 0x0100 // Binary ..100000000
#define KEY_L 0x0200 // Binary .1000000000
// Combine all key masks (useful later)
#define KEY_MASK 0x03FF // Covers bits 0-9
// Check if A is pressed
unsigned short currentKeys = REG_KEYS;
if ((currentKeys & KEY_A) == 0) {
// The result of the AND is 0 only if bit 0 of currentKeys was 0
// So, A is pressed!
}
// A more common shorthand using the NOT operator (!)
// !0 is true, !non-zero is false
if (!(currentKeys & KEY_B)) {
// B is pressed (because currentKeys & KEY_B resulted in 0)
}
// Example: Moving a sprite based on D-Pad input
/*
if (!(currentKeys & KEY_UP)) playerY--;
if (!(currentKeys & KEY_DOWN)) playerY++;
if (!(currentKeys & KEY_LEFT)) playerX--;
if (!(currentKeys & KEY_RIGHT)) playerX++;
// Update sprite OAM entry with new playerX, playerY
SET_SPRITE_POS(0, playerX, playerY); // Assume SET_SPRITE_POS macro exists
*/
Interactive Input Register Visualizer
Simulate pressing buttons below. Notice how the corresponding bit becomes 0 when pressed, and how the overall hexadecimal value of REG_KEYS
changes. Typically, unpressed buttons read as 1, so the default value is 0x03FF
(all 1s in the lower 10 bits).
REG_KEYS
value: 0x03FF
(0000001111111111)
Example Check: Is pressed?
if (!(REG_KEYS & 0x0001)) { ... }
Result: NO
Detecting Button Presses vs. Holds
Sometimes you want an action to happen only *once* when a button is first pressed, not continuously while it's held down (e.g., shooting a single bullet, toggling a menu). This requires comparing the current button state (currentKeys
) with the state from the previous frame (oldKeys
).
unsigned short oldKeys = 0x03FF; // Initialize to non-pressed state
while(1) { // Game Loop
unsigned short currentKeys = REG_KEYS;
// Check for a *new* press of the A button
// Condition: Was NOT pressed before (bit was 1) AND IS pressed now (bit is 0)
if (!(currentKeys & KEY_A) && (oldKeys & KEY_A)) {
// A was just pressed this frame! Do single action (e.g., shoot).
}
// Check if A is currently held down (for continuous action)
if (!(currentKeys & KEY_A)) {
// A is held. Do continuous action (e.g., charge weapon).
}
// Store current state for the next frame's comparison
oldKeys = currentKeys;
// ... rest of game loop ...
}
Detecting the moment a button is *released* uses similar logic (was pressed, now isn't).
Quick Check: Input
In the GBA's REG_KEYS
register (address 0x04000130
), what does a bit value of 0
indicate for the corresponding button?
- The button is not pressed.
- The button is pressed.
- The button is broken.
- The button does not exist.
Using Interrupts (VBlank and Others) for Timing and Efficiency
GBA games need to run at a consistent speed, typically synchronized with the screen's refresh rate (~59.7 Hz, often rounded to 60 FPS). Updating graphics memory (VRAM, OAM, Palettes) must also be done carefully to avoid visual glitches like tearing.
The Vertical Blank (VBlank) Interval
The GBA screen redraws line by line. After drawing the last line (line 159), there's a short period (~1.1 milliseconds) before it starts drawing the first line (line 0) of the next frame. This period is called the Vertical Blank or VBlank.
Why VBlank is important:
- Safe Graphics Updates: It is the safest time to modify graphics memory (VRAM, OAM, Palettes, some display registers) because the hardware isn't actively reading from them to draw to the screen. Updating outside VBlank can cause visual artifacts.
- Frame Timing: Waiting for VBlank once per game loop iteration naturally paces the game to the screen's refresh rate (60 FPS).
Waiting for VBlank
There are a few ways to synchronize with VBlank:
1. Busy-Waiting (Polling REG_VCOUNT
) - Simple but inefficient
The REG_VCOUNT
register (0x04000006
) tells you the current scanline being drawn (0-159 for visible lines, 160-227 during VBlank). You can loop until it reaches 160.
#define REG_VCOUNT (*(volatile unsigned short*)0x04000006)
void waitForVBlank_BusyWait() {
// Wait until VBlank starts (scanline 160)
while (REG_VCOUNT < 160);
// Optionally, wait until VBlank ends (drawing starts again)
// while (REG_VCOUNT >= 160);
}
// In game loop:
while(1) {
waitForVBlank_BusyWait();
// Update game logic
// Update graphics (OAM, VRAM etc.)
}
This works, but wastes CPU cycles and battery power just checking the register repeatedly.
2. Using the BIOS VBlankIntrWait()
- Recommended
The GBA BIOS provides a function (System Call / SWI 0x05) called VBlankIntrWait()
. This function efficiently puts the CPU into a low-power sleep state until the GBA hardware signals a VBlank interrupt.
To use it, the VBlank interrupt must be enabled (which devkitARM's startup code usually does by default). You might need to include relevant headers.
// Assuming VBlank interrupt is enabled (e.g., via irqInit() and irqEnable(IRQ_VBLANK); from libgba)
// Need to link with libgba or have the SWI definition.
// In gba_interrupt.h or similar:
// extern void VBlankIntrWait(void); // Declaration
// In game loop:
while(1) {
VBlankIntrWait(); // CPU sleeps here until VBlank
// --- Code resumes after VBlank interrupt occurs ---
// Perform game logic updates
handleInput();
updatePlayer();
updateEnemies();
// Perform graphics updates (safe now)
updateOAM(); // Copy sprite attributes to OAM
updateBackgroundScroll(); // Change scroll registers
// Copy any changed tile/map data to VRAM (using DMA preferably)
}
This is the preferred method for simple GBA game loops as it's efficient and easy to use.
Other Interrupts
The GBA supports other hardware interrupts besides VBlank, though they are less commonly used for basic game timing:
- HBlank Interrupt: Occurs at the end of each horizontal scanline. Rarely used.
- Timer Interrupts: The GBA has 4 programmable timers that can trigger interrupts at specific intervals. Useful for precise timing independent of VBlank (e.g., for sound mixing, or fixed physics steps).
- DMA Interrupts: Can signal when a DMA transfer completes.
- Keypad Interrupt: Can trigger when specific buttons are pressed (configured via
REG_KEYCNT
). Useful for waking from sleep modes. - Serial, Game Pak, etc.
Setting up custom interrupt handlers requires more advanced C and assembly knowledge involving the Interrupt Master Enable (IME) register, Interrupt Enable (IE) register, Interrupt Flag (IF) register, and setting up the interrupt vector table. For beginners, relying on VBlankIntrWait()
is sufficient.
Quick Check: Timing & VBlank
What is the main benefit of using the BIOS VBlankIntrWait()
function in the game loop?
- It makes the game run faster than 60 FPS.
- It allows graphics updates outside of the VBlank period.
- It efficiently synchronizes the game loop to ~60 FPS and provides a safe window for graphics updates, saving CPU power.
- It automatically handles all player input.
Optimization Techniques for GBA C Programs
The GBA's 16.78 MHz CPU and limited RAM mean that optimizing your C code can be crucial for performance, especially as your game becomes more complex.
Key Optimization Strategies
-
Avoid Floating Point Math: The GBA has no hardware Floating Point Unit (FPU). Operations using
float
ordouble
are emulated in software and are very slow.- Use Integer Math: Stick to
int
,short
,char
whenever possible. - Use Fixed-Point Math: If you need fractions (for precise movement, physics), simulate them using integers. For example, store a value multiplied by 256 (or another power of 2). The upper bits represent the whole part, and the lower bits represent the fractional part. Arithmetic is done on these integers.
- Use Lookup Tables (LUTs): Pre-calculate values that are expensive to compute at runtime. For example, create an array of sine/cosine values instead of calling
sin()
/cos()
(which would likely use slow floating point).
- Use Integer Math: Stick to
- Leverage DMA for Memory Operations: Direct Memory Access (DMA) can copy blocks of memory much faster than the CPU. Use it for:
- Copying large amounts of graphics data (tiles, maps, bitmap frames) to VRAM.
- Copying sprite attributes to OAM (especially if updating many sprites).
- Clearing large memory areas (like VRAM or IWRAM/EWRAM).
- The BIOS functions `CpuSet` and `CpuFastSet` often use DMA internally for optimized fills and copies.
// Example: Using DMA3 to quickly fill OAM to hide all sprites
#define REG_DMA3SAD (*(volatile const void**)0x40000D4)
#define REG_DMA3DAD (*(volatile void**)0x40000D8)
#define REG_DMA3CNT (*(volatile unsigned int*)0x40000DC)
#define DMA_ENABLE (1 << 31)
#define DMA_16BIT (0 << 26) // Use 16-bit transfers for OAM
#define DMA_SRC_FIXED (1 << 24) // Source address doesn't change
#define DMA_DST_INC (0 << 21) // Destination address increments (default)
void hideAllSprites() {
// Value to write: Attribute 0 with Y >= 160 hides sprite
const unsigned short hiddenAttr0 = 0x0200; // (OBJ Mode = disabled)
REG_DMA3SAD = (const void*)&hiddenAttr0; // Source: address of our constant value
REG_DMA3DAD = (void*)OAM; // Destination: start of OAM
// Count: 128 sprites * 4 shorts/sprite = 512 shorts total OAM
// But we only need to set attr0 for each. OAM entries are 8 bytes (4 shorts) apart.
// DMA Fixed source is tricky here. CpuSet is easier:
// CpuFastSet(&hiddenAttr0, OAM, FILL | (128 * sizeof(OamEntry) / 4)); // Needs CpuFastSet definition
// Alternate: Clear whole OAM using DMA fill (often using a zero value)
// volatile int zero = 0;
// REG_DMA3SAD = &zero;
// REG_DMA3DAD = OAM;
// REG_DMA3CNT = DMA_ENABLE | DMA_SRC_FIXED | DMA_32BIT | (sizeof(OamEntry) * 128 / 4);
// Using CpuSet/CpuFastSet is generally simpler for fills.
}
VBlankIntrWait()
to pace your game and save power. Ensure your game logic + drawing fits within the ~16.7ms frame time.- Use the smallest integer type necessary (
u8
,s8
,u16
,s16
). - Use
const
for data that never changes (tile graphics, level maps, lookup tables). This allows the linker to place it in ROM, saving valuable RAM. - Use
volatile
ONLY for hardware registers or memory that can change asynchronously. Don't overuse it, as it prevents helpful compiler optimizations.
- Use tiled backgrounds and hardware scrolling instead of redrawing bitmap backgrounds.
- Use sprite hardware features (flipping, priority) instead of creating extra graphics or complex software layering.
- Use hardware palettes and fade effects (via
REG_BLDCNT
,REG_BLDALPHA
,REG_BLDY
) instead of manually changing pixel colors for fades.
- IWRAM (Internal Work RAM, 32KB): Very fast, 0 wait-state access. Ideal for frequently accessed variables, small lookup tables, and performance-critical code sections (which can be copied here from ROM). Use sparingly.
- EWRAM (External Work RAM, 256KB): Slower (typically 2 wait-states), but much larger. Suitable for general-purpose data, larger buffers, less critical code.
- ROM (Game Pak): Slowest access (wait-states vary by cartridge). Store code and `const` data here by default.
- You can use linker scripts or compiler attributes (`__attribute__((section(".iwram")))`) to manually place specific functions or variables in IWRAM or EWRAM.
-O2
or -O3
in GCC). This allows the compiler to significantly improve performance by rearranging code, inlining functions, etc. Only disable optimizations (-O0
) for specific debugging needs.Quick Check: Optimization
Why should you generally avoid using `float` and `double` data types in GBA development?
- They use too much ROM space.
- They are incompatible with the GBA's display modes.
- The GBA CPU lacks hardware support, making floating-point operations very slow (software emulation).
- They can only store positive numbers.
Conclusion and Next Steps
Congratulations! You've made it through this interactive introduction to C programming specifically for the Game Boy Advance.
What You've Learned:
- Core C concepts (variables, types, pointers, control flow, functions, arrays) in the context of GBA hardware.
- How to access GBA hardware using memory-mapped I/O (pointers to specific addresses).
- Understanding GBA memory regions (IWRAM, EWRAM, VRAM, OAM, etc.).
- Setting GBA video modes (Bitmap vs. Tiled).
- Working with tiled backgrounds (tiles, maps, palettes, control registers).
- Working with sprites (OAM attributes, graphics, palettes).
- Handling button input (
REG_KEYS
, bit masking). - Synchronizing game loops using VBlank (
VBlankIntrWait()
). - Key optimization techniques for the GBA's limited hardware.
Where to Go Next?
You now have a solid foundation to start building simple GBA projects. Here are some areas to explore further:
- Sound: Learn about the GBA's sound channels (Direct Sound A/B, PSG Waveform channels) and how to play music and sound effects (often involves libraries like Maxmod).
- Advanced Graphics: Explore affine transformations for rotating/scaling backgrounds and sprites, alpha blending for transparency effects, mosaics, and windowing effects.
- DMA In-Depth: Understand the different DMA channels and their capabilities for even faster data transfers.
- Interrupts In-Depth: Learn how to write your own interrupt handlers for timers, DMA completion, etc.
- Tools & Libraries:
- grit: Essential for converting graphics assets.
- Tiled Map Editor: Useful for designing levels, often used with `grit` or other converters.
- devkitPro Libraries (libgba): Explore the provided helper functions and macros (we used some conceptual examples like `RGB15`, but libgba has official versions).
- Tonc Library: A popular, well-documented GBA programming library focusing on low-level C access and explanations.
- Butano Engine: A higher-level C++ engine if you want to explore C++ development on GBA.
- Debugging: Use emulators like mGBA, which offer powerful debugging tools (memory viewers, register inspectors, GDB integration).
- Assembly (Optional): For ultimate performance in critical sections, learn ARM/Thumb assembly language.
The best way to learn is by doing! Try modifying the examples, create a simple Pong clone, make a character walk around a tiled map, or experiment with different video modes. Refer to resources like:
- GBATEK: The definitive technical reference for GBA hardware details.
- Tonc GBA Programming Tutorial: An excellent, detailed resource covering many aspects of GBA development in C.
Finally, have fun with it! The GBA is a delightful platform – its limitations spur creativity, and there's something very rewarding about seeing your C code bring a Game Boy Advance game to life. Happy coding!
Interactive Memory Layout Visualizer
Let's visualize how variables, arrays, pointers, and even hardware registers occupy specific "slots" in memory. Memory addresses increase sequentially. Writing outside the bounds allocated for a variable (especially arrays) can overwrite adjacent data, leading to bugs or crashes!
This is a simplified representation. Addresses are examples and sizes reflect common GBA types (char
: 1 byte, short
: 2 bytes, int
/ pointer: 4 bytes).
Simulated Memory View
Controls
Pointer Practice Drill (`*` and `&`)
Coming from languages like JavaScript where variables often directly hold values or references managed automatically, C's pointers can be confusing. In C, you work explicitly with memory addresses using &
(address-of) and access the data at those addresses using *
(dereference). Let's practice common scenarios.
Question 1: Address-Of
int x = 50;
int* ptr;
ptr = &x;
What kind of value is stored inside the variable `ptr` after this code executes?
Question 2: Dereference (Reading)
int x = 123;
int* ptr = &x; // ptr now holds the address of x
int y = *ptr;
What is the value of the variable `y` after this code executes?
Question 3: Dereference (Writing)
int x = 77;
int* ptr = &x;
*ptr = 99; // Note the * on the LEFT side
What is the value of the variable `x` after this code executes?
Question 4: Uninitialized Pointer
int* ptr;
*ptr = 10; // Attempting to write via ptr
What is the likely outcome of this code?
Question 5: Pointer Assignment
int* ptr;
ptr = 1000; // Assigning an integer directly
Assuming 1000 is just a number and not a known valid memory address for an integer, what is wrong with this code if the goal is to make `ptr` point to a variable?
Question 6: Pointer to Pointer
int x = 5;
int* p = &x;
int** pp = &p; // pp points to p, which points to x
What does `**pp` evaluate to?
Question 7: Pointers and Arrays
int nums[] = {100, 200, 300};
int* p;
p = nums; // Assign array name to pointer
What is the value of `*p` after this code executes?
Question 8: Pointer Arithmetic
int nums[] = {10, 20, 30};
int* p = nums; // p points to nums[0]
int val = *(p + 1);
What is the value of `val` after this code executes?
Question 9: NULL Pointers
int* ptr = NULL; // NULL is often defined as (void*)0
*ptr = 5;
What is the likely outcome of this code?
Question 10: Declaration Syntax
int* p1, p2;
What are the data types of `p1` and `p2`?
Number Base Conversion Practice (Decimal, Binary, Hexadecimal)
Understanding how numbers are represented in different bases (especially decimal/base-10, binary/base-2, and hexadecimal/base-16) is crucial for low-level programming. Addresses, colors, and hardware register flags are often expressed in hex or binary.
Quick Reference
- Binary (Base-2): Uses only digits 0 and 1. Each position represents a power of 2 (..., 8, 4, 2, 1).
- Hexadecimal (Base-16): Uses digits 0-9 and A-F (A=10, B=11, C=12, D=13, E=14, F=15). Each position represents a power of 16 (..., 4096, 256, 16, 1).
- Key Link: 1 Hex digit = 4 Binary digits (bits). (e.g., `F` hex = `1111` bin, `A` hex = `1010` bin, `3` hex = `0011` bin)
Practice converting the following numbers. New problems will appear each time you refresh the page.
1. Convert ... (...) to ...:
2. Convert ... (...) to ...:
3. Convert ... (...) to ...:
4. Convert ... (...) to ...:
5. Convert ... (...) to ...:
Number Base Conversion Practice (8-bit)
Practice converting between Decimal, Binary, and Hexadecimal for 8-bit numbers (0-255). Enter your answers and check them!
Number Base Flashcards (4-bit)
Focus on memorizing the basic 4-bit conversions (0-15 Decimal, 0-F Hex, 0000-1111 Binary):
- Practice: Hex to Decimal (0-F)
- Practice: Decimal to Hex (0-15)
- Practice: Hex to Binary (0-F)
- Practice: Binary to Hex (0000-1111)
Advanced Flashcards (5-bit & 8-bit)
Practice more complex conversions and bitwise operations:
- Practice: Hex to Binary (5-bit) (0x00-0x1F)
- Practice: Binary to Hex (5-bit)
- Practice: Bitwise AND (8-bit) - Calculate Result = Value & Mask (checks bits set in *both*)
- Practice: Bitwise OR (8-bit) - Calculate Result = Value | Mask (checks bits set in *either*)
- Practice: Bit Shift (8-bit) - Calculate Result = Value << N or Value >> N (moves bits left/right)
- Practice: Flag Checking (8-bit) - Check if (Value & Mask) != 0 (checks if a specific bit/flag is set)