ZX Spectrum Assembly, Pong – 0x02 Controls keys
In this chapter of ZX Spectrum Assembly, we will learn how to read the keyboard without using the ROM routines. We will develop the routine that checks if the control keys of our game have been pressed, and returns which keys have been pressed.
Translation by Felipe Monge Corbalán
Table of contents
Control keys
The keyboard of the ZX Spectrum is divided into eight half rows, each containing five keys.
When evaluating whether a key in a half-row has been pressed, the values come in one byte, in bits 0 to 4, whose values are one if it has not been pressed and zero if it has been pressed. Bit 0 is the key furthest from the centre (Caps Shift, A, Q, 1, 0, P, Enter, Space) and 4 is the key closest to the centre (V, G, T, 5, 6, Y, H and B).
Each half row is identified by a number:
Half-row | Hexadecimal value | Binary Value |
Caps Shift-V | $FE | 1111 1110 |
A-G | $FD | 1111 1101 |
Q-T | $FB | 1111 1011 |
1-5 | $F7 | 1111 0111 |
0-6 | $EF | 1110 1111 |
P-Y | $DF | 1101 1111 |
Enter-H | $BF | 1011 1111 |
Space-B | $7F | 0111 1111 |
Note that to calculate the value of a half row, the one before or the one after, only circular bit rotations (RLC, RRC) are needed.
Inside the Pong folder we create the Step02 folder, and inside it the main.asm and controls.asm files.
The routine we are going to use to check the controls can be found in Santiago Romero’s course: Compiler Software’s Z80 Assembly Course. You can find this course on the speccy.org wiki.
The controls we will use are A-Z for player one and 0-O for player two.
We will develop in controls.asm a routine to check if any of the control keys have been pressed, which will return the keys pressed in the D register, using bit 0 for the A key, bit 1 for the Z key, bit 2 for the 0 key and bit 3 for the O key. The value of these bits is one if the key was pressed and zero otherwise.
The routine first resets the D register to zero:
ScanKeys:
ld d, $00
It then checks that the A button has been pressed:
scanKeys_A:
ld a, $fd
in a, ($fe)
bit $00, a
jr nz, scanKeys_Z
set $00, d
With LD A, $FD we load the identifier of the half row A-G ($FD = 11111101) into A.
Next, with IN A, ($FE) we read the input port $FE (254) and leave the value at A. The input port $FE is the port from which we read the keyboard status.
The next step is to check whether key A has been pressed; we use the instruction BIT $00, A, which evaluates the status of bit 0 of register A. If the bit is zero, the Z flag is set, otherwise it is disabled.
With the following instruction, JR NZ, scanKeys_Z, if the bit becomes one, it jumps to evaluate the Z key press.
If the bit is zero, we set bit 0 of register D, SET $00, D, to indicate that key A has been pressed.
The next step is to check that the Z key has been pressed:
scanKeys_Z:
ld a, $fe
in a, ($fe)
bit $01, a
jr nz, scanKeys_0
set $01, d
The difference with the A key check is that we load the Caps Shift-V half stack, LD A, $FE, into A, check the status of bit 1 corresponding to the Z key, BIT $01, A, if the key was pressed we jump to checking the 0 key press, JR NZ, scanKeys_0, and finally, we set bit 1 of D, SET $01, D, if the Z key was pressed.
It is possible to press the A and Z buttons at the same time. In this case, we deactivate the indicators to indicate that neither key has been pressed. The other option would be to leave the indicators of both keys pressed and move the character up and then down, leaving it where it was.
Let’s check that the two keys have been pressed and, if so, deactivate the corresponding bits:
ld a, d
sub $03
jr nz, scanKeys_0
ld d, a
First we load the value of D in A, LD A, D, and check if the value is three, SUB $03, in which case both keys would have been pressed. If the result is not zero, the two keys have not been pressed and we jump to check the 0 key press, JR NZ, scanKeys_0. If it is zero, we set D to zero, LD D, A; A is already zero.
We could have used the CP instruction to evaluate the value of A with another register, a number or a value in memory pointed to by (HL), (IX + N) or (IY + N). CP subtracts one of these values from the value of register A; although it does not modify A, it does modify the pointers (register F), as follows:
Flag value | Meaning |
Z | A = Value |
NZ | A <> Value |
C | A < Value |
NC | A >= Value |
If we use CP, we must set A to zero before LD D, A, e.g. with XOR A.
The AND, OR and XOR instructions have A as their target and the result they give at bit level is as follows:
Operation | Bit 1 | Bit 2 | Result |
AND | 1 | 1 | 1 |
1 | 0 | 0 | |
0 | 1 | 0 | |
0 | 0 | 0 | |
OR | 1 | 1 | 1 |
1 | 0 | 1 | |
0 | 1 | 1 | |
0 | 0 | 0 | |
XOR | 1 | 1 | 0 |
1 | 0 | 1 | |
0 | 1 | 1 | |
0 | 0 | 0 |
The list would look like this:
ld a, d
cp $03
jr nz, scanKeys_0
xor a
ld d, a
We would use one byte and seven clock cycles with XOR A.
Finally, we need to check that the 0 and O keys have been pressed, and that both keys have been pressed at the same time. The code is almost the same as we have seen so far, let’s see the complete code of the routine:
;------------------------------------------------------------------
; ScanKeys
; Scans the control keys and returns the pressed keys.
; Output: D -> Keys pressed.
; Bit 0 -> A pressed 0/1.
; Bit 1 -> Z pressed 0/1.
; Bit 2 -> 0 pressed 0/1.
; Bit 3 -> O pressed 0/1.
; Alters the value of the AF and D registers.
;------------------------------------------------------------------
ScanKeys:
ld d, $00 ; Sets the D register to 0
scanKeys_A:
ld a, $fd ; Load in A the A-G half-stack
in a, ($fe) ; Read status of the semi-stack
bit $00, a ; Checks if the A has been pressed
jr nz, scanKeys_Z ; If not clicked, skips
set $00, d ; Set the bit corresponding to A to one
scanKeys_Z:
ld a, $fe ; Load in A the CS-V half-stack
in a, ($fe) ; Read status of the half-stack
bit $01, a ; Checks whether Z has been pressed
jr nz, scanKeys_0 ; If not clicked, skips
set $01, d ; Sets the bit corresponding to Z to one
; Check that the two arrow keys have not been pressed
ld a, d ; Load the value of D into A
sub $03 ; Checks whether A and Z have been pressed
; at the same time
jr nz, scanKeys_0 ; If not pressed, skips
ld d, a ; Sets D to zero
scanKeys_0:
ld a, $ef ; Load the half-stack 0-6
in a, ($fe) ; Read status of the semi-stack
bit $00, a ; Checks if 0 has been pressed
jr nz, scanKeys_O ; If not pressed, skip
set $02, d ; Set the bit corresponding to 0 to a one
scanKeys_O:
ld a, $cf ; Load the P-Y half-stack
in a, ($fe) ; Read status of the semi-stack
bit $01, a ; Checks if the O has been pressed
ret nz ; If not pressed, jumps to
set $03, d ; Sets the bit corresponding to O to one
; Check that the two arrow keys have not been pressed
ld a, d ; Load the value of D into A
and $0c ; Keeps the 0 and O bits
cp $0c ; Check if the two keys have been pressed
ret nz ; If they have not been pressed, it exits
ld a, d ; Pressed, loads the value of D in A
and $03 ; Takes the bits of A and Z
ld d, a ; Load the value in D
ret
The main difference from the A-Z keystroke check is that it checks whether the two keys were pressed at the same time.
Before checking that the bits of register D corresponding to 0 and O ($0C = 0000 1100) are active, it is necessary to maintain only these bits, otherwise, if A or Z were pressed, CP $0C would never return zero, so AND $0C was inserted before this instruction to maintain the value of bits 2 and 3.
The second difference is the way we reset bits 2 and 3 when 0 and O are pressed at the same time.
To check if A and Z were pressed at the same time, we put SUB $03 and LD D, A, because in A we only had these keystrokes, but now, in addition to whether 0 and O were pressed, we also have the keystrokes of A and Z, and if we were to LD D, A directly, we would destroy this information.
To avoid destroying this information, we load the value of register D into A, LD A, D, then we keep only the value of bits 0 and 1, AND $03, and we load the value back into D, LD D, A. In this way we have set the value of bits 2 and 3 to 0, but without destroying the value of bits 0 and 1.
We can optimise by replacing LD A, D and AND $03 with XOR D, which has the same effect as the other two lines, but uses four clock cycles and one byte.
If the value of A is 00001100 and the value of D is 00001101 after XOR D, the value of A is 00000001.
All that’s left now is to test the routine. We will paint the value of D in the upper left corner when it returns from the keystroke check routine. We will write the code in the main.asm file.
Specify the address where the program is loaded:
org $8000
We point HL at the top left corner of the screen:
ld hl, $4000
We make an infinite loop that calls ScanKeys and loads the value of the D register into the upper left corner of the window:
Loop:
call ScanKeys
ld (hl), d
jr Loop
Finally, we include the controls.asm file and tell PASMO the address to call when the programme is loaded.
pasmo --name ZX-Pong --tapbas main.asm pong.tap pong.log
The result of the programme will be something like this:
The final code of the main.asm file will look like this:
; Checks the operation of the A-Z and 0-O controls.
; Paints the representation of the keys pressed.
org $8000
ld hl, $4000 ; HL = first screen position
Loop:
call ScanKeys ; Scan for keystrokes
ld (hl), d ; Paints the keystroke display
jr Loop ; Infinite loop
include "controls.asm"
end $8000
We have left one optimisation, which we will see in the last chapter of ZX-Pong’s development, which will save one clock cycle in checking each keystroke, for a total of four clock cycles of savings in the ScanKeys routine.
ZX Spectrum Assembly, Pong
In the next ZX Spectrum Assembly chapter, we will paint the paddles and the centre line.
Download the source code from here.
Useful links
ZX Spectrum Assembly, Pong by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.