Your drawing routine can put a character at a chosen address. Now give that choice to the player. O moves left; P moves right. Each press moves one character cell. Release both keys before the next press.
This is deliberately one step at a time. We can inspect each decision before adding movement that repeats while a key is held.
Try a move, then inspect it
Assemble and run. Focus the Spectrum screen to use your keyboard, or use the buttons below it. The magnified cells and address table read the running machine’s RAM. They show what your source actually did, including mistakes.
Source · view and edit
Edit the source, then assemble.
Edits are saved in this browser. Keep a downloaded copy of work you want to keep.
Focus the Spectrum screen and press O or P, or hold a button below. Release before pressing again. These controls send keys to the Spectrum.
Assemble and run to inspect your character.
Position is a byte in RAM
- Column (0–31)
- —
- O/P bits
- —
- Ready for a press
- —
No keyboard reading yet.
Make a move to compare its old and new cells.
Inspect all eight addresses
| Row | Old address | Bits now | New address | Bits now |
|---|---|---|---|---|
| 1 | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5 | ||||
| 6 | ||||
| 7 | ||||
| 8 |
This is sampled live memory, not an instruction replay. The old cell is read after the move; its zero bytes show the erase. An edit that skips erasing will leave its picture here.
Try these experiments in order:
- Press O once. The position should change from 15 to 14. Hold it down: does another move happen? Release and press again.
- Hold the Both button. Neither direction should win. Release both before trying another move. If you press physical keys slightly apart, the first may already have caused its move before the second arrives.
- Reach column 0, then try moving left again. Reach 31 and try moving right. The character should stay on the screen.
- In the erase block after
move:, changeld hl,blanktold hl,patterns. Leave the clearing loop alone. Assemble again and move. Predict what remains in the old cell, then check its eight bytes. Restoreblankto repair it.
Read two keyboard bits
The Spectrum keyboard is a set of electrical switches. Reading a port returns bits describing which switches are closed. We use:
ld bc,$DFFE
in a,(c)
and 3BC holds the full port address; in a,(c) reads that port into A. $DFFE selects the half-row containing P, O, I, U and Y. Bit 0 represents P and bit 1 represents O. A pressed key reads 0, a released key 1: these are active-low inputs.
and 3 keeps only those two bits. The other keys and input signals cannot affect this decision.
| O/P bits after AND | Decimal | Meaning |
|---|---|---|
11 |
3 | Neither pressed |
01 |
1 | O pressed: left |
10 |
2 | P pressed: right |
00 |
0 | Both pressed: no move |
The program stores this masked reading in keys, so the inspector can show it. These are current switch states, not “key pressed” messages supplied by the machine.
Turn a held state into one press
armed is another byte in RAM. Reading 11 sets it to 1: we are ready for a fresh press. Any non-released reading consumes that permission and sets it to 0. Until both keys are released, the program keeps reading but does not move again.
This also makes an edge attempt or a both-keys attempt consume a press. Releasing only one of two held keys does not sneak in a move.
The loop runs continuously. Its speed does not choose the movement speed; the release-and-press rule chooses when a move happens. A later experiment will use a clock for held-key repetition.
Compare before changing the position
position: defb 15 reserves a byte with an initial value. Unlike the pattern table, this byte changes while the program runs. Parentheses distinguish reading it from naming its address: ld a,(position) loads its current contents.
For a left move, the program tests for zero before decrementing. For a right move, it compares with 31 before incrementing. The legal columns are 0–31, not 1–32.
cp 31 compares A with 31 by setting flags; it does not change A. jr z,poll branches when the comparison was equal. or a leaves A unchanged and sets the zero flag when A is zero. That gives the left-edge check the same kind of conditional branch.
A byte can hold 0–255. Without the left-edge check, decrementing zero would wrap to 255. That is a valid byte but an invalid column for this routine.
Erase, update, redraw
An accepted move saves its destination in next_position, then performs three jobs:
- Draw the eight zero bytes in
blankat the old position. - Copy
next_positionintoposition. - Draw
patternsat the new position.
Both erase and draw use draw_character. The picture changes because HL points to different data. Erasing works here because our background is plain black; a detailed background would need to be restored instead.
draw_at is a small wrapper around that routine. A supplies the column. ld e,a and ld d,$40 make DE equal to $4000 + column; HL selects the artwork. After drawing, one RET returns to draw_at, and its RET returns to the main program. These nested calls each keep their own return address on the stack.
The old position is also kept in previous. move_keys remembers the accepted input, and moves counts completed drawings, wrapping from 255 to 0. Those bytes make the last move inspectable even after you release the key. The display is sampled live memory, so it does not claim to replay every instruction between two readings.
Why save the next position before calling the drawing routine?
Show the explanation
The drawing routine changes A, B, HL and DE. A is not a safe place to leave a value we need after that call. Keeping the destination in RAM lets us reload it afterwards, following the routine’s contract.
Own a small part of the display
At startup we clear all 32 cells in the first character row by drawing blank repeatedly, then set their attributes to bright white on black. Other screen rows are outside this experiment. Our $100 step between bitmap rows still works only within this first character row; vertical movement needs a different address calculation.
The program begins with di, which disables maskable interrupts. It reads the keyboard directly and does not use the ROM’s periodic keyboard service. The ROM loader has already provided RAM and a valid stack. This program never returns to BASIC: reset the machine to leave it. Do not add HALT while interrupts remain disabled; the clock lesson will establish the interrupt environment it needs.
The complete starting program
; O left, P right. Release both before each new move.
org 32768
start:
di
xor a
out ($FE),a
; Own the first character row: clear its 32 cells.
ld e,0
clear_row:
ld d,$40
ld hl,blank
call draw_character
inc e
ld a,e
cp 32
jr nz,clear_row
ld hl,$5800
ld b,32
ld a,71
colours:
ld (hl),a
inc hl
djnz colours
ld a,(position)
call draw_at
poll:
ld bc,$DFFE
in a,(c)
and 3
ld (keys),a
cp 3
jr z,released
ld a,(armed)
or a
jr z,poll
xor a
ld (armed),a
ld a,(keys)
cp 1
jr z,left
cp 2
jr z,right
jr poll
released:
ld a,1
ld (armed),a
jr poll
left:
ld a,(position)
or a
jr z,poll
dec a
jr move
right:
ld a,(position)
cp 31
jr z,poll
inc a
move:
ld (next_position),a
ld a,(keys)
ld (move_keys),a
ld a,(position)
ld (previous),a
; Erase the old picture before changing its position.
ld e,a
ld d,$40
ld hl,blank
call draw_character
ld a,(next_position)
ld (position),a
call draw_at
ld a,(moves)
inc a
ld (moves),a
jr poll
; In: A is a column 0..31. Changes A, B, HL, DE and flags.
draw_at:
ld e,a
ld d,$40
ld hl,patterns
call draw_character
ret
; In: HL points to 8 bytes; DE is $4000..$401F.
; Out: HL += 8, D += 8, E unchanged, B = 0, A = last byte.
draw_character:
ld b,8
row:
ld a,(hl)
ld (de),a
inc hl
inc d
djnz row
ret
position: defb 15
next_position: defb 15
previous: defb 15
keys: defb 3
armed: defb 0
move_keys: defb 3
moves: defb 0
blank: defb 0,0,0,0,0,0,0,0
patterns:
defb %00011000
defb %00111100
defb %01111110
defb %11011011
defb %11111111
defb %00111100
defb %01100110
defb %01000010
end startDownload your source saves the current editor contents as move-character.asm. Build a tape with either assembler:
asm198x --dialect pasmo --cpu z80 --tapbas move-character.asm -o move-character.tappasmo --tapbas move-character.asm move-character.tapLoad it on a fresh 48K Spectrum as in lesson 1. Check a single press, a held key, both keys and both edges. A picture that moves is only part of the result: explain the input decision, the changed position byte and the erased screen addresses too.
Sources
Sinclair Research, ZX Spectrum BASIC Programming, chapter 23, keyboard half-rows and active-low bits; chapter 24, bitmap and attribute memory. Zilog, Z80 CPU User Manual, UM008011-0816, instruction entries “IN r, (C)”, “AND n”, “OR r”, “CP n”, “JR cc, e” and “DI”.
Run it here
This unit's program, built for you.
- Program
- move-character.tap
- Machine
- ZX Spectrum 48K (PAL)