The Game Boy Advance is a 16.78 MHz ARM7TDMI system with a 3-stage pipeline, 96 KB of VRAM, 6 display modes, and a hybrid audio subsystem combining legacy Game Boy PSG channels with dual 8-bit PCM DMA streams. Every register address, bit field, cycle count, and behavioral quirk documented below targets the implementation needs of a static recompilation engine. The ARM7TDMI executes both 32-bit ARM and 16-bit THUMB instruction sets, with the vast majority of GBA game code running in THUMB from the 16-bit ROM bus for bandwidth efficiency.
CHAPTER 01ARM7TDMI core at 2²⁴ Hz
The CPU clock is exactly 16,777,216 Hz (2²⁴). The ARM7TDMI implements the ARMv4T architecture with a 3-stage pipeline (fetch → decode → execute). Due to prefetch, reading PC yields current instruction address + 8 in ARM state and + 4 in THUMB state. Any taken branch flushes the pipeline at a cost of 2S+1N cycles.
Register file and processor modes
The processor exposes 16 registers (R0–R15) at any time, drawn from 37 physical registers (31 GPRs + 6 status registers). R13 serves as SP by convention, R14 is LR (link register), and R15 is PC.
CPSR layout (32 bits):
| Bit(s) | Field | Description |
|---|---|---|
| 31 | N | Negative / sign flag |
| 30 | Z | Zero flag |
| 29 | C | Carry flag |
| 28 | V | Overflow flag |
| 27–8 | — | Reserved |
| 7 | I | IRQ disable (1 = masked) |
| 6 | F | FIQ disable (1 = masked) |
| 5 | T | State bit (0 = ARM, 1 = THUMB) |
| 4–0 | M[4:0] | Processor mode |
Processor modes and banked registers:
| Mode | M[4:0] | Hex | Banked registers |
|---|---|---|---|
| User | 10000 | 0x10 | (none) |
| FIQ | 10001 | 0x11 | R8\_fiq–R14\_fiq, SPSR\_fiq |
| IRQ | 10010 | 0x12 | R13\_irq, R14\_irq, SPSR\_irq |
| Supervisor | 10011 | 0x13 | R13\_svc, R14\_svc, SPSR\_svc |
| Abort | 10111 | 0x17 | R13\_abt, R14\_abt, SPSR\_abt |
| Undefined | 11011 | 0x1B | R13\_und, R14\_und, SPSR\_und |
| System | 11111 | 0x1F | (shares User registers) |
FIQ uniquely banks R8–R12 in addition to R13–R14, giving it 7 private registers for fast interrupt handling. Each exception mode except User/System has a dedicated SPSR that saves CPSR on entry.
Exception vectors:
| Address | Exception | Entry mode |
|---|---|---|
$00000000 | Reset | SVC |
$00000004 | Undefined Instruction | UND |
$00000008 | SWI | SVC |
$0000000C | Prefetch Abort | ABT |
$00000010 | Data Abort | ABT |
$00000018 | IRQ | IRQ |
$0000001C | FIQ | FIQ |
On exception entry the CPU saves CPSR → SPSR\_mode, sets mode bits, forces T=0 (ARM state), sets I=1 (disables IRQ; FIQ additionally sets F=1), and jumps to the vector.
CHAPTER 02ARM instruction set (32-bit)
Every ARM instruction is conditionally executed. Bits 31–28 encode one of 16 conditions evaluated against CPSR flags:
| Code | Mnemonic | Condition |
|---|---|---|
0000 | EQ | Z=1 |
0001 | NE | Z=0 |
0010 | CS/HS | C=1 |
0011 | CC/LO | C=0 |
0100 | MI | N=1 |
0101 | PL | N=0 |
0110 | VS | V=1 |
0111 | VC | V=0 |
1000 | HI | C=1 ∧ Z=0 |
1001 | LS | C=0 ∨ Z=1 |
1010 | GE | N=V |
1011 | LT | N≠V |
1100 | GT | Z=0 ∧ N=V |
1101 | LE | Z=1 ∨ N≠V |
1110 | AL | Always |
1111 | NV | Never (reserved, unpredictable) |
A failed condition costs 1S cycle with no execution.
Data processing encoding
31-28 27-26 25 24-21 20 19-16 15-12 11-0
cond 0 0 I opcode S Rn Rd operand2The 16 opcodes in bits 24–21
| Bits | Op | Operation | Flags (if S=1) |
|---|---|---|---|
0000 | AND | Rd = Rn AND Op2 | N,Z,C |
0001 | EOR | Rd = Rn XOR Op2 | N,Z,C |
0010 | SUB | Rd = Rn − Op2 | N,Z,C,V |
0011 | RSB | Rd = Op2 − Rn | N,Z,C,V |
0100 | ADD | Rd = Rn + Op2 | N,Z,C,V |
0101 | ADC | Rd = Rn + Op2 + C | N,Z,C,V |
0110 | SBC | Rd = Rn − Op2 − NOT(C) | N,Z,C,V |
0111 | RSC | Rd = Op2 − Rn − NOT(C) | N,Z,C,V |
1000 | TST | test Rn AND Op2 | N,Z,C |
1001 | TEQ | test Rn XOR Op2 | N,Z,C |
1010 | CMP | test Rn − Op2 | N,Z,C,V |
1011 | CMN | test Rn + Op2 | N,Z,C,V |
1100 | ORR | Rd = Rn OR Op2 | N,Z,C |
1101 | MOV | Rd = Op2 | N,Z,C |
1110 | BIC | Rd = Rn AND NOT Op2 | N,Z,C |
1111 | MVN | Rd = NOT Op2 | N,Z,C |
TST/TEQ/CMP/CMN always have S=1 and do not write Rd. If S=1 and Rd=R15, then CPSR ← SPSR (exception return).
Operand 2 when I=1 (immediate)bits 11–8 = rotate_imm, bits 7–0 = imm_8. Value = imm_8 ROR (rotate_imm × 2). When I=0 (register): bits 3–0 = Rm, bits 6–5 = shift type, bit 4 selects shift-by-immediate (0) vs shift-by-register (1).
Barrel shifter operations and special cases
| Bits 6–5 | Operation | Shift-by-0 special |
|---|---|---|
00 | LSL | No shift, C unchanged |
01 | LSR | Encodes LSR #32 (result=0, C=Rm[31]) |
10 | ASR | Encodes ASR #32 (result=sign-fill, C=Rm[31]) |
11 | ROR | Encodes RRX (33-bit rotate right through carry: C→bit31, bit0→C) |
Shift-by-register uses Rs[7:0]. If Rs=0, result=Rm and C is unchanged. Register-specified shifts add +1I cycle.
Multiply instructions
All multiplies use the encoding pattern cond 0000_xxAS with bits 7–4 = 1001.
| Instruction | Bits 23–21 | Operation | Cycles |
|---|---|---|---|
| MUL | 000 | Rd = Rm × Rs | 1S + mI |
| MLA | 001 | Rd = Rm × Rs + Rn | 1S + (m+1)I |
| UMULL | 100 | RdHi:RdLo = Rm × Rs (unsigned) | 1S + (m+1)I |
| UMLAL | 101 | RdHi:RdLo += Rm × Rs (unsigned) | 1S + (m+2)I |
| SMULL | 110 | RdHi:RdLo = Rm × Rs (signed) | 1S + (m+1)I |
| SMLAL | 111 | RdHi:RdLo += Rm × Rs (signed) | 1S + (m+2)I |
The multiplier cycle count m depends on Rs leading bits: m=1 if bits 31–8 are all 0s or all 1s, m=2 if 31–16, m=3 if 31–24, m=4 otherwise. Restrictions: Rd ≠ Rm, R15 not allowed. Flags (S=1): N and Z set, C is destroyed on ARMv4, V unaffected.
Branch, load/store, block transfer
B/BL: cond 101L imm24. L=1 sets LR to return address. PC += sign_extend(imm24) << 2. Range ±32 MB. 2S+1N cycles.
BX: cond 0001_0010_1111_1111_1111_0001 Rn. PC = Rn & ~1. T flag = Rn[0]. This is the ARM/THUMB interworking instruction. 2S+1N cycles.
LDR/STR: cond 01 I P U B W L Rn Rd offset. I=0 for 12-bit immediate offset, I=1 for shifted register. P=pre/post, U=add/subtract, B=byte, W=writeback, L=load/store. LDR costs 1S+1N+1I (add 1S+1N if Rd=PC). STR costs 2N.
LDRH/STRH/LDRSB/LDRSH: cond 000 PUIW L Rn Rd offset_hi 1SH1 offset_lo. S,H bits select: 01=LDRH/STRH, 10=LDRSB, 11=LDRSH. I=1 for 8-bit immediate (split across fields), I=0 for register offset.
LDM/STM: cond 100 PUSW L Rn register_list. P,U = {IA,IB,DA,DB}. S=1 with R15 loaded → CPSR←SPSR. S=1 without R15 → user bank transfer. LDM: nS+1N+1I (n = register count; +1S+1N if PC loaded). STM: (n−1)S+2N.
SWP/SWPB: cond 00010 B 00 Rn Rd SBZ 1001 Rm. Atomic read-modify-write. 1S+2N+1I.
MRS: cond 00010 R 001111 Rd 000000000000. R=0→CPSR, R=1→SPSR. 1S.
MSR: Register form or immediate form. field_mask bits 19–16 select which CPSR/SPSR fields to write (c=control, x=extension, s=status, f=flags). 1S.
SWI: cond 1111 imm24. Enters SVC mode, ARM state, IRQ disabled, PC → $00000008. 2S+1N.
Cycle timing summary
| Instruction | Cycles |
|---|---|
| Data processing (no shift-by-register) | 1S |
| Data processing (shift-by-register) | 1S + 1I |
| Data processing (Rd=PC) | 2S + 1N |
| MRS, MSR | 1S |
| B, BL, BX | 2S + 1N |
| LDR | 1S + 1N + 1I |
| LDR (Rd=PC) | 2S + 2N + 1I |
| STR | 2N |
| LDM (n regs) | nS + 1N + 1I |
| STM (n regs) | (n−1)S + 2N |
| SWP | 1S + 2N + 1I |
| MUL | 1S + mI |
| MLA | 1S + (m+1)I |
| MULL | 1S + (m+1)I |
| MLAL | 1S + (m+2)I |
| SWI | 2S + 1N |
| Condition false | 1S |
S (sequential), N (non-sequential), and I (internal/no-bus) cycles map to actual clock ticks based on the memory region's wait-state configuration.
CHAPTER 03THUMB instruction set (16-bit)
THUMB encodes a useful subset of ARM operations in 16-bit opcodes. Only R0–R7 ("Lo" registers) are freely accessible; R8–R15 require Format 5 hi-register operations. Below are all 19 formats with exact bit encodings.
Format 1 — Move shifted register: 000 Op[1:0] Offset5[4:0] Rs[2:0] Rd[2:0]. Op: 00=LSL, 01=LSR, 10=ASR. LSL#0 = MOV. LSR#0 encodes LSR#32. ASR#0 encodes ASR#32. Sets N,Z,C.
Format 2 — Add/subtract: 00011 I Op Rn/nn3[2:0] Rs[2:0] Rd[2:0]. I=0 register, I=1 3-bit immediate. Op: 0=ADD, 1=SUB. Sets N,Z,C,V.
Format 3 — Move/compare/add/sub immediate: 001 Op[1:0] Rd[2:0] Offset8[7:0]. Op: 00=MOV, 01=CMP, 10=ADD, 11=SUB. 8-bit unsigned immediate.
Format 4 — ALU operations: 010000 Op[3:0] Rs[2:0] Rd[2:0]. All 16 ALU ops
| Op | Mnemonic | Operation | |
|---|---|---|---|
0000 | AND | Rd &= Rs | |
0001 | EOR | Rd ^= Rs | |
0010 | LSL | Rd <<= Rs | |
0011 | LSR | Rd >>= Rs (logical) | |
0100 | ASR | Rd >>= Rs (arithmetic) | |
0101 | ADC | Rd += Rs + C | |
0110 | SBC | Rd -= Rs + NOT(C) | |
0111 | ROR | Rd = Rd ROR Rs | |
1000 | TST | test Rd AND Rs | |
1001 | NEG | Rd = 0 − Rs | |
1010 | CMP | test Rd − Rs | |
1011 | CMN | test Rd + Rs | |
1100 | ORR | Rd | = Rs |
1101 | MUL | Rd *= Rs | |
1110 | BIC | Rd &= ~Rs | |
1111 | MVN | Rd = ~Rs |
Format 5 — Hi register ops / BX: 010001 Op[1:0] H1 H2 Rs[2:0] Rd[2:0]. Op: 00=ADD, 01=CMP, 10=MOV, 11=BX. H1/H2 extend Rs/Rd to 4-bit (accessing R8–R15). Only CMP sets flags. BX uses Rm[0] to select ARM (0) or THUMB (1) state.
Format 6 — PC-relative load: 01001 Rd[2:0] Word8[7:0]. Address = (PC & ~2) + (Word8 << 2). Loads 32-bit word. Max offset 1020 bytes.
Format 7 — Load/store register offset: 0101 L B 0 Ro[2:0] Rb[2:0] Rd[2:0]. Address = Rb + Ro.
Format 8 — Load/store sign-extended: 0101 H S 1 Ro[2:0] Rb[2:0] Rd[2:0]. S|H: 00=STRH, 01=LDRH, 10=LDSB, 11=LDSH.
Format 9 — Load/store immediate offset: 011 B L Offset5[4:0] Rb[2:0] Rd[2:0]. Word: offset = Offset5 << 2. Byte: offset = Offset5.
Format 10 — Load/store halfword: 1000 L Offset5[4:0] Rb[2:0] Rd[2:0]. Offset = Offset5 << 1.
Format 11 — SP-relative load/store: 1001 L Rd[2:0] Word8[7:0]. Address = SP + (Word8 << 2).
Format 12 — Load address: 1010 SP Rd[2:0] Word8[7:0]. SP=0: Rd = (PC & ~2) + (Word8 << 2). SP=1: Rd = SP + (Word8 << 2). Flags unaffected.
Format 13 — Add offset to SP: 10110000 S SWord7[6:0]. S=0: SP += SWord7 << 2. S=1: SP -= SWord7 << 2.
Format 14 — Push/pop: 1011 L 10 R Rlist[7:0]. PUSH = STMDB SP!,{Rlist,LR if R=1}. POP = LDMIA SP!,{Rlist,PC if R=1}. Full descending stack.
Format 15 — Multiple load/store: 1100 L Rb[2:0] Rlist[7:0]. STMIA/LDMIA Rb!, {Rlist}. Always increment-after with writeback.
Format 16 — Conditional branch: 1101 Cond[3:0] SOffset8[7:0]. Target = PC + sign_extend(SOffset8) << 1. Range −256 to +254 bytes. Same condition codes as ARM.
Format 17 — SWI: 11011111 Value8[7:0]. Enters SVC/ARM mode, PC → $00000008.
Format 18 — Unconditional branch: 11100 Offset11[10:0]. Target = PC + sign_extend(Offset11) << 1. Range ±2048 bytes.
Format 19 — Long branch with link (BL): Two-instruction pair. First: 11110 OffsetHigh[10:0] → LR = PC + (sign_extend(OffsetHigh) << 12). Second: 11111 OffsetLow[10:0] → temp = next_instr | 1; PC = LR + (OffsetLow << 1); LR = temp. Total range ±4 MB.
Critical ARMv4T interworking note: On the GBA's ARMv4T, LDR PC, POP {PC}, and MOV PC, Rn in THUMB do not interwork — bit 0 is ignored (no state switch). Only BX performs interworking. ARMv5T+ changed this behavior, so recompilers must enforce v4T semantics.
CHAPTER 04Memory map and bus architecture
| Region | Address | Size | Bus | Access timing (N/S cycles) |
|---|---|---|---|---|
| BIOS | $00000000–$00003FFF | 16 KB | 32-bit | 1/1/1 (8/16/32) |
| EWRAM | $02000000–$0203FFFF | 256 KB | 16-bit | 3/3/6 |
| IWRAM | $03000000–$03007FFF | 32 KB | 32-bit | 1/1/1 |
| I/O | $04000000–$040003FE | ~1 KB | 32-bit | 1/1/1 |
| Palette | $05000000–$050003FF | 1 KB | 16-bit | 1/1/2 |
| VRAM | $06000000–$06017FFF | 96 KB | 16-bit | 1/1/2 |
| OAM | $07000000–$070003FF | 1 KB | 32-bit | 1/1/1 |
| ROM WS0 | $08000000–$09FFFFFF | 32 MB | 16-bit | configurable |
| ROM WS1 | $0A000000–$0BFFFFFF | 32 MB | 16-bit | configurable |
| ROM WS2 | $0C000000–$0DFFFFFF | 32 MB | 16-bit | configurable |
| SRAM | $0E000000–$0E00FFFF | 64 KB | 8-bit | configurable |
EWRAM's 16-bit bus makes 32-bit ARM accesses slow (6 cycles). IWRAM at 32-bit/0-wait is the fastest RAM and ideal for ARM code hot loops. Palette and VRAM add +1 cycle when the display controller is simultaneously accessing them.
BIOS protection: Reads from BIOS ROM when PC is outside the BIOS region return the last successfully fetched BIOS opcode. Typical values: [$00DCh+8] after SoftReset, [$0134h+8] during IRQ, [$013Ch+8] after IRQ return.
Mirroring: EWRAM mirrors across the full $02XXXXXX 16 MB block. IWRAM mirrors across $03XXXXXX. Palette, VRAM, and OAM mirror across their respective 16 MB blocks. VRAM mirrors in 128 KB steps with the upper 32 KB being a mirror of $06010000–$06017FFF.
WAITCNT register ($4000204, 16-bit R/W)
Bits 0-1: SRAM wait (0=4, 1=3, 2=2, 3=8 cycles)
Bits 2-3: WS0 first access (0=4, 1=3, 2=2, 3=8 cycles)
Bit 4: WS0 second access (0=2, 1=1 cycle)
Bits 5-6: WS1 first access (0=4, 1=3, 2=2, 3=8 cycles)
Bit 7: WS1 second access (0=4, 1=1 cycle)
Bits 8-9: WS2 first access (0=4, 1=3, 2=2, 3=8 cycles)
Bit 10: WS2 second access (0=8, 1=1 cycle)
Bits 11-12: PHI terminal out (0=Disable, 1=4.19MHz, 2=8.38MHz, 3=16.78MHz)
Bit 14: Prefetch buffer (0=Disable, 1=Enable)
Bit 15: Game Pak type (Read Only, 0=GBA)Default at startup$0000. Typical cart setting: $4317 (WS0=3/1, SRAM=8, prefetch on). 32-bit ROM accesses are split into two 16-bit accesses (second always sequential). Non-sequential access is forced at every 128 KB boundary.
Open bus behavior
Reads from unmapped addresses return the most recently prefetched opcode. In ARM state this is the 32-bit word at [PC+8]. In THUMB state the result depends on bus width — on 32-bit bus regions: LSW=MSW=[$PC+4]; on 16-bit bus regions: LSW=[$PC+4], MSW=[$PC+2]. No cartridge inserted returns (address/2) & 0xFFFF.
Internal memory control ($4000800, 32-bit R/W)
Initialized to $0D000020. Controls EWRAM access timing. Setting bit 0 can disable EWRAM entirely. Writing $0E000020 reduces EWRAM wait states from 2 to 1 on original GBA/SP hardware. Mirrored every 64 KB in the I/O region.
CHAPTER 05Graphics subsystem
The LCD renders at 240×160 pixels, 59.7275 Hz. Each frame is 280,896 CPU cycles across 228 scanlines (160 visible + 68 VBlank). Each scanline is 1232 cycles (960 HDraw + 272 HBlank). The HBlank flag in DISPSTAT is set for only 226 cycles of the 272-cycle HBlank period.
DISPCNT — $4000000 (16-bit R/W)
Bit 0-2: BG Mode (0–5)
Bit 3: CGB Mode (set by BIOS only)
Bit 4: Display Frame Select (0/1, bitmap modes 4–5)
Bit 5: HBlank Interval Free (1 = allow OAM access during HBlank)
Bit 6: OBJ Character Mapping (0=2D, 1=1D)
Bit 7: Forced Blank (1 = white screen, all video RAM freely accessible)
Bit 8: BG0 Enable
Bit 9: BG1 Enable
Bit 10: BG2 Enable
Bit 11: BG3 Enable
Bit 12: OBJ Enable
Bit 13: Window 0 Enable
Bit 14: Window 1 Enable
Bit 15: OBJ Window EnableDisplay modes
| Mode | Type | BG Layers | Color depth | Resolution | Notes |
|---|---|---|---|---|---|
| 0 | Tile | BG0–BG3 (all text) | 4bpp/8bpp | up to 512×512 | Most flexible tile mode |
| 1 | Tile | BG0,BG1 (text) + BG2 (affine) | 4/8bpp text, 8bpp affine | — | BG3 unavailable |
| 2 | Tile | BG2,BG3 (both affine) | 8bpp only | up to 1024×1024 | No tile flip in affine |
| 3 | Bitmap | BG2 only | 15bpp direct (RGB555) | 240×160 | Single frame, ~75 KB |
| 4 | Bitmap | BG2 only | 8bpp paletted | 240×160 | 2 frames, page flip |
| 5 | Bitmap | BG2 only | 15bpp direct | 160×128 | 2 frames, page flip |
Text BG map sizesSize 0 = 256×256 (1 screenblock), Size 1 = 512×256 (2 SBs horizontal), Size 2 = 256×512 (2 SBs vertical), Size 3 = 512×512 (4 SBs). Affine BG map sizes: 128×128 / 256×256 / 512×512 / 1024×1024.
Text map entries are 16-bitbits 0–9 = tile number (0–1023), bit 10 = H-flip, bit 11 = V-flip, bits 12–15 = palette (4bpp). Affine map entries are 8-bit: tile number 0–255, no flip, always 8bpp.
DISPSTAT ($4000004) and VCOUNT ($4000006)
DISPSTAT: Bit 0 = V-Blank flag (R), Bit 1 = H-Blank flag (R), Bit 2 = V-Counter match flag (R), Bit 3 = VBlank IRQ enable, Bit 4 = HBlank IRQ enable, Bit 5 = VCounter IRQ enable, Bits 8–15 = LYC (V-Count setting).
VCOUNT: Bits 0–7 = current scanline (0–227), read-only.
BGxCNT — $4000008–$400000E (16-bit R/W per BG)
Bits 0-1: Priority (0=highest, 3=lowest; ties: BG0 > BG1 > BG2 > BG3)
Bits 2-3: Character Base Block (0–3, addr = $06000000 + CBB×$4000)
Bit 6: Mosaic enable
Bit 7: Color mode (0=4bpp/16pal, 1=8bpp/256col)
Bits 8-12: Screen Base Block (0–31, addr = $06000000 + SBB×$800)
Bit 13: Overflow wrap (affine BG2/BG3 only; 0=transparent, 1=wrap)
Bits 14-15: Screen Size (0–3)BG scroll and affine registers
Scroll registers ($4000010–$400001E, write-only)BGxHOFS/BGxVOFS, 9-bit offsets (0–511). Used in text mode only.
Affine parameters ($4000020–$400003F)BG2/BG3 each have PA (dx), PB (dmx), PC (dy), PD (dmy) as 8.8 signed fixed-point (16-bit write-only), and reference points BG2X/BG2Y/BG3X/BG3Y as 20.8 signed fixed-point (28 significant bits in 32-bit write-only registers).
Critical internal behavior: Reference points are copied to internal registers at VBlank start. Each scanline, internal registers increment by PB/PD. Writing reference points outside VBlank immediately updates the internal register for the current scanline. Per-pixel rendering: src_x = (internal_x + PA×pixel) >> 8, src_y = (internal_y + PC×pixel) >> 8.
VRAM layout (96 KB at $06000000)
In tile modes$06000000–$0600FFFF (64 KB) for BG tiles and maps, $06010000–$06017FFF (32 KB) for OBJ tiles. Character Base Blocks at 16 KB intervals, Screen Base Blocks at 2 KB intervals.
In bitmap modesframe buffers start at $06000000. Mode 4 Frame 1 at $0600A000. OBJ tiles limited to $06014000–$06017FFF (16 KB, tiles 512–1023 only).
8-bit writes to VRAM write the byte to both halves of the 16-bit unit. 8-bit writes to OAM are ignored. 8-bit writes to Palette double the byte.
OAM and sprites ($07000000, 1 KB)
128 OAM entries × 8 bytes. Three 16-bit attributes interleaved with rotation/scaling data. 32 rotation groups of 4 parameters each (PA,PB,PC,PD as 8.8 fixed-point), spread at 32-byte intervals.
Attribute 0: Bits 0–7 = Y coord; Bit 8 = rotation flag; Bit 9 = double-size (if rot) or disable (if no rot); Bits 10–11 = OBJ mode (0=normal, 1=semi-transparent, 2=OBJ window); Bit 12 = mosaic; Bit 13 = color mode (0=4bpp, 1=8bpp); Bits 14–15 = shape (0=square, 1=horizontal, 2=vertical).
Attribute 1: Bits 0–8 = X coord (9-bit); if rotation: bits 9–13 = rotation group (0–31); if no rotation: bit 12 = H-flip, bit 13 = V-flip; Bits 14–15 = size (0–3).
Attribute 2: Bits 0–9 = tile number (0–1023); Bits 10–11 = priority (0–3); Bits 12–15 = palette (4bpp).
Size table (shape × size):
| Size 0 | Size 1 | Size 2 | Size 3 | |
|---|---|---|---|---|
| Square | 8×8 | 16×16 | 32×32 | 64×64 |
| Horizontal | 16×8 | 32×8 | 32×16 | 64×32 |
| Vertical | 8×16 | 8×32 | 16×32 | 32×64 |
Palette RAM ($05000000, 1 KB)
BG palette at $05000000 (256 entries), OBJ palette at $05000200 (256 entries). RGB555 format: bits 0–4 = Red, 5–9 = Green, 10–14 = Blue, bit 15 unused. 32,768 possible colors. In 4bpp mode, 16 sub-palettes of 16 colors each (base = $05000000 + palette × $20). Color 0 of any palette is transparent. BG palette 0, color 0 = backdrop color.
Windowing
WIN0H/WIN1H ($4000040/$4000042, write-only)Bits 8–15 = X1 (left), Bits 0–7 = X2 (right+1). WIN0V/WIN1V ($4000044/$4000046): same layout for Y. If X2>240 or X1>X2, treated as X2=240.
WININ ($4000048)Bits 0–3 = Win0 BG0–3 enable, Bit 4 = Win0 OBJ, Bit 5 = Win0 special effects; Bits 8–13 = same for Win1.
WINOUT ($400004A)Bits 0–5 = outside window settings; Bits 8–13 = OBJ window settings.
PriorityWIN0 > WIN1 > OBJ Window > Outside.
Blending and special effects
BLDCNT ($4000050)Bits 0–5 = 1st target (BG0–3, OBJ, Backdrop); Bits 6–7 = effect mode (0=none, 1=alpha, 2=brighten, 3=darken); Bits 8–13 = 2nd target.
BLDALPHA ($4000052)Bits 0–4 = EVA (0–16), Bits 8–12 = EVB (0–16). Alpha formula per channel: I = min(31, I_1st × EVA/16 + I_2nd × EVB/16).
BLDY ($4000054)Bits 0–4 = EVY (0–16). Brighten: I = I + (31−I) × EVY/16. Darken: I = I − I × EVY/16.
Semi-transparent OBJs (mode 1) always use alpha blending regardless of BLDCNT mode setting, and are always treated as 1st target regardless of BLDCNT bit 4.
Mosaic ($400004C, write-only)
Bits 0–3 = BG mosaic H-size (value N → (N+1) pixel blocks), Bits 4–7 = BG mosaic V-size, Bits 8–11 = OBJ mosaic H-size, Bits 12–15 = OBJ mosaic V-size.
CHAPTER 06Audio subsystem
Legacy PSG channels (channels 1–4)
Registers at $4000060–$400009F. Internal generation at 262,144 Hz (2²⁴/64).
Channel 1 — Square wave with sweep:
SOUND1CNT_L($4000060): Bits 0–2 = sweep shift (n), Bit 3 = sweep direction (0=increase, 1=decrease), Bits 4–6 = sweep time in 7.8 ms units (0=disabled). Sweep formula:freq(t) = freq(t−1) ± freq(t−1)/2ⁿ. Overflow (>2047) kills the channel.SOUND1CNT_H($4000062): Bits 0–5 = length (duration = (64−n)/256 s), Bits 6–7 = duty (0=12.5%, 1=25%, 2=50%, 3=75%), Bits 8–10 = envelope step (n/64 s, 0=no envelope), Bit 11 = envelope direction (0=decrease, 1=increase), Bits 12–15 = initial volume (0=silent).SOUND1CNT_X($4000064): Bits 0–10 = frequency (output = 131072/(2048−n) Hz), Bit 14 = length flag, Bit 15 = restart (write 1).
Channel 2 mirrors Channel 1 without sweepSOUND2CNT_L ($4000068) = duty/length/envelope, SOUND2CNT_H ($400006C) = frequency/control.
Channel 3 — Programmable wave:
SOUND3CNT_L($4000070): Bit 5 = wave RAM dimension (0=one bank/32 digits, 1=two banks/64 digits), Bit 6 = bank select, Bit 7 = playback on/off.SOUND3CNT_H($4000072): Bits 0–7 = length, Bits 13–14 = volume (0=mute, 1=100%, 2=50%, 3=25%), Bit 15 = force 75%.SOUND3CNT_X($4000074): Bits 0–10 = sample rate (2097152/(2048−n) Hz), Bit 14 = length flag, Bit 15 = restart.- Wave RAM (
$4000090–$400009F): 16 bytes = 32 × 4-bit samples per bank. Selected bank plays; CPU reads/writes the other bank.
Channel 4 — Noise:
SOUND4CNT_L($4000078): Bits 0–5 = length, Bits 8–10 = envelope step, Bit 11 = envelope direction, Bits 12–15 = initial volume.SOUND4CNT_H($400007C): Bits 0–2 = dividing ratio (r), Bit 3 = counter width (0=15-bit LFSR, 1=7-bit), Bits 4–7 = shift clock (s), Bit 14 = length flag, Bit 15 = restart. Frequency =524288/r/2^(s+1)Hz (r=0 treated as 0.5).
DMA sound channels (Direct Sound A & B)
Two 8-bit signed PCM channels fed via 32-byte FIFOs at $40000A0 (FIFO A) and $40000A4 (FIFO B), both 32-bit write-only. Each write delivers 4 signed bytes. The FIFOs are drained by timer overflow — one sample per overflow. When FIFO drops to ≤16 bytes, a DMA request refills 16 bytes (4×32-bit, always, ignoring word count/transfer type in DMA control).
DMA1 feeds FIFO A. DMA2 feeds FIFO B. No other DMA channels can service sound FIFOs.
Sound control registers
SOUNDCNT_L ($4000080)Bits 0–2 = right volume (0–7), Bits 4–6 = left volume (0–7), Bits 8–11 = Ch1–4 right enable, Bits 12–15 = Ch1–4 left enable.
SOUNDCNT_H ($4000082)Bits 0–1 = PSG volume (0=25%, 1=50%, 2=100%), Bit 2 = DMA-A volume (0=50%, 1=100%), Bit 3 = DMA-B volume (0=50%, 1=100%), Bit 8 = DMA-A right enable, Bit 9 = DMA-A left enable, Bit 10 = DMA-A timer select (0=Timer0, 1=Timer1), Bit 11 = DMA-A FIFO reset, Bit 12 = DMA-B right, Bit 13 = DMA-B left, Bit 14 = DMA-B timer select, Bit 15 = DMA-B FIFO reset.
SOUNDCNT_X ($4000084)Bits 0–3 = channel 1–4 ON status (read-only), Bit 7 = master enable (clearing this resets all PSG registers to zero).
SOUNDBIAS ($4000088)Bits 1–9 = bias level (default $100 = register value $200), Bits 14–15 = amplitude resolution (0 = 9-bit/32.768 kHz default, 1 = 8-bit/65.536 kHz, 2 = 7-bit/131.072 kHz, 3 = 6-bit/262.144 kHz).
Mixing pipeline
PSG channels output ±$80 each. DMA channels span ±$200. After volume scaling and L/R mixing, bias ($200 default) is added, result clipped to 0–$3FF (10-bit), then resampled to the SOUNDBIAS-selected rate.
CHAPTER 07Timer-DMA-audio chain
This is the most timing-critical subsystem for recompilation accuracy.
4 timers at $4000100–$400010E
| Timer | Counter/Reload | Control |
|---|---|---|
| TM0 | $4000100 | $4000102 |
| TM1 | $4000104 | $4000106 |
| TM2 | $4000108 | $400010A |
| TM3 | $400010C | $400010E |
TMxCNT_L (16-bit)Write sets reload value; read returns current counter. Reload copied to counter on overflow or when start bit transitions 0→1.
TMxCNT_H (16-bit)
Bits 0-1: Prescaler (0=F/1=16.78MHz, 1=F/64=262kHz, 2=F/256=65.5kHz, 3=F/1024=16.4kHz)
Bit 2: Cascade mode (increment on previous timer overflow; prescaler ignored)
Bit 6: IRQ enable on overflow
Bit 7: Timer start/stopAudio sample rate formula: reload = 65536 − round(16777216 / desired_rate). Common values: 16384 Hz → $FC00, 32768 Hz → $FE00, 65536 Hz → $FF00. Use prescaler 0 (F/1) for audio timers.
The complete audio DMA chain
Timer overflow → pop 1 byte from FIFO → output to DAC
→ if FIFO ≤ 16 bytes → trigger DMA request
→ DMA transfers 4×32-bit (16 bytes) to FIFO
→ DMA source address increments by 16
→ FIFO destination address stays FIXEDOnly Timer 0 or Timer 1 can drive DMA sound (selected per-channel in SOUNDCNT_H). Timer 2/3 cannot. Both DMA Sound A and B can share the same timer for synchronized stereo.
CHAPTER 08DMA controller (4 channels)
Register map
| Ch | SAD | DAD | CNT_L | CNT_H |
|---|---|---|---|---|
| 0 | $40000B0 | $40000B4 | $40000B8 | $40000BA |
| 1 | $40000BC | $40000C0 | $40000C4 | $40000C6 |
| 2 | $40000C8 | $40000CC | $40000D0 | $40000D2 |
| 3 | $40000D4 | $40000D8 | $40000DC | $40000DE |
SAD/DAD are 32-bit write-only. CNT_L is 16-bit write-only (word count). CNT_H is 16-bit R/W.
DMA CNT_H layout:
Bits 5-6: Destination control (0=Inc, 1=Dec, 2=Fixed, 3=Inc/Reload)
Bits 7-8: Source control (0=Inc, 1=Dec, 2=Fixed, 3=Prohibited)
Bit 9: Repeat
Bit 10: Transfer type (0=16-bit, 1=32-bit)
Bit 11: Game Pak DRQ (DMA3 only)
Bits 12-13: Start timing (0=Immediately, 1=VBlank, 2=HBlank, 3=Special)
Bit 14: IRQ on completion
Bit 15: DMA EnableSpecial timing (bits 12–13 = 3): DMA0 = prohibited, DMA1/DMA2 = sound FIFO mode (always 4×32-bit, ignoring word count), DMA3 = video capture.
Priority: DMA0 (highest) > DMA1 > DMA2 > DMA3. CPU halts during DMA. Source address limits: DMA0 = 27-bit (internal only), DMA1–3 = 28-bit. Word count: DMA0–2 = 14-bit (max 16384), DMA3 = 16-bit (max 65536).
Transfer timing: 2N + 2(n−1)S + xI per transfer, where x=2 normally, x=4 if both source and destination are GamePak.
CHAPTER 09Interrupt system
Registers
IME ($4000208)Bit 0 = master interrupt enable. IE ($4000200): interrupt enable mask. IF ($4000202): interrupt request flags (write 1 to acknowledge).
| Bit | Source |
|---|---|
| 0 | V-Blank |
| 1 | H-Blank |
| 2 | V-Counter match |
| 3 | Timer 0 |
| 4 | Timer 1 |
| 5 | Timer 2 |
| 6 | Timer 3 |
| 7 | Serial |
| 8 | DMA 0 |
| 9 | DMA 1 |
| 10 | DMA 2 |
| 11 | DMA 3 |
| 12 | Keypad |
| 13 | Game Pak |
Interrupt dispatch flow
- Hardware fires IRQ → CPU enters IRQ mode (ARM state), CPSR→SPSR\_irq, I=1, PC→
$00000018 - BIOS handler at
$00000018saves context, reads user handler address from$03007FFC(top of IWRAM) - BIOS switches to System mode and branches to user handler
- User handler checks IE & IF, processes interrupt, writes 1s to IF to acknowledge, and also ORs into BIOS IF at
$03007FF8(required for IntrWait/Halt functions) - Returns → BIOS restores context, executes
SUBS PC, LR, #4
HALTCNT ($4000301, write-only)Bit 7 = 0 for Halt (CPU paused until interrupt), Bit 7 = 1 for Stop (full power-down, wake only via keypad/gamepak/SIO).
CHAPTER 10BIOS SWI functions
Invoke via SWI n in THUMB or SWI n<<16 in ARM. Parameters in R0–R3; returns in R0, R1, R3.
| SWI | Name | Inputs → Outputs |
|---|---|---|
$00 | SoftReset | Clears $03007E00+, resets SP (svc=$03007FE0, irq=$03007FA0, sys=$03007F00), jumps to $08000000 or $02000000 per flag at $03007FFA |
$01 | RegisterRamReset | R0=flags (bit0=EWRAM, 1=IWRAM, 2=Palette, 3=VRAM, 4=OAM, 5=SIO, 6=Sound, 7=Other regs). Sets DISPCNT=$0080 |
$02 | Halt | Writes $00 to HALTCNT; CPU sleeps until any interrupt |
$03 | Stop | Writes $80 to HALTCNT; deep sleep until keypad/gamepak/SIO |
$04 | IntrWait | R0=1 discard old flags; R1=interrupt mask. Halts until (BIOS_IF & R1) != 0 |
$05 | VBlankIntrWait | Calls IntrWait with R0=1, R1=1 (VBlank). VBlank IRQ must be enabled |
$06 | Div | R0=numerator, R1=denominator → R0=quotient, R1=remainder, R3=abs(quotient) |
$07 | DivArm | R0=denominator, R1=numerator (swapped) → same outputs |
$08 | Sqrt | R0=value → R0=sqrt (u16) |
$09 | ArcTan | R0=tan (1.14 fixed) → R0=angle |
$0A | ArcTan2 | R0=x, R1=y → R0=angle (0–$FFFF for 0–2π) |
$0B | CpuSet | R0=src, R1=dst, R2=count/mode. Bit24: fill. Bit26: 32-bit |
$0C | CpuFastSet | 32-bit only, 8-word blocks (LDMIA/STMIA). Bit24: fill |
$0E | BgAffineSet | R0=src params, R1=dst affine, R2=count |
$0F | ObjAffineSet | R0=src, R1=dst, R2=count, R3=dst stride (2 or 8) |
$10 | BitUnPack | R0=src, R1=dst, R2=info ptr. Expands 1/2/4/8-bit to wider |
$11 | LZ77UnCompWram | LZ77/LZSS decompress, 8-bit writes. Header: type $10, 24-bit size |
$12 | LZ77UnCompVram | Same with 16-bit writes (for VRAM) |
$13 | HuffUnComp | Huffman decompress. Header bit0–3 = data size (4 or 8 bit) |
$14 | RLUnCompWram | Run-length decompress, 8-bit writes. Header type $30 |
$15 | RLUnCompVram | Same with 16-bit writes |
$16 | Diff8bitUnFilterWram | 8-bit differential unfilter, 8-bit writes |
$17 | Diff8bitUnFilterVram | 8-bit differential unfilter, 16-bit writes |
$18 | Diff16bitUnFilter | 16-bit differential unfilter |
$19 | SoundBias | R0 bit0: 0=decrease to 0, 1=increase to $200 |
$1F | MidiKey2Freq | R0=wave ptr, R1=MIDI key, R2=pitch adj → R0=freq |
CHAPTER 11Cartridge and save system
ROM is up to 32 MB on a 16-bit bus, mirrored across WS0 ($08000000), WS1 ($0A000000), and WS2 ($0C000000). Only CPU and DMA3 can access ROM. 32-bit fetches split into two 16-bit accesses.
Save type detection
Games embed ASCII identification strings in ROM
| String | Type | Size |
|---|---|---|
SRAM_V | SRAM/FRAM | 32 KB at $0E000000, 8-bit bus |
FLASH_V | Flash 64 KB | 64 KB at $0E000000 |
FLASH512_V | Flash 64 KB | 64 KB |
FLASH1M_V | Flash 128 KB | 128 KB (bank-switched) |
EEPROM_V | EEPROM | 512 B or 8 KB, serial DMA3 access |
Flash command protocol
All flash commands use three sequential 8-bit writes$AA→$0E005555, $55→$0E002AAA, then command byte → $0E005555. Key commands: $90 = enter chip ID mode (read manufacturer at $0E000000, device at $0E000001), $F0 = exit ID mode, $80 + $10 = erase chip, $80 + $30 = erase 4 KB sector, $A0 = write byte, $B0 = set bank (128 KB only, write 0 or 1 to $0E000000).
Example chip IDsPanasonic 64K = $32/$1B, Sanyo 128K = $62/$13, Macronix 128K = $C2/$09.
EEPROM protocol
Accessed via DMA3 at the top of ROM address space (≤16 MB ROM: $0D000000+, >16 MB: $0DFFFF00+). Serial, 1 bit per halfword. 512-byte EEPROM uses 6-bit addresses (64 blocks × 64 bits). 8 KB EEPROM uses 14-bit addresses (1024 blocks × 64 bits). Read request: send 11 + address + 0, then read 4 dummy bits + 64 data bits. Write: send 10 + address + 64 data bits + 0, poll until bit 0 returns 1.
CHAPTER 12Static recompilation pitfalls for REFORGE
ARM/THUMB interworking on ARMv4T is the single largest source of recompiler bugs. On the GBA, only BX performs interworking. LDR PC, POP {PC}, and MOV PC, Rn do not switch state — bit 0 of the loaded address is placed into PC directly (potentially causing misaligned execution, not a mode switch). ARMv5T+ changed this, and many reference materials describe the newer behavior. REFORGE must enforce strict v4T semantics.
Open bus must be emulated for correct behavior. Some games detect emulators by reading unmapped addresses and checking the prefetch value. The BIOS protection mechanism (returning last-fetched BIOS opcode) is also tested.
Forced blank (DISPCNT bit 7) renders white and makes all video memory freely accessible with no timing penalties. Clearing forced blank during active scanlines restarts rendering with a 2-line delay. Games toggle this during VBlank for bulk VRAM transfers.
Timer-DMA audio requires cycle-accurate timer overflow → DMA triggering. The FIFO holds only 32 bytes. At 64 kHz, there is a 0.25 ms window before underrun. Higher-priority DMA (HBlank DMA via DMA0) can delay sound DMA1/2, causing audio pops. Games that dynamically change timer reload values mid-playback require precise cycle accounting.
Self-modifying code in IWRAM (where ARM code hot loops live) requires invalidating recompiled blocks when writes to the code region are detected. Some games load decompressed overlays into IWRAM and branch to them.
Prefetch buffer (WAITCNT bit 14) allows the CPU to prefetch ROM opcodes during internal cycles, effectively hiding wait states for sequential execution. When enabled, sequential ROM fetches may appear as 1-cycle if the prefetch buffer has data ready. A recompiler that pre-computes cycle counts must model this buffer or accept timing inaccuracy on ROM-heavy code paths.
CHAPTER 13I/O Dispatch Reference (Iteration 13 Synthesis)
Complete Register Dispatch Table
| Address | Name | R/W | Side Effects |
|---|---|---|---|
$4000000 | DISPCNT | R/W | Mode change resets internal counters. Bit 7 (Forced Blank) enables 1-cycle VRAM/Palette/OAM access |
$4000004 | DISPSTAT | R/W | Bits 0-2 read-only (VBlank/HBlank/VCount flags). Bits 3-5 writable (IRQ enables). Bits 8-15 writable (LYC) |
$4000006 | VCOUNT | R | Current scanline 0-227. Stable during VBlank (160-227) |
$4000008-0E | BG0-3CNT | R/W | Some bits mode-locked (e.g. BG2/3 affine bits in mode 0) |
$4000010-1E | BGxHOFS/VOFS | W | 9-bit scroll. Internal latch updated on write |
$4000020-3C | BG2/3 affine | W | Reference points latched at VBlank; PA/PB/PC/PD are 8.8 fixed-point |
$4000040-4A | Window | R/W | WIN0/1 H/V + WININ/WINOUT per-layer enable |
$400004C | MOSAIC | W | BG/OBJ mosaic H/V sizes |
$4000050 | BLDCNT | R/W | Blend target selection + mode (none/alpha/brighten/darken) |
$4000052 | BLDALPHA | R/W | EVA/EVB coefficients (0-16 each) |
$4000054 | BLDY | W | EVY brightness coefficient (0-16) |
$4000060-66 | SOUND1CNT | R/W | Ch1 sweep + duty + envelope + freq. Bit 15 of CNT_X = restart |
$4000068-6E | SOUND2CNT | R/W | Ch2 duty + envelope + freq |
$4000070-76 | SOUND3CNT | R/W | Ch3 wave bank/enable + volume + rate |
$4000078-7E | SOUND4CNT | R/W | Ch4 envelope + LFSR noise |
$4000080 | SOUNDCNT_L | R/W | PSG volume/panning per-channel L/R |
$4000082 | SOUNDCNT_H | R/W | DMA sound volume, timer select, FIFO reset strobes (bits 11/15) |
$4000084 | SOUNDCNT_X | R/W | Bits 0-3 read-only (channel active). Bit 7 only writable (master enable; clearing resets all PSG) |
$4000088 | SOUNDBIAS | R/W | Bias level (default $200) + amplitude resolution |
$4000090-9F | Wave RAM | R/W | 2 banks x 32 x 4-bit samples. CPU accesses inactive bank |
$40000A0 | FIFO_A | W | DirectSound FIFO A (32-byte ring, 8-bit signed PCM) |
$40000A4 | FIFO_B | W | DirectSound FIFO B (32-byte ring, 8-bit signed PCM) |
$40000B0-BA | DMA0 | R/W | SAD/DAD/CNT_L (W), CNT_H (R/W). Internal memory only. Priority 0 |
$40000BC-C6 | DMA1 | R/W | Sound FIFO A special mode (timing=3: fixed dest, 16-byte burst) |
$40000C8-D2 | DMA2 | R/W | Sound FIFO B special mode |
$40000D4-DE | DMA3 | R/W | General purpose + GamePak + EEPROM + video capture |
$4000100-02 | TM0 | R/W | CNT_L (reload/counter), CNT_H (prescaler/cascade/IRQ/start). Drives FIFO A/B |
$4000104-06 | TM1 | R/W | Can also drive FIFO A/B via SOUNDCNT_H timer select |
$4000108-0A | TM2 | R/W | General timer |
$400010C-0E | TM3 | R/W | General timer |
$4000130 | KEYINPUT | R | Active-low buttons (default $03FF = all released) |
$4000132 | KEYCNT | R/W | Keypad IRQ control (AND/OR mode) |
$4000200 | IE | R/W | 14-bit interrupt enable mask |
$4000202 | IF | R/W | Interrupt flags. Write-1-to-clear (AND NOT pattern) |
$4000204 | WAITCNT | R/W | GamePak wait states + prefetch buffer enable (bit 14) |
$4000208 | IME | R/W | Master interrupt enable (bit 0) |
$4000301 | HALTCNT | W | Bit 7: 0=halt (wake on IRQ), 1=stop (deep sleep) |
$4000800 | Internal Mem Ctrl | R/W | EWRAM wait states. Default $0D000020. Set to $0E000020 for overclock (3/3/6 → 2/2/4) |
Sound FIFO Internal Details
The DirectSound FIFOs are 8-entry x 32-bit ring buffers (32 bytes each) with a separate 32-bit playing buffer:
- Pre-pop DMA request: Triggered when FIFO drops to ≤4 entries (≤16 bytes remaining)
- Timer overflow behavior: Pops one 8-bit sample from the playing buffer (MSB-first within each 32-bit word)
- Playing buffer refill: When all 4 bytes consumed from playing buffer, next 32-bit word dequeued from ring
- Underflow: Repeats the last sample (no silence — causes a DC offset click if sustained)
- FIFO reset (SOUNDCNT_H bits 11/15): Clears ring buffer and playing buffer, resets pointers
Sound DMA behavior (DMA1/DMA2 in FIFO mode):
- Always transfers exactly 4 x 32-bit words (16 bytes) regardless of CNT_L word count
- Destination address is FIXED (FIFO register), source increments
- Transfer type forced to 32-bit regardless of CNT_H bit 10
- Repeat bit must be set for continuous playback
Internal Memory Control ($4000800) Deep Dive
Bits 0: EWRAM disable (0=enabled, 1=disabled)
Bits 3-5: EWRAM area
Bits 24-27: EWRAM wait state control- Default value:
$0D000020→ EWRAM at 3/3/6 wait states (non-seq/seq/32-bit) - Overclock value:
$0E000020→ EWRAM at 2/2/4 wait states - Only original GBA and GBA SP respond to the overclock; DS/micro ignore it
- Mirrored every 64 KB throughout the I/O region
- Some commercial games (e.g., Golden Sun) use the overclock for performance
Flash 128KB Save System (Pokémon Emerald / FireRed / LeafGreen)
Chip detection: Enter ID mode ($90 command), read manufacturer at $0E000000 and device at $0E000001. Common IDs: Sanyo LE26FV10N1TS = $62/$13, Macronix MX29L010 = $C2/$09.
Bank switching (128KB only): Command $B0 then write bank number (0 or 1) to $0E000000. This maps the selected 64KB bank into the $0E000000-$0E00FFFF window.
128KB .sav format (Pokémon Emerald):
- Two save blocks (A and B) for double-buffering, each containing 14 sections
- Each section has a 12-byte footer:
- Bytes 0-1: Section ID (0-13)
- Bytes 2-3: Checksum (16-bit, see below)
- Bytes 4-7: Signature (
$08012025) - Bytes 8-11: Save Index (increments each save; higher = newer)
Checksum algorithm:
uint32_t sum = 0;
for (int i = 0; i < data_size / 4; i++)
sum += ((uint32_t*)data)[i];
uint16_t checksum = ((sum >> 16) + (sum & 0xFFFF)) & 0xFFFF;Security key (Pokémon Emerald):
- Located in Section 0 at offset
$0AF8(Emerald) or$0F20(FRLG) - XORs sensitive fields: money (Section 1 @
$0490), coins ($0494), bag item quantities (lower 16 bits of key) - Key is regenerated on each save; verifier must decrypt before checksum validation
Load/verify path:
- Read both blocks A and B
- Compare Save Index — highest is primary, other is backup
- Verify signature (
$08012025) in all 14 sections - Verify checksums; if primary fails, fall back to backup
- Reassemble sections 0-13 in order by Section ID into contiguous save data
x86-64 Dispatch Table Skeleton
// Conceptual dispatch for REFORGE GBA I/O
void mmio_dispatch(uint32_t addr, uint32_t value, bool is_write) {
switch (addr & 0xFFFF) {
// LCD
case 0x0000: dispcnt_write(value); break;
case 0x0004: dispstat_rw(value, is_write); break;
case 0x0006: return vcount_read();
// Sound
case 0x0082: soundcnt_h_write(value); break; // check FIFO reset bits
case 0x0084: soundcnt_x_partial_write(value); break; // bit 7 only
case 0x00A0: fifo_a_write(value); break;
case 0x00A4: fifo_b_write(value); break;
// DMA (enable bits trigger transfer)
case 0x00BA: dma0_cnt_h_write(value); break;
case 0x00C6: dma1_cnt_h_write(value); break; // sound FIFO A
case 0x00D2: dma2_cnt_h_write(value); break; // sound FIFO B
case 0x00DE: dma3_cnt_h_write(value); break;
// Timers
case 0x0102: case 0x0106: case 0x010A: case 0x010E:
timer_cnt_h_write(addr, value); break;
// Interrupts
case 0x0200: ie_write(value); break;
case 0x0202: if_w1c(value); break; // current &= ~value
case 0x0208: ime_write(value); break;
// System
case 0x0301: haltcnt_write(value); break;
case 0x0800: memctrl_write(value); break; // EWRAM overclock
}
// +1 cycle penalty on VRAM/Palette/OAM access during active display
if (video_contention) add_cycle_penalty(1);
// Prefetch suspended during DMA
if (dma_active) prefetch_suspend();
}Verification Notes
Test ROM approach (devkitARM):
- VBlank OAM DMA — verify 128 OBJs transferred within 272-cycle HBlank window
- Sound FIFO — Timer 0 at 16384 Hz feeding DMA1, verify no underflow for 60 frames
- Affine BG — set reference point mid-frame, verify internal latch update
- Flash save — write/read/verify cycle with bank switch + checksum + security key
- IF write-1-to-clear — set IF bits via hardware, write partial mask, verify only specified bits cleared
mGBA trace validation: Compare register read/write traces between mGBA and REFORGE dispatch output. Expected match: ≥99.9% for all MMIO addresses, <0.1% deviation on cycle-timing-dependent reads (VCOUNT during mode transitions).