ALAN JAHNEL / Software engineering

Writing

GBA Assembly — 3. Your first ROM

Set up the tools and build a complete ROM that fills the screen and lights up one pixel. Understand every step from text to hardware.

We will turn a text file into a program the Game Boy Advance can execute. The result will be a red screen with one white pixel in the middle. That may seem small, but it follows the entire chain: ARM instructions, ROM layout, video configuration, and writing to VRAM.

If registers, words, or addresses are still confusing, revisit bits, registers, and memory. The series index shows the complete path.

Your route through this chapter

  1. Choose the setup instructions for your computer and check all four tool versions.
  2. Extract the package and identify the three files used here: primeira-rom.s, gba.ld, and rom.py.
  3. Build the unchanged example using the four commands, one line at a time, and open primeira.gba in mGBA.
  4. Once the red screen appears, read how startup, video writes, and the linker produced it. Then change one color and rebuild.

Checkpoint: you can produce the original red screen, change it to green, and explain which source value you edited. You do not need to memorize every startup directive to continue.

Tools and their jobs

Consult build commands and options to distinguish terminal tools from CPU instructions.

We will use GNU Binutils targeting arm-none-eabi, Python 3, and a ready-to-run installation of mGBA. The Binutils target indicates that the tools generate ARM code without assuming an operating system on the device. The tools run on your computer; their output runs on the emulated machine.

ToolInputOutput or job
arm-none-eabi-asAssembly text .sObject .o, containing instructions and references
arm-none-eabi-ldObject and memory rulesExecutable .elf, with resolved addresses
arm-none-eabi-objcopyELF executableROM byte sequence .gba
rom.pyNewly created ROMFills and verifies the cartridge header
mGBAROM .gbaExecutes it and displays the result

We will not use a C compiler. Python only prepares the header on your computer; none of the program's rules run in Python on the GBA.

The terminal is where you type build commands. The text editor is where you save assembly source. mGBA runs the resulting game. Do not paste shell commands into the .s file or type ARM instructions into the terminal.

Choose one setup route:

  • macOS: open Terminal. If Homebrew is not installed, complete the official Homebrew installation and its shell setup instructions first, then use brew below.
  • Ubuntu: open Terminal and use the apt commands below.
  • Windows: follow Microsoft's WSL installation guide to install and initialize Ubuntu. Open Ubuntu from Start for the Linux commands below; use mGBA for Windows to play. The Linux commands are not intended for an ordinary PowerShell prompt.

Run one line at a time, press Enter, and wait for completion. The installation commands are alternatives: you do not need both brew and apt. A sudo password prompt may show no characters while you type.

On macOS, with Homebrew already installed, install the build tools with:

Shell
brew install arm-none-eabi-binutils python

On Ubuntu, including Ubuntu inside WSL on Windows:

Shell
sudo apt update
sudo apt install binutils-arm-none-eabi python3

The package sources are the official Homebrew formula and the Ubuntu package catalog. Use the mGBA installer for your operating system. On Windows, build inside WSL and open the .gba in mGBA for Windows by accessing the WSL folder through File Explorer.

Check the environment before writing code:

Shell
arm-none-eabi-as --version
arm-none-eabi-ld --version
arm-none-eabi-objcopy --version
python3 --version

If a command cannot be found, fix the installation or PATH first. Your computer's own assembler, named just as, may target a different CPU. Do not substitute it without checking.

Preparing the folder

Download the example package, extract it, and open a terminal in the extracted folder containing gba.ld and rom.py. The six .s files are separate programs. In this chapter, use only primeira-rom.s.

To enter the extracted folder, type cd, a space, and its path, putting the path in quotes if it contains spaces, then press Enter. On macOS you can drag the folder into Terminal after typing cd to insert its path. In Ubuntu you can use the file manager's “Open in Terminal” option. In WSL, use a folder inside Ubuntu; explorer.exe . opens the current Ubuntu folder in Windows File Explorer so you can put the extracted files there and later select the ROM from mGBA.

Check where you are and what is in the folder:

Shell
pwd
ls

pwd prints the current folder; ls lists its files. Confirm that all three required filenames appear. If you only see another folder, enter it before building. Use a plain-text or code editor, rather than a word processor, for .s, .ld, and .py files.

If you prefer to type the example, create primeira-rom.s using the complete block below. Save gba.ld and rom.py in the same folder. Check that your editor did not add .txt to the filenames.

<!-- source: en/primeira-rom.s -->
ARM assembly
.syntax unified
.cpu arm7tdmi
.arm

.section .header, "ax"
.global _start
_start:
    b boot
    .space 188

.section .text
boot:
    msr cpsr_c, #0xdf
    ldr sp, =0x03007f00

    ldr r0, =0x04000000
    ldr r1, =0x0483          @ Mode 3 + BG2 + forced blank.
    strh r1, [r0]

    ldr r0, =0x06000000
    ldr r1, =0x001f          @ Red: intensity 31 in the R channel.
    ldr r2, =38400          @ 240 * 160 pixels.
paint:
    strh r1, [r0], #2
    subs r2, r2, #1
    bne paint

    ldr r0, =(0x06000000 + 2 * (80 * 240 + 120))
    ldr r1, =0x7fff          @ White at pixel (120,80).
    strh r1, [r0]

    ldr r0, =0x04000000
    ldr r1, =0x0403          @ Display the completed framebuffer.
    strh r1, [r0]
stop:
    b stop
.ltorg

First, build the unchanged program using the next section. After you have seen the result, the following explanations unpack its startup and drawing code.

Assembling, linking, and opening

Run these four commands in order from the folder containing the files. If one fails, resolve the error before continuing.

Shell
arm-none-eabi-as -mcpu=arm7tdmi -g -o primeira.o primeira-rom.s
arm-none-eabi-ld -T gba.ld -o primeira.elf primeira.o
arm-none-eabi-objcopy -O binary primeira.elf primeira.gba
python3 rom.py primeira.gba

The final command prints the ROM's size and SHA-256 and verifies its header. Keep the .elf: it holds names and useful debugging information that the raw binary does not preserve in the same way.

Check the output after each command:

StepWhat the command doesExpected output
1: asTranslates ARM text into an object; -mcpu selects the CPU, -g includes debugging information, and -o names the outputprimeira.o
2: ldResolves addresses using the rules selected by -T gba.ld; -o names the executableprimeira.elf
3: objcopyWrites raw bytes using -O binaryprimeira.gba, not yet header-fixed
4: python3Runs the supplied header tool on that ROMA validated primeira.gba; its size and hash appear in the terminal

The first three commands can succeed silently. Run ls to check their output files. Open only the final .gba in mGBA. An older ROM can remain after a failed build, so its presence alone does not prove that the latest attempt succeeded.

Open primeira.gba using mGBA's open-file command. You should see a red screen with a white point at (120,80). Increase the window scale to see the pixel. If a change does not appear, check that you opened the ROM from the current folder and repeated all four commands.

The example running in mGBA: a red background and one white pixel in the center

If the first run fails, choose a check from its symptom:

SymptomNext check
command not foundReturn to installation. PATH lists the folders searched for tools; reopen the terminal after completing the installer's shell setup.
A required source file cannot be openedRun pwd and ls; check the folder, spelling, and hidden .txt extensions.
Syntax error with a line numberCheck that line in the editor, save, and rerun the failed command. Continue only after it succeeds.
Undefined symbol during linkingUse the complete original .s, rather than an explanatory fragment.
Old image or white screenSuccessfully repeat all four commands, including rom.py, and reopen the .gba from the folder printed by pwd.

For every later edit, save the source and repeat all four commands in order. Closing mGBA does not rebuild the ROM.

From the cartridge to the first instruction

Startup uses assembler directives, branches and labels, and MRS and MSR.

.syntax unified, .cpu arm7tdmi, and .arm select the syntax, processor, and instruction set. These directives guide the assembler.

The first instruction, b boot, branches to our code. The next 188 bytes reserve the rest of the first 192 bytes, where the cartridge header belongs. rom.py fills the identification fields and checksum. It is not a BIOS, contains no other person's game, and does not execute the example's logic.

At boot, msr cpsr_c, #0xdf selects System mode with IRQ and FIQ disabled. We will poll the hardware directly without installing interrupt handlers. sp receives an internal RAM address for the stack. This first ROM does not yet call routines that use the stack, but later examples will.

These values belong to this example's startup. They are not a general recipe for every ARM program and do not replace a larger library's complete initialization.

Configuring video and filling the image

To read each line of the loop, consult LDR and STR, addressing with base updates, and SUB and its flags.

0x04000000 is the address of DISPCNT. We first write 0x0483: Mode 3, BG2 enabled, and forced blank enabled. Forced blank keeps the display white while we prepare the image.

Starting at 0x06000000, the program writes 38,400 pixels because 240 × 160 = 38,400. Each pixel takes two bytes. 0x001f represents full-intensity red in the format we will study next.

With strh r1, [r0], #2, the store uses the current address, then r0 advances two bytes. subs decreases the counter and updates the flags. bne paint repeats while the counter has not reached zero.

We then write white to a single address:

Plain text
address = VRAM base + 2 × (y × width + x)
         = 0x06000000 + 2 × (80 × 240 + 120)
         = 0x060096F0

Finally, we write 0x0403 to DISPCNT, disabling forced blank while keeping Mode 3 and BG2. The completed image becomes visible. The stop loop keeps the CPU branching to itself; the image stays in VRAM without being repainted.

Choosing where the program lives

The fields in this file are covered under linker and header.

gba.ld contains the linker rules:

<!-- source: en/gba.ld -->
GNU ld
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
MEMORY {
  ROM (rx) : ORIGIN = 0x08000000, LENGTH = 32M
  RAM (rw) : ORIGIN = 0x02000000, LENGTH = 256K
}
SECTIONS {
  .text : { KEEP(*(.header)) *(.text*) *(.rodata*) . = ALIGN(4); } > ROM
  .bss (NOLOAD) : {
    . = ALIGN(4); __bss_start = .; *(.bss*) *(COMMON)
    . = ALIGN(4); __bss_end = .;
  } > RAM
  /DISCARD/ : { *(.comment) *(.note*) }
}
ASSERT(SIZEOF(.bss) < 0x3F000, "Game state exceeds EWRAM")

The key point is that instructions and constants go into ROM, starting at 0x08000000. The BSS, which will hold variables in later examples, goes into EWRAM at 0x02000000.

KEEP(*(.header)) keeps the header at the start of the code section. __bss_start and __bss_end name the bounds of the variable area. NOLOAD avoids using ROM bytes to hold this empty space. It does not clear RAM: our programs with variables will do that during boot.

This script serves the series examples. There is no .data section with initial values to copy and no C runtime. Adding new kinds of sections requires corresponding changes to startup and the linker. The GNU ld documentation explains this layout.

Three small experiments

First, change only the fill color from 0x001f to 0x03e0. Rebuild: the screen should turn green while the white pixel stays in place.

Then restore the original color and change the horizontal coordinate from 120 to 121. The point should move one pixel right. Finally, change Y from 80 to 81: it should move down one row. In screen coordinates, Y increases downward.

You have observed three different things: a color value, a pixel address, and the effect of a 16-bit store to VRAM. This connection between code and a visible result will be our foundation.

Previous: bits, registers, and memory. Next: how pixels form a rectangle.

Back to home