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.

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
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,aWriting $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
; 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
The complete caller
Working setup and integration
; 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-@,0For 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.