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.

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 filaValor HexadecimalValor Binario
Caps Shift-V$FE1111 1110
A-G$FD1111 1101
Q-T$FB1111 1011
1-5$F71111 0111
0-6$EF1110 1111
P-Y$DF1101 1111
Enter-H$BF1011 1111
Space-B$7F0111 1111
Ensamblador ZX Spectrum, valor de cada semi fila

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 flagSignificado
ZA = Valor
NZA <> Valor
CA < Valor
NCA >= Valor
Ensamblador ZX Spectrum, resultados de CP

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ónBit 1Bit 2Resultado
AND111
100
010
000
OR111
101
011
000
XOR110
101
011
000
Ensamblador ZX Spectrum, resultado a nivel de bit de la operaciones lógicas

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í:

Ensamblador ZX Spectrum - Teclas de control
Ensamblador ZX Spectrum, teclas de control

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

Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, teclas de control

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
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.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies
A %d blogueros les gusta esto: