Ensamblador para ZX Spectrum – Pong: $04 Teclas de control
Empezamos con el manejo de las teclas de control.
Ensamblador para ZX Spectrum – Pong: Paso 2, teclas de control
En este paso vamos a desarrollar la rutina que comprueba si se han pulsado las teclas de control de nuestro juego, y que devuelve cuales son las teclas pulsadas.
El teclado del ZX Spectrum está dividido 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 (Caps Shift, A, Q, 1, 0, P, Enter, Space) y el 4 a la tecla más cercana al centro (V, G, 5, 6, Y, H, 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.
Los controles que vamos a usar son: A-Z para el jugador 1, y 0-O para el jugador 2.
La rutina 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
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 cero se activa el flag Z, de lo contrario se desactiva.
Con la siguiente 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.
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.
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 a 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 3, 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 la 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 registro A. CP no altera el valor de A, pero si altera los flags (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: si 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 un byte de LD D, A, ahorrándonos 3 o 4 ciclos, y un byte.
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 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 solo con estos bits, de lo 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 con 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, porque 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
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 izquierda 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 se 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á algo así como esto:
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 cual 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.
En el próximo capítulo de Ensamblador para ZX Spectrum, dibujaremos y moveremos las palas, y dibujaremos también la línea central.
Enlaces de interés
- Notepad++.
- Visual Studio Code.
- Sublime Text.
- ZEsarUX.
- PASMO.
- Git.
- Curso de ensamblador Z80 de Compiler Software.
- Z80 instruction set.