Skip to content

Wait for a player action on MTX500 in Z80 assembly

Reuse an untimed update with a small native caller, clear state ownership, a custom step and a reset mistake you can recognise.

Press Space. The backdrop changes once. Hold it and the colour stays put; release and press again to restore the first colour. The world waits for a new action while the display-service loop continues.

The backdrop shows parity, rather than an exact count: odd and even values use different colours. It is a deliberately small output adapter, leaving the rule update easy to reuse.

MTX500 native assembly caller after two actions.
The working caller preserves progress between separate presses.

Build and enter the caller

Keep demo.asm, caller.inc, routines.inc and presentation.inc together. From the code-samples root:

Shell
python3 scripts/build-aquarius-mtx-loops.py --assembler /path/to/asm198x --system memotech-mtx

Enter the generated build/working/load.bas line by line under normal firmware. The builder derives its DATA bytes from the actual assembled image. Follow the source README’s entry sequence after loading.

On an MTX500, the 135-byte image occupies $E000–$E086. The generated READ/POKE loader stops after loading; LET X=USR(57344) enters it. This caller takes over until reset: it disables maskable interrupts, sets SP to $F7F0 and owns the keyboard and VDP control ports. It does not return to BASIC. Reserve its code, context and stack in your own memory plan; this small loader is not a general allocator for a mixed BASIC/assembly game.

Origin, settings and includes

demo.asm
; SPDX-License-Identifier: MIT
    org $E000
INITIAL equ 0
STEP equ 1
RESET_EACH equ 0
NO_EDGE equ 0
CTX equ $DF00
    include "caller.inc"
    include "routines.inc"
    include "presentation.inc"

Initialise once, then decide when to update

caller.inc
; Reuses the stock BASIC READ/POKE/USR entry, then takes over until reset.
start:
    di
    ld sp,$F7F0
    ld a,INITIAL
    call turn_init
    xor a
    ld (CTX+1),a
    ld (CTX+2),a
    ld (CTX+3),a
    in a,($02)
    xor a
    out ($02),a
    ld a,$80
    out ($02),a
    xor a
    out ($02),a
    ld a,$81
    out ($02),a
    call show_turn
main:
    call wait_frame
    ld hl,(CTX+2)
    inc hl
    ld (CTX+2),hl
    ld a,RESET_EACH
    or a
    jr z,keep_state
    xor a
    call turn_init
keep_state:
    call sample_action
    or a
    jr z,no_action
    call turn_advance
no_action:
    call show_turn
    jr main

Reuse the rule without the caller

Put the starting byte in A and call turn_init once. Call turn_advance only when your own command handler accepts an action. Neither routine reads a key, draws a cell or consults a clock. Include the implementation once in your build.

Persistent state and the byte update

routines.inc
; Reusable state operations: no display, keyboard or clock access.
; CTX owns four bytes: count, previous sample, display-service count (16-bit LE).
turn_init:
    ld (CTX),a
    ret
turn_advance:
    ld a,(CTX)
    add a,STEP
    ld (CTX),a
    ret
; Returns A=1 for one new Space press, otherwise zero. AF/BC changed.
; Always save the sample, including on a rejected/held action.
sample_action:
    ld a,$7F
    out ($05),a
    in a,($06)
    cpl
    and 1
    ld b,a
    ld a,NO_EDGE
    or a
    ld a,b
    jr nz,accept_sample
    ld a,(CTX+1)
    cpl
    and b
accept_sample:
    ld c,a
    ld a,b
    ld (CTX+1),a
    ld a,c
    ret
; Fresh VDP status edge. AF changed; exclusively owns the status latch.
wait_frame:
    in a,($02)
wait_new:
    in a,($02)
    and $80
    jr z,wait_new
    ret
Routine Contract
turn_init Store A into count; A and flags remain unchanged.
turn_advance Add compile-time STEP modulo 256; changes A, flags and count.

Define CTX as four private RAM bytes. This caller uses $DF00–$DF03 for count, previous Space sample and a little-endian 16-bit display-service counter. turn_init changes only count; the caller initialises the other bytes once.

sample_action drives keyboard column 7 through OUT $05=$7F, reads Space on port $06 bit 0, and updates the previous sample on every call. It returns 1 only for a new press; AF/BC change. wait_frame clears stale VDP status, then waits for a fresh bit-7 indication; AF changes. It exclusively owns status/control port $02, so it cannot share that code unchanged with a firmware VDP interrupt handler or another status reader.

The service counter advances every native field while the rule count waits. It wraps after 65535. A running display clock does not automatically make this a timed game.

The machine-specific output adapter

presentation.inc
; Alternate backdrop 2/3 using count parity. AF changed, no rule update.
; Direct control/status port ownership; no CPU-mapped text screen is implied.
show_turn:
    ld a,(CTX)
    and 1
    add a,2
    out ($02),a
    ld a,$87
    out ($02),a
    ret

show_turn selects backdrop index 2 or 3 through VDP register 7; AF changes. It blanks the character display and lets the VDP maintain refresh. This is neither a CPU-mapped text screen nor a renderer that must draw every scanline. Use a different presentation adapter when you want room descriptions, sprites or an exact numeric status.

Customise the step

Build with --variant step-three to use this alternative. Two presses now produce 0 → 3 → 6, with no extra state or image bytes.

Same caller, three units per accepted action

customise.asm
; SPDX-License-Identifier: MIT
    org $E000
INITIAL equ 0
STEP equ 3
RESET_EACH equ 0
NO_EDGE equ 0
CTX equ $DF00
    include "caller.inc"
    include "routines.inc"
    include "presentation.inc"
MTX500 customised assembly caller after two presses.
The action policy stays the same when STEP changes.

Parity still alternates for a step of three. A two-colour display cannot tell you whether a change was one or three; inspect the state byte when checking the amount.

Common mistakes: resetting and repeating

This variant is deliberately broken. RESET_EACH=1 puts initialisation inside the iteration. Each new pass erases the previous result, so the visible output returns to its initial state.

Broken: erase the previous action

reset-each-command.asm
; Deliberately broken.
; SPDX-License-Identifier: MIT
    org $E000
INITIAL equ 0
STEP equ 1
RESET_EACH equ 1
NO_EDGE equ 0
CTX equ $DF00
    include "caller.inc"
    include "routines.inc"
    include "presentation.inc"
MTX500 broken assembly caller showing its initial state.
The caller discards the state that the update was meant to preserve.

Restore startup-only initialisation to repair it. The separate no-edge test variant demonstrates another fault: accepting a held key on every sample. It must fail the ordinary one-action-per-press check. Deliberate auto-repeat needs its own initial delay and repeat policy.

Where time belongs

An untimed world advances on an accepted action. A timed turn also needs a deadline and a rule for an action arriving at that deadline. Display refresh, firmware interrupts and CPU activity do not decide that policy for you.

Compare the loop choices and the BASIC implementation. The languages have different storage and ownership agreements; choose the one that suits your project.

Source and verification

Native-emulator executed: Emu198x 0.44.1, with MTX500 firmware; assembly is built with Asm198x 0.0.71. Actual keyboard input drives the program. Host writes do not supply game state. The images above are native framebuffer captures.

The maintained source includes the complete caller, reusable pieces and deliberate fault. The retained observations cover normal use, idle and held input, a custom step, reset and repair, 254 → 255 → 0, and a nonzero start. Assembly adds exact loaded-byte comparison and a disabled-edge failure control. The MTX assembly run checks the service counter during idle and compares both actual backdrop images. An independent checker rejects altered counts, sources and captures, missing observations and falsely repaired faults.

This qualifies these small workloads on the named configuration. It does not establish original-hardware, cassette-transfer, other-model or whole-emulator compatibility. The older input/drawing/sound probe remains a separate record; this loop entry makes no fresh audio claim.

Primary references

Memotech’s MTX Series BASIC Tutor, Reference and Operator’s Manual, original manual scan: BASIC control flow, USR and return value in BC. Memotech’s MTX Video Memory Mapping, document scan: separate VDP RAM, ports and access spacing. Keyboard drive/sense follows the retained stock-firmware input-loop and MTX hardware reference.