diff --git a/src/SUMMARY.md b/src/SUMMARY.md index da308cbb..79923648 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -26,6 +26,7 @@ - [Getting started](part2/getting-started.md) - [Objects](part2/objects.md) - [Functions](part2/functions.md) +- [VBlank interrupts](part2/vblank-interrupts.md) - [Input](part2/input.md) - [Collision](part2/collision.md) - [Bricks](part2/bricks.md) diff --git a/src/part2/functions.md b/src/part2/functions.md index 7a22385d..76e2c1b3 100644 --- a/src/part2/functions.md +++ b/src/part2/functions.md @@ -62,4 +62,4 @@ The registers serve as parameters to the function, so we'll leave them as-is. -In the next chapter, we'll write another function, this time to read player input. +In the next chapter, we'll clean up the frame loop using the VBlank interrupt. diff --git a/src/part2/vblank-interrupts.md b/src/part2/vblank-interrupts.md new file mode 100644 index 00000000..33645e92 --- /dev/null +++ b/src/part2/vblank-interrupts.md @@ -0,0 +1,61 @@ +# VBlank interrupts + +So far, we have waited for VBlank by repeatedly reading `rLY` until the PPU reaches line 144. +That works, but it keeps the CPU busy doing nothing useful. +The Game Boy can tell the CPU when VBlank begins instead, using the VBlank interrupt. + +An interrupt is a request from the hardware to pause the code currently running, jump to a fixed address, run a small handler, then return to the paused code. +Each interrupt has an address called its *vector*. +The VBlank interrupt vector is `$0040`, and `hardware.inc` gives that address the name `INT_HANDLER_VBLANK`. + +Let's add a handler for it above the `"Header"` section: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:vblank-interrupt}} +{{#include ../../unbricked/vblank-interrupts/main.asm:vblank-interrupt}} +``` + +The handler does two small jobs. +It marks that VBlank happened, and it increments our frame counter. + +Notice the `push af` and `pop af`. +An interrupt can happen between any two instructions in our main code, so the handler must not leave CPU registers changed unexpectedly. +Here we only use `a` and the flags, so saving `af` is enough. +If a handler used more registers, it would need to save those too. +Finally, `reti` returns from the interrupt handler and allows interrupts again. + +Next, add one byte next to `wFrameCounter`: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:variables}} +{{#include ../../unbricked/vblank-interrupts/main.asm:variables}} +``` + +The CPU will not jump to our handler until we enable interrupts. +After initializing our variables, clear any pending interrupt requests, enable the VBlank interrupt in `rIE`, then enable interrupt handling with `ei`. + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:enable-vblank-interrupt}} +{{#include ../../unbricked/vblank-interrupts/main.asm:enable-vblank-interrupt}} +``` + +Now we can replace the `rLY` wait loop with a function: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:wait-for-vblank}} +{{#include ../../unbricked/vblank-interrupts/main.asm:wait-for-vblank}} +``` + +`halt` stops the CPU until an interrupt occurs. +This lets the CPU sleep instead of burning cycles in a loop. +The `nop` after `halt` is a harmless instruction to resume on, which is a common convention around `halt`. + +The function clears `wVBlankDone`, sleeps, and then checks whether the VBlank handler set the byte back to 1. +This matters more once a program has more than one interrupt enabled: if some other interrupt wakes the CPU first, the function just waits again. + +Finally, clean up the main loop: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:main}} +{{#include ../../unbricked/vblank-interrupts/main.asm:main}} +``` + +The frame counter is now updated by the interrupt handler, so the main loop no longer has to increment it manually. +This also makes the frame boundary explicit: each pass through the game logic starts after `WaitForVBlank` returns. + +Up next, we will use that frame loop to read input once per frame. diff --git a/unbricked/audio/main.asm b/unbricked/audio/main.asm index 6e2ed4be..29dd779c 100644 --- a/unbricked/audio/main.asm +++ b/unbricked/audio/main.asm @@ -6,6 +6,20 @@ DEF BRICK_RIGHT EQU $06 DEF BLANK_TILE EQU $08 ; ANCHOR_END: constants +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti + SECTION "Header", ROM0[$100] jp EntryPoint @@ -90,17 +104,18 @@ ClearOam: ld a, %11100100 ld [rOBP0], a - ld a, 0 + xor a, a ld [wFrameCounter], a + ld [wVBlankDone], a + + ; Enable the VBlank interrupt + ldh [rIF], a + ld a, IE_VBLANK + ldh [rIE], a + ei Main: - ld a, [rLY] - cp 144 - jp nc, Main -WaitVBlank2: - ld a, [rLY] - cp 144 - jp c, WaitVBlank2 + call WaitForVBlank ; Add the ball's momentum to its position in OAM. ld a, [wBallMomentumX] @@ -240,6 +255,17 @@ Right: ld [STARTOF(OAM) + 1], a jp Main +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; Convert a pixel position to a tilemap address ; hl = $9800 + X + Y * 32 ; @param b: X @@ -644,6 +670,7 @@ BallEnd: SECTION "Counter", WRAM0 wFrameCounter: db +wVBlankDone: db SECTION "Input Variables", WRAM0 wCurKeys: db diff --git a/unbricked/bcd/main.asm b/unbricked/bcd/main.asm index 87e17d96..e9ef822e 100644 --- a/unbricked/bcd/main.asm +++ b/unbricked/bcd/main.asm @@ -10,6 +10,20 @@ DEF SCORE_TENS EQU $9870 DEF SCORE_ONES EQU $9871 ; ANCHOR_END: score-tile-location +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti + SECTION "Header", ROM0[$100] jp EntryPoint @@ -95,21 +109,22 @@ ClearOam: ld [rOBP0], a ; ANCHOR: init-variables ; Initialize global variables - ld a, 0 + xor a, a ld [wFrameCounter], a + ld [wVBlankDone], a ld [wCurKeys], a ld [wNewKeys], a ld [wScore], a ; ANCHOR_END: init-variables + ; Enable the VBlank interrupt + ldh [rIF], a + ld a, IE_VBLANK + ldh [rIE], a + ei + Main: - ld a, [rLY] - cp 144 - jp nc, Main -WaitVBlank2: - ld a, [rLY] - cp 144 - jp c, WaitVBlank2 + call WaitForVBlank ; Add the ball's momentum to its position in OAM. ld a, [wBallMomentumX] @@ -242,6 +257,17 @@ Right: ld [STARTOF(OAM) + 1], a jp Main +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; Convert a pixel position to a tilemap address ; hl = $9800 + X + Y * 32 ; @param b: X @@ -741,6 +767,7 @@ BallEnd: SECTION "Counter", WRAM0 wFrameCounter: db +wVBlankDone: db SECTION "Input Variables", WRAM0 wCurKeys: db diff --git a/unbricked/bricks/main.asm b/unbricked/bricks/main.asm index 38668e4f..30459949 100644 --- a/unbricked/bricks/main.asm +++ b/unbricked/bricks/main.asm @@ -6,6 +6,20 @@ DEF BRICK_RIGHT EQU $06 DEF BLANK_TILE EQU $08 ; ANCHOR_END: constants +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti + SECTION "Header", ROM0[$100] jp EntryPoint @@ -91,19 +105,20 @@ ClearOam: ld [rOBP0], a ; Initialize global variables - ld a, 0 + xor a, a ld [wFrameCounter], a + ld [wVBlankDone], a ld [wCurKeys], a ld [wNewKeys], a + ; Enable the VBlank interrupt + ldh [rIF], a + ld a, IE_VBLANK + ldh [rIE], a + ei + Main: - ld a, [rLY] - cp 144 - jp nc, Main -WaitVBlank2: - ld a, [rLY] - cp 144 - jp c, WaitVBlank2 + call WaitForVBlank ; Add the ball's momentum to its position in OAM. ld a, [wBallMomentumX] @@ -238,6 +253,17 @@ Right: ld [STARTOF(OAM) + 1], a jp Main +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; Convert a pixel position to a tilemap address ; hl = $9800 + X + Y * 32 ; @param b: X @@ -614,6 +640,7 @@ BallEnd: SECTION "Counter", WRAM0 wFrameCounter: db +wVBlankDone: db SECTION "Input Variables", WRAM0 wCurKeys: db diff --git a/unbricked/collision/main.asm b/unbricked/collision/main.asm index 70ae528e..b52a5e56 100644 --- a/unbricked/collision/main.asm +++ b/unbricked/collision/main.asm @@ -1,5 +1,19 @@ INCLUDE "hardware.inc" +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti + SECTION "Header", ROM0[$100] jp EntryPoint @@ -91,20 +105,21 @@ ClearOam: ld [rOBP0], a ; Initialize global variables - ld a, 0 + xor a, a ld [wFrameCounter], a + ld [wVBlankDone], a ld [wCurKeys], a ld [wNewKeys], a + ; Enable the VBlank interrupt + ldh [rIF], a + ld a, IE_VBLANK + ldh [rIE], a + ei + ; ANCHOR: momentum Main: - ld a, [rLY] - cp 144 - jp nc, Main -WaitVBlank2: - ld a, [rLY] - cp 144 - jp c, WaitVBlank2 + call WaitForVBlank ; Add the ball's momentum to its position in OAM. ld a, [wBallMomentumX] @@ -240,6 +255,17 @@ Right: ld [STARTOF(OAM) + 1], a jp Main +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; ANCHOR: get-tile ; Convert a pixel position to a tilemap address ; hl = $9800 + X + Y * 32 @@ -604,6 +630,7 @@ BallEnd: ; ANCHOR: ram SECTION "Counter", WRAM0 wFrameCounter: db +wVBlankDone: db SECTION "Input Variables", WRAM0 wCurKeys: db diff --git a/unbricked/input/main.asm b/unbricked/input/main.asm index 89e756ca..51188840 100644 --- a/unbricked/input/main.asm +++ b/unbricked/input/main.asm @@ -1,5 +1,19 @@ INCLUDE "hardware.inc" +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti + SECTION "Header", ROM0[$100] jp EntryPoint @@ -70,21 +84,22 @@ ClearOam: ; ANCHOR: initialize-vars ; Initialize global variables - ld a, 0 + xor a, a ld [wFrameCounter], a + ld [wVBlankDone], a ld [wCurKeys], a ld [wNewKeys], a ; ANCHOR_END: initialize-vars + ; Enable the VBlank interrupt + ldh [rIF], a + ld a, IE_VBLANK + ldh [rIE], a + ei + ; ANCHOR: main Main: - ld a, [rLY] - cp 144 - jp nc, Main -WaitVBlank2: - ld a, [rLY] - cp 144 - jp c, WaitVBlank2 + call WaitForVBlank ; Check the current keys every frame and move left or right. call UpdateKeys @@ -120,6 +135,17 @@ Right: jp Main ; ANCHOR_END: main +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; ANCHOR: input-routine UpdateKeys: ; Poll half the controller @@ -420,6 +446,7 @@ PaddleEnd: SECTION "Counter", WRAM0 wFrameCounter: db +wVBlankDone: db ; ANCHOR: vars SECTION "Input Variables", WRAM0 diff --git a/unbricked/serial-link/demo.asm b/unbricked/serial-link/demo.asm index d694b2c0..6dc5d5f1 100644 --- a/unbricked/serial-link/demo.asm +++ b/unbricked/serial-link/demo.asm @@ -63,6 +63,20 @@ DEF HANDSHAKE_COUNT EQU 5 DEF HANDSHAKE_FAILED EQU $F0 ; ANCHOR_END: handshake-codes +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti + ; ANCHOR: serial-interrupt-vector SECTION "Serial Interrupt", ROM0[$58] @@ -141,35 +155,42 @@ WaitVBlank: ld [rOBP0], a ; Initialize global variables - ld a, 0 + xor a, a ld [wFrameCounter], a + ld [wVBlankDone], a ld [wCurKeys], a ld [wNewKeys], a + ; Enable the VBlank interrupt. LinkInit adds the serial interrupt below. + ldh [rIF], a + ld a, IEF_VBLANK + ldh [rIE], a + ; ANCHOR: serial-demo-init-callsite call LinkInit Main: ; ANCHOR_END: serial-demo-init-callsite - ld a, [rLY] - cp 144 - jp nc, Main - ; ANCHOR: serial-demo-update-callsite call Input call MainUpdate ; ANCHOR_END: serial-demo-update-callsite -WaitVBlank2: - ld a, [rLY] - cp 144 - jp c, WaitVBlank2 + call WaitForVBlank call LinkDisplay - ld a, [wFrameCounter] - inc a - ld [wFrameCounter], a jp Main +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; ANCHOR: serial-demo-update MainUpdate: @@ -802,6 +823,7 @@ TilesEnd: SECTION "Counter", WRAM0 wFrameCounter: db +wVBlankDone: db SECTION "Input Variables", WRAM0 wCurKeys: db diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/main.asm index e791fc68..11136862 100644 --- a/unbricked/serial-link/main.asm +++ b/unbricked/serial-link/main.asm @@ -28,6 +28,20 @@ DEF MSG_SHAKE EQU $80 DEF MSG_GAME EQU $81 ; ANCHOR_END: link-defs +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti + ; ANCHOR: serial-interrupt-vector SECTION "Serial Interrupt", ROM0[$58] @@ -332,12 +346,18 @@ EntryPoint: ld a, %11100100 ld [rOBP0], a ; Initialize global variables - ld a, 0 + xor a, a ld [wFrameCounter], a + ld [wVBlankDone], a ld [wCurKeys], a ld [wNewKeys], a ld [wScore], a + ; Enable the VBlank interrupt. LinkInit adds the serial interrupt below. + ldh [rIF], a + ld a, IEF_VBLANK + ldh [rIE], a + ; ANCHOR: link-main call LinkInit @@ -346,15 +366,7 @@ Main: ei ; enable interrupts to process transfers call LinkUpdate -.wait_vblank_end - ldh a, [rLY] - cp 144 - jr nc, .wait_vblank_end - -.wait_vblank_start - ldh a, [rLY] - cp 144 - jr c, .wait_vblank_start + call WaitForVBlank di ; disable interrupts for OAM/VRAM access @@ -517,6 +529,17 @@ Right: ld [_OAMRAM + 1], a jp Main +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; Convert a pixel position to a tilemap address ; hl = $9800 + X + Y * 32 ; @param b: X @@ -1101,6 +1124,7 @@ BallEnd: SECTION "Counter", WRAM0 wFrameCounter: db +wVBlankDone: db SECTION "Input Variables", WRAM0 wCurKeys: db @@ -1120,4 +1144,3 @@ wLinkPacketCount: db wShakeFailed: db wRemoteScore: db ; ANCHOR_END: link-state - diff --git a/unbricked/title-screen/main.asm b/unbricked/title-screen/main.asm index 634d4a62..fe795b3b 100644 --- a/unbricked/title-screen/main.asm +++ b/unbricked/title-screen/main.asm @@ -4,6 +4,14 @@ INCLUDE "hardware.inc" ; ANCHOR_END: includes +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + ld a, 1 + ld [wVBlankDone], a + pop af + reti + ; ANCHOR: header SECTION "Header", ROM0[$100] @@ -45,17 +53,22 @@ TitleScreen: ld a, %11100100 ld [rBGP], a + xor a, a + ld [wVBlankDone], a + ldh [rIF], a + ld a, IE_VBLANK + ldh [rIE], a + ei + TitleScreenLoop: + call WaitForVBlank call UpdateKeys ld a, [wCurKeys] and PAD_START jr z, TitleScreenLoop ; ANCHOR_END: title_screen -WaitVBlank2: - ld a, [rLY] - cp 144 - jp c, WaitVBlank2 + call WaitForVBlank ; Turn the LCD off ld a, 0 @@ -99,6 +112,17 @@ Done: jp Done ; ANCHOR_END: end +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret + ; ANCHOR: memcpy ; Copy bytes from one area to another. ; @param de: Source @@ -337,3 +361,6 @@ Unbricked_Title_Screen_Map_Begin: DB $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, 0,0,0,0,0,0,0,0,0,0,0,0 DB $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, 0,0,0,0,0,0,0,0,0,0,0,0 Unbricked_Title_Screen_Map_End: + +SECTION "VBlank Variables", WRAM0 +wVBlankDone: db diff --git a/unbricked/vblank-interrupts/build.sh b/unbricked/vblank-interrupts/build.sh new file mode 100644 index 00000000..4205789a --- /dev/null +++ b/unbricked/vblank-interrupts/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +rgbasm -o main.o main.asm +rgblink -o unbricked.gb main.o +rgbfix -v -p 0xFF unbricked.gb diff --git a/unbricked/vblank-interrupts/hardware.inc b/unbricked/vblank-interrupts/hardware.inc new file mode 100644 index 00000000..7b180b4f --- /dev/null +++ b/unbricked/vblank-interrupts/hardware.inc @@ -0,0 +1,1158 @@ +;****************************************************************************** +; Game Boy hardware constant definitions +; https://github.com/gbdev/hardware.inc +;****************************************************************************** + +; To the extent possible under law, the authors of this work have +; waived all copyright and related or neighboring rights to the work. +; See https://creativecommons.org/publicdomain/zero/1.0/ for details. +; SPDX-License-Identifier: CC0-1.0 + +; If this file was already included, don't do it again +if !def(HARDWARE_INC) + +; Check for the minimum supported RGBDS version +if !def(__RGBDS_MAJOR__) || !def(__RGBDS_MINOR__) || !def(__RGBDS_PATCH__) + fail "This version of 'hardware.inc' requires RGBDS version 0.5.0 or later" +endc +if __RGBDS_MAJOR__ == 0 && __RGBDS_MINOR__ < 5 + fail "This version of 'hardware.inc' requires RGBDS version 0.5.0 or later." +endc + +; Define the include guard and the current hardware.inc version +; (do this after the RGBDS version check since the `def` syntax depends on it) +def HARDWARE_INC equ 1 +def HARDWARE_INC_VERSION equs "5.3.0" + +; Usage: rev_Check_hardware_inc +; Examples: +; rev_Check_hardware_inc 1.2.3 +; rev_Check_hardware_inc 1.2 (equivalent to 1.2.0) +; rev_Check_hardware_inc 1 (equivalent to 1.0.0) +MACRO rev_Check_hardware_inc + if _NARG == 1 ; Actual invocation by the user + def hw_inc_cur_ver\@ equs strrpl("{HARDWARE_INC_VERSION}", ".", ",") + def hw_inc_min_ver\@ equs strrpl("\1", ".", ",") + rev_Check_hardware_inc {hw_inc_cur_ver\@}, {hw_inc_min_ver\@}, 0, 0 + purge hw_inc_cur_ver\@, hw_inc_min_ver\@ + else ; Recursive invocation + if \1 != \4 || (\2 < \5 || (\2 == \5 && \3 < \6)) + fail "Version \1.\2.\3 of 'hardware.inc' is incompatible with requested version \4.\5.\6" + endc + endc +ENDM + + +;****************************************************************************** +; Memory-mapped registers ($FFxx range) +;****************************************************************************** + +; -- JOYP / P1 ($FF00) -------------------------------------------------------- +; Joypad face buttons +def rJOYP equ $FF00 + +def B_JOYP_GET_BUTTONS equ 5 ; 0 = reading buttons [r/w] +def B_JOYP_GET_CTRL_PAD equ 4 ; 0 = reading Control Pad [r/w] + def JOYP_GET equ %00_11_0000 ; select which inputs to read from the lower nybble + def JOYP_GET_BUTTONS equ %00_01_0000 ; reading A/B/Select/Start buttons + def JOYP_GET_CTRL_PAD equ %00_10_0000 ; reading Control Pad directions + def JOYP_GET_NONE equ %00_11_0000 ; reading nothing + +def B_JOYP_START equ 3 ; 0 = Start is pressed (if reading buttons) [ro] +def B_JOYP_SELECT equ 2 ; 0 = Select is pressed (if reading buttons) [ro] +def B_JOYP_B equ 1 ; 0 = B is pressed (if reading buttons) [ro] +def B_JOYP_A equ 0 ; 0 = A is pressed (if reading buttons) [ro] +def B_JOYP_DOWN equ 3 ; 0 = Down is pressed (if reading Control Pad) [ro] +def B_JOYP_UP equ 2 ; 0 = Up is pressed (if reading Control Pad) [ro] +def B_JOYP_LEFT equ 1 ; 0 = Left is pressed (if reading Control Pad) [ro] +def B_JOYP_RIGHT equ 0 ; 0 = Right is pressed (if reading Control Pad) [ro] + def JOYP_INPUTS equ %0000_1111 ; bits equal to 0 indicate pressed (when reading inputs) + def JOYP_START equ 1 << B_JOYP_START + def JOYP_SELECT equ 1 << B_JOYP_SELECT + def JOYP_B equ 1 << B_JOYP_B + def JOYP_A equ 1 << B_JOYP_A + def JOYP_DOWN equ 1 << B_JOYP_DOWN + def JOYP_UP equ 1 << B_JOYP_UP + def JOYP_LEFT equ 1 << B_JOYP_LEFT + def JOYP_RIGHT equ 1 << B_JOYP_RIGHT + +; SGB command packet transfer uses for JOYP bits +def B_JOYP_SGB_ONE equ 5 ; 0 = sending 1 bit +def B_JOYP_SGB_ZERO equ 4 ; 0 = sending 0 bit + def JOYP_SGB_START equ %00_00_0000 ; start SGB packet transfer + def JOYP_SGB_ONE equ %00_01_0000 ; send 1 bit + def JOYP_SGB_ZERO equ %00_10_0000 ; send 0 bit + def JOYP_SGB_FINISH equ %00_11_0000 ; finish SGB packet transfer + +; Combined input byte, with Control Pad in high nybble (conventional order) +def B_PAD_DOWN equ 7 +def B_PAD_UP equ 6 +def B_PAD_LEFT equ 5 +def B_PAD_RIGHT equ 4 +def B_PAD_START equ 3 +def B_PAD_SELECT equ 2 +def B_PAD_B equ 1 +def B_PAD_A equ 0 + def PAD_CTRL_PAD equ %1111_0000 + def PAD_BUTTONS equ %0000_1111 + def PAD_DOWN equ 1 << B_PAD_DOWN + def PAD_UP equ 1 << B_PAD_UP + def PAD_LEFT equ 1 << B_PAD_LEFT + def PAD_RIGHT equ 1 << B_PAD_RIGHT + def PAD_START equ 1 << B_PAD_START + def PAD_SELECT equ 1 << B_PAD_SELECT + def PAD_B equ 1 << B_PAD_B + def PAD_A equ 1 << B_PAD_A + +; Combined input byte, with Control Pad in low nybble (swapped order) +def B_PAD_SWAP_START equ 7 +def B_PAD_SWAP_SELECT equ 6 +def B_PAD_SWAP_B equ 5 +def B_PAD_SWAP_A equ 4 +def B_PAD_SWAP_DOWN equ 3 +def B_PAD_SWAP_UP equ 2 +def B_PAD_SWAP_LEFT equ 1 +def B_PAD_SWAP_RIGHT equ 0 + def PAD_SWAP_CTRL_PAD equ %0000_1111 + def PAD_SWAP_BUTTONS equ %1111_0000 + def PAD_SWAP_START equ 1 << B_PAD_SWAP_START + def PAD_SWAP_SELECT equ 1 << B_PAD_SWAP_SELECT + def PAD_SWAP_B equ 1 << B_PAD_SWAP_B + def PAD_SWAP_A equ 1 << B_PAD_SWAP_A + def PAD_SWAP_DOWN equ 1 << B_PAD_SWAP_DOWN + def PAD_SWAP_UP equ 1 << B_PAD_SWAP_UP + def PAD_SWAP_LEFT equ 1 << B_PAD_SWAP_LEFT + def PAD_SWAP_RIGHT equ 1 << B_PAD_SWAP_RIGHT + +; -- SB ($FF01) --------------------------------------------------------------- +; Serial transfer data [r/w] +def rSB equ $FF01 + +; -- SC ($FF02) --------------------------------------------------------------- +; Serial transfer control +def rSC equ $FF02 + +def B_SC_START equ 7 ; reading 1 = transfer in progress, writing 1 = start transfer [r/w] +def B_SC_SPEED equ 1 ; (CGB only) 1 = use faster internal clock [r/w] +def B_SC_SOURCE equ 0 ; 0 = use external clock ("slave"), 1 = use internal clock ("master") [r/w] + def SC_START equ 1 << B_SC_START + def SC_SPEED equ 1 << B_SC_SPEED + def SC_SLOW equ 0 << B_SC_SPEED + def SC_FAST equ 1 << B_SC_SPEED + def SC_SOURCE equ 1 << B_SC_SOURCE + def SC_EXTERNAL equ 0 << B_SC_SOURCE + def SC_INTERNAL equ 1 << B_SC_SOURCE + +; -- $FF03 is unused ---------------------------------------------------------- + +; -- DIV ($FF04) -------------------------------------------------------------- +; Divider register [r/w] +def rDIV equ $FF04 + +; -- TIMA ($FF05) ------------------------------------------------------------- +; Timer counter [r/w] +def rTIMA equ $FF05 + +; -- TMA ($FF06) -------------------------------------------------------------- +; Timer modulo [r/w] +def rTMA equ $FF06 + +; -- TAC ($FF07) -------------------------------------------------------------- +; Timer control +def rTAC equ $FF07 + +def B_TAC_START equ 2 ; enable incrementing TIMA [r/w] + def TAC_STOP equ 0 << B_TAC_START + def TAC_START equ 1 << B_TAC_START + +def TAC_CLOCK equ %000000_11 ; the frequency at which TIMA increments [r/w] + def TAC_4KHZ equ %000000_00 ; every 256 M-cycles = ~4 KHz on DMG + def TAC_262KHZ equ %000000_01 ; every 4 M-cycles = ~262 KHz on DMG + def TAC_65KHZ equ %000000_10 ; every 16 M-cycles = ~65 KHz on DMG + def TAC_16KHZ equ %000000_11 ; every 64 M-cycles = ~16 KHz on DMG + +; -- $FF08-$FF0E are unused --------------------------------------------------- + +; -- IF ($FF0F) --------------------------------------------------------------- +; Pending interrupts +def rIF equ $FF0F + +def B_IF_JOYPAD equ 4 ; 1 = joypad interrupt is pending [r/w] +def B_IF_SERIAL equ 3 ; 1 = serial interrupt is pending [r/w] +def B_IF_TIMER equ 2 ; 1 = timer interrupt is pending [r/w] +def B_IF_STAT equ 1 ; 1 = STAT interrupt is pending [r/w] +def B_IF_VBLANK equ 0 ; 1 = VBlank interrupt is pending [r/w] + def IF_JOYPAD equ 1 << B_IF_JOYPAD + def IF_SERIAL equ 1 << B_IF_SERIAL + def IF_TIMER equ 1 << B_IF_TIMER + def IF_STAT equ 1 << B_IF_STAT + def IF_VBLANK equ 1 << B_IF_VBLANK + +; -- AUD1SWEEP / NR10 ($FF10) ------------------------------------------------- +; Audio channel 1 sweep +def rAUD1SWEEP equ $FF10 + +def AUD1SWEEP_TIME equ %0_111_0000 ; how long between sweep iterations + ; (in 128 Hz ticks, ~7.8 ms apart) [r/w] + +def B_AUD1SWEEP_DIR equ 3 ; sweep direction [r/w] + def AUD1SWEEP_DIR equ 1 << B_AUD1SWEEP_DIR + def AUD1SWEEP_UP equ 0 << B_AUD1SWEEP_DIR + def AUD1SWEEP_DOWN equ 1 << B_AUD1SWEEP_DIR + +def AUD1SWEEP_SHIFT equ %00000_111 ; how much the period increases/decreases per iteration [r/w] + +; -- AUD1LEN / NR11 ($FF11) --------------------------------------------------- +; Audio channel 1 length timer and duty cycle +def rAUD1LEN equ $FF11 + +def AUD1LEN_DUTY equ %11_000000 ; ratio of time spent high vs. time spent low [r/w] + def AUD1LEN_DUTY_12_5 equ %00_000000 ; 12.5% + def AUD1LEN_DUTY_25 equ %01_000000 ; 25% + def AUD1LEN_DUTY_50 equ %10_000000 ; 50% + def AUD1LEN_DUTY_75 equ %11_000000 ; 75% + +def AUD1LEN_TIMER equ %00_111111 ; initial length timer (0-63) [wo] + +; -- AUD1ENV / NR12 ($FF12) --------------------------------------------------- +; Audio channel 1 volume and envelope +def rAUD1ENV equ $FF12 + +def AUD1ENV_INIT_VOLUME equ %1111_0000 ; initial volume [r/w] + +def B_AUD1ENV_DIR equ 3 ; direction of volume envelope [r/w] + def AUD1ENV_DIR equ 1 << B_AUD1ENV_DIR + def AUD1ENV_DOWN equ 0 << B_AUD1ENV_DIR + def AUD1ENV_UP equ 1 << B_AUD1ENV_DIR + +def AUD1ENV_PACE equ %00000_111 ; how long between envelope iterations + ; (in 64 Hz ticks, ~15.6 ms apart) [r/w] + +; -- AUD1LOW / NR13 ($FF13) --------------------------------------------------- +; Audio channel 1 period (low 8 bits) [wo] +def rAUD1LOW equ $FF13 + +; -- AUD1HIGH / NR14 ($FF14) -------------------------------------------------- +; Audio channel 1 period (high 3 bits) and control +def rAUD1HIGH equ $FF14 + +def B_AUD1HIGH_RESTART equ 7 ; 1 = restart the channel [wo] +def B_AUD1HIGH_LEN_ENABLE equ 6 ; 1 = reset the channel after the length timer expires [r/w] + def AUD1HIGH_RESTART equ 1 << B_AUD1HIGH_RESTART + def AUD1HIGH_LENGTH_OFF equ 0 << B_AUD1HIGH_LEN_ENABLE + def AUD1HIGH_LENGTH_ON equ 1 << B_AUD1HIGH_LEN_ENABLE + +def AUD1HIGH_PERIOD_HIGH equ %00000_111 ; upper 3 bits of the channel's period [wo] + +; -- $FF15 is unused ---------------------------------------------------------- + +; -- AUD2LEN / NR21 ($FF16) --------------------------------------------------- +; Audio channel 2 length timer and duty cycle +def rAUD2LEN equ $FF16 + +def AUD2LEN_DUTY equ %11_000000 ; ratio of time spent high vs. time spent low [r/w] + def AUD2LEN_DUTY_12_5 equ %00_000000 ; 12.5% + def AUD2LEN_DUTY_25 equ %01_000000 ; 25% + def AUD2LEN_DUTY_50 equ %10_000000 ; 50% + def AUD2LEN_DUTY_75 equ %11_000000 ; 75% + +def AUD2LEN_TIMER equ %00_111111 ; initial length timer (0-63) [wo] + +; -- AUD2ENV / NR22 ($FF17) --------------------------------------------------- +; Audio channel 2 volume and envelope +def rAUD2ENV equ $FF17 + +def AUD2ENV_INIT_VOLUME equ %1111_0000 ; initial volume [r/w] + +def B_AUD2ENV_DIR equ 3 ; direction of volume envelope [r/w] + def AUD2ENV_DIR equ 1 << B_AUD2ENV_DIR + def AUD2ENV_DOWN equ 0 << B_AUD2ENV_DIR + def AUD2ENV_UP equ 1 << B_AUD2ENV_DIR + +def AUD2ENV_PACE equ %00000_111 ; how long between envelope iterations + ; (in 64 Hz ticks, ~15.6 ms apart) [r/w] + +; -- AUD2LOW / NR23 ($FF18) --------------------------------------------------- +; Audio channel 2 period (low 8 bits) [wo] +def rAUD2LOW equ $FF18 + +; -- AUD2HIGH / NR24 ($FF19) -------------------------------------------------- +; Audio channel 2 period (high 3 bits) and control +def rAUD2HIGH equ $FF19 + +def B_AUD2HIGH_RESTART equ 7 ; 1 = restart the channel [wo] +def B_AUD2HIGH_LEN_ENABLE equ 6 ; 1 = reset the channel after the length timer expires [r/w] + def AUD2HIGH_RESTART equ 1 << B_AUD2HIGH_RESTART + def AUD2HIGH_LENGTH_OFF equ 0 << B_AUD2HIGH_LEN_ENABLE + def AUD2HIGH_LENGTH_ON equ 1 << B_AUD2HIGH_LEN_ENABLE + +def AUD2HIGH_PERIOD_HIGH equ %00000_111 ; upper 3 bits of the channel's period [wo] + +; -- AUD3ENA / NR30 ($FF1A) --------------------------------------------------- +; Audio channel 3 enable +def rAUD3ENA equ $FF1A + +def B_AUD3ENA_ENABLE equ 7 ; 1 = channel is active [r/w] + def AUD3ENA_OFF equ 0 << B_AUD3ENA_ENABLE + def AUD3ENA_ON equ 1 << B_AUD3ENA_ENABLE + +; -- AUD3LEN / NR31 ($FF1B) --------------------------------------------------- +; Audio channel 3 length timer [wo] +def rAUD3LEN equ $FF1B + +; -- AUD3LEVEL / NR32 ($FF1C) ------------------------------------------------- +; Audio channel 3 volume +def rAUD3LEVEL equ $FF1C + +def AUD3LEVEL_VOLUME equ %0_11_00000 ; volume level [r/w] + def AUD3LEVEL_MUTE equ %0_00_00000 ; 0% (muted) + def AUD3LEVEL_100 equ %0_01_00000 ; 100% + def AUD3LEVEL_50 equ %0_10_00000 ; 50% + def AUD3LEVEL_25 equ %0_11_00000 ; 25% + +; -- AUD3LOW / NR33 ($FF1D) --------------------------------------------------- +; Audio channel 3 period (low 8 bits) [wo] +def rAUD3LOW equ $FF1D + +; -- AUD3HIGH / NR34 ($FF1E) -------------------------------------------------- +; Audio channel 3 period (high 3 bits) and control +def rAUD3HIGH equ $FF1E + +def B_AUD3HIGH_RESTART equ 7 ; 1 = restart the channel [wo] +def B_AUD3HIGH_LEN_ENABLE equ 6 ; 1 = reset the channel after the length timer expires [r/w] + def AUD3HIGH_RESTART equ 1 << B_AUD3HIGH_RESTART + def AUD3HIGH_LENGTH_OFF equ 0 << B_AUD3HIGH_LEN_ENABLE + def AUD3HIGH_LENGTH_ON equ 1 << B_AUD3HIGH_LEN_ENABLE + +def AUD3HIGH_PERIOD_HIGH equ %00000_111 ; upper 3 bits of the channel's period [wo] + +; -- $FF1F is unused ---------------------------------------------------------- + +; -- AUD4LEN / NR41 ($FF20) --------------------------------------------------- +; Audio channel 4 length timer +def rAUD4LEN equ $FF20 + +def AUD4LEN_TIMER equ %00_111111 ; initial length timer (0-63) [wo] + +; -- AUD4ENV / NR42 ($FF21) --------------------------------------------------- +; Audio channel 4 volume and envelope +def rAUD4ENV equ $FF21 + +def AUD4ENV_INIT_VOLUME equ %1111_0000 ; initial volume [r/w] + +def B_AUD4ENV_DIR equ 3 ; direction of volume envelope [r/w] + def AUD4ENV_DIR equ 1 << B_AUD4ENV_DIR + def AUD4ENV_DOWN equ 0 << B_AUD4ENV_DIR + def AUD4ENV_UP equ 1 << B_AUD4ENV_DIR + +def AUD4ENV_PACE equ %00000_111 ; how long between envelope iterations + ; (in 64 Hz ticks, ~15.6 ms apart) [r/w] + +; -- AUD4POLY / NR43 ($FF22) -------------------------------------------------- +; Audio channel 4 period and randomness +def rAUD4POLY equ $FF22 + +def AUD4POLY_SHIFT equ %1111_0000 ; coarse control of the channel's period [r/w] + +def B_AUD4POLY_WIDTH equ 3 ; controls the noise generator (LFSR)'s step width [r/w] + def AUD4POLY_15STEP equ 0 << B_AUD4POLY_WIDTH + def AUD4POLY_7STEP equ 1 << B_AUD4POLY_WIDTH + +def AUD4POLY_DIV equ %00000_111 ; fine control of the channel's period [r/w] + +; -- AUD4GO / NR44 ($FF23) ---------------------------------------------------- +; Audio channel 4 control +def rAUD4GO equ $FF23 + +def B_AUD4GO_RESTART equ 7 ; 1 = restart the channel [wo] +def B_AUD4GO_LEN_ENABLE equ 6 ; 1 = reset the channel after the length timer expires [r/w] + def AUD4GO_RESTART equ 1 << B_AUD4GO_RESTART + def AUD4GO_LENGTH_OFF equ 0 << B_AUD4GO_LEN_ENABLE + def AUD4GO_LENGTH_ON equ 1 << B_AUD4GO_LEN_ENABLE + +; -- AUDVOL / NR50 ($FF24) ---------------------------------------------------- +; Audio master volume and VIN mixer +def rAUDVOL equ $FF24 + +def B_AUDVOL_VIN_LEFT equ 7 ; 1 = output VIN to left ear (SO2, speaker 2) [r/w] + def AUDVOL_VIN_LEFT equ 1 << B_AUDVOL_VIN_LEFT + +def AUDVOL_LEFT equ %0_111_0000 ; 0 = barely audible, 7 = full volume [r/w] + +def B_AUDVOL_VIN_RIGHT equ 3 ; 1 = output VIN to right ear (SO1, speaker 1) [r/w] + def AUDVOL_VIN_RIGHT equ 1 << B_AUDVOL_VIN_RIGHT + +def AUDVOL_RIGHT equ %00000_111 ; 0 = barely audible, 7 = full volume [r/w] + +; -- AUDTERM / NR51 ($FF25) --------------------------------------------------- +; Audio channel mixer +def rAUDTERM equ $FF25 + +def B_AUDTERM_4_LEFT equ 7 ; 1 = output channel 4 to left ear [r/w] +def B_AUDTERM_3_LEFT equ 6 ; 1 = output channel 3 to left ear [r/w] +def B_AUDTERM_2_LEFT equ 5 ; 1 = output channel 2 to left ear [r/w] +def B_AUDTERM_1_LEFT equ 4 ; 1 = output channel 1 to left ear [r/w] +def B_AUDTERM_4_RIGHT equ 3 ; 1 = output channel 4 to right ear [r/w] +def B_AUDTERM_3_RIGHT equ 2 ; 1 = output channel 3 to right ear [r/w] +def B_AUDTERM_2_RIGHT equ 1 ; 1 = output channel 2 to right ear [r/w] +def B_AUDTERM_1_RIGHT equ 0 ; 1 = output channel 1 to right ear [r/w] + def AUDTERM_4_LEFT equ 1 << B_AUDTERM_4_LEFT + def AUDTERM_3_LEFT equ 1 << B_AUDTERM_3_LEFT + def AUDTERM_2_LEFT equ 1 << B_AUDTERM_2_LEFT + def AUDTERM_1_LEFT equ 1 << B_AUDTERM_1_LEFT + def AUDTERM_4_RIGHT equ 1 << B_AUDTERM_4_RIGHT + def AUDTERM_3_RIGHT equ 1 << B_AUDTERM_3_RIGHT + def AUDTERM_2_RIGHT equ 1 << B_AUDTERM_2_RIGHT + def AUDTERM_1_RIGHT equ 1 << B_AUDTERM_1_RIGHT + +; -- AUDENA / NR52 ($FF26) ---------------------------------------------------- +; Audio master enable +def rAUDENA equ $FF26 + +def B_AUDENA_ENABLE equ 7 ; 0 = disable the APU (resets all audio registers to 0!) [r/w] +def B_AUDENA_ENABLE_CH4 equ 3 ; 1 = channel 4 is running [ro] +def B_AUDENA_ENABLE_CH3 equ 2 ; 1 = channel 3 is running [ro] +def B_AUDENA_ENABLE_CH2 equ 1 ; 1 = channel 2 is running [ro] +def B_AUDENA_ENABLE_CH1 equ 0 ; 1 = channel 1 is running [ro] + def AUDENA_OFF equ 0 << B_AUDENA_ENABLE + def AUDENA_ON equ 1 << B_AUDENA_ENABLE + def AUDENA_CH4_OFF equ 0 << B_AUDENA_ENABLE_CH4 + def AUDENA_CH4_ON equ 1 << B_AUDENA_ENABLE_CH4 + def AUDENA_CH3_OFF equ 0 << B_AUDENA_ENABLE_CH3 + def AUDENA_CH3_ON equ 1 << B_AUDENA_ENABLE_CH3 + def AUDENA_CH2_OFF equ 0 << B_AUDENA_ENABLE_CH2 + def AUDENA_CH2_ON equ 1 << B_AUDENA_ENABLE_CH2 + def AUDENA_CH1_OFF equ 0 << B_AUDENA_ENABLE_CH1 + def AUDENA_CH1_ON equ 1 << B_AUDENA_ENABLE_CH1 + +; -- $FF27-$FF2F are unused --------------------------------------------------- + +; -- AUD3WAVE ($FF30-$FF3F) --------------------------------------------------- +; Audio channel 3 wave pattern RAM [r/w] +def rAUD3WAVE_0 equ $FF30 +def rAUD3WAVE_1 equ $FF31 +def rAUD3WAVE_2 equ $FF32 +def rAUD3WAVE_3 equ $FF33 +def rAUD3WAVE_4 equ $FF34 +def rAUD3WAVE_5 equ $FF35 +def rAUD3WAVE_6 equ $FF36 +def rAUD3WAVE_7 equ $FF37 +def rAUD3WAVE_8 equ $FF38 +def rAUD3WAVE_9 equ $FF39 +def rAUD3WAVE_A equ $FF3A +def rAUD3WAVE_B equ $FF3B +def rAUD3WAVE_C equ $FF3C +def rAUD3WAVE_D equ $FF3D +def rAUD3WAVE_E equ $FF3E +def rAUD3WAVE_F equ $FF3F + +; -- LCDC ($FF40) ------------------------------------------------------------- +; PPU graphics control +def rLCDC equ $FF40 + +def B_LCDC_ENABLE equ 7 ; whether the PPU (and LCD) are turned on [r/w] +def B_LCDC_WIN_MAP equ 6 ; which tilemap the Window reads from [r/w] +def B_LCDC_WINDOW equ 5 ; whether the Window is enabled [r/w] +def B_LCDC_BLOCKS equ 4 ; which "tile blocks" the BG and Window use [r/w] +def B_LCDC_BG_MAP equ 3 ; which tilemap the BG reads from [r/w] +def B_LCDC_OBJ_SIZE equ 2 ; how many pixels tall each OBJ is [r/w] +def B_LCDC_OBJS equ 1 ; whether OBJs are enabled [r/w] +def B_LCDC_BG equ 0 ; (DMG only) whether the BG is enabled [r/w] +def B_LCDC_PRIO equ 0 ; (CGB only) whether OBJ priority bits are enabled [r/w] + def LCDC_ENABLE equ 1 << B_LCDC_ENABLE + def LCDC_OFF equ 0 << B_LCDC_ENABLE + def LCDC_ON equ 1 << B_LCDC_ENABLE + def LCDC_WIN_MAP equ 1 << B_LCDC_WIN_MAP + def LCDC_WIN_9800 equ 0 << B_LCDC_WIN_MAP + def LCDC_WIN_9C00 equ 1 << B_LCDC_WIN_MAP + def LCDC_WINDOW equ 1 << B_LCDC_WINDOW + def LCDC_WIN_OFF equ 0 << B_LCDC_WINDOW + def LCDC_WIN_ON equ 1 << B_LCDC_WINDOW + def LCDC_BLOCKS equ 1 << B_LCDC_BLOCKS + def LCDC_BLOCK21 equ 0 << B_LCDC_BLOCKS + def LCDC_BLOCK01 equ 1 << B_LCDC_BLOCKS + def LCDC_BG_MAP equ 1 << B_LCDC_BG_MAP + def LCDC_BG_9800 equ 0 << B_LCDC_BG_MAP + def LCDC_BG_9C00 equ 1 << B_LCDC_BG_MAP + def LCDC_OBJ_SIZE equ 1 << B_LCDC_OBJ_SIZE + def LCDC_OBJ_8 equ 0 << B_LCDC_OBJ_SIZE + def LCDC_OBJ_16 equ 1 << B_LCDC_OBJ_SIZE + def LCDC_OBJS equ 1 << B_LCDC_OBJS + def LCDC_OBJ_OFF equ 0 << B_LCDC_OBJS + def LCDC_OBJ_ON equ 1 << B_LCDC_OBJS + def LCDC_BG equ 1 << B_LCDC_BG + def LCDC_BG_OFF equ 0 << B_LCDC_BG + def LCDC_BG_ON equ 1 << B_LCDC_BG + def LCDC_PRIO equ 1 << B_LCDC_PRIO + def LCDC_PRIO_OFF equ 0 << B_LCDC_PRIO + def LCDC_PRIO_ON equ 1 << B_LCDC_PRIO + +; -- STAT ($FF41) ------------------------------------------------------------- +; Graphics status and interrupt control +def rSTAT equ $FF41 + +def B_STAT_LYC equ 6 ; 1 = LY match triggers the STAT interrupt [r/w] +def B_STAT_MODE_2 equ 5 ; 1 = OAM Scan triggers the PPU interrupt [r/w] +def B_STAT_MODE_1 equ 4 ; 1 = VBlank triggers the PPU interrupt [r/w] +def B_STAT_MODE_0 equ 3 ; 1 = HBlank triggers the PPU interrupt [r/w] +def B_STAT_LYCF equ 2 ; 1 = LY is currently equal to LYC [ro] +def B_STAT_BUSY equ 1 ; 1 = the PPU is currently accessing VRAM [ro] + def STAT_LYC equ 1 << B_STAT_LYC + def STAT_MODE_2 equ 1 << B_STAT_MODE_2 + def STAT_MODE_1 equ 1 << B_STAT_MODE_1 + def STAT_MODE_0 equ 1 << B_STAT_MODE_0 + def STAT_LYCF equ 1 << B_STAT_LYCF + def STAT_BUSY equ 1 << B_STAT_BUSY + +def STAT_MODE equ %000000_11 ; PPU's current status [ro] + def STAT_HBLANK equ %000000_00 ; waiting after a line's rendering (HBlank) + def STAT_VBLANK equ %000000_01 ; waiting between frames (VBlank) + def STAT_OAM equ %000000_10 ; checking which OBJs will be rendered on this line (OAM scan) + def STAT_LCD equ %000000_11 ; pushing pixels to the LCD + +; -- SCY ($FF42) -------------------------------------------------------------- +; Background Y scroll offset (in pixels) [r/w] +def rSCY equ $FF42 + +; -- SCX ($FF43) -------------------------------------------------------------- +; Background X scroll offset (in pixels) [r/w] +def rSCX equ $FF43 + +; -- LY ($FF44) --------------------------------------------------------------- +; Y coordinate of the line currently processed by the PPU (0-153) [ro] +def rLY equ $FF44 + +def LY_VBLANK equ 144 ; 144-153 is the VBlank period + +; -- LYC ($FF45) -------------------------------------------------------------- +; Value that LY is constantly compared to [r/w] +def rLYC equ $FF45 + +; -- DMA ($FF46) -------------------------------------------------------------- +; OAM DMA start address (high 8 bits) and start [wo] +def rDMA equ $FF46 + +; -- BGP ($FF47) -------------------------------------------------------------- +; (DMG only) Background color mapping [r/w] +def rBGP equ $FF47 + +def BGP_SGB_TRANSFER equ %11_10_01_00 ; set BGP to this value before SGB VRAM transfer + +; -- OBP0 ($FF48) ------------------------------------------------------------- +; (DMG only) OBJ color mapping #0 [r/w] +def rOBP0 equ $FF48 + +; -- OBP1 ($FF49) ------------------------------------------------------------- +; (DMG only) OBJ color mapping #1 [r/w] +def rOBP1 equ $FF49 + +; -- WY ($FF4A) --------------------------------------------------------------- +; Y coordinate of the Window's top-left pixel (0-143) [r/w] +def rWY equ $FF4A + +; -- WX ($FF4B) --------------------------------------------------------------- +; X coordinate of the Window's top-left pixel, plus 7 (7-166) [r/w] +def rWX equ $FF4B + +def WX_OFS equ 7 ; subtract this to get the actual Window X coordinate + +; -- SYS / KEY0 ($FF4C) ------------------------------------------------------- +; (CGB boot ROM only) CPU mode select +def rSYS equ $FF4C + +; This is known as the "CPU mode register" in Fig. 11 of this patent: +; https://patents.google.com/patent/US6322447B1/en?oq=US6322447bi +; "OBJ priority mode designating register" in the same patent +; Credit to @mattcurrie for this finding! + +def SYS_MODE equ %0000_11_00 ; current system mode [r/w] + def SYS_CGB equ %0000_00_00 ; CGB mode + def SYS_DMG equ %0000_01_00 ; DMG compatibility mode + def SYS_PGB1 equ %0000_10_00 ; LCD is driven externally, CPU is stopped + def SYS_PGB2 equ %0000_11_00 ; LCD is driven externally, CPU is running + +; -- SPD / KEY1 ($FF4D) ------------------------------------------------------- +; (CGB only) Double-speed mode control +def rSPD equ $FF4D + +def B_SPD_DOUBLE equ 7 ; current clock speed [ro] +def B_SPD_PREPARE equ 0 ; 1 = next `stop` instruction will switch clock speeds [r/w] + def SPD_SINGLE equ 0 << B_SPD_DOUBLE + def SPD_DOUBLE equ 1 << B_SPD_DOUBLE + def SPD_PREPARE equ 1 << B_SPD_PREPARE + +; -- $FF4E is unused ---------------------------------------------------------- + +; -- VBK ($FF4F) -------------------------------------------------------------- +; (CGB only) VRAM bank number (0 or 1) +def rVBK equ $FF4F + +def VBK_BANK equ %0000000_1 ; mapped VRAM bank [r/w] + +; -- BANK ($FF50) ------------------------------------------------------------- +; (boot ROM only) Boot ROM mapping control +def rBANK equ $FF50 + +def B_BANK_ON equ 0 ; whether the boot ROM is mapped [wo] + def BANK_ON equ 0 << B_BANK_ON + def BANK_OFF equ 1 << B_BANK_ON + +; -- VDMA_SRC_HIGH / HDMA1 ($FF51) -------------------------------------------- +; (CGB only) VRAM DMA source address (high 8 bits) [wo] +def rVDMA_SRC_HIGH equ $FF51 + +; -- VDMA_SRC_LOW / HDMA2 ($FF52) --------------------------------------------- +; (CGB only) VRAM DMA source address (low 8 bits) [wo] +def rVDMA_SRC_LOW equ $FF52 + +; -- VDMA_DEST_HIGH / HDMA3 ($FF53) ------------------------------------------- +; (CGB only) VRAM DMA destination address (high 8 bits) [wo] +def rVDMA_DEST_HIGH equ $FF53 + +; -- VDMA_DEST_LOW / HDMA4 ($FF54) -------------------------------------------- +; (CGB only) VRAM DMA destination address (low 8 bits) [wo] +def rVDMA_DEST_LOW equ $FF54 + +; -- VDMA_LEN / HDMA5 ($FF55) ------------------------------------------------- +; (CGB only) VRAM DMA length, mode, and start +def rVDMA_LEN equ $FF55 + +def B_VDMA_LEN_MODE equ 7 ; on write: VRAM DMA mode [wo] + def VDMA_LEN_MODE equ 1 << B_VDMA_LEN_MODE + def VDMA_LEN_MODE_GENERAL equ 0 << B_VDMA_LEN_MODE ; GDMA (general-purpose) + def VDMA_LEN_MODE_HBLANK equ 1 << B_VDMA_LEN_MODE ; HDMA (HBlank) + +def B_VDMA_LEN_BUSY equ 7 ; on read: is a VRAM DMA active? + def VDMA_LEN_BUSY equ 1 << B_VDMA_LEN_BUSY + def VDMA_LEN_NO equ 0 << B_VDMA_LEN_BUSY + def VDMA_LEN_YES equ 1 << B_VDMA_LEN_BUSY + +def VDMA_LEN_SIZE equ %0_1111111 ; how many 16-byte blocks (minus 1) to transfer [r/w] + +; -- RP ($FF56) --------------------------------------------------------------- +; (CGB only) Infrared communications port +def rRP equ $FF56 + +def RP_READ equ %11_000000 ; whether the IR read is enabled [r/w] + def RP_DISABLE equ %00_000000 + def RP_ENABLE equ %11_000000 + +def B_RP_DATA_IN equ 1 ; 0 = IR light is being received [ro] +def B_RP_LED_ON equ 0 ; 1 = IR light is being sent [r/w] + def RP_DATA_IN equ 1 << B_RP_DATA_IN + def RP_LED_ON equ 1 << B_RP_LED_ON + def RP_WRITE_LOW equ 0 << B_RP_LED_ON + def RP_WRITE_HIGH equ 1 << B_RP_LED_ON + +; -- $FF57-$FF67 are unused --------------------------------------------------- + +; -- BGPI / BCPS ($FF68) ------------------------------------------------------ +; (CGB only) Background palette I/O index +def rBGPI equ $FF68 + +def B_BGPI_AUTOINC equ 7 ; whether the index field is incremented after each write to BCPD [r/w] + def BGPI_AUTOINC equ 1 << B_BGPI_AUTOINC + +def BGPI_INDEX equ %00_111111 ; the index within Palette RAM accessed via BCPD [r/w] + +; -- BGPD / BCPD ($FF69) ------------------------------------------------------ +; (CGB only) Background palette I/O access [r/w] +def rBGPD equ $FF69 + +; -- OBPI / OCPS ($FF6A) ------------------------------------------------------ +; (CGB only) OBJ palette I/O index +def rOBPI equ $FF6A + +def B_OBPI_AUTOINC equ 7 ; whether the index field is incremented after each write to OBPD [r/w] + def OBPI_AUTOINC equ 1 << B_OBPI_AUTOINC + +def OBPI_INDEX equ %00_111111 ; the index within Palette RAM accessed via OBPD [r/w] + +; -- OBPD / OCPD ($FF6B) ------------------------------------------------------ +; (CGB only) OBJ palette I/O access [r/w] +def rOBPD equ $FF6B + +; -- OPRI ($FF6C) ------------------------------------------------------------- +; (CGB boot ROM only) OBJ draw priority mode +def rOPRI equ $FF6C + +def B_OPRI_PRIORITY equ 0 ; which drawing priority is used for OBJs [r/w] + def OPRI_PRIORITY equ 1 << B_OPRI_PRIORITY + def OPRI_OAM equ 0 << B_OPRI_PRIORITY ; CGB mode default: earliest OBJ in OAM wins + def OPRI_COORD equ 1 << B_OPRI_PRIORITY ; DMG mode default: leftmost OBJ wins + +; -- $FF6D-$FF6F are unused --------------------------------------------------- + +; -- WBK / SVBK ($FF70) ------------------------------------------------------- +; (CGB only) WRAM bank number +def rWBK equ $FF70 + +def WBK_BANK equ %00000_111 ; mapped WRAM bank (0-7) [r/w] + +; -- PSW ($FF71) -------------------------------------------------------------- +; (CGB boot ROM's DMG mode only) Palette Selection Window and NMI control. [r/w] +; Bits 1-6 are always 1. +; In CGB mode, reads return $FF and writes are ignored. +def rPSW equ $FF71 + +def B_PSW_WINDOW equ 7 ; whether the Palette Selection Window is enabled [r/w] +def B_PSW_NMI equ 0 ; whether the NMI is enabled [r/w] + def PSW_WINDOW equ 1 << B_PSW_WINDOW + def PSW_WIN_OFF equ 0 << B_PSW_WINDOW + def PSW_WIN_ON equ 1 << B_PSW_WINDOW + def PSW_NMI equ 1 << B_PSW_NMI + def PSW_NMI_DISABLE equ 0 << B_PSW_NMI + def PSW_NMI_ENABLE equ 1 << B_PSW_NMI + +; -- PSWX ($FF72) ------------------------------------------------------------- +; (CGB boot ROM only) X coordinate of the Palette Selection Window's top-left pixel, plus 7 (7-166) [r/w] +; Readable and writable in both CGB and DMG mode. +def rPSWX equ $FF72 + +; -- PSWY ($FF73) ------------------------------------------------------------- +; (CGB boot ROM only) Y coordinate of the Palette Selection Window's top-left pixel (0-143) [r/w] +; Readable and writable in both CGB and DMG mode. +def rPSWY equ $FF73 + +; -- PSM ($FF74) -------------------------------------------------------------- +; (CGB boot ROM only) Set the Palette Selection Window button mask (triggers NMI when pressed) [r/w] +; Readable and writable in both CGB and DMG mode. +def rPSM equ $FF74 + +def B_PSM_START equ 7 +def B_PSM_SELECT equ 6 +def B_PSM_B equ 5 +def B_PSM_A equ 4 +def B_PSM_DOWN equ 3 +def B_PSM_UP equ 2 +def B_PSM_LEFT equ 1 +def B_PSM_RIGHT equ 0 + def PSM_START equ 1 << B_PSM_START + def PSM_SELECT equ 1 << B_PSM_SELECT + def PSM_B equ 1 << B_PSM_B + def PSM_A equ 1 << B_PSM_A + def PSM_DOWN equ 1 << B_PSM_DOWN + def PSM_UP equ 1 << B_PSM_UP + def PSM_LEFT equ 1 << B_PSM_LEFT + def PSM_RIGHT equ 1 << B_PSM_RIGHT + +; -- $FF75 is unused ---------------------------------------------------------- + +; -- PCM12 ($FF76) ------------------------------------------------------------ +; Audio channels 1 and 2 output +def rPCM12 equ $FF76 + +def PCM12_CH2 equ %1111_0000 ; audio channel 2 output [ro] +def PCM12_CH1 equ %0000_1111 ; audio channel 1 output [ro] + +; -- PCM34 ($FF77) ------------------------------------------------------------ +; Audio channels 3 and 4 output +def rPCM34 equ $FF77 + +def PCM34_CH4 equ %1111_0000 ; audio channel 4 output [ro] +def PCM34_CH3 equ %0000_1111 ; audio channel 3 output [ro] + +; -- $FF78-$FF7F are unused --------------------------------------------------- + +; -- IE ($FFFF) --------------------------------------------------------------- +; Interrupt enable +def rIE equ $FFFF + +def B_IE_JOYPAD equ 4 ; 1 = joypad interrupt is enabled [r/w] +def B_IE_SERIAL equ 3 ; 1 = serial interrupt is enabled [r/w] +def B_IE_TIMER equ 2 ; 1 = timer interrupt is enabled [r/w] +def B_IE_STAT equ 1 ; 1 = STAT interrupt is enabled [r/w] +def B_IE_VBLANK equ 0 ; 1 = VBlank interrupt is enabled [r/w] + def IE_JOYPAD equ 1 << B_IE_JOYPAD + def IE_SERIAL equ 1 << B_IE_SERIAL + def IE_TIMER equ 1 << B_IE_TIMER + def IE_STAT equ 1 << B_IE_STAT + def IE_VBLANK equ 1 << B_IE_VBLANK + + +;****************************************************************************** +; Cartridge registers (MBC) +;****************************************************************************** + +; Note that these "registers" are each actually accessible at an entire address range; +; however, one address for each of these ranges is considered the "canonical" one, and +; these addresses are what's provided here. + + +; ** Common to most MBCs ****************************************************** + +; -- RAMG ($0000-$1FFF) ------------------------------------------------------- +; Whether SRAM can be accessed [wo] +def rRAMG equ $0000 + +; Common values (not for HuC1 or HuC-3) +def RAMG_SRAM_DISABLE equ $00 +def RAMG_SRAM_ENABLE equ $0A ; some MBCs accept any value whose low nybble is $A + +; (HuC-3 only) switch SRAM to map cartridge RAM, RTC, or IR +def RAMG_CART_RAM_RO equ $00 ; select cartridge RAM [ro] +def RAMG_CART_RAM equ $0A ; select cartridge RAM [r/w] +def RAMG_RTC_IN equ $0B ; select RTC command/argument [wo] + def RAMG_RTC_IN_CMD equ %0_111_0000 ; command + def RAMG_RTC_IN_ARG equ %0_000_1111 ; argument +def RAMG_RTC_OUT equ $0C ; select RTC command/response [ro] + def RAMG_RTC_OUT_CMD equ %0_111_0000 ; command + def RAMG_RTC_OUT_RESULT equ %0_000_1111 ; result +def RAMG_RTC_SEMAPHORE equ $0D ; select RTC semaphore [r/w] +def RAMG_IR equ $0E ; (HuC1 and HuC-3 only) select IR [r/w] + +; -- ROMB ($2000-$3FFF) ------------------------------------------------------- +; ROM bank number (not for MBC5 or MBC6) [wo] +def rROMB equ $2000 + +; -- RAMB ($4000-$5FFF) ------------------------------------------------------- +; SRAM bank number (not for MBC2, MBC6, or MBC7) [wo] +def rRAMB equ $4000 + +; (MBC3 only) Special RAM bank numbers that actually map values into RTCREG +def RAMB_RTC_S equ $08 ; seconds counter (0-59) +def RAMB_RTC_M equ $09 ; minutes counter (0-59) +def RAMB_RTC_H equ $0A ; hours counter (0-23) +def RAMB_RTC_DL equ $0B ; days counter, low byte (0-255) +def RAMB_RTC_DH equ $0C ; days counter, high bit and other flags + def B_RAMB_RTC_DH_CARRY equ 7 ; 1 = days counter overflowed [wo] + def B_RAMB_RTC_DH_HALT equ 6 ; 0 = run timer, 1 = stop timer [wo] + def B_RAMB_RTC_DH_HIGH equ 0 ; days counter, high bit (bit 8) [wo] + def RAMB_RTC_DH_CARRY equ 1 << B_RAMB_RTC_DH_CARRY + def RAMB_RTC_DH_HALT equ 1 << B_RAMB_RTC_DH_HALT + def RAMB_RTC_DH_HIGH equ 1 << B_RAMB_RTC_DH_HIGH + +def B_RAMB_RUMBLE equ 3 ; (MBC5 and MBC7 only) enable the rumble motor (if any) + def RAMB_RUMBLE equ 1 << B_RAMB_RUMBLE + def RAMB_RUMBLE_OFF equ 0 << B_RAMB_RUMBLE + def RAMB_RUMBLE_ON equ 1 << B_RAMB_RUMBLE + + +; ** MBC1 and MMM01 only ****************************************************** + +; -- BMODE ($6000-$7FFF) ------------------------------------------------------ +; Banking mode select [wo] +def rBMODE equ $6000 + +def BMODE_SIMPLE equ $00 ; locks ROMB and RAMB to bank 0 +def BMODE_ADVANCED equ $01 ; allows bank-switching with RAMB + + +; ** MBC2 only **************************************************************** + +; -- ROM2B ($0000-$3FFF with bit 8 set) --------------------------------------- +; ROM bank number [wo] +def rROM2B equ $2100 + + +; ** MBC3 only **************************************************************** + +; -- RTCLATCH ($6000-$7FFF) --------------------------------------------------- +; RTC latch clock data [wo] +def rRTCLATCH equ $6000 + +; Write $00 then $01 to latch the current time into RTCREG +def RTCLATCH_START equ $00 +def RTCLATCH_FINISH equ $01 + +; -- RTCREG ($A000-$BFFF) ----------------------------------------------------- +; RTC register [r/w] +def rRTCREG equ $A000 + + +; ** MBC5 only **************************************************************** + +; -- ROMB0 ($2000-$2FFF) ------------------------------------------------------ +; ROM bank number low byte (bits 0-7) [wo] +def rROMB0 equ $2000 + +; -- ROMB1 ($3000-$3FFF) ------------------------------------------------------ +; ROM bank number high bit (bit 8) [wo] +def rROMB1 equ $3000 + + +; ** MBC6 only **************************************************************** + +; -- RAMBA ($0400-$07FF) ------------------------------------------------------ +; RAM bank A number [wo] +def rRAMBA equ $0400 + +; -- RAMBB ($0800-$0BFF) ------------------------------------------------------ +; RAM bank B number [wo] +def rRAMBB equ $0800 + +; -- FLASH ($0C00-$0FFF) ------------------------------------------------------ +; Whether the flash chip can be accessed [wo] +def rFLASH equ $0C00 + +; -- FMODE ($1000) ------------------------------------------------------------ +; Write mode select for the flash chip +def rFMODE equ $1000 + +; -- ROMBA ($2000-$27FF) ------------------------------------------------------ +; ROM/Flash bank A number [wo] +def rROMBA equ $2000 + +; -- FLASHA ($2800-$2FFF) ----------------------------------------------------- +; ROM/Flash bank A select [wo] +def rFLASHA equ $2800 + +; -- ROMBB ($3000-$37FF) ------------------------------------------------------ +; ROM/Flash bank B number [wo] +def rROMBB equ $3000 + +; -- FLASHB ($3800-$3FFF) ----------------------------------------------------- +; ROM/Flash bank B select [wo] +def rFLASHB equ $3800 + + +; ** MBC7 only **************************************************************** + +; -- RAMREG ($4000-$5FFF) ----------------------------------------------------- +; Enable RAM register access [wo] +def rRAMREG equ $4000 + +def RAMREG_ENABLE equ $40 + +; -- ACCLATCH0 ($Ax0x) -------------------------------------------------------- +; Latch accelerometer start [wo] +def rACCLATCH0 equ $A000 + +; Write $55 to ACCLATCH0 to erase the latched data +def ACCLATCH0_START equ $55 + +; -- ACCLATCH1 ($Ax1x) -------------------------------------------------------- +; Latch accelerometer finish [wo] +def rACCLATCH1 equ $A010 + +; Write $AA to ACCLATCH1 to latch the accelerometer and update ACCEL* +def ACCLATCH1_FINISH equ $AA + +; -- ACCELX0 ($Ax2x) ---------------------------------------------------------- +; Accelerometer X value low byte [ro] +def rACCELX0 equ $A020 + +; -- ACCELX1 ($Ax3x) ---------------------------------------------------------- +; Accelerometer X value high byte [ro] +def rACCELX1 equ $A030 + +; -- ACCELY0 ($Ax4x) ---------------------------------------------------------- +; Accelerometer Y value low byte [ro] +def rACCELY0 equ $A040 + +; -- ACCELY1 ($Ax5x) ---------------------------------------------------------- +; Accelerometer Y value high byte [ro] +def rACCELY1 equ $A050 + +; -- EEPROM ($Ax8x) ----------------------------------------------------------- +; EEPROM access [r/w] +def rEEPROM equ $A080 + + +; ** HuC1 only **************************************************************** + +; -- IRREG ($A000-$BFFF) ------------------------------------------------------ +; IR register [r/w] +def rIRREG equ $A000 + +; whether the IR transmitter sees light +def IR_LED_OFF equ $C0 +def IR_LED_ON equ $C1 + + +;****************************************************************************** +; Screen-related constants +;****************************************************************************** + +def SCREEN_WIDTH_PX equ 160 ; width of screen in pixels +def SCREEN_HEIGHT_PX equ 144 ; height of screen in pixels +def SCREEN_WIDTH equ 20 ; width of screen in bytes +def SCREEN_HEIGHT equ 18 ; height of screen in bytes +def SCREEN_AREA equ SCREEN_WIDTH * SCREEN_HEIGHT ; size of screen in bytes + +def TILEMAP_WIDTH_PX equ 256 ; width of tilemap in pixels +def TILEMAP_HEIGHT_PX equ 256 ; height of tilemap in pixels +def TILEMAP_WIDTH equ 32 ; width of tilemap in bytes +def TILEMAP_HEIGHT equ 32 ; height of tilemap in bytes +def TILEMAP_AREA equ TILEMAP_WIDTH * TILEMAP_HEIGHT ; size of tilemap in bytes + +def TILE_WIDTH equ 8 ; width of tile in pixels +def TILE_HEIGHT equ 8 ; height of tile in pixels +def TILE_SIZE equ 16 ; size of tile in bytes (2 bits/pixel) + +def COLOR_SIZE equ 2 ; size of color in bytes (little-endian BGR555) +def PAL_COLORS equ 4 ; colors per palette +def PAL_SIZE equ COLOR_SIZE * PAL_COLORS ; size of palette in bytes + +def COLOR_CH_WIDTH equ 5 ; bits per RGB color channel +def COLOR_CH_MAX equ (1 << COLOR_CH_WIDTH) - 1 + def B_COLOR_RED equ COLOR_CH_WIDTH * 0 ; bits 4-0 + def B_COLOR_GREEN equ COLOR_CH_WIDTH * 1 ; bits 9-5 + def B_COLOR_BLUE equ COLOR_CH_WIDTH * 2 ; bits 14-10 + def COLOR_RED equ %000_11111 ; for the low byte + def COLOR_GREEN_LOW equ %111_00000 ; for the low byte + def COLOR_GREEN_HIGH equ %0_00000_11 ; for the high byte + def COLOR_BLUE equ %0_11111_00 ; for the high byte + +; (DMG only) grayscale shade indexes for BGP, OBP0, and OBP1 +def SHADE_WHITE equ %00 +def SHADE_LIGHT equ %01 +def SHADE_DARK equ %10 +def SHADE_BLACK equ %11 + +; Tilemaps the BG or Window can read from (controlled by LCDC) +def TILEMAP0 equ $9800 ; $9800-$9BFF +def TILEMAP1 equ $9C00 ; $9C00-$9FFF + +; (CGB only) BG tile attribute fields +def B_BG_PRIO equ 7 ; whether the BG tile colors 1-3 are drawn above OBJs +def B_BG_YFLIP equ 6 ; whether the whole BG tile is flipped vertically +def B_BG_XFLIP equ 5 ; whether the whole BG tile is flipped horizontally +def B_BG_BANK1 equ 3 ; which VRAM bank the BG tile is taken from +def BG_PALETTE equ %00000_111 ; which palette the BG tile uses + def BG_PRIO equ 1 << B_BG_PRIO + def BG_YFLIP equ 1 << B_BG_YFLIP + def BG_XFLIP equ 1 << B_BG_XFLIP + def BG_BANK0 equ 0 << B_BG_BANK1 + def BG_BANK1 equ 1 << B_BG_BANK1 + + +;****************************************************************************** +; OBJ-related constants +;****************************************************************************** + +; OAM attribute field offsets +rsreset +def OAMA_Y rb ; 0 + def OAM_Y_OFS equ 16 ; subtract 16 from what's written to OAM to get the real Y position +def OAMA_X rb ; 1 + def OAM_X_OFS equ 8 ; subtract 8 from what's written to OAM to get the real X position +def OAMA_TILEID rb ; 2 +def OAMA_FLAGS rb ; 3 + def B_OAM_PRIO equ 7 ; whether the OBJ is drawn below BG colors 1-3 + def B_OAM_YFLIP equ 6 ; whether the whole OBJ is flipped vertically + def B_OAM_XFLIP equ 5 ; whether the whole OBJ is flipped horizontally + def B_OAM_PAL1 equ 4 ; (DMG only) which of the two palettes the OBJ uses + def B_OAM_BANK1 equ 3 ; (CGB only) which VRAM bank the OBJ takes its tile(s) from + def OAM_PALETTE equ %00000_111 ; (CGB only) which palette the OBJ uses + def OAM_PRIO equ 1 << B_OAM_PRIO + def OAM_YFLIP equ 1 << B_OAM_YFLIP + def OAM_XFLIP equ 1 << B_OAM_XFLIP + def OAM_PAL0 equ 0 << B_OAM_PAL1 + def OAM_PAL1 equ 1 << B_OAM_PAL1 + def OAM_BANK0 equ 0 << B_OAM_BANK1 + def OAM_BANK1 equ 1 << B_OAM_BANK1 +def OBJ_SIZE rb 0 ; size of OBJ in bytes = 4 + +def OAM_COUNT equ 40 ; how many OBJs there are room for in OAM +def OAM_SIZE equ OBJ_SIZE * OAM_COUNT + + +;****************************************************************************** +; Audio channel RAM addresses +;****************************************************************************** + +def AUD1RAM equ $FF10 ; $FF10-$FF14 +def AUD2RAM equ $FF15 ; $FF15-$FF19 +def AUD3RAM equ $FF1A ; $FF1A-$FF1E +def AUD4RAM equ $FF1F ; $FF1F-$FF23 +def AUDRAM_SIZE equ 5 ; size of each audio channel RAM in bytes + +def _AUD3WAVERAM equ $FF30 ; $FF30-$FF3F +def AUD3WAVE_SIZE equ 16 ; size of wave pattern RAM in bytes + + +;****************************************************************************** +; Interrupt vector addresses +;****************************************************************************** + +def INT_HANDLER_VBLANK equ $0040 ; VBlank interrupt handler address +def INT_HANDLER_STAT equ $0048 ; STAT interrupt handler address +def INT_HANDLER_TIMER equ $0050 ; timer interrupt handler address +def INT_HANDLER_SERIAL equ $0058 ; serial interrupt handler address +def INT_HANDLER_JOYPAD equ $0060 ; joypad interrupt handler address + + +;****************************************************************************** +; Boot-up register values +;****************************************************************************** + +; Register A = CPU type +def BOOTUP_A_DMG equ $01 +def BOOTUP_A_CGB equ $11 ; CGB or AGB +def BOOTUP_A_MGB equ $FF + def BOOTUP_A_SGB equ BOOTUP_A_DMG + def BOOTUP_A_SGB2 equ BOOTUP_A_MGB + +; Register B = CPU qualifier (if A is BOOTUP_A_CGB) +def B_BOOTUP_B_AGB equ 0 + def BOOTUP_B_CGB equ 0 << B_BOOTUP_B_AGB + def BOOTUP_B_AGB equ 1 << B_BOOTUP_B_AGB + +; Register C = CPU qualifier +def BOOTUP_C_DMG equ $13 +def BOOTUP_C_SGB equ $14 +def BOOTUP_C_CGB equ $00 ; CGB or AGB + +; Register D = color qualifier +def BOOTUP_D_MONO equ $00 ; DMG, MGB, SGB, or CGB or AGB in DMG mode +def BOOTUP_D_COLOR equ $FF ; CGB or AGB + +; Register E = CPU qualifier (distinguishes DMG variants) +def BOOTUP_E_DMG0 equ $C1 +def BOOTUP_E_DMG equ $C8 +def BOOTUP_E_SGB equ $00 +def BOOTUP_E_CGB_DMGMODE equ $08 ; CGB or AGB in DMG mode +def BOOTUP_E_CGB equ $56 ; CGB or AGB + + +;****************************************************************************** +; Aliases +;****************************************************************************** + +; Prefer the standard names to these aliases, which may be official but are +; less directly meaningful or human-readable. + +def rP1 equ rJOYP + +def rNR10 equ rAUD1SWEEP +def rNR11 equ rAUD1LEN +def rNR12 equ rAUD1ENV +def rNR13 equ rAUD1LOW +def rNR14 equ rAUD1HIGH +def rNR21 equ rAUD2LEN +def rNR22 equ rAUD2ENV +def rNR23 equ rAUD2LOW +def rNR24 equ rAUD2HIGH +def rNR30 equ rAUD3ENA +def rNR31 equ rAUD3LEN +def rNR32 equ rAUD3LEVEL +def rNR33 equ rAUD3LOW +def rNR34 equ rAUD3HIGH +def rNR41 equ rAUD4LEN +def rNR42 equ rAUD4ENV +def rNR43 equ rAUD4POLY +def rNR44 equ rAUD4GO +def rNR50 equ rAUDVOL +def rNR51 equ rAUDTERM +def rNR52 equ rAUDENA + +def rKEY0 equ rSYS +def rKEY1 equ rSPD + +def rHDMA1 equ rVDMA_SRC_HIGH +def rHDMA2 equ rVDMA_SRC_LOW +def rHDMA3 equ rVDMA_DEST_HIGH +def rHDMA4 equ rVDMA_DEST_LOW +def rHDMA5 equ rVDMA_LEN + +def rBCPS equ rBGPI +def rBCPD equ rBGPD + +def rOCPS equ rOBPI +def rOCPD equ rOBPD + +def rSVBK equ rWBK + +endc ; HARDWARE_INC + diff --git a/unbricked/vblank-interrupts/main.asm b/unbricked/vblank-interrupts/main.asm new file mode 100644 index 00000000..9a91081b --- /dev/null +++ b/unbricked/vblank-interrupts/main.asm @@ -0,0 +1,400 @@ +INCLUDE "hardware.inc" + +; ANCHOR: vblank-interrupt +SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK] +VBlankInterrupt: + push af + + ld a, 1 + ld [wVBlankDone], a + + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + + pop af + reti +; ANCHOR_END: vblank-interrupt + +SECTION "Header", ROM0[$100] + + jp EntryPoint + + ds $150 - @, 0 ; Make room for the header + +EntryPoint: + ; Do not turn the LCD off outside of VBlank +WaitVBlank: + ld a, [rLY] + cp 144 + jp c, WaitVBlank + + ; Turn the LCD off + ld a, 0 + ld [rLCDC], a + +; ANCHOR: copy_tiles + ; Copy the tile data + ld de, Tiles + ld hl, $9000 + ld bc, TilesEnd - Tiles + call MemCopy +; ANCHOR_END: copy_tiles + +; ANCHOR: copy_map + ; Copy the tilemap + ld de, Tilemap + ld hl, $9800 + ld bc, TilemapEnd - Tilemap + call MemCopy +; ANCHOR_END: copy_map + +; ANCHOR: copy_paddle + ; Copy the paddle tile + ld de, Paddle + ld hl, $8000 + ld bc, PaddleEnd - Paddle + call MemCopy +; ANCHOR_END: copy_paddle + + xor a, a + ld b, 160 + ld hl, STARTOF(OAM) +ClearOam: + ld [hli], a + dec b + jp nz, ClearOam + + ld hl, STARTOF(OAM) + ld a, 128 + 16 + ld [hli], a + ld a, 16 + 8 + ld [hli], a + ld a, 0 + ld [hli], a + ld [hl], a + + ; Turn the LCD on + ld a, LCDC_ON | LCDC_BG_ON | LCDC_OBJ_ON + ld [rLCDC], a + + ; During the first (blank) frame, initialize display registers + ld a, %11100100 + ld [rBGP], a + ld a, %11100100 + ld [rOBP0], a + + ; Initialize global variables + xor a, a + ld [wFrameCounter], a + ld [wVBlankDone], a + +; ANCHOR: enable-vblank-interrupt + ; Enable the VBlank interrupt + ldh [rIF], a + ld a, IE_VBLANK + ldh [rIE], a + ei +; ANCHOR_END: enable-vblank-interrupt + +; ANCHOR: main +Main: + call WaitForVBlank + + ld a, [wFrameCounter] + cp a, 15 ; Every 15 frames (a quarter of a second), run the following code + jp c, Main + + ; Reset the frame counter back to 0 + xor a, a + ld [wFrameCounter], a + + ; Move the paddle one pixel to the right. + ld a, [STARTOF(OAM) + 1] + inc a + ld [STARTOF(OAM) + 1], a + jp Main +; ANCHOR_END: main + +; ANCHOR: wait-for-vblank +WaitForVBlank: + xor a, a + ld [wVBlankDone], a +.wait + halt + nop + ld a, [wVBlankDone] + and a, a + jp z, .wait + ret +; ANCHOR_END: wait-for-vblank + +; ANCHOR: memcpy +; Copy bytes from one area to another. +; @param de: Source +; @param hl: Destination +; @param bc: Length +MemCopy: + ld a, [de] + ld [hli], a + inc de + dec bc + ld a, b + or a, c + jp nz, MemCopy + ret +; ANCHOR_END: memcpy + +Tiles: + dw `33333333 + dw `33333333 + dw `33333333 + dw `33322222 + dw `33322222 + dw `33322222 + dw `33322211 + dw `33322211 + dw `33333333 + dw `33333333 + dw `33333333 + dw `22222222 + dw `22222222 + dw `22222222 + dw `11111111 + dw `11111111 + dw `33333333 + dw `33333333 + dw `33333333 + dw `22222333 + dw `22222333 + dw `22222333 + dw `11222333 + dw `11222333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33322211 + dw `33322211 + dw `33322211 + dw `33322211 + dw `33322211 + dw `33322211 + dw `33322211 + dw `33322211 + dw `22222222 + dw `20000000 + dw `20111111 + dw `20111111 + dw `20111111 + dw `20111111 + dw `22222222 + dw `33333333 + dw `22222223 + dw `00000023 + dw `11111123 + dw `11111123 + dw `11111123 + dw `11111123 + dw `22222223 + dw `33333333 + dw `11222333 + dw `11222333 + dw `11222333 + dw `11222333 + dw `11222333 + dw `11222333 + dw `11222333 + dw `11222333 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `11001100 + dw `11111111 + dw `11111111 + dw `21212121 + dw `22222222 + dw `22322232 + dw `23232323 + dw `33333333 + ; My custom logo (tail) + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33302333 + dw `33333133 + dw `33300313 + dw `33300303 + dw `33013330 + dw `30333333 + dw `03333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `03333333 + dw `30333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333330 + dw `33333320 + dw `33333013 + dw `33330333 + dw `33100333 + dw `31001333 + dw `20001333 + dw `00000333 + dw `00000033 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33330333 + dw `33300333 + dw `33333333 + dw `33033333 + dw `33133333 + dw `33303333 + dw `33303333 + dw `33303333 + dw `33332333 + dw `33332333 + dw `33333330 + dw `33333300 + dw `33333300 + dw `33333100 + dw `33333000 + dw `33333000 + dw `33333100 + dw `33333300 + dw `00000001 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `10000333 + dw `00000033 + dw `00000003 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `33332333 + dw `33302333 + dw `32003333 + dw `00003333 + dw `00003333 + dw `00013333 + dw `00033333 + dw `00033333 + dw `33333300 + dw `33333310 + dw `33333330 + dw `33333332 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `30000000 + dw `33000000 + dw `33333000 + dw `33333333 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000003 + dw `00000033 + dw `00003333 + dw `02333333 + dw `33333333 + dw `00333333 + dw `03333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 + dw `33333333 +TilesEnd: + +Tilemap: + db $00, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $02, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $0A, $0B, $0C, $0D, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $0E, $0F, $10, $11, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $12, $13, $14, $15, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $16, $17, $18, $19, $03, 0,0,0,0,0,0,0,0,0,0,0,0 + db $04, $09, $09, $09, $09, $09, $09, $09, $09, $09, $09, $09, $09, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0 +TilemapEnd: + +Paddle: + dw `33333333 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 + dw `00000000 +PaddleEnd: + +; ANCHOR: variables +SECTION "Counter", WRAM0 +wFrameCounter: db +wVBlankDone: db +; ANCHOR_END: variables + +SECTION "Input Variables", WRAM0 +wCurKeys: db +wNewKeys: db