0x02 Ensamblador ZX Spectrum Pong – Teclas de control
En esta entrega de Ensamblador ZX Spectrum Pong vamos a desarrollar la rutina que comprueba si se han pulsado las teclas de control de nuestro juego, y devuelve cuales son las teclas pulsadas.
Tabla de contenidos
Ensamblador ZX Spectrum Pong – Teclas de control
El teclado del ZX Spectrum está divido en ocho semi filas, cada una de las cuales contiene cinco teclas.
Cuando se evalúa si se ha pulsado alguna tecla de una semi fila, los valores vienen en un byte, en los bits 0 a 4, cuyos valores son 1 si no se ha pulsado, y 0 si se ha pulsado. El bit 0 hace referencia a la tecla más alejada del centro del teclado (Caps Shift, A, Q, 1, 0, P, Enter y Space) y el 4 a la tecla más cercana al centro (V, G, T, 5, 6, Y, H y B).
Cada semi fila está identificada por un número:
Semi fila | Valor Hexadecimal | Valor Binario |
---|---|---|
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 |
Como se puede observar, para calcular el valor de la semi fila anterior o posterior, solo hay que hacer rotaciones circulares de bits (RLC, RRC).
Dentro de la carpeta Pong, creamos una carpeta llamada Paso02, y dentro de la misma los ficheros Main.asm y Controls.asm.
La rutina que vamos a usar para verificar los controles está sacada del Curso de Ensamblador para Z80 de Compiler Software de Santiago Romero. Podéis encontrar dicho curso en El wiki de speccy.org.
ScanKeys
Los controles que vamos a usar son: A-Z para el jugador 1, y 0-O para el jugador 2.
La rutina que vamos que vamos a implementar para comprobar si se ha pulsado alguna de las teclas expuestas, devuelve en el registro D las teclas que se han pulsado, usando el bit 0 para la tecla A, el bit 1 para la tecla Z, el bit 2 para la tecla 0 y el bit 3 para la tecla O. Los valores que toman estos bits son 1 si se ha pulsado la tecla y 0 en el caso contrario.
Lo primero que va a hacer la rutina es poner a 0 el registro D.
ScanKeys:
ld d, $00
Comprobación de la tecla A
A continuación, comprueba si se ha pulsado la tecla A.
scanKeys_A:
ld a, $fd
in a, ($fe)
bit $00, a
jr nz, scanKeys_Z
set $00, d
Con LD A, $FD cargamos el identificador de la semi fila A-G ($FD = 11111101) en A.
A continuación, con IN A, ($FE), leemos el puerto de entrada $FE (254) y dejamos el valor en A. El puerto de entrada $FE es el puerto desde el que leemos el estado del teclado.
Lo siguiente es comprobar si se ha pulsado la tecla A; para ello usamos la sentencia BIT $00, A, que evalúa el estado del bit 0 del registro A. Si el bit está a 0 se activa el flag Z, de lo contrario se desactiva.
Con la siguientes instrucción, JR NZ, scanKeys_Z, si el bit viene a 1 salta a evaluar la pulsación de la tecla Z.
Si el bit viene a 0, activamos el bit 0 del registro D, SET $00, D, para devolver que se ha pulsado la tecla A.
Comprobación de la tecla Z
El siguiente paso es comprobar si se ha pulsado la tecla Z.
scanKeys_Z:
ld a, $fe
in a, ($fe)
bit $01, a
jr nz, scanKeys_0
set $01, d
La diferencia con la comprobación de la tecla A radica en que cargamos en A la semi fila Caps Shift-V, LD A, $FE, comprobamos el estado del bit 1 correspondiente a la tecla Z, BIT $01, A, si no se ha pulsado saltamos a comprobar la pulsación de la tecla 0, JR NZ, scanKeys_0, y, por último, activamos el bit 1 de D, SET $01, D, si se ha pulsado la tecla Z.
Comprobación de pulsación simultánea de A y Z
Se puede dar el caso de que se pulsen a la vez las teclas A y Z. Si se diera, vamos a desactivar los indicadores para asimilar que no se ha pulsado ninguna. La otra opción sería dejar los indicadores de las dos teclas pulsadas y mover el personaje primero hacia arriba y luego hacia abajo, quedándose donde estaba.
Vamos comprobar si se han pulsado las dos teclas, y si es así desactivamos los bits correspondientes.
ld a, d
cp $03
jr nz, scanKeys_0
xor a
ld d, a
Lo primero es cargar el valor de D en A, LD A, D, y verificar si el valor es $03, CP $03, en cuyo caso se habrían pulsado las dos teclas. Si el valor de la comprobación no es 0, no se han pulsado las dos teclas y saltamos a comprobar las pulsación de la tecla 0, JR NZ, scanKeys_0.
Si el resultado es 0, ponemos A = 0, XOR A, y cargamos el valor en D, LD D, A.
La instrucción CP evalúa el valor del registro A con el valor de otro registro, un número o el valor de una dirección de memoria apuntada por (HL), (IX+n) o (IY+n). CP resta cualquiera de estos valores al valor del registro A. CP no altera el valor de A, pero sí altera los indicadores (registro F), de la siguiente manera:
Valor del flag | Significado |
---|---|
Z | A = Valor |
NZ | A <> Valor |
C | A < Valor |
NC | A >= Valor |
Para cargar 0 en A, en lugar de LD A, $00 hemos utilizado XOR A.
Las instrucciones AND, OR y XOR, tienen como destino, siempre, el registro A y el resultado que dan a nivel de bits es el siguiente:
Operación | Bit 1 | Bit 2 | Resultado |
---|---|---|---|
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 |
Como se puede ver en la tabla, XOR A siempre da como resultado 0, una operación que tiene 1 byte y consume 4 ciclos de reloj. Por el contrario, LD A, $00 tiene 2 bytes y consume 7 ciclos de reloj, por lo que ganamos 1 byte y 3 ciclos. Pero no todo son ventajas, ya que XOR afecta a los flags mientras que LD no.
También podríamos haber puesto D a 0, LD D, $00, pero no habríamos visto la instrucción XOR, aunque habríamos ahorrado un ciclo de reloj.
Hay otra forma más óptima de hacerlo; sustituimos CP $03 por SUB $03, y luego cargamos A en D, LD D, A.
ld a, d
sub $03
jr nz, scanKeys_0
ld d, a
Estaríamos consumiendo 7 ciclos y 2 bytes con SUB $03, y 4 ciclos y 1 byte con LD D, A, ahorrándonos 3 o 4 ciclos, y 1 byte.
Comprobación de las teclas 0 y O
Por último, hay que comprobar si han pulsado las teclas 0 y O, y si se han pulsado las dos a la vez. El código es casi igual a lo que hemos visto hasta ahora, por lo que vamos a ver el código completo de la rutina.
; -----------------------------------------------------------------------------
; ScanKeys
; Escanea las teclas de control y devuelve las pulsadas.
; Salida: D = Teclas pulsadas.
; Bit 0 = A pulsada 0/1.
; Bit 1 = Z pulsada 0/1.
; Bit 2 = 0 pulsada 0/1.
; Bit 3 = O pulsada 0/1.
; Altera el valor de los registros AF y D.
; -----------------------------------------------------------------------------
ScanKeys:
ld d, $00 ; Pone el registro D a 0.
scanKeys_A:
ld a, $fd ; Carga en A la semi fila A-G
in a, ($fe) ; Lee el estado de la semi fila
bit $00, a ; Comprueba si se ha pulsado la A
jr nz, scanKeys_Z ; Si no se ha pulsado, salta
set $00, d ; Pone a 1 el bit correspondiente a la A
scanKeys_Z:
ld a, $fe ; Carga en A la semi fila CS-V
in a, ($fe) ; Lee el estado de la semi fila
bit $01, a ; Comprueba si se ha pulsado la Z
jr nz, scanKeys_0 ; Si no se ha pulsado, salta
set $01, d ; Pone a 1 el bit correspondiente a la Z
; Comprueba que no se hayan pulsado las dos teclas de dirección
ld a, d ; Carga el valor de D en A
sub $03 ; Comprueba si se han pulsado la A y la Z a la vez
jr nz, scanKeys_0 ; Si no se han pulsado, salta
ld d, a ; Pone D a 0
scanKeys_0:
ld a, $ef ; Carga la semi fila 0-6
in a, ($fe) ; Lee el estado de la semi fila
bit $00, a ; Comprueba si se ha pulsado el 0
jr nz, scanKeys_O ; Si no se ha pulsado, salta
set $02, d ; Pone a 1 el bit correspondiente al 0
scanKeys_O:
ld a, $cf ; Carga la semi fila P-Y
in a, ($fe) ; Lee el estado de la semi fila
bit $01, a ; Comprueba si se ha pulsado la O
ret nz ; Si no se ha pulsado, salta
set $03, d ; Pone a 1 el bit correspondiente a la O
; Comprueba que no se hayan pulsado las dos teclas de dirección
ld a, d ; Carga el valor de D en A
and $0c ; Se queda con los bits correspondientes a 0 y O
cp $0c ; Comprueba si se han pulsado las dos teclas
ret nz ; Si no se han pulsado, sale
ld a, d ; Se han pulsado, carga el valor de D en A
and $03 ; Se queda con los bits correspondientes a la A y Z
ld d, a ; Carga el valor en D
ret
Las diferencias más importantes con respecto a la comprobación de la pulsación de A-Z, están en la comprobación de si se han pulsado a la vez las dos teclas.
Antes de comprobar si están activos los bits del registro D, que se corresponden con 0 y O ($0C = 0000 1100), hay que quedarse sólo con estos bits, de los contrario, si se hubieran pulsado la A o la Z, CP $0C nunca daría 0, es por eso que antes de esta instrucción se ha incluido AND $0C, para quedarnos el valor de los bits 2 y 3.
La segunda diferencia es la forma en la que ponemos a 0 los bits 2 y 3, en el caso de que se hayan pulsado a la vez 0 y O.
Anteriormente hicimos XOR A o SUB $03 y LD D, A, pues lo único que teníamos en A era si se habían pulsado a la vez A y Z, pero esta vez, además de si se han pulsado 0 y O, tenemos las pulsaciones de A y Z, y si hiciéramos XOR A o SUB $03 y LD D, A, estaríamos destruyendo esta información.
Para evitar destruir esta información, cargamos en A el valor del registro D, LD A, D, luego nos quedamos solo con el valor de los bits 0 y 1, AND $03, y volvemos a cargar el valor en D, LD D, A. De esta manera hemos puesto a 0 el valor de los bits 2 y 3 sin destruir el valor de los bits 0 y 1.
Podemos optimizar sustituyendo LD A, D y AND $03 por XOR D. XOR D tendría el mismo efecto que las otras dos líneas, y solo consumiríamos 4 ciclos de reloj y un byte.
Si el valor de A es 00001100 y el valor de D es 00001101, después de XOR D, el valor de A es 00000001
Test de ScanKeys
Ya solo queda probar la rutina. Para ello vamos a pintar en la esquina superior izquierda el valor de D, una vez que vuelve de la rutina de chequeo de las pulsaciones de las teclas. El código lo vamos a escribir en el archivo Main.asm.
El primer paso es especificar la dirección donde se carga el programa.
org $8000
Apuntamos HL a la esquina superior izquierda de la pantalla.
ld hl, $4000
Y hacemos un bucle infinito que llame a la rutina ScanKeys y cargue en la esquina superior derecha de la ventana el valor del registro D.
Bucle:
call ScanKeys
ld (hl), d
jr Bucle
Por último, incluimos el archivo Controls.asm y le indicamos a PASMO la dirección donde llamar cuando cargue el programa.
include "Controls.asm"
end $8000
Llegados a este punto, compilamos el programa y cargamos en el emulador para ver el resultado:
pasmo --name PoromPong --tapbas Main.asm PorompomPong.tap --public
El resultado del programa sería algo así:

El código final del archivo Main.asm quedará como sigue.
; Comprueba el funcionamiento de los controles A-Z y 0-O
; Pinta la representación de las teclas pulsadas.
org $8000
ld hl, $4000 ; Posiciona HL en la primera posición de la pantalla
Bucle:
call ScanKeys ; Escanea las teclas pulsadas
ld (hl), d ; Pinta la representación de las teclas pulsadas
jr Bucle ; Bucle infinito
include "Controls.asm"
end $8000
Hemos dejado una optimización pendiente, que veremos en la última entrega del tutorial, con la que ahorraremos un ciclo de reloj en la comprobación de cada tecla pulsada, lo que hará un total de 4 ciclos de reloj de ahorro en la rutina ScanKeys.
Podéis descargar todo el código que hemos generado.
Enlaces de interés
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF
- Poyecto en itch.io.
- Archivo .dsk con los juegos de los tutoriales
- Personalización y depuración con ZEsarUX
Vídeo
Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.
Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.
No olvides visitar las webs amigas.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.
Pingback: 0x04 Ensamblador ZX Spectrum Marciano – Nave - Espamática