Reading the Keys
Fill in the loop's empty INPUT stage. Read the half-row holding O and P through port $DFFE with IN A,(C), test the active-low key bits, and recolour the lamplighter the instant you press — the machine feeling you, fifty times a second.
The heartbeat beats, but the lamplighter can't feel you. The loop's INPUT stage is still empty — each frame it reads nothing, decides nothing. This unit fills it in, one key at a time. By the end, the figure changes colour the instant you touch a key: red while you hold O, green while you hold P, pale white when you hold nothing.
He won't move yet — that's the next unit — but he will react, fifty times a second. This is the moment the machine starts listening.
Where we start
Unit 3's heartbeat, looping at 50 Hz with an empty INPUT stage — the figure stands white and unfeeling. We give the loop something to read.
One port, eight half-rows
The Spectrum reads its whole keyboard through a single port: $FE. Forty keys would never fit in one byte, so they're wired into eight half-rows of five keys each, and you choose which half-row to read using the high byte of the port address. The low byte is always $FE; the high byte selects the row.
The half-row we want holds O and P — the classic left/right keys — and it's selected by high byte $DF. So the port we read is $DFFE, and its bottom five bits are:
port $DFFE, bottom five bits:
bit 4 3 2 1 0
Y U I O P
P is bit 0, O is bit 1. (The other three — I, U, Y — are along for the ride; we ignore them today.)
Active low: a held key reads zero
Here's the catch that trips everyone once. The bits are active low. A key's bit reads 1 when the key is up, and 0 when it's held down. It's backwards from what you'd guess — but it's how the hardware works: pressing a key pulls its line down to zero. So "pressed" is the absence of the usual 1, and to find a held key we test for a zero bit.
Milestone 1 — read one key
Start with just P. Read the port with IN A,(C) — where BC holds the full address $DFFE, so the row-select rides on the bus — then test P's bit with BIT 0,A. BIT n,A sets the Z flag when bit n is zero, and zero means held — so jr nz is "key is up, skip." If P is down we colour the figure green; otherwise he stays white. All of it goes in the loop's INPUT stage, so it runs every single frame.
| 1 | 1 | ; Gloaming — Unit 4: Reading the Keys | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 is Unit 3's heartbeat — the INPUT stage still empty. | |
| 3 | + | ; step-01 reads one key — P — and recolours the figure green while it's held. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground | |
| 8 | 8 | WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone | |
| 9 | - | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black (0), INK white (7) — the figure | |
| 9 | + | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure at rest | |
| 10 | + | RIGHT_ATTR equ %01000100 ; BRIGHT, INK green (4) — holding P | |
| 10 | 11 | | |
| 11 | - | ; --- where the lamplighter stands: a cell named by (column 0-31, row 0-23) --- | |
| 12 | 12 | LAMP_COL equ 15 | |
| 13 | 13 | LAMP_ROW equ 11 | |
| 14 | - | | |
| 15 | - | ; The screen splits top/middle/bottom into THIRDS of 8 character rows. The top | |
| 16 | - | ; pixel-row of a cell lives at $4000 + third*$0800 + (row-within-third)*32 + col. | |
| 17 | 14 | THIRD equ LAMP_ROW / 8 | |
| 18 | 15 | CHARROW equ LAMP_ROW - THIRD * 8 | |
| 19 | 16 | LAMP_SCR equ $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL | |
| 20 | - | | |
| 21 | - | ; The attribute cell for the same (col,row) is the simpler linear address. | |
| 22 | 17 | LAMP_ATTR_ADDR equ $5800 + LAMP_ROW * 32 + LAMP_COL | |
| 18 | + | | |
| 19 | + | ; --- the keyboard half-row holding O and P --- | |
| 20 | + | KEYS_OP equ $DFFE ; high byte $DF selects this half-row | |
| 23 | 21 | | |
| 24 | 22 | start: | |
| 25 | 23 | ; --- the border goes black — the night beyond the square --- | |
| ... | |||
| 62 | 60 | add hl, de | |
| 63 | 61 | djnz .sides | |
| 64 | 62 | | |
| 65 | - | ; --- give the figure's cell a warm colour so its pixels read --- | |
| 63 | + | ; --- draw the lamplighter (colour, then shape) --- | |
| 66 | 64 | ld hl, LAMP_ATTR_ADDR | |
| 67 | 65 | ld (hl), LAMP_ATTR | |
| 68 | 66 | | |
| 69 | - | ; --- draw his eight-byte shape down the eight rows of the cell --- | |
| 70 | - | ; HL walks the screen rows (INC H = down one row, +256). | |
| 71 | - | ; DE walks the sprite bytes (INC DE = next row of the shape). | |
| 72 | - | ld hl, LAMP_SCR ; top pixel-row of his cell | |
| 73 | - | ld de, lamplighter ; his shape, eight bytes | |
| 74 | - | ld b, 8 ; eight rows | |
| 67 | + | ld hl, LAMP_SCR | |
| 68 | + | ld de, lamplighter | |
| 69 | + | ld b, 8 | |
| 75 | 70 | .draw: | |
| 76 | - | ld a, (de) ; one row of the shape | |
| 77 | - | ld (hl), a ; into the screen | |
| 78 | - | inc de ; next shape byte | |
| 79 | - | inc h ; next screen row down (+256) | |
| 71 | + | ld a, (de) | |
| 72 | + | ld (hl), a | |
| 73 | + | inc de | |
| 74 | + | inc h | |
| 80 | 75 | djnz .draw | |
| 81 | 76 | | |
| 82 | 77 | ; --- start the 50 Hz heartbeat --- | |
| 83 | - | ; IM 1 selects the ROM's interrupt handler (fired once per screen, | |
| 84 | - | ; 50 times a second); EI lets the taps through. From here the loop | |
| 85 | - | ; can never run faster than one pass per frame. | |
| 86 | 78 | im 1 | |
| 87 | 79 | ei | |
| 88 | 80 | | |
| 89 | 81 | game_loop: | |
| 90 | - | halt ; sleep here until the next frame interrupt | |
| 82 | + | halt ; wait for the next frame | |
| 91 | 83 | | |
| 92 | - | ; --- INPUT --- read the keys. (Unit 4 fills this in.) | |
| 93 | - | ; --- UPDATE --- move the world on. (Unit 5 fills this in.) | |
| 94 | - | ; --- DRAW --- redraw what changed. (nothing moves yet.) | |
| 84 | + | ; --- INPUT: read P and recolour the figure while it's held --- | |
| 85 | + | ld bc, KEYS_OP ; BC = $DFFE — the address IS the question | |
| 86 | + | in a, (c) ; bottom 5 bits = keys, 0 = held (active low) | |
| 87 | + | ld d, LAMP_ATTR ; assume nothing held → white at rest | |
| 88 | + | bit 0, a ; P (right)? Z set = bit is 0 = held | |
| 89 | + | jr nz, .not_right | |
| 90 | + | ld d, RIGHT_ATTR ; green | |
| 91 | + | .not_right: | |
| 92 | + | ld a, d | |
| 93 | + | ld (LAMP_ATTR_ADDR), a ; one attribute write — his cell recolours | |
| 95 | 94 | | |
| 96 | - | jr game_loop ; round again — one pass per frame, forever | |
| 95 | + | jr game_loop | |
| 97 | 96 | | |
| 98 | - | ; The lamplighter's shape — eight bytes, one per pixel row. A 1 bit is a lit | |
| 99 | - | ; pixel (drawn in the cell's INK); a 0 bit shows the PAPER behind. Read the | |
| 100 | - | ; bytes top-down and the little figure stands up. | |
| 97 | + | ; The lamplighter's shape — eight bytes, one per pixel row (from Unit 2). | |
| 101 | 98 | lamplighter: | |
| 102 | 99 | defb %00111100 ; ..XXXX.. head | |
| 103 | 100 | defb %00111100 ; ..XXXX.. head |
The complete step 1 program
; Gloaming — Unit 4: Reading the Keys
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 reads one key — P — and recolours the figure green while it's held.
org 32768
COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure at rest
RIGHT_ATTR equ %01000100 ; BRIGHT, INK green (4) — holding P
LAMP_COL equ 15
LAMP_ROW equ 11
THIRD equ LAMP_ROW / 8
CHARROW equ LAMP_ROW - THIRD * 8
LAMP_SCR equ $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL
LAMP_ATTR_ADDR equ $5800 + LAMP_ROW * 32 + LAMP_COL
; --- the keyboard half-row holding O and P ---
KEYS_OP equ $DFFE ; high byte $DF selects this half-row
start:
; --- the border goes black — the night beyond the square ---
ld a, 0
out ($FE), a
; --- wash the whole grid in cobbles ---
ld hl, $5800
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
; --- top and bottom walls ---
ld hl, $5800
ld b, 32
.top:
ld (hl), WALL
inc hl
djnz .top
ld hl, $5AE0
ld b, 32
.bottom:
ld (hl), WALL
inc hl
djnz .bottom
; --- left and right walls ---
ld hl, $5800
ld b, 24
.sides:
ld (hl), WALL
push hl
ld de, 31
add hl, de
ld (hl), WALL
pop hl
ld de, 32
add hl, de
djnz .sides
; --- draw the lamplighter (colour, then shape) ---
ld hl, LAMP_ATTR_ADDR
ld (hl), LAMP_ATTR
ld hl, LAMP_SCR
ld de, lamplighter
ld b, 8
.draw:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .draw
; --- start the 50 Hz heartbeat ---
im 1
ei
game_loop:
halt ; wait for the next frame
; --- INPUT: read P and recolour the figure while it's held ---
ld bc, KEYS_OP ; BC = $DFFE — the address IS the question
in a, (c) ; bottom 5 bits = keys, 0 = held (active low)
ld d, LAMP_ATTR ; assume nothing held → white at rest
bit 0, a ; P (right)? Z set = bit is 0 = held
jr nz, .not_right
ld d, RIGHT_ATTR ; green
.not_right:
ld a, d
ld (LAMP_ATTR_ADDR), a ; one attribute write — his cell recolours
jr game_loop
; The lamplighter's shape — eight bytes, one per pixel row (from Unit 2).
lamplighter:
defb %00111100 ; ..XXXX.. head
defb %00111100 ; ..XXXX.. head
defb %00011000 ; ...XX... neck
defb %01111110 ; .XXXXXX. arms
defb %00011000 ; ...XX... body
defb %00011000 ; ...XX... body
defb %00100100 ; ..X..X.. legs
defb %01000010 ; .X....X. feet
end start
Hold nothing and he's white; hold P and he turns green — and the change feels instant because it is, happening fifty times a second:
The colour is just a flag we raise to prove the read worked — every frame, the program asks the keyboard a question and acts on the answer.
Milestone 2 — add the other key
The single in a, (c) already read all five keys of the half-row — we only looked at P. Add a test for O (bit 1) above the P test, colouring the figure red when it's held. Nothing about the read changes; we just check one more bit.
| 1 | 1 | ; Gloaming — Unit 4: Reading the Keys | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-01 reads one key — P — and recolours the figure green while it's held. | |
| 3 | + | ; step-02 adds the second key — O — red while held; the full O/P read. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 8 | 8 | WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone | |
| 9 | 9 | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure at rest | |
| 10 | 10 | RIGHT_ATTR equ %01000100 ; BRIGHT, INK green (4) — holding P | |
| 11 | + | LEFT_ATTR equ %01000010 ; BRIGHT, INK red (2) — holding O | |
| 11 | 12 | | |
| 12 | 13 | LAMP_COL equ 15 | |
| 13 | 14 | LAMP_ROW equ 11 | |
| ... | |||
| 81 | 82 | game_loop: | |
| 82 | 83 | halt ; wait for the next frame | |
| 83 | 84 | | |
| 84 | - | ; --- INPUT: read P and recolour the figure while it's held --- | |
| 85 | + | ; --- INPUT: read O and P and recolour to show what's held --- | |
| 85 | 86 | ld bc, KEYS_OP ; BC = $DFFE — the address IS the question | |
| 86 | 87 | in a, (c) ; bottom 5 bits = keys, 0 = held (active low) | |
| 87 | 88 | ld d, LAMP_ATTR ; assume nothing held → white at rest | |
| 88 | - | bit 0, a ; P (right)? Z set = bit is 0 = held | |
| 89 | + | bit 1, a ; O (left)? Z set = bit is 0 = held | |
| 90 | + | jr nz, .not_left | |
| 91 | + | ld d, LEFT_ATTR ; red | |
| 92 | + | .not_left: | |
| 93 | + | bit 0, a ; P (right)? | |
| 89 | 94 | jr nz, .not_right | |
| 90 | 95 | ld d, RIGHT_ATTR ; green | |
| 91 | 96 | .not_right: |
The complete program
; Gloaming — Unit 4: Reading the Keys
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 adds the second key — O — red while held; the full O/P read.
org 32768
COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure at rest
RIGHT_ATTR equ %01000100 ; BRIGHT, INK green (4) — holding P
LEFT_ATTR equ %01000010 ; BRIGHT, INK red (2) — holding O
LAMP_COL equ 15
LAMP_ROW equ 11
THIRD equ LAMP_ROW / 8
CHARROW equ LAMP_ROW - THIRD * 8
LAMP_SCR equ $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL
LAMP_ATTR_ADDR equ $5800 + LAMP_ROW * 32 + LAMP_COL
; --- the keyboard half-row holding O and P ---
KEYS_OP equ $DFFE ; high byte $DF selects this half-row
start:
; --- the border goes black — the night beyond the square ---
ld a, 0
out ($FE), a
; --- wash the whole grid in cobbles ---
ld hl, $5800
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
; --- top and bottom walls ---
ld hl, $5800
ld b, 32
.top:
ld (hl), WALL
inc hl
djnz .top
ld hl, $5AE0
ld b, 32
.bottom:
ld (hl), WALL
inc hl
djnz .bottom
; --- left and right walls ---
ld hl, $5800
ld b, 24
.sides:
ld (hl), WALL
push hl
ld de, 31
add hl, de
ld (hl), WALL
pop hl
ld de, 32
add hl, de
djnz .sides
; --- draw the lamplighter (colour, then shape) ---
ld hl, LAMP_ATTR_ADDR
ld (hl), LAMP_ATTR
ld hl, LAMP_SCR
ld de, lamplighter
ld b, 8
.draw:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .draw
; --- start the 50 Hz heartbeat ---
im 1
ei
game_loop:
halt ; wait for the next frame
; --- INPUT: read O and P and recolour to show what's held ---
ld bc, KEYS_OP ; BC = $DFFE — the address IS the question
in a, (c) ; bottom 5 bits = keys, 0 = held (active low)
ld d, LAMP_ATTR ; assume nothing held → white at rest
bit 1, a ; O (left)? Z set = bit is 0 = held
jr nz, .not_left
ld d, LEFT_ATTR ; red
.not_left:
bit 0, a ; P (right)?
jr nz, .not_right
ld d, RIGHT_ATTR ; green
.not_right:
ld a, d
ld (LAMP_ATTR_ADDR), a ; one attribute write — his cell recolours
jr game_loop
; The lamplighter's shape — eight bytes, one per pixel row (from Unit 2).
lamplighter:
defb %00111100 ; ..XXXX.. head
defb %00111100 ; ..XXXX.. head
defb %00011000 ; ...XX... neck
defb %01111110 ; .XXXXXX. arms
defb %00011000 ; ...XX... body
defb %00011000 ; ...XX... body
defb %00100100 ; ..X..X.. legs
defb %01000010 ; .X....X. feet
end start
Now O paints him red and P paints him green — two keys answered from one read:
There's a detail hiding in the order. We test O first, then P, and the last match wins — so holding both shows green. Swap the two blocks and O would win instead. When two inputs can be true at once, the order you test them in decides; you choose it on purpose.
When it's wrong, see why
The read fails in ways that point at which half of it slipped:
- The figure shows a "pressed" colour even when you hold nothing. The active-low test is backwards — you're treating bit-set as pressed. A held key reads 0: use
jr zfor "down",jr nzfor "up", and reset to white at the top of each frame. - Nothing responds at all. Wrong port, or only
Cloaded. O and P need$DFFE, andIN A,(C)drives the whole of BC onto the bus — so loadbc, $DFFE, not justc. - The wrong keys respond. Wrong half-row. Map the key you want to its half-row and use that high byte.
- The colour flickers or sticks. You didn't reset the default (
ld d, LAMP_ATTR) at the top of the INPUT stage, so last frame's colour leaks into this one. Reset it every frame before testing.
Before and after
You started with a loop that read nothing and finished with one that feels the keyboard every frame — built up one key at a time, so a wrong colour told you exactly which test to check. The recolour is throwaway (Unit 5 drops it), but the read underneath — select a half-row, IN A,(C), test an active-low bit — is the exact mechanism that will steer the lamplighter for the rest of the game.
Try this: hold both at once
Hold O and P together. The figure goes green — P wins, because we test O first and P overwrites it. Swap the two bit blocks and now O wins. Two inputs true at once, and order decides.
Try this: a third key from the same row
You already read all five keys of the half-row. Add a test for I (bit 2) and give it a third colour, say bright cyan (%01000101):
bit 2, a
jr nz, .not_i
ld d, %01000101
.not_i:
One read, three keys answered. That's the half-row paying off.
Try this: a different half-row
Pick a key on another row — say Q (in the half-row at high byte $FB). Read $FBFE into BC and test Q's bit. Each half-row is one IN; the high byte is the only thing that changes. (This is exactly how we'll add up/down movement later.)
What you've learnt
- The whole keyboard is read through port
$FE; the high byte of the address picks one of eight half-rows of five keys. - Read a half-row with
IN A,(C), where BC is the full port (e.g.$DFFEfor O and P). - Keys are active low: a bit is 0 while held. Test for zero with
BIT— Z set means pressed. - The INPUT stage runs every frame, so the machine feels the keys continuously — and one read serves every key in the row.
What's next
Now the machine knows which way you're pointing. In Unit 5 it finally acts on that: erase the lamplighter from his cell and redraw him one cell over in the direction you held. That's his first step — and the start of Gloaming's second big technique, the cell sprite: a figure that moves, saves what's beneath it, and lives by the rules of the grid.