Skip to content
Game 1 Unit 4 of 20 1 hr learning time

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.

20% of Gloaming

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.

Step 1: read P through $DFFE and recolour while it's held
+27-30
11 ; Gloaming — Unit 4: Reading the Keys
22 ; 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.
44
55 org 32768
66
77 COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
88 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
1011
11-; --- where the lamplighter stands: a cell named by (column 0-31, row 0-23) ---
1212 LAMP_COL equ 15
1313 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.
1714 THIRD equ LAMP_ROW / 8
1815 CHARROW equ LAMP_ROW - THIRD * 8
1916 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.
2217 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
2321
2422 start:
2523 ; --- the border goes black — the night beyond the square ---
...
6260 add hl, de
6361 djnz .sides
6462
65- ; --- give the figure's cell a warm colour so its pixels read ---
63+ ; --- draw the lamplighter (colour, then shape) ---
6664 ld hl, LAMP_ATTR_ADDR
6765 ld (hl), LAMP_ATTR
6866
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
7570 .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
8075 djnz .draw
8176
8277 ; --- 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.
8678 im 1
8779 ei
8880
8981 game_loop:
90- halt ; sleep here until the next frame interrupt
82+ halt ; wait for the next frame
9183
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
9594
96- jr game_loop ; round again — one pass per frame, forever
95+ jr game_loop
9796
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).
10198 lamplighter:
10299 defb %00111100 ; ..XXXX.. head
103100 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 lamplighter in the centre of the square, pale white — no key held.
At rest: nothing held, the figure stays white.
The lamplighter in the centre of the square, now green — captured while P is held.
Holding P: the loop reads bit 0 as zero, sees the key down, and recolours his cell green this frame.

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.

Step 2: test O as well, red while it's held
+8-3
11 ; Gloaming — Unit 4: Reading the Keys
22 ; 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.
44
55 org 32768
66
...
88 WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
99 LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure at rest
1010 RIGHT_ATTR equ %01000100 ; BRIGHT, INK green (4) — holding P
11+LEFT_ATTR equ %01000010 ; BRIGHT, INK red (2) — holding O
1112
1213 LAMP_COL equ 15
1314 LAMP_ROW equ 11
...
8182 game_loop:
8283 halt ; wait for the next frame
8384
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 ---
8586 ld bc, KEYS_OP ; BC = $DFFE — the address IS the question
8687 in a, (c) ; bottom 5 bits = keys, 0 = held (active low)
8788 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)?
8994 jr nz, .not_right
9095 ld d, RIGHT_ATTR ; green
9196 .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:

The lamplighter in the centre of the square, red — captured while O is held.
Holding O: bit 1 reads zero, and his cell recolours red.
The lamplighter in the centre of the square, green — captured while P is held.
Holding P: bit 0 reads zero, and the same loop recolours him green — two keys, 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 z for "down", jr nz for "up", and reset to white at the top of each frame.
  • Nothing responds at all. Wrong port, or only C loaded. O and P need $DFFE, and IN A,(C) drives the whole of BC onto the bus — so load bc, $DFFE, not just c.
  • 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. $DFFE for O and P).
  • Keys are active low: a bit is 0 while held. Test for zero with BITZ 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.