← Back to Courses
module
25

Every key at once

Why this matters

Hold W and press D. With the key call from S8, one of them does not exist: the call hands back a single byte, so a second held key has nowhere to go. No diagonals. No firing on the move. Every action game you have ever enjoyed is impossible on that interface.

The keyboard itself has no such limit. Every key is a switch at the crossing of an 8 x 8 grid, all sixty-four readable at once - the ROM's key call is a convenience built on top of that grid, and it flattens the grid's whole truth into one byte. This section goes underneath: read the grid, and a chord of keys is as visible as one.

The grid, and the chip that holds it

The keyboard is wired as eight rows of eight columns. Your program selects one row at a time, then reads back a byte in which each key of that row owns one bit. Both directions are active-low: you select row n by sending 255 with bit n removed, and a held key answers with a 0 in its bit. Nothing held reads 255.

The chip holding the grid is a guest star: the AY-3-8910, the Einstein's sound chip, whose two spare I/O ports were wired to the keyboard. (Its day job comes later in the course.) It lives behind two Z80 ports:

  • OUT (2),A - choose one of the AY's registers
  • OUT (3),A - write to the chosen register
  • IN A,(2) - read from the chosen register

Three of its registers matter here. Register 7 is set once, to 127 - that points the row port outward and the column port inward; its other bits belong to the sound side of the chip, which stays untouched until the course gets there. Register 14 takes the row select. Register 15 answers with the columns:

readrow:
        LD A,14         ; the row-select register
        OUT (2),A
        LD A,C          ; the row byte - one bit low
        OUT (3),A
        LD A,15         ; the column register
        OUT (2),A
        IN A,(2)
        RET

Which keys live where is Appendix V - measured, not copied - with the select byte for every row. The two rows a game wants most: row 5 holds Q W E R T Y U, and row 6 holds A S D F G H J. WASD needs just those two reads.

Watch it working

Before wiring it to the rocket, look at the whole grid live. This program takes the screen over, borrows the font, draws the matrix as an 8 x 8 grid of dots, and repaints it forever - '#' wherever a key is down:

        ORG 256

        ; --- blank tile 0 in every bank
        LD A,64
        CALL zero8
        LD A,72
        CALL zero8
        LD A,80
        CALL zero8

        ; --- clear the name table
        LD A,0
        OUT (9),A
        LD A,120
        OUT (9),A
        LD BC,768
clr:    LD A,0
        OUT (8),A
        DEC BC
        LD A,B
        OR C
        JR NZ,clr

        ; --- borrow the font: read glyphs 32-127 once...
        LD A,0
        OUT (9),A
        LD A,25
        OUT (9),A
        LD HL,buffer
        LD BC,768
rd:     IN A,(8)
        LD (HL),A
        INC HL
        DEC BC
        LD A,B
        OR C
        JR NZ,rd

        ; --- ...and write them into bank 0 and bank 1
        LD A,0
        OUT (9),A
        LD A,65         ; bank 0 at 256
        CALL wr768
        LD A,0
        OUT (9),A
        LD A,73         ; bank 1 at 2048+256
        CALL wr768

        ; --- AY: port A output, port B input
        LD A,7
        OUT (2),A
        LD A,127
        OUT (3),A

        ; --- static text
        LD HL,title
        LD DE,38        ; row 1, column 6
        CALL prstr
        LD HL,bits
        LD DE,108       ; row 3, column 12
        CALL prstr

        LD DE,165       ; "ROW n" labels at rows 5-12, column 5
        LD B,8
rlbl:   PUSH BC
        PUSH DE
        LD HL,rowtxt
        CALL prstr
        POP DE
        LD HL,32
        ADD HL,DE
        EX DE,HL
        LD A,(rowdig)
        INC A
        LD (rowdig),A
        POP BC
        DJNZ rlbl

        ; --- forever: scan, then paint
live:   LD C,254
        LD HL,keybuf
        LD E,8
lrow:   LD A,14
        OUT (2),A
        LD A,C
        OUT (3),A
        LD A,15
        OUT (2),A
        IN A,(2)
        LD (HL),A
        INC HL
        RLC C
        DEC E
        JR NZ,lrow

        LD DE,172       ; grid cells start at row 5, column 12
        LD HL,keybuf
        LD B,8
prow:   PUSH BC
        LD A,E
        OUT (9),A
        LD A,D
        ADD A,120
        OUT (9),A
        LD C,(HL)
        LD B,8
pbit:   RLC C           ; bit 7 first: leftmost cell
        LD A,'#'
        JR NC,mark      ; active-low: no carry means pressed
        LD A,'.'
mark:   OUT (8),A
        DJNZ pbit
        INC HL
        PUSH HL
        LD HL,32
        ADD HL,DE
        EX DE,HL
        POP HL
        POP BC
        DJNZ prow
        JR live

; --- write a zero-terminated string at name entry DE
prstr:  LD A,E
        OUT (9),A
        LD A,D
        ADD A,120
        OUT (9),A
ploop:  LD A,(HL)
        OR A
        RET Z
        OUT (8),A
        INC HL
        JR ploop

; --- 768 buffered bytes to the VDP (address already set)
wr768:  OUT (9),A
        LD HL,buffer
        LD BC,768
w7:     LD A,(HL)
        OUT (8),A
        INC HL
        DEC BC
        LD A,B
        OR C
        JR NZ,w7
        RET

zero8:  LD D,A
        LD A,0
        OUT (9),A
        LD A,D
        OUT (9),A
        LD A,0
        LD B,8
z8:     OUT (8),A
        DJNZ z8
        RET

title:  DEFM "THE KEYBOARD MATRIX"
        DEFB 0
bits:   DEFM "76543210"
        DEFB 0
rowtxt: DEFM "ROW "
rowdig: DEFB '0'
        DEFB 0
keybuf: DEFS 8
buffer: DEFS 768

What you should see

The grid, all dots. Now play: every key you press lights its own cell the instant it goes down and darkens when it comes up. Hold W - row 5 lights under the 5 column. Add A and D - two cells in row 6, at the same time, while W stays lit. Add space. Four keys, four cells, one picture: that is the fact the whole section stands on, and no single-byte key call could draw it.

The keyboard matrix grid with W, A, D and space all lit at once

Type your name across it. Find the arrow keys (rows 1 to 3). Notice row 5 light left-to-right as you run a finger along QWERTYU - the grid is the physical keyboard, wired.

Wiring it to the rocket

The game loop's input routine changes from asking-for-one-key to checking every key it cares about. BIT n,A tests one bit; CALL Z acts when it is low. Three of WASD share row 6, so one read serves all three - park it in a register and test it three times:

input:  LD C,223        ; row 5: W lives at bit 5
        CALL readrow
        BIT 5,A
        CALL Z,up       ; active-low: zero means pressed

        LD C,191        ; row 6: A, S and D live here together
        CALL readrow
        LD D,A          ; keep the row - three tests want it
        BIT 6,D
        CALL Z,left
        BIT 5,D
        CALL Z,down
        BIT 4,D
        CALL Z,right
        RET

Every held key gets its CALL. W and D down together means up and right run in the same pass - and that is a diagonal, born.

The code

        ORG 256

        ; --- three patterns: body, flame A, flame B
        LD A,0
        OUT (9),A
        LD A,88
        OUT (9),A
        LD HL,rocket
        LD B,24
pat:    LD A,(HL)
        OUT (8),A
        INC HL
        DJNZ pat

        ; --- close the sprite list after our two
        LD A,8
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,208
        OUT (8),A

        ; --- AY: port A output, port B input
        LD A,7
        OUT (2),A
        LD A,127
        OUT (3),A

main:   CALL place
        CALL pause
        CALL input
        CALL animate
        JR main

; --- the matrix replaces call 181: act on every key that is down
input:  LD C,223        ; row 5: W lives at bit 5
        CALL readrow
        BIT 5,A
        CALL Z,up       ; active-low: zero means pressed

        LD C,191        ; row 6: A, S and D live here together
        CALL readrow
        LD D,A          ; keep the row - three tests want it
        BIT 6,D
        CALL Z,left
        BIT 5,D
        CALL Z,down
        BIT 4,D
        CALL Z,right
        RET

; --- select the row in C, return its columns in A
readrow:
        LD A,14
        OUT (2),A
        LD A,C
        OUT (3),A
        LD A,15
        OUT (2),A
        IN A,(2)
        RET

place:  LD A,0
        OUT (9),A
        LD A,123
        OUT (9),A
        LD A,(rockY)
        OUT (8),A
        LD A,(rockX)
        OUT (8),A
        LD A,0
        OUT (8),A
        LD A,15
        OUT (8),A
        LD A,(rockY)
        ADD A,8
        OUT (8),A
        LD A,(rockX)
        OUT (8),A
        LD A,(flameP)
        OUT (8),A
        LD A,11
        OUT (8),A
        RET

animate:
        LD A,(flick)
        DEC A
        LD (flick),A
        RET NZ
        LD A,8
        LD (flick),A
        LD A,(flameP)
        XOR 3
        LD (flameP),A
        RET

up:     LD A,(rockY)
        OR A
        RET Z
        DEC A
        LD (rockY),A
        RET

down:   LD A,(rockY)
        CP 175
        RET Z
        INC A
        LD (rockY),A
        RET

left:   LD A,(rockX)
        OR A
        RET Z
        DEC A
        LD (rockX),A
        RET

right:  LD A,(rockX)
        CP 248
        RET Z
        INC A
        LD (rockX),A
        RET

pause:  LD BC,3000
dloop:  DEC BC
        LD A,B
        OR C
        JR NZ,dloop
        RET

rockX:  DEFB 124
rockY:  DEFB 88
flameP: DEFB 1
flick:  DEFB 8

rocket: ; body
        DEFB 00011000b
        DEFB 00111100b
        DEFB 00111100b
        DEFB 01111110b
        DEFB 01111110b
        DEFB 11111111b
        DEFB 11011011b
        DEFB 10011001b
        ; flame frame A
        DEFB 01011010b
        DEFB 01111110b
        DEFB 01111110b
        DEFB 00111100b
        DEFB 00111100b
        DEFB 00011000b
        DEFB 00010000b
        DEFB 00001000b
        ; flame frame B
        DEFB 00111100b
        DEFB 01111110b
        DEFB 00111100b
        DEFB 00111100b
        DEFB 00011000b
        DEFB 00000000b
        DEFB 00000000b
        DEFB 00000000b

What you should see

The rocket as before - but fly it properly now. Hold W and D and it climbs at forty-five degrees. Roll a finger from A to D without lifting and it banks. It stops exactly at the edges, flame burning all the while. The program is S22's with one organ transplanted: input reads the grid instead of asking the ROM, and everything else - the loop, the clamps, the clock - carries over untouched.

Change one thing

  • Hold W and S together. The rocket sits still - but is it resting, or running? Explain what both CALLs are doing to rockY each pass.
  • Swap the CALL Z,up for CALL NZ,up. Fly. What does the rocket do when you touch nothing, and why exactly that?
  • Add ESC from the appendix - row 0, bit 7 - and make it JP to a bare JP $. You have written "pause".
  • In the X-ray program, change the scan to select TWO rows at once - 159, say, which lowers bits 5 and 6 together. What does the grid show for one held key now, and what does that tell you about why rows are selected one at a time?

When it goes wrong

Symptom Cause
A key's bit seems to be in the wrong column A map with its columns numbered backwards - other references print this table reversed. Appendix V numbers by BIT; when in doubt, hold the key and read what the scan says.
Everything reads as pressed, or nothing ever does The sense is inverted somewhere: selects are 255-minus-a-bit, and pressed is ZERO. BIT then Z for held.
SHIFT or CTRL never registers They are not in the matrix. Use ordinary keys for game controls.

Summary

  • The keyboard is an 8 x 8 grid behind the sound chip's I/O ports: select a row through AY register 14 (port 2 to choose, port 3 to write), read its columns from register 15 (IN A,(2)). Register 7 = 127, once.
  • Active-low both ways: one 0 bit selects the row; a held key answers 0.
  • The grid shows every held key at once - chords, diagonals, fire-on-the-move. The ROM's key call flattens all that into one byte.
  • Game input becomes BIT tests and CALL Zs: act on everything down this pass. Three keys on one row cost one read.
  • The map is Appendix V. Row 5 is QWERTYU; row 6 is ASDFGHJ; the arrows are ordinary keys; SHIFT and CTRL are not in the grid.

Next

S26. The heartbeat. The rocket answers your fingers every pass now - but the pass itself is still paced by a guess in a pause loop. The screen has a real clock, a flag that rises once per frame, and locking the loop to it is the difference between something that moves and something that moves smoothly.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.