Press fire and the background changes colour. Keep holding it: nothing more happens. Release and press again, and the first colour returns. The game has accepted two actions, while the display has kept running throughout.
That small difference is worth getting right. A player waiting for their next move should not accidentally stop the picture, and a held button should not become hundreds of moves.

Build and try it
make -C atari-5200/patterns/assembly/framework/turn-loopStart build/demo.bin in Emu198x for Atari 5200. The qualified configuration is NTSC 32 KiB cartridge handed over by the identified 5200 BIOS. Reset to leave. The caller is a deliberately small backdrop demonstration, not a complete display engine.
Include and initialise the reusable state
Copy turn.inc and its neighbouring edge-filter.inc together, and include the routine once after your main code. Define T as seven private, consecutive RAM bytes. The caller uses $1000–$1006; choose another valid allocation if your game owns that memory already.
Call init_turn with the starting progress counter in A. It clears the previous/current samples, accepted flag and 16-bit service count, and sets the step to 1. Change T+5 after initialisation when you want another step. These routines require decimal mode clear; the complete reset caller uses CLD.
Rule progression and display service are separate routines
; Caller defines T as the first of seven disjoint private RAM bytes.
; T+0 turns, +1 previous sample, +2/+3 serviced fields, +4 current,
; +5 step size, +6 accepted flag. Decimal mode must be clear.
; init_turn: A = initial counter; initialises all seven bytes, step=1.
init_turn:
sta T
lda #0
sta T+1
sta T+2
sta T+3
sta T+4
sta T+6
lda #1
sta T+5
rts
; sample_turn: A = active-high button mask, same bit convention each call.
; Changes A/flags, preserves X/Y. Counter wraps modulo 256.
; No hardware access or waiting. 2 stack bytes for a direct JSR.
sample_turn:
sta T+4
!source "edge-filter.inc"
beq held_or_released
lda T
clc
adc T+5
sta T
lda #1
sta T+6
bne remember_sample
held_or_released:
lda #0
sta T+6
remember_sample:
lda T+4
sta T+1
rts
; One accepted display-service tick, independent of rule progression.
; Changes flags only; wraps at 65536. Call exactly once per serviced field.
service_tick:
inc T+2
bne tick_done
inc T+3
tick_done:
rts
turn_end:Keep only bits that became pressed since the last sample
; Preserve current bits that were absent in the previous sample.
lda T+1
eor #$ff
and T+4| Entry or state | Contract |
|---|---|
init_turn |
A is the initial 8-bit counter; initialises all seven bytes |
sample_turn |
A is the active-high button mask; accepts newly pressed bits |
T+0 |
Progress counter, increased by T+5 on acceptance; wraps modulo 256 |
T+1, T+4 |
Previous/current samples; keep the same bit convention each call |
T+2, T+3 |
Little-endian 16-bit service count, independent of turns |
T+5, T+6 |
Step size and the current call’s accepted flag (1 or 0) |
service_tick |
Increment the service count once per accepted display field |
The init/sample routines change A and flags; service_tick changes flags. All preserve X/Y. A direct JSR needs two stack bytes, plus your surrounding caller and interrupt depth. The routines have no nested calls, hardware access, frame wait, private stack or interrupt masking. Their shared context is not re-entrant.
Use one primary-button bit as the caller does. With a multi-bit mask, any newly pressed bit commits one update, even if another bit remains held; this routine does not queue separate actions for each button. A zero step can still report an accepted action without changing progress. Neither helper decides your game rules or imposes a deadline.
Fit it into the machine’s caller
Reset, input conversion, rule update and machine-specific display service
; atari-5200: rule state and display servicing have separate counters.
T = $1000
!source "settings.inc"
* = $4000
start:
sei
cld
ldx #$ff
txs
lda #0
sta $d40e ; disable ANTIC NMI ownership from BIOS
sta $d400 ; display-list DMA off: background-only output
lda #INITIAL_TURN
jsr init_turn
lda #$a5
sta T-1
sta T+7
lda #STEP_SIZE
sta T+5
main:
jsr wait_frame
jsr service_tick
lda $c010
and #1
eor #1
jsr sample_turn
lda T
and #FEEDBACK_BIT
beq even_turn
lda #$44
bne show
even_turn:
lda #$84
show:
sta $c01a
jmp main
!source "frame.inc"
!source "../../../../../cross-platform/patterns/framework/atari-turn-loops/turn.inc"
!fill $bffe-*, $ff
!word startsample_turn reports acceptance only for that call. Read T+6 before another sample replaces it, then let your own rules respond. service_tick does not change that flag. The two counters have different meanings: one counts accepted progress, the other counts serviced fields.
The guards immediately outside the seven-byte allocation are diagnostic caller state, not part of the reusable routine. The reset caller takes over the stack and display interfaces. Keep its ownership separate from other routines that scan input, alter interrupt enables or own the same video registers.
Let ANTIC keep time
This machine does not need a TIA-style program that emits every scanline. ANTIC supplies a running vertical counter. The caller disables display-list DMA for a background-only picture and uses a small polling wait to accept one field boundary.
Wait out the current high interval, then accept the next one
wait_frame:
lda $d40b ; VCOUNT bit 7 covers the tail of the NTSC field
bmi wait_frame
wait_new:
lda $d40b
bpl wait_new
rtsVCOUNT at $D40B counts in two-line units. This NTSC wait first leaves any interval with bit 7 set, then waits for the next one. A call made while that bit is already high cannot immediately count the same interval twice. This is a useful field announcement, not a general-purpose clock that remembers missed fields.
The primary trigger is GTIA TRIG0 bit 0 at $C010. The caller converts its active-low state before sample_turn. That is a different address and bit from the 2600’s fire input. It does not read the analogue stick, keypad, Start or second fire button.
The BIOS hands control to the cartridge’s vector at $BFFE. The caller then disables ANTIC NMI and display-list DMA; IRQ remains disabled. It owns that takeover. A BIOS service or your own interrupt-driven display needs a separately agreed arrangement. Background colour is written to GTIA COLBK at $C01A.
The measured service count advances 30 times in 30 native frames. Long work can miss a counter interval; it does not produce an unlimited backlog. Add an ANTIC display list through its own setup and ownership contract when a game needs more than this backdrop.
Make it your own
The default caller reads these settings:
One progress unit per new press
INITIAL_TURN = 0
STEP_SIZE = 1
FEEDBACK_BIT = 1Replace settings.inc with this maintained alternative and rebuild:
Two progress units, with feedback using the next counter bit
INITIAL_TURN = 0
STEP_SIZE = 2
FEEDBACK_BIT = 2The counter now goes 0 → 2 → 4, still with one accepted action per press. We also select bit 1 for colour feedback: bit 0 never becomes set when adding two, so leaving the original feedback mask would hide the progress. This is a useful distinction between changing the rules and choosing how to show them.
The routine code and seven-byte context stay the same size; only caller constants change. The compiled routine occupies 77 bytes for this allocation, excluding the reset/display caller, stack and cartridge padding. The absolute addressing of this allocation costs more code bytes than the 2600’s zero-page allocation. Moving it can change both size and execution cost; preserve the machine’s timing contract when relocating it. The customisation is separately executed, not just assembled.
Common mistake: held means new
The next fragment is deliberately broken. It replaces edge-filter.inc and treats every held sample as another press.
Broken filter: the current state is not an edge
; Deliberately wrong: a held bit is treated as a new press every sample.
lda T+4In the native run, holding fire for a further 30 frames changes the faulty counter again. The working counter remains at 1. Its picture may alternate too quickly for a still to explain the error: the measured state transition is the evidence. Restore the previous-sample filter above. Keep the update of T+1 on every call, including releases, so another press can be recognised.
Common mistake: a plausible picture, the wrong clock
Replacing the field wait with RTS makes the service counter a count of CPU iterations, even though its name still says “fields”. The background may look unchanged.
Deliberately wrong: return without accepting a field boundary
; Deliberately wrong: no field boundary is accepted.
wait_frame:
rtsThe native observation is 8,090 service ticks in 30 frames, against 30 for the working wait. The counter is 16-bit and wraps; this measured interval is not a universal maximum speed. Restore frame.inc above and the original rate check passes again.
What was checked
Code status: assembled and emulator-executed with Asm198x 0.0.71 and Emu198x 0.44.0. Seven independent builds/runs cover the working caller, step-two customisation, 254 → 255 → 0 wrap, both deliberate faults and both repairs. The checks use real button input, 120 idle frames, a 30-frame hold, a second press, protected state boundaries and native captures. The repairs restore the working executable and captured results.
The records retain source, tool and executable identities plus the BIOS hash. The CI checker rejects missing bindings, omitted variants and altered behavioural evidence; it checks retained evidence and does not claim a new emulator run. No PAL, arbitrary game workload, original-hardware or whole-emulator qualification follows from this example.
- Complete callers, routine and reproduction instructions
- Native execution record
- Choose progression, clocks and presentation
Hardware references
Atari, 400/800 Hardware Manual (1982), ANTIC’s vertical counter; the original ANTIC discussion in De Re Atari supplies related display context. The 5200’s translated GTIA mapping and BIOS handover are corroborated by cc65’s 5200 platform documentation. See the 5200 drawing backend for a real ANTIC bitmap caller.