This documentation contains both hardware and software knowledge.
GBA start routine
BIOS (power on)
When I turn my GBA, the first instruction executed is at 0. That’s because GBA uses ARM CPU (ARM7TDMI) and pc/r15 is 0 when power on. According to GBA Memory Map, 0 is in BIOS (System ROM). BIOS has protection, so it is a black box. However, Nintendo doesn’t protect it well, so it can be dumped and reversed. In addition, it can even be extracted from a 3DS VC title directly (agb.bin). I ripped one from “Pokemon Gold Version.cia” and checked its CRC32. It is the same as the official original GBA BIOS: 81977335. Now you know the reason why we can read code in it.
0 is _start. it jumps to reset_vector, and runs to _000000B4 eventually. 0x4000000 - 6 = 0x3fffffa = 0x3007ffa (Memory Mirror, useless pins). It checks the value at 0x3007ffa to decide wether to jump to ROM (0x8000000) for a cart game or jump to EWRAM (0x2000000) for a multiboot game.
crt0 (bootloader)
0x8000000 is _start. The instruction there is fixed: jump to start_vector (0x8000000C0). The data between them is cart header. Code at start_vector is not fixed. You can write your own bootloader and it works. Why not jump to game main function directly? Because main function cannot run now. It is written in C so it follows C convention and requires c runtime environment. For example, C function needs a stack but sp/r13 is not set yet. The entry point is runtime library called crt0 here. It is written in assembly because C code cannot run now.
Official crt0.s (Start Up Routine provided by Nintendo):
@--------------------------------------------------------------------
@- Reset -
@--------------------------------------------------------------------
.EXTERN AgbMain
.GLOBAL start_vector
.CODE 32
start_vector:
mov r0, #PSR_IRQ_MODE @ Switch to IRQ Mode
msr cpsr, r0
ldr sp, sp_irq @ Set SP_irq
mov r0, #PSR_SYS_MODE @ Switch to System Mode
msr cpsr, r0
ldr sp, sp_usr @ Set SP_usr
ldr r1, =INTR_VECTOR_BUF @ Set Interrupt Address
adr r0, intr_main
str r0, [r1]
ldr r1, =AgbMain @ Start & Switch to 16bit Code
mov lr, pc
bx r1
b start_vector @ Reset
.ALIGN
sp_usr: .word WRAM_END - 0x100
sp_irq: .word WRAM_END - 0x60
It sets stack, interrupt and jumps to AgbMain. However, it doesn’t initialize global variables in WRAM.
AgbMain (main function)
This is the main logic of the game. A main function in C doesn’t have to be named as main. For example, it is named as WinMain in Windows Applications. That totally depends on the crt0. It jumps to AgbMain at last, so it is named as AgbMain for GBA applications. AgbMain is written by the game developer, so it has no fixed pattern. It is different from game to game.
// clear RAM
DmaFill32(3, 0, (void *)IWRAM_START, 0x7F80); // reset the area for the IWRAM ARM section.
CpuFastFill(0, (void *)EWRAM_START, 0x40000);
The first thing is does is to clear IWRAM & EWRAM. The data on a RAM chip is random when power on. It doesn’t have to be full of 0. If you doesn’t clear the RAM, the global variables have random initial values. Because the code here fills RAM with zero, all global variables will have a zero initial value.
RegisterRamReset(RESET_ALL);
The first thing it does is to trigger a RegisterRamReset. It also clears EWRAM & IWRAM. Therefore, all global variables are initialized to 0.
AgbMain in Demo Dolphin
void AgbMain(void)
{
......
GameInit(); // initialize game
......
}
//------------------------------------------------------------------//
// Game Initialization //
//------------------------------------------------------------------//
void GameInit()
{
DmaClear(3, 0, EX_WRAM, EX_WRAM_SIZE, 32); // CPU External Work RAM clear
DmaClear(3, 0, CPU_WRAM, CPU_WRAM_SIZE - 0x200,32); // CPU Internal Work RAM clear
DmaClear(3, 0, VRAM, VRAM_SIZE, 32); // VRAM clear
DmaClear(3, 160, OAM, OAM_SIZE, 32); // OAM clear
DmaClear(3, 0, PLTT, PLTT_SIZE, 32); // palette clear
......
}
It has a special function to clear WRAM.
AgbMain in Demo Yoshi
void AgbMain(void)
{
s16 i;
//AGBPrintInit();
//AGBPrint("TEST \n");
#ifdef ENABLE_PREFETCH
// *(vu16 *)REG_WAITCNT=CST_PREFETCH_ENABLE;
*(vu16 *)REG_WAITCNT=CST_ROM0_1ST_3WAIT | CST_ROM0_2ND_1WAIT | CST_PREFETCH_ENABLE;
#endif
//The First Initialization
DmaClear(3, 0, EX_WRAM, EX_WRAM_SIZE, 32); // Clear CPU external Work RAM
DmaClear(3, 0, CPU_WRAM, CPU_WRAM_SIZE - 0x200,32); // Clear CPU internal Work RAM
// DmaClear(3, 0, VRAM, VRAM_SIZE, 32); // Clear VRAM
// DmaClear(3, 160, OAM, OAM_SIZE, 32); // Clear OAM
// DmaClear(3, 0, PLTT, PLTT_SIZE, 32); // Clear Palette
......
}
It also clears WRAM first.
SoftReset (L+R+A+B/A+B+Select+Start)
When you press the key combo, the game restarts. Why?
// perform the game loop.
while (1)
{
ExecMainUpdate();
SoftResetIfKeyComboPressed();
};
It is the main loop in AgbMain.
void SoftResetIfKeyComboPressed(void)
{
if ((u8)sub_8000D18() != 0)
{
if (gKeyStatusPtr->heldKeys == (L_BUTTON | R_BUTTON | A_BUTTON | B_BUTTON))
SoftReset(0);
else if (gKeyStatusPtr->heldKeys == (A_BUTTON | B_BUTTON | SELECT_BUTTON | START_BUTTON))
SoftReset(0);
}
}
It detects the key combo and trigger a SoftReset.
SoftReset: @ 0x080D16B0
ldr r3, =REG_IME
movs r2, #0
strb r2, [r3]
ldr r1, =0x03007F00
mov sp, r1
swi #1
swi #0
.POOL
It is a library function provided by Nintendo. It triggers a RegisterRamReset(swi 1) and SoftReset(swi 0).
Read the code
or just read the doc.
SoftReset is a noreturn function. It doesn’t returns to its caller. It returns to _start (0x8000000 for a cart game) instead.
Therefore, the game restarts from crt0 and all global variables are initialized to 0 again.
global variables initialization
Read professional books if you want to understand its principle.
In order not to mislead readers, I read it before writing this part to ensure that I have understood it well myself.
In this part, I will give you the conclusion from experiment rather than teach theories in the book.
test.c
int a; // uninitialized global variable
int b = 0; // global variable with a zero initial value
int c = 1; // global variable with a non-zero initial value
int __attribute__((weak)) d = 2; // weak symbol
const int e = 3; // constraint variable
extern int f; // extern symbol
int sum()
{
int g = 4; // local variable
static int h = 5; // static local variable
return a + b + c + d + e + f + g + h;
}
g is invisible. It is in stack frame when the function is running.
The reason why they went to different sections is that they need to be allocated to different types of memory when thr program is running. Code and constants are read-only, so they are allocated to ROM. Variables are read & write, so they are allocated to RAM.
Linker script tells linker how to allocate address to variables. It maps sections to actual memory space. The variables are in WRAM, but their non-zero initial values are stored in the ROM. According to the analysis in the previous part, GBA start routine doesn’t carry data from ROM to WRAM. Therefore, global variables are uninitialized.
In fact, official games like Fire Emblem and Pokemon don’t use global variables with a non-zero initial value. That’s the reason.
To conclude, don’t define a global variable assigned a non-zero initial value when programming.
At least remember that even if you don’t understand this documentation.
However, we can add global variable initialization code into start routine to break that rule. Either crt0 or AgbMain is okay.
The initialization routine needs the address of initial value and variable to copy data from ROM to WRAM. You cann’t get the address before linking. Which came first, chicken or egg? The answer is linker script defined symbol. Leave a hole there and linker will fill it.
Example : devkitPro\devkitARM\arm-none-eabi\lib\gba_crt0.s
@---------------------------------------------------------------------------------
@ Clear BSS section to 0x00
@---------------------------------------------------------------------------------
ldr r0, =__bss_start__
ldr r1, =__bss_end__
sub r1, r0
bl ClearMem
@---------------------------------------------------------------------------------
@ Clear SBSS section to 0x00
@---------------------------------------------------------------------------------
ldr r0, =__sbss_start__
ldr r1, =__sbss_end__
sub r1, r0
bl ClearMem
@---------------------------------------------------------------------------------
@ Copy initialized data (data section) from LMA to VMA (ROM to RAM)
@---------------------------------------------------------------------------------
ldr r1, =__data_lma
ldr r2, =__data_start__
ldr r4, =__data_end__
bl CopyMemChk
Then you can initialize global variables to non-zero values.
Official crt0 provided by Nintendo doesn’t include that. Homebrew crt0 provided by devkitpro includes that.