Skip to content

Wait for a command in Amstrad CPC assembly

A reusable character-driven turn update, with a stock-firmware caller, owned state and verified mistakes and repairs.

Press G. TURN 00 becomes TURN 01. Press it again for TURN 02. Try X: the counter stays where it is. While you decide what to do, the game rules wait.

That makes a useful starting point for an untimed command game. The reusable piece accepts a character and updates state. Reading the keyboard and drawing the result belong to its caller, so you can replace either without rewriting the rule routine.

The Amstrad CPC assembly caller after two accepted commands, showing TURN 02.

Run the complete caller

Use a CPC464 with stock firmware, starting in Locomotive BASIC. The generated loader selects MODE 1 and lowers BASIC’s memory limit with MEMORY &8FEF before it installs the code. It reads original executable bytes from DATA, writes them above that boundary and enters them with CALL &9000.

The 141-byte default executable starts at &9000. Four context bytes start at &8FF0, with two diagnostic guards immediately afterwards. Keep those addresses out of BASIC’s workspace and any other machine-code allocation. If you relocate the code, change its origin, loader address and memory reservation together.

Download the caller and shared include at the recorded revision as part of the samples tree, keeping their relative paths. With released Asm198x 0.0.71 available as asm198x, run make in the caller folder. Enter the whole generated build/loader.bas listing, then type RUN. No manufacturer ROM is included in the download.

The firmware caller: initialise once, read, dispatch and draw

demo.asm
include "settings.inc"
org $9000
start:
    ei
    ld a,INITIAL
    call init_turn
    ld a,STEP
    ld (TURN+1),a
    call display
next_command:
    call $bb06
    call dispatch
    call display
    jr next_command
include "dispatch.inc"
include "../../../../../cross-platform/patterns/framework/command-turns/z80.inc"
display:
    ld a,12
    call $bb5a
    ld hl,label
print_label:
    ld a,(hl)
    inc hl
    or a
    jr z,print_value
    call $bb5a
    jr print_label
print_value:
    ld a,(TURN)
    rrca
    rrca
    rrca
    rrca
    and 15
    call hex_digit
    ld a,(TURN)
    and 15
hex_digit:
    ld hl,digits
    ld e,a
    ld d,0
    add hl,de
    ld a,(hl)
    jp $bb5a
label: db "TURN ",0
digits: db "0123456789ABCDEF"

KM WAIT CHAR at &BB06 waits for the next character and returns it in A; TXT OUTPUT at &BB5A writes it. The caller leaves normal firmware interrupt servicing enabled with EI. It neither takes over the display hardware nor replaces the firmware keyboard scanner.

The demo owns the text screen and clears it after every delivered character. That is presentation code, not part of the reusable state update. Its two hexadecimal digits make byte wrap visible: FE → FF → 00 means 254 → 255 → 0.

Include, initialise and call

Define TURN as four private, consecutive writable bytes and ACCEPT_KEY as the character code you want to accept. Include the routine once. Pass the starting counter in A and call init_turn once, before the recurring input path.

The reusable rule update: no keyboard or display ownership

z80.inc
; Caller defines TURN (four writable bytes) and ACCEPT_KEY.
; init_turn: A/F clobbered. accept_command: A/F/HL clobbered.
init_turn:
    ld (TURN),a
    ld a,1
    ld (TURN+1),a
    xor a
    ld (TURN+2),a
    ld (TURN+3),a
    ret
accept_command:
    ld (TURN+3),a
    xor a
    ld (TURN+2),a
    ld a,(TURN+3)
    cp ACCEPT_KEY
    ret nz
    ld hl,TURN+1
    ld a,(TURN)
    add a,(hl)
    ld (TURN),a
    ld a,1
    ld (TURN+2),a
    ret

init_turn stores the starting counter, sets the step to one and clears the acceptance result and saved input. Set TURN+1 after that call if you want another step. For each delivered character, put it in A and call accept_command.

Offset Meaning after a call
TURN+0 Counter; accepted input adds the step modulo 256
TURN+1 Step; preserved by the update
TURN+2 1 for accepted, 0 for rejected; replaced on every call
TURN+3 Last delivered character; replaced on every call

Test TURN+2 before another owner reuses the context. A rejected command leaves the counter unchanged. Your game can use the accepted flag to spend a turn or move enemies; the tiny counter is just a visible way to expose that decision.

init_turn changes AF. accept_command changes AF and HL and preserves BC/DE. A direct CALL needs two return-address stack bytes, in addition to the caller and firmware stack use. There is no heap allocation, index-register state or private interrupt handler.

The two routines occupy 45 code bytes, including their returns, plus the four-byte context. Firmware input and output costs are additional. Keep this context exclusive to one caller at a time; the routine is not safe for concurrent foreground and interrupt calls using the same bytes.

Change the rule without losing the loop

Memory ownership, accepted character, initial value and step

settings.inc
TURN equ $8ff0
ACCEPT_KEY equ 71
INITIAL equ 0
STEP equ 1

Change STEP to 2, rebuild and start the generated program again. Two accepted commands now give 00 → 02 → 04. We changed the amount added, not the number of commands consumed. No extra context byte or routine is required.

The customised Amstrad CPC caller after two commands, showing TURN 04.

Change INITIAL to 254 to exercise FE → FF → 00. That wrap is the chosen policy. A score or move count that must exceed 255 needs a wider representation; a limited resource may need saturation instead. Those choices change arithmetic and code cost, so make them explicit rather than quietly relying on overflow.

ACCEPT_KEY can select another delivered character. This is a character reader: firmware autorepeat can supply several characters from one held key. Character arrival is not physical press detection. Use held-key sampling and an edge filter if that is the behaviour you need.

Common mistake: erasing the previous move

The next dispatch is deliberately wrong. Replacing dispatch.inc with it resets the counter before every command. The first accepted command looks fine; the second still shows TURN 01. A rejected character can erase progress too.

Broken dispatch: reset state before applying the command

reset-each-command.inc
dispatch:
    push af
    xor a
    ld (TURN),a
    pop af
    jp accept_command

The faulty Amstrad CPC caller after two accepted commands, still showing TURN 01.

Restore the original dispatch and rebuild. Initialisation stays outside the loop; the same two commands now give 00 → 01 → 02. The repaired caller runs separately from cold firmware in the native checks.

Fixed dispatch: preserve state between commands

dispatch.inc
dispatch:
    jp accept_command

When this reader needs more help

The firmware can keep doing its own work while the caller waits. Your game code cannot rely on getting control back in time for an animation, a countdown or regular sound updates. A timed turn needs an explicit deadline and a non-blocking reader, or an independently owned service with a clear contract.

Choose how your game advances explains that distinction. The BASIC command caller demonstrates the same untimed policy in another language; its interpreter and input conventions are different.

What we checked

Code status: firmware-entered and emulator-executed. Released Emu198x 0.44.0 ran the whole typed loader, then real keyboard input. The native record binds the maintained sources, observer, tool binaries, firmware identities and captures.

Six cold cases cover the default, step two, a nonzero start, byte wrap, the faulty reset and a repaired caller. Checks compare installed executable bytes with the assembly output, verify state stays still during idle and rejected commands, and check adjacent guards. A retained-record checker also rejects missing cases, stale inputs and falsified results. It checks saved evidence; the native verifier executes the machines again.

This qualifies the named emulator and firmware configuration. It does not establish original-hardware timing, all ROM revisions or the budget for a complete game.

Primary reference

Amstrad’s original CPC464/664/6128 Firmware, Key Manager chapter, KM WAIT CHAR, and firmware jumpblocks, TXT OUTPUT.