Skip to content

Read a button on Game Boy

Reuse the actual machine-specific reader, with its caller, state, ownership and a diagnosed input mistake.

Build the complete caller with make -C nintendo-game-boy/patterns/assembly/input/read-action. Press A once, keep it held, then release and press again. The piece moves on each new press and returns to its starting position after two; holding does not repeat the action. Load the cartridge produced by the Makefile in the named configuration; retain its reset/header setup.

Game Boy native caller after accepted input.
Captured from the running caller in the configuration below.

Take the reader

sample.inc is an inline hardware sampler: include it where the caller samples input. It has no RET/RTS and must not be called as a subroutine. It supplies the held state described below; the caller supplies previous-state storage and decides what a new press does.

The reusable reader

sample.inc
    ld a,$10
    ld [$ff00],a             ; select action buttons (P15 low)
    ld a,[$ff00]
    ld a,[$ff00]
    ld a,[$ff00]
    ld a,[$ff00]
    ld a,[$ff00]
    ld a,[$ff00]
    cpl
    and 1
    ld b,a

Writing $10 to JOYP selects action buttons. Six settling reads precede active-low bit-0 normalisation; B receives 0/1 for A. AF/B change, and the row remains selected.

Calculate a new-press mask as current AND NOT previous, then store current as previous on every update, including release. Test held state for continuous movement and the new-press mask for an action that should happen once. A key already held at the caller’s first sample counts as a press because previous starts at zero.

Caller storage: previous=$C001; turn=$C000, field clock=$C002/$C003; SP=$FFFE, interrupts disabled. The sample’s display initialiser and any interrupts have their own ownership requirements; keep them with this complete example.

Change it and recognise a mistake

Use another JOYP row or combine rows deliberately. Hardware switch bounce and input lost between polls are separate from the release/press filter.

The broken caller feeds held state into the action decision. Keeping the button down advances repeatedly; the native fault run reaches 32 instead of remaining at one. Restore the previous-state comparison and update previous on release as well.

Deliberately broken input/update path

no-edge.asm
; Capability caller: original input/frame service, explicit drawing and sound.
; DMG cartridge: video keeps running; only a new A press advances the turn.
; Owns LCD/VRAM, JOYP and $C000-$C003; maskable interrupts stay disabled.
SECTION "Entry", ROM0[$100]
    nop
    jp start
    ds $134-@,0
    db "TURNPROBE"
    ds $150-@,0
SECTION "Program", ROM0[$150]
start:
    di
    ld sp,$fffe
wait_to_disable:
    ld a,[$ff44]
    cp 144
    jr c,wait_to_disable
    xor a
    ld [$ff40],a             ; disable LCD in VBlank before clearing VRAM
    ld hl,$8000
    ld bc,$2000
clear_vram:
    xor a
    ld [hl+],a
    dec bc
    ld a,b
    or c
    jr nz,clear_vram
    xor a
    ld [$c000],a             ; turn counter
    ld [$c001],a             ; previous A state
    ld [$c002],a             ; accepted frame counter, low
    ld [$c003],a             ; high
    call init_picture
    ld a,$e4
    ld [$ff47],a
    ld a,$91
    ld [$ff40],a             ; background enabled, tile/map zero initialised
main:
    call wait_frame
    ld a,[$c002]
    inc a
    ld [$c002],a
    jr nz,sample
    ld a,[$c003]
    inc a
    ld [$c003],a
sample:
    INCLUDE "../sample.inc"
    ld a,[$c001]
    ld a,b
    nop
    jr z,no_action
    ld a,[$c000]
    inc a
    ld [$c000],a
    call trigger_sound
no_action:
    ld a,b
    ld [$c001],a
    call draw_picture
    call service_sound
    jp main

; AF clobbered. Requires LCD enabled. Waits beyond any current VBlank,
; then accepts the next VBlank. Does not count missed announcements.
wait_frame:
    ld a,[$ff44]
    cp 144
    jr nc,wait_frame
wait_vblank:
    ld a,[$ff44]
    cp 144
    jr c,wait_vblank
    ret
    INCLUDE "../picture.inc"
    INCLUDE "../sound.inc"
    ds $8000-@,0
Game Boy deliberately broken input caller.
Observed fault; use the working caller to repair it.

The complete caller

Working setup and integration

demo.asm
; Capability caller: original input/frame service, explicit drawing and sound.
; DMG cartridge: video keeps running; only a new A press advances the turn.
; Owns LCD/VRAM, JOYP and $C000-$C003; maskable interrupts stay disabled.
SECTION "Entry", ROM0[$100]
    nop
    jp start
    ds $134-@,0
    db "TURNPROBE"
    ds $150-@,0
SECTION "Program", ROM0[$150]
start:
    di
    ld sp,$fffe
wait_to_disable:
    ld a,[$ff44]
    cp 144
    jr c,wait_to_disable
    xor a
    ld [$ff40],a             ; disable LCD in VBlank before clearing VRAM
    ld hl,$8000
    ld bc,$2000
clear_vram:
    xor a
    ld [hl+],a
    dec bc
    ld a,b
    or c
    jr nz,clear_vram
    xor a
    ld [$c000],a             ; turn counter
    ld [$c001],a             ; previous A state
    ld [$c002],a             ; accepted frame counter, low
    ld [$c003],a             ; high
    call init_picture
    ld a,$e4
    ld [$ff47],a
    ld a,$91
    ld [$ff40],a             ; background enabled, tile/map zero initialised
main:
    call wait_frame
    ld a,[$c002]
    inc a
    ld [$c002],a
    jr nz,sample
    ld a,[$c003]
    inc a
    ld [$c003],a
sample:
    INCLUDE "sample.inc"
    ld a,[$c001]
    cpl
    and b
    jr z,no_action
    ld a,[$c000]
    inc a
    ld [$c000],a
    call trigger_sound
no_action:
    ld a,b
    ld [$c001],a
    call draw_picture
    call service_sound
    jp main

; AF clobbered. Requires LCD enabled. Waits beyond any current VBlank,
; then accepts the next VBlank. Does not count missed announcements.
wait_frame:
    ld a,[$ff44]
    cp 144
    jr nc,wait_frame
wait_vblank:
    ld a,[$ff44]
    cp 144
    jr c,wait_vblank
    ret
    INCLUDE "picture.inc"
    INCLUDE "sound.inc"
    ds $8000-@,0

For more responsive controls, compare polling, buffering and press edges in input choices.

Verification and sources

Target: DMG default reset without boot ROM, 32 KiB cartridge. These callers were executed with released Emu198x 0.44.1; assembly uses Asm198x 0.0.71. The sample folder contains reproduction instructions and retained observations. The independent checker binds source, native captures and actual output; its rejection tests exercise wrong state, missing files and false audio claims.

This qualifies the named emulator/firmware path. Pitch accuracy, original hardware and other peripherals require their own observations.

Nintendo, Game Boy Programming Manual v1.1: P1/JOYP scan example (six action-button reads), LCDC/LY access rules and NR10–NR14/NR50–NR52.