Curso Programación ZX SpectrumCursos

Ensamblador para ZX Spectrum – Pong: $04 Teclas de control

Empezamos con el manejo de las teclas de control.

5
(6)

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 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 para ZX Spectrum, semi filas del teclado

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 flagSignificado
ZA = Valor
NZA <> Valor
CA < Valor
NCA >= Valor
Ensamblador para ZX Spectrum, posibles valores del registro F tras una operación 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 para ZX Spectrum, resultados de las 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: 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:

Ensamblador para ZX Spectrum, teclas de control
Ensamblador para 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 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

Ficheros

¿Te ha Resultado útil este artículo?

Ayúdanos a mejorar y danos tu opinión:

Mostrar más

Deja una respuesta

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

Publicaciones relacionadas

Mira también
Cerrar
Botón volver arriba