The Game Boy’s 16-bit address bus can only access 64KB of memory at once, but Pokémon Red/Blue contain 1MB of ROM. The MBC3 (Memory Bank Controller 3) solves this through banking - switching which 16KB section of ROM is visible at addresses $4000-$7FFF.
; Save current bankldh a, [hLoadedROMBank]push af; Switch to bank $E (Battle Engine 7)ld a, BANK(MoveData)ld [rROMB], a ldh [hLoadedROMBank], a; Access data at $4000+ in bank $Ecall ProcessMoveData; Restore previous bankpop afld [rROMB], aldh [hLoadedROMBank], a
From home/bankswitch.asm, the game provides a standard bankswitching routine:
Bankswitch::; Call a function in another bank; Input:; b = bank number; hl = function address; Returns to caller's bank after function completes ldh a, [hLoadedROMBank] push af ; Save current bank ld a, b ldh [hLoadedROMBank], a ld [rROMB], a ; Switch banks call CallHL ; Call the function pop af ; Restore bank ldh [hLoadedROMBank], a ld [rROMB], a retCallHL: jp hl ; Jump to address in hl
The most common macro for calling functions in other banks:
MACRO farcall ld b, BANK(\1) ; Load bank number ld hl, \1 ; Load function address call Bankswitch ; Switch and callENDM
Usage example:
; Call a function in another bankfarcall LoadPokemonData; Automatically:; 1. Saves current bank; 2. Switches to LoadPokemonData's bank ; 3. Calls LoadPokemonData; 4. Restores original bank; 5. Returns to caller
MACRO homecall ldh a, [hLoadedROMBank] push af ; Save current bank ld a, BANK(\1) ; Load bank 0 ldh [hLoadedROMBank], a ld [rROMB], a call \1 ; Direct call pop af ; Restore bank ldh [hLoadedROMBank], a ld [rROMB], aENDM
Since Bank 0 is always accessible at $0000-$3FFF, you can usually just call functions there directly without banking. homecall is mainly used when the function might need to know which bank called it.
MACRO homecall_sf ; "sf" = save flags ldh a, [hLoadedROMBank] push af ld a, BANK(\1) ldh [hLoadedROMBank], a ld [rROMB], a call \1 pop bc ; Pop into bc instead of af ld a, b ; to preserve flags ldh [hLoadedROMBank], a ld [rROMB], aENDM
Useful when you need zero flag, carry flag, etc. to persist:
cp 5 ; Compare with 5homecall_sf DoSomething ; Zero flag preservedjr z, .wasEqual ; Can still branch on result
hLoadedROMBank:: db ; Current ROM bank ($FF80)hSavedROMBank:: db ; Saved bank for nested calls ($FF81)
These HRAM variables track the current bank:
; Check current bankldh a, [hLoadedROMBank]cp BANK(SomeFunction)jr nz, .wrongBank; Temporarily save for nested callldh a, [hLoadedROMBank]ldh [hSavedROMBank], a
HRAM ($FF80-$FFFE) allows fast access using the ldh instruction, which is why bank tracking variables live there.
; Load bank containing Pokémon namesld a, BANK(MonNames)ld [rROMB], aldh [hLoadedROMBank], a; Now read from $4000+ in that bankld hl, MonNamesld a, [hli] ; Read first character; Restore previous bankld a, [wPreviousBank]ld [rROMB], a
; In Bank 3FunctionInBank3:: ; Bankswitch automatically handles nesting farcall FunctionInBank7 ; Returns here farcall FunctionInBank10 ; Returns here ret ; Returns to original caller
The Bankswitch function’s push/pop mechanism handles this automatically.
Interrupts can occur during bank switching! The VBlank handler must:
Save the current bank
Switch to its required bank (if any)
Do VBlank work
Restore the saved bank
This is why hLoadedROMBank is in HRAM - fast access during critical sections.
VBlankHandler:: push af push bc push de push hl ldh a, [hLoadedROMBank] push af ; Save bank state ; Do VBlank work (may need specific banks) call UpdateSprites pop af ld [rROMB], a ; Restore bank ldh [hLoadedROMBank], a pop hl pop de pop bc pop af reti
; Caller wants to load Pikachu's base statsLoadPikachuStats:: ld a, PIKACHU ld [wCurSpecies], a ; GetMonHeader is in bank $E with the data farcall GetMonHeader ; wMonHeader now contains Pikachu's stats ld a, [wMonHBaseHP] ; Use the data... ret
; Text pointers include bank:ProfOakText:: db BANK(ProfOakTextString) dw ProfOakTextString; Display function:DisplayText:: ld a, [wTextPointer] ; Get bank ld [rROMB], a ; Switch ld hl, [wTextPointer + 1] ; Get address call PrintText ; Display ; (Bank restored elsewhere) ret
; In Battle Core (bank $F)ExecutePlayerMove:: ld a, [wPlayerSelectedMove] ; Move effects are in bank $E farcall GetMoveEffect ; Execute in current bank call DoMoveEffect ret