Press Space and the turn counter advances. Wait a while: nothing happens. Now tap T. Keep your hands off the keyboard and the next turn arrives by itself. A turn can wait for a decision, or it can have a deadline. Those are separate choices.
Build and try it
make -C acorn-atom/patterns/assembly/framework/turn-deadlineUse Asm198x 0.0.71, then enter the complete generated build/loader.bas in the machine’s BASIC and RUN. Start with short Space and T taps. Reset leaves the example. The internal turn count is 16-bit; the two displayed hexadecimal digits show its low byte and wrap after FF.
This caller needs the expanded 32 KiB Atom. Its deadline context is at $5F00, ten caller-state bytes at $5F20, and the 479-byte program at $6000. The typed BASIC loader lives below these reservations. The stock 2.5 KiB Atom is outside this layout. The caller owns keyboard sampling and the text display until Break/reset.

The reusable part
Reserve a consecutive DEADLINE context of 12 bytes, then include this routine after your caller. The duration is an unsigned little-endian word at offsets 0 and 1. Set it once, sample your clock and call deadline_init. The timer starts disabled.
Pass the current clock to deadline_poll each service iteration, including while untimed. A holds the low clock byte and X the high byte; Y is preserved. Clear decimal mode. A/X and flags are clobbered across the interface. A call uses two return-address stack bytes. The helper does not change interrupt state or allocate memory. It is one mutable context, so do not call it concurrently.
; DEADLINE owns 12 bytes. A=clock low, X=clock high; decimal mode clear.
; Poll at least once per 65536 clock ticks. A/X/status clobbered; Y preserved.
deadline_init:
sta DEADLINE+8
stx DEADLINE+9
lda #0
sta DEADLINE+2
sta DEADLINE+3
sta DEADLINE+4
sta DEADLINE+5
sta DEADLINE+6
sta DEADLINE+7
rts
deadline_start:
lda DEADLINE
sta DEADLINE+2
lda DEADLINE+1
sta DEADLINE+3
ora DEADLINE
beq deadline_off
lda #1
deadline_off:
sta DEADLINE+4
lda #0
sta DEADLINE+5
rts
deadline_poll:
sta DEADLINE+10
stx DEADLINE+11
sec
sbc DEADLINE+8
sta DEADLINE+6
txa
sbc DEADLINE+9
sta DEADLINE+7
lda DEADLINE+10
sta DEADLINE+8
lda DEADLINE+11
sta DEADLINE+9
lda #0
sta DEADLINE+5
lda DEADLINE+4
beq deadline_done
sec
lda DEADLINE+2
sbc DEADLINE+6
sta DEADLINE+2
lda DEADLINE+3
sbc DEADLINE+7
sta DEADLINE+3
bcc deadline_expired
ora DEADLINE+2
bne deadline_done
deadline_expired:
lda #0
sta DEADLINE+2
sta DEADLINE+3
sta DEADLINE+4
lda #1
sta DEADLINE+5
deadline_done:
rts| Offset | Meaning |
|---|---|
| 0–1 | Duration: 0 disables; otherwise 1..65535 ticks |
| 2–3 | Remaining ticks |
| 4 | Enabled flag |
| 5 | Expired in this poll |
| 6–7 | Elapsed ticks consumed |
| 8–9 | Previous clock sample |
| 10–11 | 6502 temporary clock storage |
Call deadline_start to copy the duration into remaining time and enable it. Writing zero to offset 4 disables it. Read offset 5 immediately after polling: an expiry is an event for this poll, rather than a flag that stays set forever. The helper disables the expired timer and holds remaining time at zero. It does not end a game turn or decide whether another deadline should start.
Connect it to a caller
The caller polls time first. If expiry and an action arrive together, expiry wins: it commits one automatic turn, starts a new deadline, and discards that iteration’s input. Otherwise Space commits a turn and restarts an already enabled timer. An untimed action stays untimed. T switches the timing policy.
dispatch:
lda DEADLINE+5
beq consider_action
inc STATE+6
lda #2
sta STATE+7
jsr commit_turn
jmp deadline_start
consider_action:
lda STATE+8
and #1
beq dispatch_done
lda #1
sta STATE+7
jsr commit_turn
lda DEADLINE+4
beq dispatch_done
jsr deadline_start
dispatch_done:
rtsThe clock and input adapters provide the machine-specific part:
The Atom adapter samples the flyback signal at $B002. Each observed falling edge adds one tick. In the checked PAL configuration that gives 50 ticks per second while the loop keeps sampling. A 100-tick deadline is roughly two seconds under that condition.
There is a catch: if drawing or sound keeps the CPU away for a whole flyback, that tick is lost. The counter does not know how much time it missed. This is an honest, small polling clock, useful when you can bound the work between samples. An interrupt-maintained clock would need a separate design and ownership agreement.
Space and T are sampled through the keyboard matrix. The adapter restores the PPI selector and preserves the graphics-mode bits. A held Space key counts once; a press between samples may be missed. The display uses stock character output.
!source "settings.inc"
OSWRCH = $fff4
* = $6000
start:
cld
lda #0
ldx #9
clear_state:
sta STATE,x
dex
bpl clear_state
lda #<DURATION
sta DEADLINE
lda #>DURATION
sta DEADLINE+1
lda #<INITIAL_CLOCK
sta STATE
lda #>INITIAL_CLOCK
sta STATE+1
lda $b002
and #128
sta STATE+2
lda STATE
ldx STATE+1
jsr deadline_init
jsr display
next_sample:
jsr sample_clock
jsr deadline_poll
jsr sample_keys
lda DEADLINE+5
bne no_toggle
lda STATE+8
and #2
beq no_toggle
lda DEADLINE+4
beq enable_timer
lda #0
sta DEADLINE+4
beq no_toggle
enable_timer:
jsr deadline_start
no_toggle:
jsr dispatch
lda STATE+7
beq next_sample
jsr display
lda #0
sta STATE+7
jmp next_sample
; Count observed falling edges of PC7. No wait; missed flybacks are lost.
sample_clock:
lda $b002
and #128
cmp STATE+2
beq clock_sampled
sta STATE+2
cmp #0
bne clock_sampled
inc STATE
bne clock_sampled
inc STATE+1
clock_sampled:
lda STATE
ldx STATE+1
rts
; Space=bit 0, T=bit 1. Preserve the display-mode nibble and old selector.
sample_keys:
lda $b000
pha
and #$f0
ora #9
sta $b000
lda $b001
eor #255
and #1
sta STATE+9
lda $b000
and #$f0
ora #7
sta $b000
lda $b001
and #32
bne no_t
lda STATE+9
ora #2
sta STATE+9
no_t:
pla
sta $b000
lda STATE+3
eor #255
and STATE+9
sta STATE+8
lda STATE+9
sta STATE+3
rts
commit_turn:
inc STATE+4
bne turn_committed
inc STATE+5
turn_committed:
rts
!source "dispatch.inc"
!source "../../../../../cross-platform/patterns/framework/elapsed-deadline/6502.inc"
display:
lda #12
jsr OSWRCH
ldx #0
print_label:
lda label,x
beq print_value
jsr OSWRCH
inx
bne print_label
print_value:
lda STATE+4
lsr
lsr
lsr
lsr
jsr hex_digit
lda STATE+4
and #15
hex_digit:
tax
lda digits,x
jmp OSWRCH
label: !text "SPACE:TURN T:TIMER TURN "
!byte 0
digits: !text "0123456789ABCDEF"The screen redraws only after a turn changes. Replace that feedback with your own drawing or sound, while respecting the memory, port and timing owners. The example clears its text screen and does not restore the previous picture on exit.
Make it your own
Change DURATION from 100 to 50 and rebuild/re-enter the loader. The independently executed customisation uses this shorter value. The turn expires after half as many ticks, with the same helper code and state allocation.
DEADLINE = $5f00
STATE = $5f20
DURATION = 100
INITIAL_CLOCK = 0Clock units matter: 100 ticks means different amounts of time with a 50 Hz field counter and a 300 Hz firmware clock. Think of duration as a quantity with a unit, not just a number. The Maths for Games material develops the same habit of choosing a representation and checking its range; no extra lesson is required to use this routine.
Common mistake: waiting for input before handling expiry
Deliberately broken include below. This adds a command test before the expiry test. With no input, the timer reaches zero and disables itself, but the displayed turn never advances. The clock kept working; the caller discarded its event.
; DELIBERATELY WRONG: ignores expiry until a command arrives.
dispatch:
lda STATE+8
and #1
beq dispatch_done
lda DEADLINE+5
beq consider_action
inc STATE+6
lda #2
sta STATE+7
jsr commit_turn
jmp deadline_start
consider_action:
lda STATE+8
and #1
beq dispatch_done
lda #1
sta STATE+7
jsr commit_turn
lda DEADLINE+4
beq dispatch_done
jsr deadline_start
dispatch_done:
rts
Keep this wrong include separate from normal builds. The fix is the working dispatch above: check expiry before asking whether a command arrived.
Limits and useful next steps
The helper subtracts consecutive 16-bit clock samples modulo 65536. For example, a move from 65530 to 5 means eleven ticks, provided a whole rollover was not missed. A 100-tick deadline then has 89 ticks left. Exact and overshot deadlines both become zero rather than wrapping into a long wait.
It reports one expiry, even after a long overrun. Restarting from the observation time does not replay all the turns that might have happened. A fixed-step accumulator, queued actions, longer clocks and carrying lateness into the next deadline are useful extensions when the game needs them; they are not hidden guarantees here. See choosing progression and clocks for the wider distinction.
The measured helper is 140 executable bytes with 12 context bytes. The complete caller is 479 bytes, plus ten caller-state bytes, two guards, stack, firmware, display and loader requirements. Polling does bounded work rather than looping once per elapsed tick. Firmware calls and display work still need measurement before claiming a whole-game frame budget.
What was checked
Code status: assembled and emulator-executed. Five fresh stock-firmware callers cover normal use, shorter duration, the input-gated mistake and a separate repaired run, plus a near-wrap observed clock. The verifier types the complete loader, compares every installed code byte, checks guards and observes independent action, deadline expiry, restart and disable behaviour. The observed clock advances 30 ticks across 30 native frames.
Separate original reset-ROM fixtures execute twelve arithmetic cases, including wrap, exact/overshot expiry and zero/maximum duration. A completed wrong-branch fixture is rejected by the independent arithmetic oracle. Those CPU/RAM fixtures do not establish stock-firmware loading or display timing. No original-hardware result or general emulator certification is claimed.
The maintained caller and retained evidence include the repeatable commands. The shared routine contract explains the state layout and alternatives.
Primary references
David Johnson-Davies, Atomic Theory and Practice (1980), §25.5 (PPI ports and flyback), §25.6 (memory map), §13.14 (OS calls) and §14.2 (LINK). The PAL field rate above is the named execution observation.