cursos:ensamblador:teclado

Lectura del teclado en el Spectrum

Este capítulo está íntegramente dedicado a la lectura del teclado mediante la instrucción de lectura de puertos IN. A lo largo del mismo veremos cómo responde el estado del puerto a los cambios del teclado, y subrutinas útiles para la gestión del mismo.

Tras este capítulo seremos capaces de consultar el estado del teclado, ya sean teclas concretas predefinidas en el código o redefinidas por el usuario, permitiéndonos interactuar con él y cubriendo una de las principales necesidades a la hora de programar juegos para Spectrum: el control, tanto de los menúes como del juego en sí.


Como ya vimos en el capítulo dedicado a las rutinas de la ROM y variables del sistema, tenemos diferentes rutinas y mecanismos para leer el teclado mediante los procesos de la ROM.

La ROM dispone de diferentes rutinas para la lectura del teclado y el tratamiento de scancodes (por ejemplo, conversión a ASCII, guardar la última tecla pulsada en una variable de sistema, etc).

Como veremos en la entrega dedicada a las Interrupciones del procesador, ciertas rutinas de servicio (ISR), en concreto rst $38, son llamadas regularmente por el procesador en el modo de interrupciones 1. Estas rutinas son utilizadas por el intérprete de BASIC, por ejemplo, pero pueden ser utilizadas por nosotros siempre y cuando no cambiemos del modo de Interrupción 1 (por defecto) al modo de Interrupción 2 (un modo que nos permite programar nuestras propias rutinas ISR personalizadas).

Estando en modo 1, la ROM lee regularmente el teclado y actualiza ciertas variables del sistema como LAST_K (variable ubicada en la dirección de memoria $5c08 ó 23560), que contiene el código ASCII de la última tecla que se haya pulsado. La tecla pulsada permanece en dicha variable del sistema incluso aunque ya no esté siendo pulsada. La rutina de la ROM ya nos asegura una correcta gestión de los microrebotes del teclado y su conversión a un ASCII válido.

Mientras estemos en modo de interrupciones im1 (el modo en que arranca el Spectrum), podemos hacer uso de LAST_K si lo consideramos conveniente, ya que como se ha comentado, la Rutina de Interrupciones por defecto del Spectrum se ejecutará regularmente y actualizará su valor.

No obstante, no es lo normal utilizar las rutinas de teclado de la ROM en juegos, puesto que en la mayoría de ellos necesitaremos entrar en el modo 2 de Interrupciones, en el cual no se ejecuta rst $38 y por lo tanto no se actualizan variables como LAST_K. Además, es posible que nuestro programa tenga que hacer uso de la zona de memoria en que las rutinas de la ROM guardan los datos de las teclas leídas (de 23552 a 23560), lo que no haría factible el uso de estas rutinas (ni siquiera llamando a rst $38) a menos que preservemos el contenido de esta zona de memoria y de otros registros del procesador antes de volver a a im1, llamar a rst $38, y saltar de nuevo a im 2. Seguramente todo este proceso es totalmente innecesario en un juego si éste ya dispone de rutinas para leer el teclado y podemos aprovecharlas.

En la sección de ficheros se incluye un listado de las rutinas de la ROM desensambladas y comentadas, obtenidas del libro “The Complete Spectrum ROM Disassembly”. Estas rutinas incluyen funciones de lectura de scancodes y decodificación de los mismos para convertirlos en ASCIIs. Podemos aprovecharlas en programas que no requieran precisión con el teclado (por ejemplo, para aplicaciones en lugar de para juegos).

Las rutinas de la ROM están en general orientadas a devolver si hay una tecla pulsada, y cuál es, pero no a leer el estado de múltiples teclas, como son necesarias para mover a un personaje en diagonal o andar mientras disparamos.

Este capítulo está orientado a mostrar cómo hacer unas rutinas propias de lectura de teclado sin pasar por la ROM.

Así pues, veamos cómo leer el teclado directamente accediendo al puerto E/S que lo conecta al Z80.


El teclado del Spectrum es una matriz de 40 pulsadores (40 teclas) que proporcionan al microprocesador, a través de las líneas de Entrada/Salida, para diferentes filas, un valor de 8 bits en el rango 0-255, donde cada bit indica el estado de una determinada tecla. Estrictamente hablando, la encargada de la lectura del teclado (como de otros periféricos) es realmente la ULA, pero para nosotros esto es transparente y podremos utilizar las funciones estándar de IO del microprocesador Z80 para conocer el estado del teclado. Para leer el estado de cada una de las teclas del teclado (0 = pulsada, 1 = no pulsada), debemos obtener el estado del puerto $fe.

La pregunta en este momento debe de ser: si $fe es el puerto de lectura del teclado, y el Z80A es un microprocesador con un bus de datos de 8 bits, ¿cómo leemos el estado de 40 teclas en un registro de 8 bits?

La respuesta es: organizando el teclado en filas de teclas y seleccionando qué fila leer mediante el registro B.


Teclado del Spectrum

Si abrimos físicamente nuestro Spectrum, veremos que el teclado está conectado a la placa base del mismo mediante 2 cintas de datos: una de ellas de 8 líneas y la otra de 5. La de 8 líneas (8 bits) se puede considerar como el “byte de direcciones” del teclado, y es la que está unida al byte alto (bits 8 al 15) del bus de direcciones del Spectrum. La de 5 bits está conectado a los 5 bits inferiores (del 0 al 4) del bus de datos. Mediante la primera seleccionamos qué fila queremos leer (con el registro B), y mediante la segunda leemos el estado del teclado (en ceros y unos).


El aspecto de la membrana de teclado

Así, en nuestros programas podemos leer el estado del teclado accediendo a los puertos de Entrada / Salida del microprocesador a los que están conectadas las diferentes líneas de dicha matriz.


Plantilla eléctrica del teclado

Rescatemos el siguiente programa BASIC de uno de los capítulos iniciales del curso:

5  REM Mostrando el estado de la fila 1-5 del teclado ($f7fe)
10 LET puerto=63486
20 LET V=IN puerto: PRINT AT 20,0; V ; "  " : GO TO 20

Este ejemplo lee (en un bucle infinito) una de las filas del teclado, concretamente la fila de las teclas del 1 al 5. Esta fila tiene sus diferentes bits de estado conectados al puerto 63486 ($f7feh), y como podéis suponer, mediante la instrucción IN de BASIC realizamos la misma función que con su equivalente ensamblador: consultar el valor contenido en dicho puerto.

El valor $fe se corresponde con el puerto del teclado, mientras que $f7 se corresponde con 11110111 en binario, donde el cero selecciona la fila concreta del teclado que queremos leer, en este caso, la fila de teclas del 1 al 5.

Por defecto, sin pulsar ninguna tecla, los diferentes bits del valor leído en dicho puerto estarán a 1. Cuando pulsamos una tecla, el valor del bit correspondiente a dicha tecla aparecerá como 0, y soltándola volverá a su valor 1 original.

Al ejecutar el ejemplo en BASIC, veremos que la pulsación de cualquier tecla modifica el valor numérico que aparece en pantalla. Si pasamos dicho valor numérico a formato binario veremos cómo el cambio del valor se corresponde con las teclas que vamos pulsando y liberando.

Si no hay ninguna tecla pulsada, los 5 bits más bajos del byte que hay en el puerto estarán todos a 1, mientras que si se pulsa alguna de las teclas del 1 al 5, el bit correspondiente a dicha tecla pasará a estado 0. Nosotros podemos leer el estado del puerto y saber, mirando los unos y los ceros, si las teclas están pulsadas o no. Los 3 bits más altos del byte debemos ignorarlos para este propósito, ya que no tienen relación alguna con el teclado (son los bits de acceso al cassette, a la unidad de cinta y al altavoz del Spectrum).

Así, por ejemplo, leyendo del puerto 63486 ($f7fe) obtenemos un byte cuyos 5 últimos bits tienen como significado el estado de cada una de las teclas de la semifila del “1” al “5”.


Bits: D7 D6 D5 D4 D3 D2 D1 D0
Teclas: XX XX XX “5” “4” “3” “2” “1”


Como ya hemos dicho, los bits D7 a D5 no nos interesan en el caso del teclado (por ejemplo, D6 tiene relación con la unidad de cinta), mientras que los bits de D4 a D0 son bits de teclas (0=pulsada, 1=no pulsada). Tenemos pues el teclado dividido en filas de teclas y disponemos de una serie de puertos para leer el estado de todas ellas:


Puerto Teclas
65278d ($fefe) de CAPS SHIFT a V
65022d ($fdfe) de A a G
64510d ($fbfe) de Q a T
63486d ($f7fe) de 1 a 5 (y JOYSTICK 1)
61438d ($effe) de 6 a 0 (y JOYSTICK 2)
57342d ($dffe) de P a Y
49150d ($bffe) de ENTER a H
32766d ($7ffe) de (space) a B


En el resultado de la lectura de estos puertos, el bit menos significativo (D0) siempre hace referencia a la tecla más alejada del centro del teclado (“1” en nuestro ejemplo), mientras que el más significativo de los 5 (D5) lo hace a la tecla más cercana al centro del teclado.

Concretamente:


Puerto Bits: D4 D3 D2 D1 D0
65278d ($fefe) Teclas: “V” “C” “X” “Z” CAPS
65022d ($fdfe) Teclas: “G” “F” “D” “S” “A”
64510d ($fbfe) Teclas: “T” “R” “E” “W” “Q”
63486d ($f7fe) Teclas: “5” “4” “3” “2” “1”
61438d ($effe) Teclas: “6” “7” “8” “9” “0”
57342d ($dffe) Teclas: “Y” “U” “I” “O” “P”
49150d ($bffe) Teclas: “H” “J” “K” “L” ENTER
32766d ($7ffe) Teclas: “B” “N” “M” SYMB SPACE
SINCLAIR 1 y 2
(las mismas teclas 0-9):
61438d ($effe) SINCL1 LEFT RIGHT DOWN UP FIRE
63486d ($f7fe) SINCL2 FIRE DOWN UP RIGHT LEFT


Como puede verse en la tabla, la parte baja de los 16 bits del puerto representan siempre $fe (254d), el puerto al que está conectado el teclado.

La parte alta es, la única que varía según la semifila a leer, y su valor consiste, como hemos visto, en la puesta a cero de la semifila deseada, teniendo en cuenta que cada semifila de teclas está conectada a uno de los bits del bus de direcciones:

FILA DE TECLAS LINEA BUS VALOR VALOR BINARIO
CAPSSHIFT a V A8 $fe 1 1 1 1 1 1 1 0
A-G A9 $fd 1 1 1 1 1 1 0 1
Q-T A10 $fb 1 1 1 1 1 0 1 1
1-5 A11 $f7 1 1 1 1 0 1 1 1
6-0 A12 $ef 1 1 1 0 1 1 1 1
Y-P A13 $df 1 1 0 1 1 1 1 1
H-ENTER A14 $bf 1 0 1 1 1 1 1 1
B-SPACE A15 $7f 0 1 1 1 1 1 1 1

Así, al mandar estos valores en la lectura de puerto (en la parte alta del mismo), lo que hacemos realmente es seleccionar cuál de las líneas del bus de direcciones (cada una de ellas conectada a una fila del teclado) queremos leer.

En resumen: es posible obtener el estado de una tecla determinada leyendo de un puerto de Entrada / Salida del microprocesador. Este puerto de 16 bits se compone poniendo en su parte alta el valor de la semifila de teclado a leer (todo “1”s excepto un “0” en la semifila de interés, según la tabla vista anteriormente), y poniendo en su parte baja el valor $fe. El IN de dicho puerto nos proporcionará un valor de 8 bits cuyos 5 últimos bits indicarán el estado de las 5 teclas de la semifila seleccionada (1=no pulsada, 0=pulsada).

(Nótese que es posible leer el estado de 2 o más semifilas simultáneamente haciendo 0 a la vez el valor de los 2 bits del byte alto del puerto. El resultado obtenido será un AND del estado de los bits de todas las semifilas leídas).


Veamos un ejemplo en ASM que se queda en un bucle infinito hasta que pulsamos la tecla “p”:

; Lectura de la tecla "P" en un bucle
    ORG 50000
 
bucle:
    ld bc, $dffe         ; Semifila "P" a "Y"
    in a, (c)            ; Leemos el puerto
    bit 0, a             ; Testeamos el bit 0
    jr z, salir          ; Si esta a 0 (pulsado) salir.
    jr bucle             ; Si no (a 1, no pulsado) repetimos
 
salir:
    ret
 
    END 50000

De nuevo ensamblamos nuestro programa con pasmo --tapbas keyb1.asm keyb1.tap, y lo cargamos en el Spectrum o en un emulador.

Efectivamente, el programa se mantendrá en un bucle infinito hasta que se ponga a cero el bit 0 del puerto $dffe, que se corresponde con el estado de la tecla “P”. Al pulsar esa tecla, la comparación hecha con BIT hará que el Zero Flag se active y el JR Z saldrá de dicho bucle, retornando al BASIC.

En ocasiones puede ser más recomendable la utilización de IN en su formato alternativo. Recordemos que tenemos 2 opcodes para IN, con 2 formas diferentes y equivalentes de especificar el puerto:

; Forma 1
ld bc, $fffe
in a, (c)       ; A = Lectura de puerto $fffe
 
; Forma 2
ld a, $ff
in a, ($fe)     ; A = Lectura de puerto $fffe

Por ejemplo, nuestro ejemplo anterior se podría escribir con IN A, (N), y sería más recomendable, puesto que evita el tener que utilizar el registro B:

; Lectura de la tecla "P" en un bucle (FORMA 2)
    ORG 50000
 
bucle:
    ld a, $df            ; Semifila "P" a "Y"
    in a, ($fe)          ; Leemos el puerto
    bit 0, a             ; Testeamos el bit 0
    jr z, salir          ; Si esta a 0 (pulsado) salir.
    jr bucle             ; Si no (a 1, no pulsado) repetimos
 
salir:
    ret
 
    END 50000

En este ejemplo, B no se usa, usamos A para albergar la semifila a leer, que no nos afecta puesto que ya íbamos a perder su valor tras la lectura (como resultado de ella). Si estamos usando B, por ejemplo como contador de un bucle, nos interesa más esta forma de uso.

Nótese como la manera en que hemos determinado si una tecla está o no está pulsada es comprobando el estado del bit en cuestión, y no simplemente comparando el valor devuelto con el valor numérico esperado para cada tecla. Esto es muy importante porque como veremos más adelante, si no lo hacemos así podemos tener problemas en algunos modelos de Spectrum (ISSUE 2 vs ISSUE 3).


Veamos otras 2 rutinas interesantes para nuestros programas. La primera espera a que se pulse cualquier tecla (por ejemplo, para realizar una pausa), y la segunda espera a que se suelte la tecla pulsada (esta la podemos usar tras detectar una pulsación para esperar a que el usuario suelte la tecla y no volver a leer la misma tecla en una segunda iteración de nuestro bucle).

Ambas rutinas se basan en el hecho de que, realmente, es posible leer más de una semifila del teclado simultáneamente. Como ya vimos, el valor que mandamos como byte alto del puerto tiene a valor 0 el bit que representa a la línea de datos que queremos leer.

Pero como vamos a ver, podemos leer más de una línea simultáneamente, poniendo a cero tantos bits como deseemos en la parte alta del puerto. En estos ejemplos, con el xor a (que equivale a ld a, 0 dado que un XOR de un registro con sí mismo es cero) dejamos a 0 todo el byte alto con lo que leemos la información de todas las semifilas simultáneamente.

Este tipo de “multilectura” no nos permite saber de forma exacta qué tecla ha sido pulsada. Por ejemplo, si intentamos leer simultaneamente las semifilas 1-5 y Q-T, el bit 0 del resultado valdrá “0” (tecla pulsada) si se pulsa “1”, se pulsa “Q”, o se pulsan ambas.

Así, podemos saber que una de las teclas de las semifilas está siendo leída, pero no cuál de ellas. Para saber qué valor debemos enviar al puerto para leer varias semifilas de forma simultánea, recordemos la tabla vista en el anterior apartado:

FILA DE TECLAS BIT A CERO VALOR BINARIO
CAPSSHIFT a V A8 1 1 1 1 1 1 1 0
A-G A9 1 1 1 1 1 1 0 1
Q-T A10 1 1 1 1 1 0 1 1
1-5 A11 1 1 1 1 0 1 1 1
6-0 A12 1 1 1 0 1 1 1 1
Y-P A13 1 1 0 1 1 1 1 1
H-ENTER A14 1 0 1 1 1 1 1 1
B-SPACE A15 0 1 1 1 1 1 1 1

Así, para leer tanto 1-5 como Q-T, necesitamos tener 2 ceros: uno en el bit 10 y otro en el bit 11. El valor decimal que corresponde con (11110011d), 243d, nos permite la lectura de ambas semifilas de forma simultánea.

En el caso de una rutina de espera de pulsación o liberación de tecla, podemos hacer la parte alta del byte igual a cero (activar todas las semifilas) para leer todo el teclado, ya que no nos importa cuál de las teclas ha sido pulsada sino el hecho de que lo haya sido o no.

;-----------------------------------------------------------------------
; Esta rutina espera a que haya alguna tecla pulsada para volver,
; consultando las diferentes filas del teclado en un bucle.
;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
    xor a                     ; A = 0 => leer todas las semifilas
    in a, ($fe)               ; Leer del puerto del teclado
    or %11100000              ; Poner a 1 los 3 bits más altos
    inc a                     ; Comprobamos el estado del teclado con: A=A+1. 
                              ; Si A=0 => ZF = 1 => no hay tecla pulsada
                              ; Si A!=0 => ZF = 0 => hay alguna tecla pulsada
    jr z, Wait_For_Keys_Pressed
    ret
 
;-----------------------------------------------------------------------
; Esta rutina espera a que no haya ninguna tecla pulsada para volver,
; consultando las diferentes filas del teclado en un bucle.
;-----------------------------------------------------------------------
Wait_For_Keys_Released:
    xor a                     ; A = 0 => leer todas las semifilas
    in a, ($fe)               ; Leer del puerto del teclado
    or %11100000              ; Poner a 1 los 3 bits más altos
    inc a                     ; Comprobamos el estado del teclado con: A=A+1. 
                              ; Si A=0 => ZF = 1 => no hay tecla pulsada
                              ; Si A!=0 => ZF = 0 => hay alguna tecla pulsada
    jr nz, Wait_For_Keys_Released
    ret

Si necesitamos preservar el valor de A y de los flags en las llamadas a estas rutinas, podemos hacernos versiones como las utilizadas en utils.asm:

;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
    push af
wait_for_keypressed_loop:
    xor a                     ; A = 0 => leer todas las semifilas
    in a, ($fe)               ; Leer del puerto del teclado
    or %11100000              ; Poner a 1 los 3 bits más altos
    inc a                     ; Comprobamos el estado del teclado con: A=A+1. 
                              ; Si A=0 => ZF = 1 => no hay tecla pulsada
                              ; Si A!=0 => ZF = 0 => hay alguna tecla pulsada
    jr z, wait_for_keypressed_loop
    pop af
    ret
 
;-----------------------------------------------------------------------
Wait_For_Keys_Released:
    push af
wait_for_no_pressedkey_loop:
    xor a                     ; A = 0 => leer todas las semifilas
    in a, ($fe)               ; Leer del puerto del teclado
    or %11100000              ; Poner a 1 los 3 bits más altos
    inc a                     ; Comprobamos el estado del teclado con: A=A+1. 
                              ; Si A=0 => ZF = 1 => no hay tecla pulsada
                              ; Si A!=0 => ZF = 0 => hay alguna tecla pulsada
    jr nz, wait_for_no_pressedkey_loop
    pop af
    ret


En el siguiente ejemplo veremos cómo leer las 4 direcciones más la tecla de disparo en un juego y codificar la información de la lectura en una variable para cada tecla.

Esto es poco eficiente en términos de espacio y posteriormente veremos cómo almacenar todo en un único byte, pero la idea es ilustrar cómo podríamos chequear unas teclas predefinidas.

En este caso vamos a leer primero Q y luego A porque van en filas separadas, y luego veremos cómo O y P, que están en la misma semifila, sólo requiere de una lectura de puerto para ambas:

; Lee el estado de O, P, Q, A, ESPACIO.
Leer_Teclado:
    push af
    push bc
    push hl
 
    ld hl, estado_tecla_arriba
    ld (hl), 0                 ; marcamos estado_tecla_arriba como no pulsada
 
    ld bc, $fbfe
    in a, (c)
    bit 0, a                   ; Leemos la tecla Q
                               ; (podríamos haber usado "and %00000001")
    jr nz, Control_no_up       ; No pulsada, saltamos para no cambiar valor 0
    ld (hl), 1                 ; Pulsada, ponemos a 1 el bit 0 => (HL) = 1
Control_no_up:
 
    ld hl, estado_tecla_abajo
    ld (hl), 0                 ; marcamos estado_tecla_abajo como no pulsada
    ld bc, $fdfe
    in a, (c)
    bit 0, a                   ; Leemos la tecla A
    jr nz, Control_no_down     ; No pulsada, saltamos para no cambiar valor 0
    ld (hl), 1                 ; Pulsada, ponemos a 1 el bit 0 => (HL) = 1
Control_no_down:
 
    ld hl, estado_tecla_derecha
    ld (hl), 0                 ; marcamos estado_tecla_derecha como no pulsada
    ld bc, $dffe
    in a, (c)
    bit 0, a                   ; Leemos la tecla P (aqui no usar AND)
    jr nz, Control_no_right    ; No pulsada
    ld (hl), 1                 ; Pulsada, ponemos a 1 el bit 0 => (HL) = 1
Control_no_right:
                               ; O y P en misma fila, no leer puerto otra vez
    ld hl, estado_tecla_izquierda
    ld (hl), 0
    bit 1, a                   ; Tecla O
                               ; (podríamos haber usado "and %00000010")
    jr nz, Control_no_left     ; No pulsada, saltamos para no cambiar valor 0
    ld (hl), 1                 ; Pulsada, ponemos a 1 el bit 0 => (HL) = 1
Control_no_left:
 
    ld hl, estado_tecla_disp
    ld (hl), 0
    ld bc, $7ffe
    in a, (c)
    bit 0, a                   ; Tecla Espacio
    jr nz, Control_no_fire     ; No pulsada, saltamos para no cambiar valor 0
    ld (hl), 1                 ; Pulsada, ponemos a 1 el bit 0 => (HL) = 1
Control_no_fire:
 
    pop hl
    pop bc
    pop af
    ret
 
; Estado de cada tecla.  0 = no pulsada, 1 = pulsada
estado_tecla_izq      DEFB  0
estado_tecla_der      DEFB  0
estado_tecla_arriba   DEFB  0
estado_tecla_abajo    DEFB  0
estado_tecla_disp     DEFB  0

El bucle principal del programa deberá llamar a esta función de lectura del teclado y después, con el valor 0/1 presente en las diferentes variables, actuar consecuentemente. De esta forma, al volver de la llamada a esta subrutina sabremos si el usuario pretende mover el personaje en una dirección u otra, o si ha pulsado disparo, según el estado de las variables. La rutina que hemos visto trabaja con 5 teclas, pero podríamos añadir más variables para más teclas como un segundo disparo, pausa, etc.

Con esta información sobre el teclado y las instrucciones IN y OUT, nada nos impide dotar de interacción y movimiento vuestros programas, utilizando la lectura del teclado en menúes, acciones sobre los personajes, etc.

Por otra parte, el código que acabamos de ver se basa en leer las semifilas de teclado y comprobar el estado de teclas concretas y definidas (“hardcodeadas”, es decir, “fijadas manualmente en el código”) en el propio listado del programa. Esto quiere decir que las teclas de control son fijas y no seleccionables por el usuario. Este es el mecanismo de “lectura de teclado” más rápido y que menos espacio ocupa puesto que no es necesario crear rutinas para re-definir las teclas de juego, almacenarlas en variables de memoria y comparar el estado del teclado de los scancodes seleccionados por el usuario.

No obstante, una de las cosas que más agradecen los usuarios es la posibilidad de que las teclas de control se puedan redefinir y no sean fijas, por lo que a continuación veremos mecanismos para obtener del usuario las teclas de control y posteriormente poder detectar su pulsación en el transcurso de la lógica del programa o juego.


Hasta ahora hemos visto cómo verificar el estado de unas teclas predeterminadas del teclado. Normalmente los juegos o programas suelen incluir la opción de redefinir las teclas asociadas a las acciones del juego, de forma que sea el jugador quien elija la combinación de teclas con la que se sienta más cómodo.

Para ello necesitaremos una rutina que haga lo siguiente:


  • Antes de ser llamada, deberemos llegar a ella con el mensaje apropiado en pantalla (“Pulse tecla para ARRIBA”, por ejemplo). La rutina se dedicará sólo al escaneo del teclado en sí mismo, de forma que pueda ser llamada tantas veces como teclas a redefinir. Además, tenemos que asegurarnos de que cuando la llamamos no haya ninguna tecla pulsada. Para eso podemos usar la rutina Wait_For_Keys_Releasedvista previamente.
  • La rutina deberá devolver un valor único para cada tecla pulsada, y asegurarse (o informarnos) de que no está siendo pulsada más de una tecla simultáneamente.
  • Dicho valor devuelto por la rutina será almacenado para su posterior chequeo durante el juego, utilizando una rutina que nos indique si la tecla asociada a ese “valor único” está siendo pulsada.


David Webb, en su libro Lenguaje máquina avanzado para el ZX Spectrum, nos ofrece el siguiente conjunto de rutinas para este propósito. Existen bastantes posibilidades de realizar esta tarea (tablas con las semifilas y bits y sus correspondientes en ASCII, modificación en tiempo real de los opcodes que hacen los testeos, elegir entre un conjunto de combinaciones de teclas predeterminadas, etc), pero la que nos muestra David Webb es elegante y sencilla de utilizar.

Consiste en escanear el teclado completo y, al detectar la pulsación de una tecla, codificar la semifila y el bit donde se han detectado en un mismo byte, utilizando los 3 bits más bajos para “el bit de la tecla pulsada” y los 3 siguientes para “la semifila (puerto)” en que se ha detectado la pulsación.

;-----------------------------------------------------------------------
; Chequea el teclado para detectar la pulsación de una tecla.
; Modifica A, H, DE y BC.
; Devuelve un código en el registro D que indica:
;
;    Bits 0, 1 y 2 de "D": Semifila de teclas (puerto) detectada.
;    Bits 3, 4 y 5 de "D": Tecla (posición) en esa semifila
;    => (00TTTSSS)
;
; Así, el valor devuelto nos indica la semifila a leer y el bit a testear.
;
; El registro D valdrá 255 ($ff) si no hay ninguna tecla pulsada.
;
; Flags: ZF 0: Más de una tecla pulsada
;        ZF 1: Tecla correctamente leída
;-----------------------------------------------------------------------
Find_Key:
    ld de, $ff2f         ; Valor inicial "ninguna tecla"
    ld bc, $fefe         ; Puerto
 
NXHALF:
    in a, (c)
    cpl
    and %00011111
    jr z, NPRESS         ; Saltar si ninguna tecla pulsada
 
    inc d                ; Comprobamos si hay más de 1 tecla pulsada
    ret nz               ; Si es así volver con Z a 0
 
    ld h, a              ; Cálculo del valor de la tecla
    ld a, e
 
KLOOP:
    sub 8
    srl h
    jr nc, KLOOP
 
    ret nz               ; Comprobar si más de una tecla pulsada
 
    ld d, a              ; Guardar valor de tecla en D
 
NPRESS:                  ; Comprobar el resto de semifilas
    dec e
    rlc b
    jr c, NXHALF         ; Repetimos escaneo para otra semifila
 
    cp a                 ; Ponemos flag a zero
    ret z                ; Volvemos

La forma en que llamaríamos a esta subrutina sería la siguiente:

    ;; Pedimos tecla ARRIBA
    call Imprimir_Texto_Pulse_Arriba
    call Wait_For_Keys_Released     ; Esperamos teclado libre
 
Pedir_Arriba:
 
    call Find_Key                   ; Llamamos a la rutina
    jr nz, Pedir_Arriba             ; Repetir si la tecla no es válida
    inc d
    jr z, Pedir_Arriba              ; Repetir si no se pulsó ninguna tecla
    dec d
 
    ld a, d
    ld (tecla_arriba), a
 
    ;; Pedimos siguiente tecla (ABAJO)
    call Imprimir_Texto_Pulse_Abajo
    call Wait_For_Keys_Released     ; Esperamos teclado libre
 
Pedir_Abajo:
 
    call Find_Key                   ; Llamamos a la rutina
    jr nz, Pedir_Abajo              ; Repetir si la tecla no es válida
    inc d
    jr z, Pedir_Abajo               ; Repetir si no se pulsó ninguna tecla
    dec d
 
    ld a, d
    ld (tecla_abajo), a
 
    ;;; Repetir el mismo código para IZQ, DERECHA, DISPARO, etc.
 
tecla_arriba DEFB 0
tecla_abajo  DEFB 0

A continuación podemos ver un ejemplo (scancode.asm) que imprime en pantalla el SCANCODE que devuelve la función Find_Key:

; Visualizando los scancodes de las teclas codificadas con "Find_Key"
 
    ORG 50000
 
Bucle_entrada:
    call Wait_For_Key
 
 
Pedir_Tecla:
    call Find_Key                   ; Llamamos a la rutina
 
    jr nz, Pedir_Tecla              ; Repetir si la tecla no es valida
    inc d
    jr z, Pedir_Tecla               ; Repetir si no se pulsa ninguna tecla
    dec d
 
    ld a, d                         ; Guardamos en A copia del resultado
 
    cp $21                          ; Comprobamos si A == 21h (enter)
    ret z                           ; Si no lo es, repetir
 
    call PrintHex                   ; Imprimimos el scancode hex en pantalla
 
    call PrintSpace                 ; Espacio para separar
 
    call Wait_For_No_Key            ; Esperamos a que el usuario SUELTE la tecla
    jr Pedir_Tecla                  ; Repetir hasta que arriba un ENTER
                                   ; ($21 pulsado) haga el ret z a BASIC
 
    INCLUDE "utils.asm"
 
; Debemos incluir, además, el código de Find_Key dentro de
; este ejemplo para que ensamble correctamente.

Este ejemplo proporcionará en pantalla (hasta que se pulse ENTER) una salida como la siguiente:


Representación binaria de las teclas pulsadas

Los scancodes asociados a las diferentes teclas son:


Teclas: 1 2 3 4 5 6 7 8 9 0
Scancodes: $24 $1c $14 $0c $04 $03 $0b $13 $1b $23
Teclas: Q W E R T Y U I O P
Scancodes: $25 $1d $15 $0d $05 $02 $0a $12 $1a $22
Teclas: A S D F G H J K L ENTER
Scancodes: $26 $1e $16 $0e $06 $01 $09 $11 $19 $21
Teclas: CAPS Z X C V B N M SYMB SPACE
Scancodes: $27 $1f $17 $0f $07 $00 $08 $10 $18 $20


O, en forma visual sobre un teclado de 48K, extraído del libro Mastering Machine Code on your ZX Spectrum (los valores numéricos están en formato hexadecimal pese a no llevar prefijo ni sufijo):


scancodes_gomas.jpg

Estos valores nos serán necesarios si queremos establecer unos scancodes por defecto para las teclas del programa, de forma que si el usuario no las redefine, tengan unos valores de comprobación determinados para la rutina de chequeo que veremos a continuación.


En las secciones anteriores hemos visto las rutinas Find_Key y Check_Key para detectar las pulsaciones de teclas y chequear el estado de una tecla concreta. Estas teclas concretas las guardamos en variables de memoria y así podemos permitir al jugador redefinirlas.

El menú principal deberá de tener una opción que permita modificar el contenido de estas variables de memoria con aquellos scancodes que el jugador elija para controlar el juego.

El sistema de redefinición de teclas debe:

A.- Establecer en el arranque del programa unos valores por defecto para las teclas:

tecla_arriba   DEFB  $25
tecla_abajo    DEFB  $26
tecla_izq      DEFB  $1a
tecla_der      DEFB  $22
tecla_disp     DEFB  $20

B.- Repetir N veces (uno por cada control a redefinir):

  • Esperar a que ninguna tecla del teclado esté pulsada (para evitar que la tecla de selección del menú para entrar en la redefinición, o la anterior tecla pulsada, se seleccione como tecla pulsada por el usuario).
  • Mostrar por pantalla el mensaje de “Pulse una tecla para (dirección a redefinir)”.
  • Esperar una pulsación de teclado del usuario.
  • Opcionalmente, comprobar que esa pulsación no se corresponda con ninguna de las teclas anteriores, para evitar que el usuario seleccione la misma dirección para, por ejemplo, izquierda y derecha. Este paso es opcional porque el usuario, si se equivoca, siempre puede redefinir de nuevo el teclado con las teclas adecuadas, y para nosotros esta comprobación representa tiempo de programación y espacio ocupado innecesariamente en el programa.
  • Mostrar al usuario la tecla que ha pulsado a la derecha del mensaje impreso pidiendo dicha tecla. Para eso tenemos que convertir el Scancode en un código ASCII imprimible en pantalla.
  • Modificar la variable en memoria que deba almacenar el scancode de la tecla pulsada para poder usarla posteriormente en el transcurso del juego (es decir, guardar el scancode obtenido en tecla_arriba, tecla_abajo, tecla_izq, o en la variable que corresponda).

Tenemos hasta ahora todos los mecanismos necesarios para crear nuestra propia rutina de redefinición de teclas, salvo la rutina para convertir un scancode en su correspondiente ASCII. A continuación tenemos una rutina Scancode2Ascii basada en una tabla que relaciona cada scancode con su ASCII (40 bytes más adelante en la misma tabla):

;-----------------------------------------------------------------------
; Scancode2Ascii: convierte un scancode en un valor ASCII
; IN:  D = scancode de la tecla a analizar
; OUT: A = Codigo ASCII de la tecla (0-9 y A-Z)
;      minusculas: e = ENTER, s = SPACE, c = CAPSSHIFT e = SYMBOLSHIFT
;-----------------------------------------------------------------------
Scancode2Ascii:
    push hl
    push bc
 
    ld hl, 0
    ld bc, TABLA_Scancode2ASCII
    add hl, bc                    ; HL apunta al inicio de la tabla
 
    ; buscamos en la tabla un max de 40 veces por el codigo
    ; le sumamos 40 a HL, leemos el valor de (HL) y ret A
SC2Ascii_1:
    ld a, (hl)                   ; leemos un byte de la tabla
    cp '1'                       ; Si es '1' (caracter) fin de la rutina (porque
                                 ; en la tabla habriamos llegado a los ASCIIs)
    jr z, SC2Ascii_Exit          ; (y es condicion de forzado de salida)
    inc hl                       ; incrementamos puntero de HL
    cp d                         ; comparamos si A==D (nuestro scancode)
    jr nz, SC2Ascii_1
 
SC2Ascii_Found:
    ld bc, 39                    ; Sumamos 39(+inc hl=40) para ir a la
    add hl, bc                   ; seccion de la tabla con el codigo ASCII
    ld a, (hl)                   ; leemos el codigo ASCII de esa tabla
 
SC2Ascii_Exit:
    pop bc
    pop hl
    ret
 
    ; 40 scancodes seguidos de sus ASCIIs equivalentes
TABLA_Scancode2ASCII:
    DEFB $24, $1c, $14, $0c, $04, $03, $0b, $13, $1b, $23
    DEFB $25, $1d, $15, $0d, $05, $02, $0a, $12, $1a, $22
    DEFB $26, $1e, $16, $0e, $06, $01, $09, $11, $19, $21
    DEFB $27, $1f, $17, $0f, $07, $00, $08, $10, $18, $20
    DEFB "1234567890QWERTYUIOPASDFGHJKLecZXCVBNMys"

La rutina recibe en el registro D el scancode obtenido con la rutina Find_Key y devuelve en el registro A el código ASCII correspondiente directamente imprimible. Los primeros 40 bytes de la tabla contienen los Scancodes y la última línea DEFB (últimos 40 bytes) los ASCIIs a los que corresponden, en orden, los anterior 40 códigos.

Nótese que las teclas se devuelven como ASCIIs en mayúsculas, aprovechando las letras minúsculas para los caracteres especiales no directamente representables:


  • e = ENTER
  • c = CAPS SHIFT
  • y = SYMBOL SHIFT
  • s = SPACE


De esta forma, “E” se corresponde a la tecla E y “e” a la tecla de ENTER. Podemos utilizar estos ASCIIs en minúsculas para mostrar en pantalla cadenas como “ENTER” o “SPACE” durante la redefinición de las teclas. También podríamos color UDGs preparados para ENTER, ESPACIO, CS o SS, o bien usar cadenas en vez de caracteres para poder imprimir “ENTER” en lugar de un símbolo de Enter.

A continuación podemos ver un ejemplo que utiliza las rutinas Find_Key y Scancode2Ascii para mostrar en pantalla el código ASCII de cualquier tecla pulsada:

; Prueba de conversion de Scancode a ASCII
 
    ORG 50000
 
    call CLS
 
START:
 
    call Wait_For_No_Key
 
chequear_teclas:
    call Find_Key                   ; Llamamos a la rutina
    jr nz, chequear_teclas          ; Repetir si la tecla no es válida
    inc d
    jr z, chequear_teclas           ; Repetir si no se pulsó ninguna tecla
    dec d
 
    ; En este punto D es un scancode valido
    call Scancode2Ascii
 
    ; En este punto A contiene el ASCII del scancode en D
    ; lo imprimimos por pantalla con rst 16.
    rst 16
 
    jr START                        ; vuelta a empezar
 
    INCLUDE "utils.asm"
 
;;--- Introducir aquí las rutinas Find_Key y Scancode2ASCII ------------
 
    END 50000

Una vez en ejecución y tras pulsar múltiples teclas, este es el aspecto del programa anterior:


Conversión de Scancode a ASCII

Juntemos ahora todas las funciones que hemos desarrollado en un programa de ejemplo que permite al usuario redefinir la teclas.

El programa deberá:

  • Imprimir los diferentes mensajes al usuario (“Izquierda?” “Derecha?”)
  • Después de cada mensaje, esperar a que se pulse una tecla.
  • Almacenar el scancode en la variable de tecla correspondiente.
  • Convertir el scancode en un ASCII e imprimirlo por pantalla para informar al usuario visualmente de la tecla que pulsó.
  • Al acabar el proceso, tendremos los scancodes en las variables tecla_*.


; Redefinir teclas y ver el estado de las teclas elegidas "en el juego".
    ORG 33500
 
    call CLS
    call Redefinir_Teclas
 
bucle:
    jr bucle
 
; Mensajes del programa
msg_izq        DEFB  "Izquierda? ", _EOS
msg_der        DEFB  "Derecha? ", _EOS
msg_arriba     DEFB  "Arriba? ", _EOS
msg_abajo      DEFB  "Abajo? ", _EOS
msg_disp       DEFB  "Disparo? ", _EOS
 
; Teclas por defecto si no se redefine: O P Q A SPACE
tecla_izq      DEFB  $1a
tecla_der      DEFB  $22
tecla_arriba   DEFB  $25
tecla_abajo    DEFB  $26
tecla_disp     DEFB  $20
 
;-----------------------------------------------------------------------
; Utiliza Redefine_Key para obtener en las variables "tecla_*" los
; scancodes de las diferentes teclas que selecciona el usuario.
; Llama a "Redefine_Key" para mostrar el mensaje y pedir la tecla.
;
; ENTRADA: Nada
; SALIDA:  Nada
; MODIFICA: AF, DE, HL, Flags
;-----------------------------------------------------------------------
Redefinir_Teclas:
    ; Los siguientes textos se podrian haber impreso tambien usando un bucle
    ; con inc de (avanzar cadena) e inc hl (avanzar tecla a escribir)
    ld de, msg_izq
    call PrintString              ; Imprimir mensaje "Izquierda?"
    call Redefine_Key             ; Esperar pulsacion (e imprimir ASCII)
    ld hl, tecla_izq              ; Apuntamos HL a la variable tecla_izq
    ld (hl), a
 
    ld de, msg_der                ; Siguiente mensaje: "Derecha?"
    call PrintString
    call Redefine_Key             ; Esperar pulsacion (e imprimir ASCII)
    ld hl, tecla_der
    ld (hl), a                    ; Guardamos tecla pulsada
 
    ld de, msg_arriba             ; Repetimos con ARRIBA
    call PrintString
    call Redefine_Key
    ld hl, tecla_arriba
    ld (hl), a
 
    ld de, msg_abajo              ; Repetimos con ABAJO
    call PrintString
    call Redefine_Key
    ld hl, tecla_abajo
    ld (hl), a
 
    ld de, msg_disp               ; Repetimos con DISPARO
    call PrintString
    call Redefine_Key
    ld hl, tecla_disp
    ld (hl), a
    ret
 
;-----------------------------------------------------------------------
; Utiliza Find_Key para obtener una tecla válida. Se queda en un bucle
; de espera hasta que una sola tecla esté pulsada, y la devuelve en A.
; Además, imprime por pantalla el codigo ASCII de la tecla.
;
; ENTRADA: Nada
; SALIDA:  A = scancode
; MODIFICA: Flags
;-----------------------------------------------------------------------
Redefine_Key:
    push de
    push hl
    call Wait_For_No_Key
 
wait_for_scan_loop:
    call Find_Key
    jr nz, wait_for_scan_loop          ; Mas de una tecla leida, repetir
    ld a, d
    cp $ff                             ; si A es $ff => ninguna tecla pulsada
    jr z, wait_for_scan_loop           ; Repetimos hasta que A != $ff
    ld h, d                            ; Nos hacemos copia de D en H
    call Scancode2Ascii                ; Convertir D (scancode) en A (ASCII)
 
    cp 'e'                             ; ¿Es 'e'? Imprimir "ENTER"
    jr nz, redef_key_NO_ENTER
    ld de, redef_key_enter
    call PrintString
    jr redef_key_end                   ; Impreso texto, salimos
redef_key_NO_ENTER:
 
    cp 's'                             ; ¿Es 's'? Imprimir "SPACE"
    jr nz, redef_key_NO_SPACE
    ld de, redef_key_space
    call PrintString
    jr redef_key_end
redef_key_NO_SPACE:
 
    cp 'c'                             ; ¿Es 'c'? Imprimir "CS"
    jr nz, redef_key_NO_CAPSSHIFT
    ld de, redef_key_cs
    call PrintString
    jr redef_key_end
redef_key_NO_CAPSSHIFT:
 
    cp 'y'                             ; ¿Es 'y'? Imprimir "SS"
    jr nz, redef_key_NO_SYMBOLSHIFT
    ld de, redef_key_ss
    call PrintString
    jr redef_key_end
redef_key_NO_SYMBOLSHIFT:
                                       ; Si llegamos aqui no era tecla especial.
    rst 16                             ; Ninguna tecla especial => Print ASCII
    ld a, d
 
redef_key_end:
    ld a, h                            ; Recuperamos scancode
    call PrintCR                       ; Imprimir retorno de carro
 
    pop hl
    pop de
    ret                                ; Volver con registros preservados
 
redef_key_enter DB "ENTER", _EOS
redef_key_space DB "SPACE", _EOS
redef_key_ss    DB "SS", _EOS
redef_key_cs    DB "CS", _EOS
 
;;; Insertar aqui el codigo de Find_Key
;;; Insertar aqui el codigo de Scancode2ASCII
 
    ;-- Incluir libreria de utilidades --
    INCLUDE "utils.asm"
 
    END 33500

Para esto, hemos creado 2 nuevas funciones:

  • Redefinir_Teclas: Imprime los mensajes y llama a una función para redefinir cada tecla.
  • Redefine_Key: Lee el teclado con Find_Key en un bucle que se repite mientras no haya ninguna tecla pulsada, y después imprimir por pantalla el ASCII pulsado. Si el ASCII es 'e', 's', 'c' o 'y' es porque estamos ante una de las teclas especiales (ENTER, SPACE, CAPS-SHIFT o SYMBOL-SHIFT) por lo que en ese caso lo que se imprime no es el ASCII sino una cadena.

Este es el aspecto del programa en ejecución:


Redefinición del teclado


Si en lugar de imprimir las cadenas “ENTER”, “SPACE”, “SS” y “CS” (como se hace en el ejemplo anterior) queremos imprimir unos caracteres con símbolos compactos para las teclas, podemos utilizar UDGs para la tabla de Scancode2Ascii.

Nuestro compañero Juan Antonio Rubio nos proporciona los siguientes UDGs que podemos “pokear” en la dirección adecuada para utilizarlos con los código de carácter que deseemos:

    DB $10,$28,$44,$C6,$28,$28,$38,$00 ; Caps Shift
    DB $60,$80,$46,$28,$C4,$02,$0C,$00 ; Symbol Shift
    DB $05,$05,$25,$5D,$81,$5E,$20,$00 ; Enter
    DB $00,$00,$00,$00,$82,$82,$FE,$00 ; Espacio

Los cuales tienen el siguiente aspecto:




Llegados a este punto disponemos de una función que nos devuelve el scancode de una tecla pulsada, y una función de redefinición de teclas que permite que el usuario pulse la tecla correspondiente, y nosotros podamos almacenar en variables de memoria los valores que nos devuelve dicha función. En lugar de dar a estas variables un valor de 0 por defecto, tenemos una tabla de “scancodes” que nos permitiría definir unas “teclas iniciales” (como O, P, Q, A, ESPACIO) si el jugador decide no utilizar la función de redefinición de teclado:

tecla_izq     DEFB  $1a       ; O
tecla_der     DEFB  $22       ; P
tecla_arriba  DEFB  $25       ; Q
tecla_abajo   DEFB  $26       ; A
tecla_disp    DEFB  $20       ; SPACE

Como hemos dicho, estos valores podrán ser modificados (o no) por la rutina de redefinición del teclado en el menú del juego.

Lo único que nos falta para desarrollar la lectura del teclado en el juego sería una rutina que reciba un scancode y nos indique si dicho scancode está pulsado o no. De esta forma, llamaríamos a la rutina 5 veces, poniendo el valor de las diferentes teclas (tecla_arriba, tecla_abajo, etc.) en el registro A antes de cada llamada, para conocer el estado de las mismas.

Llamaremos a esta rutina Check_Key:

;-----------------------------------------------------------------------
; Chequea el estado de una tecla concreta, aquella de scancode
; codificado en A (como parametro de entrada).
;
; Entrada:     A = scancode (00SSSTTT).
; Devuelve:    CARRY FLAG = 0 -> Tecla pulsada
;              CARRY FLAG = 1 y BC = 0 -> Tecla no pulsada
;-----------------------------------------------------------------------
Check_Key:
    push bc
    ld c, a             ; Copia de A
 
                        ; Operaciones para extraer la semifila
                        ; y la tecla de A (00SSSTTT), para leer
                        ; el puerto y chequear el bit adecuados
    and %00000111       ; Nos quedamos con la tecla (bit)
    inc a
    ld b, a             ; B = 16 - (num. linea direccion)
    srl c
    srl c
    srl c
    ld a, 5
    sub c
    ld c, a             ; C = (semifila + 1)
 
    ld a, $fe
 
CKHiFind:               ; Calcular el octeto de mayor peso del puerto
    rrca
    djnz CKHiFind
    in a, ($fe)         ; Leemos la semifila
 
CKNXKey:
    rra
    dec c
    jr nz, CKNXKey      ; Ponemos el bit de tecla en el CF
    pop bc
 
    ret

La forma más básica en que se debería llamar a esta rutina en el código de nuestro programa sería la siguiente:

Comprobar_tecla_izquierda:
    ld a, (teclaizq)
    call Check_Key
    jr c, izq_no_pulsada            ; Carry = 1, tecla no pulsada
 
    (acciones a realizar si se pulso izq)
 
izq_no_pulsada:
 
Comprobar_tecla_derecha:
    ld a, (teclader)
    call Check_Key
    jr c, der_no_pulsada            ; Carry = 1, tecla no pulsada
 
    (acciones a realizar si se pulso der)
 
    ; Repetir para arriba, abajo, disparo, etc.

Veamos a continuación un ejemplo final que permite modificar el valor de una variable en memoria (“valor”) mediante las teclas Q y A (sumando o restando 1 a su valor de 8 bits). Cada vez que el valor de la variable cambie, se mostrará en pantalla con nuestra la rutina PrintNum.

; Controlando el valor de "valor" con Q y A
    ORG 50000
 
    call CLS
Imprimir_Valor:
    ld a, (valor)                   ; Guardamos en A copia del resultado
    ld b, 0
    ld c, a                         ; BC = A (B=0, C=A)
    call PrintNum                   ; Imprimimos el valor en pantalla
    call PrintSpace
 
    call Wait_For_No_Key            ; Esperamos a que se suelte la tecla
 
Bucle:
 
Comprobar_tecla_mas:
    ld a, (tecla_mas)
    call Check_Key
 
    jr c, Comprobar_tecla_menos     ; Carry = 1, tecla_mas no pulsada
 
    ld hl, valor
    inc (hl)
    jr Imprimir_Valor
 
Comprobar_tecla_menos:
    ld a, (tecla_menos)
    call Check_Key
    jr c, Bucle                     ; Carry = 1, tecla_menos no pulsada
 
    ld hl, valor
    dec (hl)
 
    jp Imprimir_Valor
 
; Variables de teclas
tecla_mas    DEFB   $25
tecla_menos  DEFB   $26
 
; Variable para alojar el valor
valor        DEFB  0
 
    INCLUDE "utils.asm"
    ;; Nota: Incluir también en el código el código de Check_Key.
 
    END 50000



Queda como ejercicio para el lector la modificación del programa para que, antes de entrar en el bucle principal, utilice nuestras funciones de redefinición de teclado y lea 2 teclas válidas para cambiar el valor de “tecla_mas” y “tecla_menos” con respecto a sus valores por defecto.


En el bucle principal de nuestro juego deberemos leer el teclado para determinar si el usuario está pulsando alguna de las teclas de dirección redefinidas. Para ello deberemos utilizar Check_Key con cada una de las 5 teclas (5 scancodes) que tenemos guardados en las variables tecla_* (tecla_izq, tecla_der, etc.) y actuar en consecuencia.

Ya hemos hablado de la importancia de empaquetar las diferentes funciones en rutinas reutilizables, así que en este punto deberíamos desarrollar una rutina de nombre Leer_Teclado la cual realizara todas las llamadas a Check_Key y nos indicara de alguna forma qué teclas están pulsadas de las que hemos redefinido. ¿Cómo debería devolvernos esa rutina el estado de las diferentes teclas? ¿En 5 registros? ¿En variables de memoria (1 byte por cada tecla), como hemos hecho en un ejemplo anterior? Evidentamente, eso es un desperdicio de espacio.

Lo más óptimo para almacenar ese estado es empaquetar en los diferentes estados como los bits de un byte, de un único registro de 8 bits. Por ejemplo, así:

; BITS            4     3     2     1    0
; SIGNIFICADO   FIRE  LEFT  RIGHT  UP   DOWN

Después de leer las 5 teclas redefinidas, dejaremos a 0 los bits correspondientes de cada tecla que no esté pulsada, y a 1 los bits de aquellas teclas pulsadas. Podemos devolver este valor en el registro A, por ejemplo, y al volver de la rutina almacenarlo en memoria en alguna variable de estado. Esto nos permitirá después en el resto del flujo del programa comprobar qué teclas están pulsadas:

    call Leer_Teclado_Empaquetado
    ld (estado_teclas), a
 
    ; (...)
 
    ; Mas adelante en el programa
    ld a, (estado_teclas)
 
    bit 4, a
    call nz, Disparo_Pulsado
 
    bit 1, a
    call nz, Salto_Pulsado
 
    ; etc...
 
estado_teclas  DEFB 0

Además, este mecanismo de empaquetado nos permite disponer más de las 5 teclas de las que hemos hablado, ya que nos quedan otros 3 bits libres (BIT 5, 6 y 7) los cuales podrían ser por ejemplo DISPARO_2, PAUSA y SALIR (o INVENTARIO, o AYUDA…).

Veamos un ejemplo de rutina Leer_Teclado_Empaquetado:

;-----------------------------------------------------------------------
; Lee el estado de las teclas definidas en variables y almacena en A
; dicho estado (1=pulsada, 0=no pulsada). El byte está codificado así:
;
; BITS            4    3    2     1    0
; SIGNIFICADO   FIRE LEFT RIGHT  UP DOWN
;
; ENTRADA: NADA (usa las variables tecla_*)
;          Se podria modificar para recibir HL = direccion tecla1
; SALIDA:  A = byte de estado de las teclas (empaquetadas en bits).
; MODIFICA: A, HL y CarryFlag
;-----------------------------------------------------------------------
Leer_Teclado_Empaquetado:
    push de
    ld d, 0                       ; D = 0
 
    ld hl, tecla_izq              ; Apuntamos HL a la primera de las teclas (izq)
 
    ld a, (hl)                    ; leemos su valor
    call Check_Key
    jr c, tecl_izq_notpressed
    set 3, d                      ; Si pulsada, ponemos bit 3 a 1.
                                  ; Usando A podríamos usar "or %00001000"
tecl_izq_notpressed:
    inc hl                        ; inc hl a siguiente tecla en memoria (der)
    ld a, (hl)                    ; leemos su valor
    call Check_Key
    jr c, tecl_der_notpressed
    set 2, d                      ; Si pulsada, ponemos bit 2 a 1.
 
tecl_der_notpressed:
    inc hl                        ; apuntamos HL a siguiente tecla (arriba)
    ld a, (hl)                    ; leemos su valor
    call Check_Key
    jr c, tecl_arr_notpressed
    set 1, d                      ; Si pulsada, ponemos bit 1 a 1.
 
tecl_arr_notpressed:
    inc hl                        ; apuntamos HL a siguiente tecla (abajo)
    ld a, (hl)                    ; leemos su valor
    call Check_Key
    jr c, tecl_aba_notpressed
    set 0, d                      ; Si pulsada, ponemos bit 0 a 1.
 
tecl_aba_notpressed:
    inc hl                        ; apuntamos HL a siguiente tecla (disparo)
    ld a, (hl)                    ; leemos su valorprimera
    call Check_Key
    jr c, tecl_fire_notpressed
    set 4, d                      ; Si pulsada, ponemos bit 4 a 1.
 
tecl_fire_notpressed:
                                  ; Podriamos añadir codigo para disparo 2
    ld a, d
    pop de
    ret                           ; Devolvemos en A el estado de las teclas

La rutina simplemente apunta HL a la primera de las teclas y llama a Check_Key para leer su estado. Si está a 1, establece a 1 el bit 3 del registro D. Si está a 0, salta a comprobar la siguiente tecla.

Utilizamos inc hl para saltar a la siguiente tecla porque las variables tecla_* están seguidas en memoria, por lo que podemos saltar de una a otra con INC o DEC.

Repetimos el proceso con las 5 teclas (podríamos añadir más chequeos para teclas adicionales después de comprobar el disparo) y antes de salir copiamos el valor de D en A para devolver el resultado en el acumulador.

A continuación se puede ver un ejemplo de programa completo el cual utiliza la redefinición de teclas y muestra por pantalla el valor de la variable de estado empaquetada tal cual la veríamos durante el bucle principal de nuestro juego:

; Redefinir teclas y ver el estado de las teclas elegidas "en el juego".
    ORG 33500
 
    call CLS
    call Redefinir_Teclas
    call CLS
    call Wait_For_No_Key          ; Esperar a que no haya teclas pulsadas
 
    ; Bucle del programa, lee nuestras teclas e imprime primero la
    ; "leyenda" de las teclas y luego el byte de estado de teclado:
bucle:
    ld de, 0
    call CursorAt                 ; Nos vamos a (0,0)
    ld de, msg_keys
    call PrintString              ; Imprimimos mensaje "*<>^v"
 
    call Leer_Teclado_Empaquetado
    call PrintBin                 ; Imprimimos estado teclas (A) en binario
 
    jr bucle                      ; Repetir hasta reset
 
; Mensajes del programa
msg_izq        DEFB  "Izquierda? ", _EOS
msg_der        DEFB  "Derecha? ", _EOS
msg_arriba     DEFB  "Arriba? ", _EOS
msg_abajo      DEFB  "Abajo? ", _EOS
msg_disp       DEFB  "Disparo? ", _EOS
msg_keys       DEFB  "    *<>^v", _CR, _EOS
 
; Teclas por defecto si no se redefine: O P Q A SPACE
tecla_izq      DEFB  $1a
tecla_der      DEFB  $22
tecla_arriba   DEFB  $25
tecla_abajo    DEFB  $26
tecla_disp     DEFB  $20
 
;;; Insertar aqui el codigo de Redefinir_Teclas
;;; Insertar aqui el codigo de Redefine_Key
;;; Insertar aqui el codigo de Leer_Teclado_Empaquetado
;;; Insertar aqui el codigo de Find_Key
;;; Insertar aqui el codigo de Scancode2ASCII
;;; Insertar aqui el codigo de Check_Key
 
    ;-- Incluir libreria de utilidades --
    INCLUDE "utils.asm"
 
    END 33500


Programa de ejemplo estado del teclado empaquetado en byte


Hasta este punto hemos creado una serie de rutinas que hacen lo siguiente:

  • Find_Key: Devuelve el scancode de una tecla pulsada. Este scancode tiene codificada la semifila y el bit a testear en un sólo byte, como (00SSSTTT). Sólo es llamada durante el proceso de redefinición del teclado.
  • Redefinir_Teclas y Redefine_Key: funciones para redefinir el teclado que llaman a Find_Key y almacenan el scancode devuelto por esta en variables del tipo tecla_izq DEFB 0. Estas funciones son llamadas para redefinir del teclado, en el menú del juego, y no durante el desarrollo del mismo.
  • Check_Key: Recibe como parámetro un scancode y hace una serie de operaciones con bits para extraer SSS (semifila) y TTT (tecla). Después realiza la lectura del puerto correspondiente y comprueba el bit correspondiendo (desplazándolo hasta el CarryFlag).
  • Leer_Teclado_Empaquetado: Esta es la función a la que llamamos en el bucle del juego para leer el estado de las diferentes teclas. Hace uso interno de Check_Key para cada una de las teclas que tenemos definidas en variables (conteniendo los scancodes para cada dirección o botón de acción).

Nótese que Find_Key y el resto de funciones asociadas para redefinir el teclado son llamadas en el menú del juego, por lo que no necesitamos que sean especialmente eficientes y optimizadas.

Por contra, con nuestra aproximación a la lectura del teclado, estamos llamando a Check_Key (a través de Leer_Teclado_Empaquetado) N veces (por cada tecla que necesitamos leer). En cada una de estas llamadas estamos (dentro de Check_Key) ejecutando el código que extrae la semifila (puerto) y número de tecla de cada scancode.

Esto es un tiempo valiosísimo desperdiciado dentro del bucle principal del programa, y que tenemos que repetir en cada iteración del mismo. Cada nueva ejecución del bucle del programa, tenemos que volver a sacar la semifila y la tecla de cada uno de los 5 scancodes (izquierda, derecha, arriba y abajo) dentro de Check_Key.

Lo lógico, dado que esos valores no cambian durante todo el juego, es que hagamos esa separación de scancode en semifila y columna durante el proceso de redefinición del juego, y no durante el bucle del mismo.

En lugar de guardarnos las teclas seleccionadas por el usuario como 1 byte de scancodes por tecla, los guardaremos en 2 bytes: semifila (representada directamente por el puerto a leer) y bit o posición de tecla a comprobar.

Asi, el chequeo de la tecla durante el juego será mucho más rápido.

Al principio del curso comentamos que no debemos obsesionarnos con la optimización extrema de las rutinas que hagamos para nuestros programas. Dijimos que simplemente debemos de buscar que sean eficientes y que funcionen bien, y esto es correcto para la mayoría que rutinas que realicemos.

Pero aquellas rutinas que se ejecutan en cada “fotograma”, en cada “ejecución del bucle principal” del juego, como imprimir los gráficos del juego, o leer el teclado, deben de revisarse e intentar conseguir que sean lo más óptimas posibles.

Por eso en este caso, vamos a intentar extraer de las rutinas Leer_Teclado_Empaquetado y Check_Key la parte que extrae la semifila y la columna a una función que usaremos durante la redefinición del juego (una vez por tecla, un total de cinco veces, y en el menú de nuestro programa) en lugar de hacerlo cinco veces en cada iteración del bucle del juego.

A continuación podemos ver el programa del ejemplo anterior, pero reescrito de forma que al redefinir las teclas (en Redefinir_Teclas_SSSTTT) extraemos del scancode de la tecla pulsada directamente el puerto a leer y la tecla a comprobar, lo cual simplificará mucho la rutina de Leer_Teclado_Empaquetado que deberemos usar en el bucle principal del juego, ya que sólo tendremos que leer de los 5 puertos indicados y comprobar las 5 teclas indicadas.


; Redefinir teclas y ver el estado de las teclas elegidas "en el juego".
; Metodo optimizado con la extraccion de semifila (puerto) y tecla durante
; la redefinicion de las teclas y no durante la lectura del teclado en
; el bucle del juego.
 
    ORG 33500
 
    call CLS
    call Redefinir_Teclas_SSSTTT  ; Redefinir teclado
    call Wait_For_No_Key          ; Esperar a que no haya teclas pulsadas
    call CLS
 
    ; Bucle del programa, lee nuestras teclas e imprime primero la
    ; "leyenda" de las teclas y luego el byte de estado de teclado:
bucle:
    ld de, 0
    call CursorAt                 ; Nos vamos a (0,0)
    ld de, msg_keys
    call PrintString              ; Imprimimos mensaje "*<>^v"
 
    call Leer_Teclado_SSSTTT
    call PrintBin                 ; Imprimimos estado teclas (A) en binario
 
    jr bucle                      ; Repetir hasta reset
 
; Mensajes del programa
msg_izq        DEFB  "Izquierda? ", _EOS
msg_der        DEFB  "Derecha? ", _EOS
msg_arriba     DEFB  "Arriba? ", _EOS
msg_abajo      DEFB  "Abajo? ", _EOS
msg_disp       DEFB  "Disparo? ", _EOS
msg_keys       DEFB  "    *<>^v", _CR, _EOS
 
; Teclas por defecto si no se redefine: O P Q A SPACE
; Hay que declararlas aqui en el mismo orden en que queremos
; que aparezcan en los bits del byte de estado
teclas_player_1:
p1_puerto_disp       DEFB  $7f      ; Semifila ' ' = puerto $fefe
p1_tecla_disp        DEFB  $01      ; Tecla ' ' = bit 1
p1_puerto_izq        DEFB  $df      ; Semifila 'O' = puerto $dfFE
p1_tecla_izq         DEFB  $02      ; Tecla 'O' = bit 2
p1_puerto_der        DEFB  $df      ; Semifila 'P' = puerto $dfFE
p1_tecla_der         DEFB  $01      ; Tecla 'P' = bit 1
p1_puerto_arriba     DEFB  $fb      ; Semifila 'Q' = puerto $fbFE
p1_tecla_arriba      DEFB  $01      ; Tecla 'Q' = bit 1
p1_puerto_abajo      DEFB  $fd      ; Semifila 'A' = puerto $fdFE
p1_tecla_abajo       DEFB  $01      ; Tecla 'A' = bit 1
 
P1_NUM_TECLAS        EQU     5      ; Usaremos 5 teclas: OPQA<ESP>
 
; Estado de las tecla pulsadas
p1_teclas_pulsadas   DEFB  0
 
;-----------------------------------------------------------------------
; Utiliza Redefine_Key para obtener las semifilas y tecla (00TTTSSS)
; del scancode seleccionado por el usuario pulsando una tecla.
; La semifila se guarda como puerto a leer  y la tecla como
; el bit (posición) de esa tecla en la respuesta.
; Ejemplo: 'O' = p1_puerto_izq = $df
;                p1_tecla_izq = 2 (%00000010)
; Llama a "Redefine_Key" para mostrar el mensaje y pedir la tecla.
;
; ENTRADA: Nada
; SALIDA:  Nada
; MODIFICA: AF, DE, HL, Flags
;-----------------------------------------------------------------------
Redefinir_Teclas_SSSTTT:
    ; Los siguientes textos se podrian haber impreso tambien usando un bucle
    ; con inc de (avanzar cadena) e inc hl (avanzar tecla a escribir)
    ld de, msg_izq
    call PrintString              ; Imprimir mensaje "Izquierda?"
    call Redefine_Key             ; Esperar pulsacion (e imprimir ASCII)
    call Scancode_To_Port_Key
    ld hl, p1_puerto_izq          ; Apuntamos HL a la p1_puerto_izq
    ld (hl), e                    ; Guardamos la semifila
    inc hl                        ; Incrementamos HL => apuntamos a p1_tecla_x
    ld (hl), d                    ; Guardamos la tecla
 
    ld de, msg_der                ; Siguiente mensaje: "Derecha?"
    call PrintString
    call Redefine_Key             ; Esperar pulsacion (e imprimir ASCII)
    call Scancode_To_Port_Key
    ld hl, p1_puerto_der          ; Apuntamos HL a la p1_puerto_X
    ld (hl), e                    ; Guardamos la semifila
    inc hl                        ; Incrementamos HL => apuntamos a p1_tecla_x
    ld (hl), d                    ; Guardamos la tecla
 
    ld de, msg_arriba             ; Repetimos con ARRIBA
    call PrintString
    call Redefine_Key
    call Scancode_To_Port_Key
    ld hl, p1_puerto_arriba       ; Apuntamos HL a la p1_puerto_X
    ld (hl), e                    ; Guardamos la semifila
    inc hl                        ; Incrementamos HL => apuntamos a p1_tecla_x
    ld (hl), d                    ; Guardamos la tecla
 
    ld de, msg_abajo              ; Repetimos con ABAJO
    call PrintString
    call Redefine_Key
    call Scancode_To_Port_Key
    ld hl, p1_puerto_abajo        ; Apuntamos HL a la p1_puerto_X
    ld (hl), e                    ; Guardamos la semifila
    inc hl                        ; Incrementamos HL => apuntamos a p1_tecla_x
    ld (hl), d                    ; Guardamos la tecla
 
    ld de, msg_disp               ; Repetimos con DISPARO
    call PrintString
    call Redefine_Key
    call Scancode_To_Port_Key
    ld hl, p1_puerto_disp         ; Apuntamos HL a la p1_puerto_X
    ld (hl), e                    ; Guardamos la semifila
    inc hl                        ; Incrementamos HL => apuntamos a p1_tecla_x
    ld (hl), d                    ; Guardamos la tecla
    ret
 
;-----------------------------------------------------------------------
; Scancode_To_Port_Key: Extrae de un scancode 00TTTSSS
; los valores TTT y SSS, para guardarlos después por separado como
; PUERTO y TECLA
; Escrita por Xor_A [Fer].
;
; ENTRADA: A = scancode de teclado (00TTTSSS)
; SALIDA:  D = NUMERO de bit de TECLA.
;          E = PUERTO (parte alta) correspondiente a esa SEMIFILA
; Ejemplo: 'O' = D = $df
;                E = 2 (%00000010)
;
; MODIFICA: A, DE, E, FLAGS
;-----------------------------------------------------------------------
Scancode_To_Port_Key:
    ld d, a                  ; Hacer copia de scancode en D
    and %00000111            ; Eliminar todos los bits menos 00000SSS
    cpl                      ; Invertir valores (1/0)
    ld e, a                  ; E = contador para bucle
    ld a, $fe                ; Empezamos por primera semifila (determina puerto)
    jr z, ConvScn_tecla      ; Si Z==1 => es primera semifila
ConvScn_loop1:
    rlca                     ; Rotamos A: Siguiente semifila
    dec e
    jr nz, ConvScn_loop1     ; Si Z==0 => no es la semifila correcta
ConvScn_tecla:
    ld e, a                  ; Salvaguardamos semifila (puerto a leer) en E
    ld a, d                  ; Recuperamos A = 00TTTSSS de nuevo
    and %00111000            ; Eliminamos todos los valores menos 00TTT000
    ld d, $10                ; Inicializamos a la tecla mas interna
    ret z                    ; Si Z==1 => es la tecla interna
ConvScn_loop2
    rr d                     ; Rotamos: Siguiente tecla de la semifila
    sub 8
    jr nz, ConvScn_loop2     ; Si Z==0 => NO ES LA TECLA CORRECTA
    ret
 
;-----------------------------------------------------------------------
; Lee el estado de las teclas definidas en variables y almacena en A
; dicho estado (1=pulsada, 0=no pulsada). El byte está codificado así:
;
; BITS            4    3    2     1    0
; SIGNIFICADO   FIRE LEFT RIGHT  UP DOWN
;
; ENTRADA: NADA (usa las variables p1_puerto_* y p1_tecla_*)
; SALIDA:  A = byte de estado de las teclas (empaquetadas en bits).
; MODIFICA: A, HL y CarryFlag
;-----------------------------------------------------------------------
Leer_Teclado_SSSTTT:
    ld de, P1_NUM_TECLAS*256     ; D = 5 (teclas a leer), E = 0
    ld hl, teclas_player_1       ; 1a tecla (p1_puerto_X) para bit + alto
    ld c, $fe                    ; Parte baja puerto teclado
 
tecl_sssttt_loop:
    ld b, (hl)                   ; cogemos puerto a leer (parte alta)
    inc hl                       ; pasamos al siguiente byte (p1_tecla_X)
    in a, (c)                    ; leer del puerto de esta tecla
    cpl                          ; Ahora: 1 teclas pulsadas, 0 no pulsadas
    and %00011111                ; aislamos las teclas
    and (hl)                     ; comparo con la tecla en memoria (carry OFF)
    jr z, tecl_sssttt_nopulsada  ; Si Z == 1 => tecla no pulsada
    scf                          ; Ponemos Carry = 1 para meterlo en A al rotar
 
tecl_sssttt_nopulsada:
    rl e                         ; meto el valor de la pulsacion de la tecla
                                 ; (carry OFF no pulsada/carry ON pulsada)
    inc hl                       ; apunto a siguiente puerto (semifila)
    dec d                        ; decrementamos el num. de teclas pendientes por leer
    jr nz, tecl_sssttt_loop      ; siempre necesita una pulsacion para salir del bucle
    ld a, e                      ; Recuperamos estado final
    ld (p1_teclas_pulsadas), a   ; Guardamos en variable el estado de las teclas
    ret                          ; Devolvemos en A el estado de las teclas
 
;;; Insertar aqui el codigo de Redefine_Key
;;; Insertar aqui el codigo de Scancode2ASCII
;;; Insertar aqui el codigo de Find_Key
 
    ;-- Incluir libreria de utilidades --
    INCLUDE "utils.asm"
 
    END 33500


  • Al empezar el programa, llamamos a Redefinir_Teclas_SSSTTT para que repita 5 veces el mismo proceso:
    • Se imprime por pantalla el mensaje informativo de cada tecla para el usuario (“Izquierda? ”).
    • Se llama a Redefine_Key, la cual:
      • Usa Find_Key para esperar una pulsación del teclado.
      • Usa Scancode2Ascii para obtener el código ASCII correspondiente al scancode pulsado.
      • Imprimir por pantalla el ASCII para que el usuario vea qué tecla ha sido pulsada.
      • Devuelve el scancode pulsado por el usuario en A
    • Se llama a la nueva rutina Scancode_To_Port_Key con el scancode obtenido. Esta rutina devuelve en E la parte alta del puerto (si devuelve $XX, el puerto es $XXFE) y en D la posición (bit) de la tecla en cuestión, si quisiéramos leer la tecla correspondiente al scancode elegido por el usuario.
    • Esta información de puerto y posicion_de_tecla se guarda en un conjunto de variables p1_puerto_* y p1_tecla_* que tienen que estar en memoria dispuestas de una forma determinada.


Al acabar la redefinición de teclas, tendremos en estas variables los datos de puerto y tecla para cada una de los “botones de acción” del juego:


; Teclas por defecto si no se redefine: O P Q A SPACE
; Hay que declararlas aqui en el mismo orden en que queremos
; que aparezcan en los bits
teclas_player_1:
p1_puerto_disp       DEFB  $7f      ; Semifila ' ' = puerto $fefe
p1_tecla_disp        DEFB  $01      ; Tecla ' ' = bit 1
p1_puerto_izq        DEFB  $df      ; Semifila 'O' = puerto $dfFE
p1_tecla_izq         DEFB  $02      ; Tecla 'O' = bit 2
p1_puerto_der        DEFB  $df      ; Semifila 'P' = puerto $dfFE
p1_tecla_der         DEFB  $01      ; Tecla 'P' = bit 1
p1_puerto_arriba     DEFB  $fb      ; Semifila 'Q' = puerto $fbFE
p1_tecla_arriba      DEFB  $01      ; Tecla 'Q' = bit 1
p1_puerto_abajo      DEFB  $fd      ; Semifila 'A' = puerto $fdFE
p1_tecla_abajo       DEFB  $01      ; Tecla 'A' = bit 1
 
P1_NUM_TECLAS        EQU     5      ; Usaremos 5 teclas: OPQA<ESP>


La rutina Check_Key, que leía el estado de una tecla y para hacerlo necesitaba realizar las mismas operaciones que hace Scancode_To_Port_Key, ya no es necesaria.

Ahora, en el bucle principal del programa, usaremos Leer_Teclado_SSSTTT para que lea las 5 teclas rápidamente, ya que sabemos sus puertos y sus posiciones en el valor leído del puerto, lo cual es muchísimo más óptimo.

Esta rutina lo que hace es un bucle de N iteraciones (siendo N el valor de P1_NUM_TECLAS) y va recorriendo con HL toda la tabla de puertos/teclas para leer el puerto y comprobar el estado de la tecla. Si encuentra la tecla pulsada, pone un 1 en el CARRY FLAG, y si no está pulsada lo deja en 0. A continuación rota con rl e el registro donde vamos almacenando el estado de las teclas para que entre ese 1 o ese 0 desde la derecha. Este es el motivo por el cual el orden en que definimos las teclas en el bloque de puertos/teclas es importante, ya que la primera tecla se quedará en el BIT P1_NUM_TECLAS-1 por el bucle que estamos realizando.


; BITS            4    3    2     1    0
; SIGNIFICADO   FIRE LEFT RIGHT  UP DOWN


Nótese que el orden de los bits de estado no es el orden en que hemos redefinido la teclas (ese orden no importa, podemos pedirle las teclas al usuario en el orden en que queramos), sino que se corresponde con el orden en que aparecen las teclas (DEFB) en el programa, junto a la variable P1_NUM_TECLAS.

Como esta variable en nuestro ejemplo vale 5, el primer bit de la lista de teclas (disparo) será el 5º bit (bit 4), la siguiente tecla (izquierda) será el 4º (BIT 3), y así hasta la última variable.

Si quisiéramos añadir una segunda tecla de disparo, deberíamos aumentar P1_NUM_TECLAS a 6, y añadir las variables p1_puerto_disp2 y p1_tecla_disp2 las primeras de la lista, justo bajo teclas_player_1, para que su estado aparezca en el 6º bit (bit 5) del registro de estado de teclas. Si queremos añadir dos teclas más, el procedimiento sería similar, y se usaría otro bit del byte de estado.


; Teclas por defecto si no se redefine: O P Q A SPACE
; Hay que declararlas aqui en el mismo orden en que queremos
; que aparezcan en los bits
teclas_player_1:
p1_puerto_pause      DEFB  $fe      ; Semifila 'C' = puerto $feFE
p1_tecla_pause       DEFB  $08      ; Tecla 'C' = bit 4
p1_puerto_disp2      DEFB  $fe      ; Semifila 'V' = puerto $feFE
p1_tecla_disp2       DEFB  $10      ; Tecla 'V' = bit 5
p1_puerto_disp       DEFB  $7f      ; Semifila ' ' = puerto $fefe
p1_tecla_disp        DEFB  $01      ; Tecla ' ' = bit 1
p1_puerto_izq        DEFB  $df      ; Semifila 'O' = puerto $dfFE
p1_tecla_izq         DEFB  $02      ; Tecla 'O' = bit 2
p1_puerto_der        DEFB  $df      ; Semifila 'P' = puerto $dfFE
p1_tecla_der         DEFB  $01      ; Tecla 'P' = bit 1
p1_puerto_arriba     DEFB  $fb      ; Semifila 'Q' = puerto $fbFE
p1_tecla_arriba      DEFB  $01      ; Tecla 'Q' = bit 1
p1_puerto_abajo      DEFB  $fd      ; Semifila 'A' = puerto $fdFE
p1_tecla_abajo       DEFB  $01      ; Tecla 'A' = bit 1
 
P1_NUM_TECLAS        EQU     7      ; Usaremos 7 teclas


Esto dejaría el byte de estado de la siguiente forma:


; BITS           6     5     4    3    2     1    0
; SIGNIFICADO  PAUSE FIRE2 FIRE LEFT RIGHT  UP DOWN


Este conjunto de rutinas es mucho más eficiente para utilizarla en el bucle principal del juego, ya que si examinamos Leer_Teclado_SSSTTT, tiene muchas menos instrucciones y puede leer directamente los 5 puertos que necesitamos leer sin operaciones intermedias innecesarias que ya hemos hecho en el proceso de redefinición a costa de necesitar (sin que ello suponga un problema en términos de tamaño) doble número de variables (de N scancodes pasamos a 2*N puertos+teclas)

Si quisiéramos tener teclas para el segundo jugador, podríamos replicar las funciones y añadir unas variables p2_puerto_* y p2_tecla_* para almacenar sus valores, y modificar Leer_Teclado_SSSTTT para que lea las teclas del segundo jugador.


Una recomendación a la hora de verificar el estado de las teclas es que, como hemos hecho en nuestras rutinas, utilicemos las herramientas de testeo de bits (BIT y AND) de que nos provee el Z80 para testear los bits del valor devuelto por IN, en lugar de, simplemente tratar de comparar el valor del estado del teclado con algún valor predefinido. Esto evitará que nuestro programa funcione de forma diferente en Spectrums con teclado ISSUE 2, ISSUE 3, o con algún periférico conectado.

Si alguna vez has cargado un snapshot de algún juego en un emulador y has visto que el personaje se movía “sólo”, como si alguien estuviera pulsando teclas que realmente no están pulsadas (por ejemplo, Abu Simbel Profanation), y has tenido que activar la opción “ISSUE 2 KEYBOARD EMULATION” para que funcione adecuadamente, entonces ya has sufrido los efectos de una incorrecta lectura del teclado.

Ahora mismo veremos por qué, y empezaremos para ello recordando uno de los primeros párrafos de esta entrega:

Así, leyendo del puerto 63486 obtenemos un byte cuyos 8 bits tienen como significado el estado de cada una de las teclas de la semifila del “1” al “5”.

Bits: D7 D6 D5 D4 D3 D2 D1 D0
Teclas: XX XX XX “5” “4” “3” “2” “1”

Con esta información “rescatada”, volvamos al punto en que estábamos: A la hora de comprobar si la tecla “2” está pulsada, lo recomendable es testear (por ejemplo, con el nmemónico BIT), el bit 1 del valor devuelto por un IN del puerto 63486. Recordemos que dicho bit valdrá 0 si la tecla está pulsada, y 1 si no lo está.

Teniendo en cuenta que un bit a “1” significa tecla no pulsada y “0” significa pulsada, si en nuestro teclado no está pulsada ninguna tecla del 1 al 5, los últimos 5 bits del valor leído del puerto serán “11111b”, y que si está pulsada la tecla “2”, tendremos “11101b”.

Viendo esto, podría surgirnos la tentación de utilizar COMPARACIONES para chequear el estado de la tecla “2”. Pulsamos “2” en nuestro Spectrum, leemos el valor del puerto, y obtenemos 253 (“11111101b”), con lo cual basamos el chequeo de teclas de nuestro programa en cosas como el siguiente pseudocódigo:

valor = IN(63486)
SI: valor == 253
ENTONCES: TECLA_DOS_PULSADA

Comparando con 253 (11111101b), estamos asumiendo que los bits D7, D6 y D5 valen siempre 1, porque en *nuestro* Spectrum es así, pero … ¿Qué valor tienen los bits D7, D6 y D5? La realidad es que la gran mayoría de las veces será, efectivamente, 1, pero este valor puede verse alterado si tenemos determinados periféricos hardware conectados al bus de expansión trasero, e incluso existen unos determinados modelos de placas (ISSUE 2) que contienen otros valores en estos bits.

Uno de los componentes del grupo Mojon Twins, na_th_an, nos proporciona a través de su blog la siguiente prueba de concepto BASIC:


Una ligera prueba en Spectaculator, que puede configurarse para que use el teclado de issue 2, nos da los valores que buscamos para los bits que desconocemos. Sólo tenemos que teclear y ejecutar este pequeño programa, y fijarnos como normalmente obtenemos 253 y 254 (255 sin pulsar nada) para las pulsaciones de O y P, respectivamente, y otros valores diferentes si activamos el teclado issue 2 en las opciones del emulador:

10 PRINT AT 0,0; IN 57342; " "
20 GOTO 10

Los valores que obtenemos para estas pulsaciones son 189 y 190 (con 191 sin pulsar), lo que significa que los bits desconocidos son nada más y nada menos que XXX = 101. Podremos garantizar que nuestro programa funcionará en todos los Spectrum si comparamos siempre con ambos valores (por ejemplo, para detectar O deberíamos mirar si IN 57342=253 or iN 57342=189).

La solución que Na_th_an nos expone en el párrafo anterior está orientada a la creación de programas en BASIC, dado que la variante “ZX Spectrum” de este lenguaje no dispone de operaciones de testeo de bits, pero en nuestro caso, en ensamblador, la mejor opción para leer una tecla (pongamos “P” en el siguiente ejemplo) sería:

    ld a, $df            ; Semifila "P" a "Y"
    in a, ($fe)          ; Leemos el puerto
    bit 0, a             ; Testeamos el bit 0
    jr z, pulsado        ; Si esta a 0 (pulsado) salir.

Si queremos utilizar la comprobación por valor, o simplemente vamos a saltar con or a y luego JZ / JNZ, podríamos simplemente enmascarar con and %00011111 el valor de A antes de la comparación, y dejaríamos a cero los bits implicados.


1.- Debido al diseño del Spectrum, existen combinaciones de teclas que siendo pulsadas simultáneamente no permiten la detección de teclas adicionales en la misma u otras filas. A la hora de definir unas teclas por defecto, deberemos de realizar pruebas con nuestro programa para asegurarnos de que podemos pulsar todas las combinaciones de teclas elegidas.

2.- Aparte, existen al menos 4 combinaciones de teclas que producen el mismo efecto que pulsar la tecla BREAK:

CAPS SHIFT + Z + SYMBOL SHIFT
CAPS SHIFT + X + M
CAPS SHIFT + C + N
CAPS SHIFT + V + B

3.- Para los casos en los cuales queramos comprobar sólo el bit 0 de una semifila, podemos ahorrarnos la sentencia BIT utilizando rra para mover el bit b0 al carry flag. ¿La utilidad de esto? Sencillamente que rra se ejecuta en 4 ciclos de reloj mientras que BIT en 8.

    ; Ejemplo: leyendo la tecla espacio (bit 0)
    ld a, $7f
    in a, ($fe)
    rra                    ; pone b0 en el carry flag
    jp nc, key_pressed

4.- Algunas compañías de videojuegos (por ejemplo, ULTIMATE), seleccionaban para los juegos teclas como Q, W, E, R y T. Como habéis podido ver en este capítulo, esa selección no es casualidad: todas estas teclas están en la misma semifila del teclado, con lo que se puede leer el estado de todas ellas con una sóla lectura de puerto. Esto permitía ahorrar tanto memoria como tiempo de proceso.

En ese sentido, la lectura de los joysticks Sinclair (1-5 y 0-9) también es muy cómoda para nuestros programas.


Miguel A. Rodríguez Jódar nos cuenta dos detalles a tener en cuenta a la hora de leer las teclas en ensamblador.

El primero está relacionado con la lectura de cadenas de texto y no simplemente “controlar” un personaje.

El problema: los microrebotes del teclado, provocarán el conocido efecto de “repetición de teclas” aún cuando sólo hayamos pulsado y liberado una tecla. Citando a Miguel Ángel:

Cuando se usa el teclado para mover un personaje, lo que se busca es ver qué tecla
se pulsa, y mientras esté pulsada se mueve en una determinada dirección. Si se suelta
un momento pero después se vuelve a pulsar, el personaje se sigue moviendo, así que
en este caso los microrrebotes parecen no afectar.

Otra cosa es cuando se usa el teclado para "teclear". En este caso la secuencia es:
esperar a que se pulse una tecla, recoger qué tecla es, almacenarla, esperar a que se
suelte, y volver al principio.

En este caso es cuando ocurre el problema: si el bucle que implementa el algoritmo
anterior es muy rápido, es posible escanear el teclado cada pocos microsegundos. Si
una trama de microrrebotes dura más que el tiempo entre escaneos de teclado, el programa
puede detectar pulsaciones incorrectas, al estar leyendo datos que corresponden a un
microrrebote. Dado que lo que buscamos son secuencias pulsado/no pulsado, estos
microrrebotes se interpretarán erróneamente como pulsaciones y nos podemos encontrar
con que lo que tecleamos aparece repetido dos o tres veces.

Bucle:
EsperaSoltar:   xor a
                in a,(254)
                and %00011111
                cp %00011111
                jr nz,EsperaSoltar

EsperaPulsar:   xor a
                in a,(254)
                and %00011111
                cp %00011111
                jr z,EsperaPulsar

                ;Se registra la pulsacion...
                jr Bucle

Un bucle así ejecutándose en un Spectrum real podría enfrentarse con la siguiente
pulsación de teclado:

Teclado: 11111111111111111111111001011010000000000000000000000000
Lectura: ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^

Esto es lo que quería destacar: si el intervalo entre dos lecturas es menor que el
tiempo que dura una trama de microrrebotes, se interpretarán como pulsaciones
independientes. Aquí se comienza con la tecla soltada. En un determinado momento,
el usuario la pulsa y genera la secuencia que se ve. El bucle de lectura detecta
una pulsación dentro de la trama de microrrebotes, en la siguiente lectura
detecta una no-pulsación, y en la siguiente, otra pulsación. El resultado final
es que se almacenan dos pulsaciones de tecla en lugar de una.

La descripción técnica del Spectrum apunta a que la supresión de rebotes de teclado
se hace por software, en la rutina de lectura de la ROM. Pero si no se usa dicha
rutina y se lee el teclado directamente, hay que tener en cuenta esto. En nuestro
software basta con insertar una pausa de 1ms en la que se ignora al teclado.
1 milisegundo cuando el teclado se usa no para mover un personaje en un arcade,
sino para registrar entrada del usuario, no afecta a la respuesta del programa.

Bucle:
EsperaSoltar:   xor a
                in a, (254)
                and %00011111
                cp %00011111
                jr nz, EsperaSoltar

                ;Hacer pausa AQUI

EsperaPulsar:   xor a
                in a, (254)
                and %00011111
                cp %00011111
                jr z, EsperaPulsar

                ; Hacer pausa AQUI

                ;Se registra la pulsacion...
                jr Bucle

 El efecto que esto tiene sobre el comportamiento de las lecturas es el siguiente:

Teclado: 11111111111111111111111001011010000000000000000000000000
Lectura: ^   ^   ^   ^   ^   ^   ^              ^   ^   ^   ^   ^

Cuando se detecta una pulsación (sea en medio de un microrrebote o no), el
teclado deja de explorarse durante 1 ms (o quizás baste con menos). Al soltar
también se generar microrrebotes, que se amortiguarían con la segunda pausa.

Resumiendo: la lectura del teclado en ensamblador sólo está limitada, “físicamente”, por la velocidad con la que responde la lectura del puerto con el comando IN. Como explica Miguel Ángel, electrónicamente existen una serie de rebotes de la señal que pueden inducir a generarnos pulsaciones de teclado “residuales” que realmente no se han dado. Esto hace necesario insertar “pausas” entre lecturas para no leer “microrebotes” de los estados del teclado al tomar caracteres de teclado en determinadas circunstancias (lectura de “cadenas de texto”, por ejemplo).

El segundo problema se refiere al “Ghosting”. Debido al funcionamiento interno del teclado, como matriz de pulsadores sin tener aislado cada uno de ellos con diodos (que hubieran encarecido el producto final al tener que acoplarlos al teclado de membrana del Spectrum), el estado de “0 voltios” se propaga por todas las líneas conectadas entre sí mediante los circuitos que han cerrado los pulsadores (teclas), por lo que en ciertas combinaciones de teclas podemos encontrar teclas no pulsadas con su línea a 0 voltios, interpretando erróneamente nuestro programa que dicha tecla está realmente pulsada.

Tal y como nos cuenta Miguel A. Rodríguez Jódar en los foros de Speccy.org:

Esto implica, por ejemplo, que al pulsar tres teclas que forman los tres vértices
de un cuadrado en la matriz, la cuarta tecla perteneciente al cuarto vértice también
aparece como pulsada, y por tanto no se puede detectar cuando NO está pulsada.

Lo que ocurre exactamente es lo siguiente: cuando se pulsan dos teclas que pertenecen
a distintas filas, pero que pertenecen a la misma columna las filas de ambas teclas
adquieren el potencial de 0 voltios, así que aunque nosotros hayamos seleccionado
una fila para leer, en realidad se están seleccionando dos filas para leer. Si en la
fila que no pretendíamos leer hay más de una tecla pulsada (la I), ésta obviamente
aparecerá en la línea de salida.

Esto es el "ghosting" en un teclado de matriz. Para detener este efecto, es necesario
impedir que se formen circuitos cerrados allí donde no queremos, o al menos que si
se forman sea porque la corriente deba circular por ese circuito en el sentido adecuado.

La solución es meramente hardware, por lo que a nosotros nos queda simplemente la posibilidad de modificar la rutina de redefinición de teclas para impedir que el usuario seleccione teclas cuya combinación provoque la pulsación no real a nivel de línea de otra.

El propio Miguel A. nos propone un programa en BASIC que nos puede mostrar las combinaciones de teclado que producen Ghosting y que podemos implementar en ASM si consideramos necesario que nuestro programa tenga en cuenta esta particularidad (Nota: se han partido los comentarios REM y las líneas largas en líneas múltiples para facilitar la lectura):

       1 REM Datos de la matriz a cargar en T. No podremos usar CAPS SHIT y SYMBOL
             SHIFT porque la rutina de la ROM que usamos no las puede detectar
             "aisladas" asi que en su lugar ponemos CHR$ 0 (NOTA: ambas a la vez
             si puede, es CHR$ 14)
       2 DATA "b","n","m",CHR$ 0," ","h","j","k","l",CHR$ 13,"y","u","i","o","p",
              "6","7","8","9","0","5","4","3","2","1","t","r","e","w","q","g","f",
              "d","s","a","v","c","x","z",CHR$ 0
      10 DIM t(8,5,2):
         REM Estado de la matriz. t(f,c,s) es:f=fila, c=columna, s=codigo ascii tecla
      15 DIM r(7,2):
         REM Nuestra seleccion de teclas. Para cada una se guarda su fila y columna.
      17 FOR f=1 TO 8: FOR c=1 TO 5: READ t$: LET t(f,c,2)=CODE t$: NEXT c: NEXT f:
         REM Rellenamos la matriz T
      20 DATA "Arriba","Abajo","Izquerda","Derecha","Fuego","Pausa","Abortar"
      30 FOR n=1 TO 7
      40 READ t$
      50 PRINT "Elige tecla para ";t$;": ";
      60 PAUSE 0: LET tecl=PEEK 23560:
         REM Leemos tecla. Valdria tambien hacer LET tecl=CODE INKEY$
      65 BEEP .05,0:
         REM pitido de realimentacion al usuario para que sepa que su tecla
             ha sido leida y va a ser procesada
      70 FOR f=1 TO 8: FOR c=1 TO 5: IF t(f,c,2)=tecl THEN GO TO 90:
         REM La buscamos en la matriz
      80 NEXT c: NEXT f: PRINT "Fallo en la matriz! :(": STOP :
         REM Esto no deberia pasar...
      90 IF t(f,c,1)=1 THEN BEEP .5,-20: GO TO 60:
         REM Si ya estaba marcada, error! y a elegir otra
    100 LET t(f,c,1)=1:
         REM No esta marcada, asi que la aceptamos y la marcamos
    105 IF tecl=13 THEN PRINT "ENTER": GO TO 110
    106 IF tecl=32 THEN PRINT "SPACE": GO TO 110
    108 PRINT CHR$ tecl
    110 BEEP .1,20:
        REM pitido para indicar tecla OK
    120 LET r(n,1)=f: LET r(n,2)=c:
        REM La guardamos en nuestra matriz de teclas seleccionadas.
    130 FOR m=1 TO n: LET fil=r(m,1): LET col=r(m,2): GO sub 900: NEXT m:
        REM Repasamos la lista de teclas seleccionadas hasta el momento para
            actualizar la matriz con las teclas "fantasma" que encontremos
    140 NEXT n: STOP
    900 FOR i=1 TO 8:
        REM recorremos todas las teclas de la misma columna que nuestra tecla
    910 IF t(i,col,1)=1 THEN GO sub 1000:
        REM si alguna esta seleccionada, significa que tenemos dos teclas en
            una misma columna. Miramos si hay una tercera en la misma fila
    920 NEXT i: RETURN
    1000 FOR j=1 TO 5:
        REM Recorremos una fila buscando una tercera tecla seleccionada
    1010 IF t(i,j,1)=1 THEN LET t(fil,j,1)=1:
        REM Si la encontramos, entonces tenemos tres teclas en un cuadrado.
            Marcamos como seleccionada la cuarta tecla del cuadrado, para que
            no podamos elegirla
    1020 NEXT j: RETURN

Por otra parte, es bastante complicado que los usuarios seleccionen combinaciones de teclado no estándar (OPQA, 6789, etc.) y que puedan suponer problemas de ghosting, por lo que lo más normal para evitar la inclusión de código adicional en nuestro programa será permitir al usuario que seleccione las teclas sin este tipo de comprobación.


  • cursos/ensamblador/teclado.txt
  • Última modificación: 22-01-2024 08:01
  • por sromero