Press button 1 and the screen changes colour. Hold it and nothing else happens. You have made one move, rather than asking for a new move every time the program checks the controller.
Now press button 2. A countdown runs while you wait, and the next turn begins when it expires. Press button 2 again and you can take as long as you like. The turn counter and the clock are separate pieces.
Build and try it
make -C sega-master-system/patterns/assembly/framework/turn-deadlineStart build/demo.sms in Emu198x for the Master System. The complete cartridge needs no firmware. The verified configuration is NTSC Master System. Reset to leave.
The reusable turn routines
Reserve and zero the eight state bytes at $C000–$C007. Include turn.inc in your program, or adjust its equates to RAM you own. commit_turn increases the turn count and restarts the countdown at 120. tick_deadline advances the clock and ends a turn when its countdown reaches zero.
Both routines change AF and HL. Neither reads input, waits or draws. Call tick_deadline only in timed mode with a nonzero countdown; the caller decides what provides each tick. The 16-bit turn/tick counts and 8-bit expiry count wrap around.
; Z80 turn state, caller must initialise all bytes to zero.
; Storage belongs to the caller; no interrupts access it.
turns equ $c000 ; 16-bit wraparound count
clock_ticks equ $c002 ; 16-bit accepted display announcements
remaining equ $c004 ; 8-bit countdown, 120 accepted ticks per turn
is_timed equ $c005 ; 0 = untimed, 1 = timed
previous equ $c006 ; previous active-high action bits
expiries equ $c007 ; 8-bit expiry count
; commit_turn: increment turn count and start a fresh deadline.
; Outputs: turns++, remaining=120. Clobbers AF, HL. No display access.
commit_turn:
ld hl,(turns)
inc hl
ld (turns),hl
ld a,120
ld (remaining),a
ret
; tick_deadline: call once for each accepted clock tick, only in timed mode.
; Outputs: clock_ticks++; at zero expiries++, commit_turn.
; Clobbers AF, HL. Does not wait, read input or draw.
tick_deadline:
ld hl,(clock_ticks)
inc hl
ld (clock_ticks),hl
ld a,(remaining)
dec a
ld (remaining),a
ret nz
ld a,(expiries)
inc a
ld (expiries),a
jp commit_turnConnect input, time and feedback
The caller reads the two action buttons at $DC, converts their active-low bits and compares them with the previous sample. A new press of button 1 calls commit_turn; holding it does not repeat. Button 2 toggles the deadline and starts a fresh countdown.
In untimed mode the caller keeps checking for input without a frame wait. In timed mode it accepts a fresh display announcement before ticking the countdown. If an action and expiry coincide in that iteration, the action goes first and restarts the deadline; that iteration’s tick then consumes its first unit. We are choosing a rule rather than letting incidental call order choose it for us.
Mode 4 backdrop colours use one byte per CRAM entry. This caller sets entries 17 and 18 to red and green. Its stack fits in system RAM.
; Standalone cartridge; no firmware required. Button 1 commits a turn.
; Button 2 toggles timed turns. Untimed: no frame wait, no ticking deadline.
; Timed: one accepted display announcement per tick, 120 ticks per turn.
; Action wins a same-iteration expiry: commit_turn runs before tick_deadline.
; Visual output: alternating full-screen backdrop colours after each turn.
; This deliberately blanks tile/sprite rendering. Not a rendered VRAM demo.
org $0000
jp start
defs $66-$,0
retn ; Pause NMI does not disturb main state
defs $100-$,0
start:
di
ld sp,$c3f0 ; inside SG-1000's 1 KiB RAM too
xor a
ld hl,$c000
ld b,8
clear_state:
ld (hl),a
inc hl
djnz clear_state
in a,($bf) ; reset VDP command latch
ld a,4
out ($bf),a
ld a,$80
out ($bf),a ; R0: mode 4, line interrupts disabled
xor a
out ($bf),a
ld a,$81
out ($bf),a ; R1: display and frame IRQ disabled
ld a,17
out ($bf),a
ld a,$c0
out ($bf),a ; CRAM entry 17
ld a,$03
out ($be),a ; red
ld a,$0c
out ($be),a ; green
call show_turn
main:
ld d,0 ; this iteration has not accepted a clock tick
ld a,(is_timed)
or a
jr z,read_actions
call wait_frame
inc d ; a fresh announcement was accepted
read_actions:
in a,($dc)
cpl
and $30 ; active-high button 1/2 state
ld c,a
ld a,(previous)
cpl
and c
ld b,a ; newly pressed bits
ld a,c
ld (previous),a
bit 5,b
jr z,check_action
ld a,(is_timed)
xor 1
ld (is_timed),a
ld a,120
ld (remaining),a ; toggling mode starts a fresh deadline
check_action:
bit 4,b
call nz,commit_turn
ld a,(is_timed)
and d ; enabling the timer does not invent its first tick
call nz,tick_deadline
call show_turn
jp main
; Backdrop selects visible feedback even with display disabled.
; AF clobbered, status reader cannot interrupt the two control writes.
show_turn:
ld a,(turns)
and 1
add a,1
out ($bf),a
ld a,$87
out ($bf),a
ret
include "../../../../../cross-platform/assembly/framework/turn-deadline/turn.inc"
include "frame.inc"
defs $7ff0-$,0
db "TMR SEGA" ; header; final checksum supplied by build.py
db 0,0,0,0,0,0,0,$4cOwn the display interface
The caller disables maskable interrupts and takes ownership of the VDP status/control ports. Reading status clears flags and resets the command latch. Another reader could consume the announcement or split a two-byte command, so an interrupt-driven renderer needs a different agreement.
; wait_frame: fresh VDP frame-status announcement, AF clobbered.
; Interrupts remain disabled. Caller owns all status/control-port reads.
; Reading status clears flags AND resets the control-port command latch.
; No other reader/IRQ may consume status or interleave register writes.
; Missed announcements collapse into a latch; there is no catch-up counter.
wait_frame:
in a,($bf) ; discard pending announcement
wait_new_frame:
in a,($bf)
and $80
jr z,wait_new_frame
retA missed announcement is lost, rather than stored as elapsed time. The deadline lasts 120 accepted ticks, which need not be exactly two seconds. The caller deliberately disables tile/sprite rendering and uses the backdrop for visible feedback; it does not demonstrate safe uploads during a rendered game. Add display, audio and background servicing deliberately when adapting it. Short presses between samples may be missed.
The stack is at $C3F0, away from our state. The $0066 handler returns safely from Pause NMI, but supplies no pause behaviour; Game Gear Start is outside this example.
What was checked
Code status: assembled and emulator-executed. The retained record identifies the exact sources and emulator. Untimed state stays still for 120 frames; holding an action for 30 frames does not repeat; timed mode accepts 30 ticks in 30 native frames. The deadline can expire without input, an action restarts it, and disabling it stops the clock again. Captured output changes with the turn.
Separate cartridge mutations remove the wait and the edge detector. The same timing/input guards reject them. This establishes the named configuration and workload, not original-hardware behaviour or a complete emulator test.
See choosing progression and clocks for other combinations. The complete source instructions explain integration and how to rerun the checks.
Hardware sources
Sega’s Mark III software reference manual, Interrupt System, VDP Register Description, Memory Management and Appendix B.