C Programming for Game Boy Advance (GBA) Development

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?

  1. IWRAM
  2. Game Pak ROM
  3. VRAM
  4. 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:

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:

  1. 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.
  2. 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.
  3. 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 from 0x08000000). The linker also inserts a small required header at the start of the ROM.
  4. 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.
  5. 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:

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:

Bit: 0 0 0 0 0 0 0 0
Value: 128 64 32 16 8 4 2 1
Decimal Value: 0
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

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 `*`

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:

0x03007F00 health 5 int
0x03007F04 healthPtr 0x???????? int*

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

2. The `*` (Dereference / Indirection) Operator

3. Confusion Point: `*` in Declarations vs. Expressions

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).

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?

  1. The memory address of score
  2. The memory address of scorePtr
  3. The value 100
  4. 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.

// 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?

  1. for (int i = 0; i < 10000; i++)
  2. do { ... } while (playerAlive);
  3. while (1) { ... }
  4. 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?

  1. To define the actions the function will perform.
  2. To declare the function's signature (name, return type, parameters) so it can be called before its full definition appears.
  3. To optimize the function's execution speed.
  4. 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?

  1. 0 to 10 (inclusive)
  2. 0 to 9 (inclusive)
  3. 1 to 10 (inclusive)
  4. 1 to 9 (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:

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:

  1. 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).
  2. 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).
  3. Palette: Colors used by the tiles. Stored in Palette RAM (0x05000000). Backgrounds use the first 256 colors.
  4. Background Control Register (REG_BGxCNT): Each background (BG0-BG3) has a control register (e.g., REG_BG0CNT at 0x04000008) 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).
  5. 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:

  1. Create graphics and map (often using tools like `grit` to convert images).
  2. Load tile data into a Charblock in VRAM.
  3. Load map data into a Screenblock in VRAM.
  4. Load the palette into Palette RAM.
  5. Configure the `REG_BGxCNT` register for the background.
  6. 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?

  1. Mode 3 (16-bit Bitmap)
  2. Mode 4 (8-bit Bitmap with Palette)
  3. Mode 0 (Tiled Mode with 4 Backgrounds)
  4. 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):

AttributeKey BitsDescription
Attribute 0 (16 bits) 0-7Y-Coordinate (0-159 visible, >=160 hides)
8Rotation/Scaling Flag (0=Off, 1=On)
9OBJ Mode (0=Normal, 1=Semi-Transparent, 2=OBJ Window) - Bit 8 must be 0 for this
10-11Shape (00=Square, 01=Horizontal, 10=Vertical)
12Color Mode (0=4bpp/16 colors, 1=8bpp/256 colors)
Attribute 1 (16 bits) 0-8X-Coordinate (0-239 visible, >=240 hides, wraps at 512)
9-11Affine Index (If Rot/Scale Flag is ON)
12Horizontal Flip (If Rot/Scale is OFF)
13Vertical Flip (If Rot/Scale is OFF)
14-15Size (Combined with Shape, determines dimensions, e.g., 8x8 to 64x64)
Attribute 2 (16 bits) 0-9Base Tile Index (Which tile(s) in VRAM to use for graphics)
10-11Priority (0-3, relative to backgrounds, 0=highest)
12-15Palette 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

  1. Prepare Graphics & Palette: Create sprite image(s) and palette (e.g., using `grit`).
  2. Load Graphics into Sprite VRAM: Copy the tile data to the sprite tile area (e.g., starting at 0x06010000). Use `memcpy` or DMA.
  3. 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).
  4. 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.
  5. Enable Sprites in REG_DISPCNT: Make sure the OBJ_ENABLE bit (and usually OBJ_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?

  1. VRAM (Video RAM)
  2. EWRAM (External Work RAM)
  3. OAM (Object Attribute Memory)
  4. 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:

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?

  1. The button is not pressed.
  2. The button is pressed.
  3. The button is broken.
  4. 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:

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:

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?

  1. It makes the game run faster than 60 FPS.
  2. It allows graphics updates outside of the VBlank period.
  3. It efficiently synchronizes the game loop to ~60 FPS and provides a safe window for graphics updates, saving CPU power.
  4. 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

Quick Check: Optimization

Why should you generally avoid using `float` and `double` data types in GBA development?

  1. They use too much ROM space.
  2. They are incompatible with the GBA's display modes.
  3. The GBA CPU lacks hardware support, making floating-point operations very slow (software emulation).
  4. 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:

Where to Go Next?

You now have a solid foundation to start building simple GBA projects. Here are some areas to explore further:

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:

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

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):

Advanced Flashcards (5-bit & 8-bit)

Practice more complex conversions and bitwise operations: