Keyboard and Joystick Control
Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.
One Key at a Time
Providing that you haven’t disabled or otherwise meddled with the Spectrum’s default interrupt mode the ROM will automatically read the keyboard and update several system variables located at memory location 23552 fifty times per second. The simplest way to check for a keypress is to first load address 23560 with a null value, then interrogate this location until it changes, the result being the ASCII value of the key pressed. This is most useful for those “press any key to continue” situations, for choosing items from a menu and for keyboard input such as high score name entry routines. Such a routine might look like this:
ld hl,23560 ; LAST K system variable. ld (hl),0 ; put null value there. loop ld a,(hl) ; new value of LAST K. cp 0 ; is it still zero? jr z,loop ; yes, so no key pressed. ret ; key was pressed.
Single key-presses are seldom any use for fast action arcade games however, for this we need to detect more than one simultaneous key-press and this is where things get a little trickier. Instead of reading memory addresses we have to read one of eight ports, each of which corresponds to a row of five keys. Of course, most Spectrum models appear to have far more keys than this so where did they all go? Well actually, they don’t. The original Spectrum keyboard layout consisted of just forty keys, arranged in eight groupings or rows of five. In order to access some of the functions it was necessary to press certain combinations of keys together – for example to delete the combination required was CAPS SHIFT and 0 together. Sinclair added these extra keys when the Spectrum Plus came onto the scene in 1985, and they work by simulating the combinations of key-presses required for the original rubber keyed models.
The original keyboard layout was separated into these groupings:
Port Keys 32766 B, N, M, Symbol Shift, Space 49150 H, J, K, L, Enter 57342 Y, U, I, O, P 61438 6, 7, 8, 9, 0 63486 5, 4, 3, 2, 1 64510 T, R, E, W, Q 65022 G, F, D, S, A 65278 V, C, X, Z, Caps Shift
To discover which keys are being pressed we read the appropriate port number, each key in the row being allocated one of the lower five bits d0-d4 (values 1,2,4,8 and 16) where d0 represents the outside key, d4 the innermost. Curiously, each bit is high where it is not pressed, low where it is – the opposite of what you might expect.
To read a row of five keys we simply load the port number into the bc register pair, then perform the instruction in a,(c). As we only need the lowest value bits we can ignore the bits we dont want either with an and 31 or by rotating the bits out of the accumulator into the carry flag using five rra:call c,(address) instructions.
If this is difficult to understand consider the following example:
ld bc,63486 ; keyboard row 1-5/joystick port 2. in a,(c) ; see what keys are pressed. rra ; outermost bit = key 1. push af ; remember the value. call nc,mpl ; it's being pressed, move left. pop af ; restore accumulator. rra ; next bit along (value 2) = key 2. push af ; remember the value. call nc,mpr ; being pressed, so move right. pop af ; restore accumulator. rra ; next bit (value 4) = key 3. push af ; remember the value. call nc,mpd ; being pressed, so move down. pop af ; restore accumulator. rra ; next bit (value 8) reads key 4. call nc,mpu ; it's being pressed, move up.
Sinclair joystick ports 1 and 2 were simply mapped to each of the rows of number keys and you can easily prove this by going into the BASIC editor and using the joystick to type numbers. Port 1 (Interface 2) was mapped to the keys 6,7,8,9 and 0, Port 2 (Interface 1) to keys 1,2,3,4 and 5. To detect joystick input we simply read the port in the same way as reading the keyboard. Sinclair joysticks use ports 63486 (Interface 1/port 2), and 61438 (Interface 2/port 1), bits d0-d4 will give a 0 for pressed, 1 for not pressed.
The popular Kempston joystick format is not mapped to the keyboard and can be read by using port 31 instead. This means we can use a simple in a,(31). Again, bit values d0-d4 are used although this time the bit settings are as you might expect, with a bit set high if the joystick is being applied in a particular direction. The resulting bit values will be 1 for pressed, 0 for not pressed.
; Example joystick control routine. joycon ld bc,31 ; Kempston joystick port. in a,(c) ; read input. and 2 ; check "left" bit. call nz,joyl ; move left. in a,(c) ; read input. and 1 ; test "right" bit. call nz,joyr ; move right. in a,(c) ; read input. and 8 ; check "up" bit. call nz,joyu ; move up. in a,(c) ; read input. and 4 ; check "down" bit. call nz,joyd ; move down. in a,(c) ; read input. and 16 ; try the fire bit. call nz,fire ; fire pressed.
A Simple Game
We can now go one step further and, putting into practice what we have already covered, write the main control section for a basic game. This will form the basis of a simple Centipede variant we will be developing over the next few chapters. We haven’t covered everything needed for such a game yet but we can make a start with a small control loop which allows the player to manipulate a small gun base around the screen. Be warned, this program has no exit to BASIC so make sure you’ve saved a copy of your source code before running it.
; We want a black screen. ld a,71 ; white ink (7) on black paper (0), ; bright (64). ld (23693),a ; set our screen colours. xor a ; quick way to load accumulator with zero. call 8859 ; set permanent border colours. ; Set up the graphics. ld hl,blocks ; address of user-defined graphics data. ld (23675),hl ; make UDGs point to it. ; Okay, let's start the game. call 3503 ; ROM routine - clears screen, opens chan 2. ; Initialise coordinates. ld hl,21+15*256 ; load hl pair with starting coords. ld (plx),hl ; set player coords. call basexy ; set the x and y positions of the player. call splayr ; show player base symbol. ; This is the main loop. mloop equ $ ; Delete the player. call basexy ; set the x and y positions of the player. call wspace ; display space over player. ; Now we've deleted the player we can move him before redisplaying him ; at his new coordinates. ld bc,63486 ; keyboard row 1-5/joystick port 2. in a,(c) ; see what keys are pressed. rra ; outermost bit = key 1. push af ; remember the value. call nc,mpl ; it's being pressed, move left. pop af ; restore accumulator. rra ; next bit along (value 2) = key 2. push af ; remember the value. call nc,mpr ; being pressed, so move right. pop af ; restore accumulator. rra ; next bit (value 4) = key 3. push af ; remember the value. call nc,mpd ; being pressed, so move down. pop af ; restore accumulator. rra ; next bit (value 8) reads key 4. call nc,mpu ; it's being pressed, move up. ; Now he's moved we can redisplay the player. call basexy ; set the x and y positions of the player. call splayr ; show player. halt ; delay. ; Jump back to beginning of main loop. jp mloop ; Move player left. mpl ld hl,ply ; remember, y is the horizontal coord! ld a,(hl) ; what's the current value? and a ; is it zero? ret z ; yes - we can't go any further left. dec (hl) ; subtract 1 from y coordinate. ret ; Move player right. mpr ld hl,ply ; remember, y is the horizontal coord! ld a,(hl) ; what's the current value? cp 31 ; is it at the right edge (31)? ret z ; yes - we can't go any further left. inc (hl) ; add 1 to y coordinate. ret ; Move player up. mpu ld hl,plx ; remember, x is the vertical coord! ld a,(hl) ; what's the current value? cp 4 ; is it at upper limit (4)? ret z ; yes - we can go no further then. dec (hl) ; subtract 1 from x coordinate. ret ; Move player down. mpd ld hl,plx ; remember, x is the vertical coord! ld a,(hl) ; what's the current value? cp 21 ; is it already at the bottom (21)? ret z ; yes - we can't go down any more. inc (hl) ; add 1 to x coordinate. ret ; Set up the x and y coordinates for the player's gunbase position, ; this routine is called prior to display and deletion of gunbase. basexy ld a,22 ; AT code. rst 16 ld a,(plx) ; player vertical coord. rst 16 ; set vertical position of player. ld a,(ply) ; player's horizontal position. rst 16 ; set the horizontal coord. ret ; Show player at current print position. splayr ld a,69 ; cyan ink (5) on black paper (0), ; bright (64). ld (23695),a ; set our temporary screen colours. ld a,144 ; ASCII code for User Defined Graphic 'A'. rst 16 ; draw player. ret wspace ld a,71 ; white ink (7) on black paper (0), ; bright (64). ld (23695),a ; set our temporary screen colours. ld a,32 ; SPACE character. rst 16 ; display space. ret plx defb 0 ; player's x coordinate. ply defb 0 ; player's y coordinate. ; UDG graphics. blocks defb 16,16,56,56,124,124,254,254 ; player base.
Fast, isn’t it? In fact, we’ve slowed the loop down with a halt instruction but it still runs at a speedy 50 frames per second, which is probably a little too fast. Don’t worry, as we add more features to the code it will begin to slow down. If you are feeling confident you might like to try adapting the above program to work with a Kempston joystick. It isn’t difficult, and merely requires changing port 63486 to port 31, and replacing the four subsequent call nc,(address) to call c,(address) (The bits are reversed, remember?)
Redefineable keys are a little more tricky. As you are probably aware, the original Spectrum keyboard was divided into 8 rows of 5 keys each, and by reading the port associated with a particular row of keys, then testing bits d0-d4 we can tell if a particular key is being pressed. If you were to replace ld bc,31 in the code snippet above with ld bc,49150 you could test for the row of keys H to Enter – though that doesn’t make for a convenient redefine keys routine. Thankfully, there is another way of going about it.
We can establish the port required for each row of keys using the formula in the Spectrum manual. Where n is the row number 0-7 the port address will be 254+256*(255-2^n). There’s a ROM routine at address 654 which does a lot of the hard work for us by returning the number of the key pressed in the e register, in the range 0-39. 0-7 correspond to the innermost key of each row in turn (that’s B, H, Y, 6, 5, T, G and V), 8-15 to the next key along in each row up to 39 for the outermost key on the last row – CAPS SHIFT. The shift key status, just for the record, is also returned in d. If no key is pressed then e returns 255.
The ROM routine can only return a single key number which is no good for detecting more than one keypress at a time. To determine whether or not a specific key is being pressed at any time we need to convert the number back into a port and bit, then read that port and check the individual bit for ourselves. There’s a very handy routine I use for the job, and it’s the only routine in my games which I didn’t write myself. Credit for that must go to Stephen Jones, a programmer who used to write excellent articles for the Spectrum Discovery Club many years ago. To use his routine, load the accumulator with the number of the key you wish to test, call ktest, then check the carry flag. If it’s set the key is not being pressed, if there’s no carry then the key is being pressed. If that’s too confusing and seems like the wrong way round, put a ccf instruction just before the ret.
; Mr. Jones' keyboard test routine. ktest ld c,a ; key to test in c. and 7 ; mask bits d0-d2 for row. inc a ; in range 1-8. ld b,a ; place in b. srl c ; divide c by 8, srl c ; to find position within row. srl c ld a,5 ; only 5 keys per row. sub c ; subtract position. ld c,a ; put in c. ld a,254 ; high byte of port to read. ktest0 rrca ; rotate into position. djnz ktest0 ; repeat until we've found relevant row. in a,(254) ; read port (a=high, 254=low). ktest1 rra ; rotate bit out of result. dec c ; loop counter. jp nz,ktest1 ; repeat until bit for position in carry. ret