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
- Choose the setup instructions for your computer and check all four tool versions.
- Extract the package and identify the three files used here:
primeira-rom.s,gba.ld, androm.py. - Build the unchanged example using the four commands, one line at a time, and open
primeira.gbain mGBA. - 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.
| Tool | Input | Output or job |
|---|---|---|
arm-none-eabi-as | Assembly text .s | Object .o, containing instructions and references |
arm-none-eabi-ld | Object and memory rules | Executable .elf, with resolved addresses |
arm-none-eabi-objcopy | ELF executable | ROM byte sequence .gba |
rom.py | Newly created ROM | Fills and verifies the cartridge header |
| mGBA | ROM .gba | Executes 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
brewbelow. - Ubuntu: open Terminal and use the
aptcommands 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:
brew install arm-none-eabi-binutils python
On Ubuntu, including Ubuntu inside WSL on Windows:
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:
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:
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.
.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.
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:
| Step | What the command does | Expected output |
|---|---|---|
1: as | Translates ARM text into an object; -mcpu selects the CPU, -g includes debugging information, and -o names the output | primeira.o |
2: ld | Resolves addresses using the rules selected by -T gba.ld; -o names the executable | primeira.elf |
3: objcopy | Writes raw bytes using -O binary | primeira.gba, not yet header-fixed |
4: python3 | Runs the supplied header tool on that ROM | A 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.

If the first run fails, choose a check from its symptom:
| Symptom | Next check |
|---|---|
command not found | Return 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 opened | Run pwd and ls; check the folder, spelling, and hidden .txt extensions. |
| Syntax error with a line number | Check that line in the editor, save, and rerun the failed command. Continue only after it succeeds. |
| Undefined symbol during linking | Use the complete original .s, rather than an explanatory fragment. |
| Old image or white screen | Successfully 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:
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:
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.