The C64, VICE, Ultimate 64 y hardware original

La compañía británica Retro Games Ltd publicó hace un par de días información sobre su nuevo producto The C64. Se trata de un equipo que imita el C64 original.

TheC64

La compañía no ha dado muchos detalles técnicos sobre el producto, más allá de que permite emular un C64 y un VIC 20, que tiene un teclado plenamente funcional (The C64 Mini no lo tenía), que tiene conexión a TV por HDMI, un puerto USB, 64 juegos precargados, que se puede programar en BASIC, y poco más.

Se echa en falta información técnica detallada sobre el microprocesador de la máquina, su memoria, el tipo de emulación, los puertos que tiene (más allá del HDMI, el USB y el joystick), si es que tiene alguno, etc. Y también si se puede programar en ensamblador o sólo en BASIC.

En todo caso, parece claro que se trata de una emulación del C64 por software.

Por supuesto, para gustos, colores. Si tienes el dinero (unos 120 euros), el espacio en casa, y te apetece mucho… Ahora bien, para tener una emulación por software yo personalmente optaría por un PC con VICE. Viene a ser lo mismo y el equipo te vale para todo, no sólo para jugar.

VICE

Otra alternativa que me parece más interesante es Ultimate 64. Ultimate 64 es una placa base basada en FPGA, es decir, en circuitos programables. Mediante estos circuitos programables se emula el hardware del C64. Podríamos decir que es una emulación por hardware.

La placa base Ultimate 64 hay que complementarla con otros elementos, como una carcasa, un teclado, una fuente de alimentación, algunos chips, etc. Esto dificulta el tener el equipo completo, ya que el usuario tiene que conseguir esas piezas y montarlo. Y el precio es más caro, unos 240 euros.

Ultimate64

Por último, está mi opción preferida: el hardware original. Se pueden comprar equipos de segunda mano en eBay, Wallapop y sitios similares. Incluso hay «tiendas especializadas» en eBay que se dedican a restaurar, «tunear», y vender equipos Commodore. Y con dispositivos tipo SD2IEC es fácil tener mucho software en poco espacio.

C64-rojo-2

Lo dicho, la oferta es variada y hay opciones para todos los gustos y bolsillos.

Revisión de objetivos (y librerías resultantes)

Allá por octubre de 2018, cuando empecé este blog, me planteé unos objetivos personales y otros relativos al contenido del blog.

Respecto a los objetivos personales (pasármelo bien, recordar viejos tiempos, aprender, ayudar a otros a aprender, etc.), he de decir que me lo he pasado pipa y he aprendido mucho sobre el C64. Digamos que he saldado una deuda que tenía pendiente desde hace 30 años.

Me han venido muchos recuerdos muy bonitos de cuando tenía unos 15 años, y también he pasado un poco de nostalgia. El blog tiene tráfico y me llegan bastantes comentarios por la sección de “Contacto”, así que entiendo que también es útil para otros, aunque me gustaría que tuviera más de participación bajo la forma de comentarios. Quizás sea cuestión de tiempo y difusión…

Respecto a los contenidos, hemos revisado referencias de todo tipo (libros y páginas), herramientas (VICE y CBM prg Studio), los sistemas de numeración, el hardware del C64, su mapa de memoria, la programación en ensamblador del 6510, las rutinas del Kernal, el VIC, el SID y la entrada/salida. Por supuesto, todo se puede tratar más en profundidad, pero desde luego es una cobertura bastante amplia. Y con unos 60 ejemplos de código en ensamblador.

Por el camino, hemos desarrollado y mejorado una colección de librerías que simplifican el desarrollo posterior. Tenemos un mapa de memoria y librerías para manejo de sprites y gráficos, sonido y música, manejo de joysticks, cadenas de texto, etc. La última versión de estas librerías, al menos hasta la fecha, queda disponible aquí. Y por supuesto se pueden seguir mejorando.

El principal objetivo pendiente es el de desarrollar un proyecto con todo lo aprendido. Quizás un juego, cómo no. Esto es un objetivo en sí mismo, con una entidad comparable o superior a la del propio blog. Por ello, habrá que buscarle el momento adecuado…


Librerías: Lib

Carga de ficheros con el Kernal

En esta entrada vamos a cargar desde un programa en ensamblador el fichero grabado en la entrada anterior. Para ello hay que utilizar la rutina del Kernal LOAD:

Load

La rutina LOAD, a su vez, hace referencia a las rutinas SETLFS y SETNAM, ya conocidas:

Setlfs y Setnam

De nuevo, comparando estas tres rutinas con la carga BASIC, la analogía es evidente:

  • Rutina SETLFS: Especificará el dispositivo y si el fichero se cargará en la dirección indicada por la cabecera o en una dirección especificada por el programa de carga.
  • Rutina SETNAM: Especificará el nombre del fichero.
  • Rutina LOAD: Especificará si se hace una carga o una verificación y, en caso de no usar la cabecera de dirección, la dirección donde tendrá lugar la carga.

Sugerimos que el lector revise y ejecute ahora el programa de ejemplo. Se trata de un programa en ensamblador que carga desde disco el contenido de un fichero (un pequeño programa en ensamblador en este caso).

Previamente habrá que conectar a VICE (“Attach”) la imagen D64 donde está el fichero “MIPROGRAMA”:

MiPrograma

Para ejecutar el programa cargador habrá que ejecutar la instrucción SYS 49152:

Loading Miprograma

Y una vez cargado el programa “MIPROGRAMA”, éste se ejecuta con SYS 49410, puesto que lo hemos cargado donde indica su cabecera:

Ejecución tras carga ens

Por tanto, hemos sido capaces de cargar desde ensamblador un programa que estaba en disco.

Y con esto terminamos la sección dedicada a entrada/salida…


Programa de ejemplo: Prog60

Grabación de ficheros con el Kernal

La rutina del Kernal involucrada en la escritura de ficheros es SAVE:

Save

A su vez, la rutina SAVE hace referencia a las rutinas SETLFS y SETNAM:

Setlfs y Setnam

Comparando estas tres rutinas con la grabación BASIC vista en la entrada anterior, la analogía salta a la vista:

  • Rutina SETLFS: Especificará el dispositivo y si el fichero incluirá cabecera de dirección.
  • Rutina SETNAM: Especificará el nombre del fichero.
  • Rutina SAVE: Especificará el rango de posiciones de memoria a grabar y dará la propia orden de grabación del fichero.

Llegados a este punto, lo mejor es que el lector revise y ejecute el programa de ejemplo. Se trata de un programa en ensamblador que graba en disco el contenido de una zona de memoria (que a su vez es otro pequeño programa).

Partiremos de una imagen D64 vacía como esta:

Crear D64

La conectaremos al emulador VICE (“Attach”) y ejecutaremos el programa de grabación con SYS 49152. Veremos esto:

Saving Miprograma

Y ahora tendremos una imagen D64 con el fichero “MIPROGRAMA”:

MiPrograma

En la siguiente entrada, veremos cómo cargar “MIPROGRAMA” desde un programa en ensamblador. Sin embargo, por el momento nos conformaremos con cargar y ejecutar “MIPROGRAMA” desde BASIC, básicamente para comprobar que está bien grabado y que se puede recuperar bien.

Cerraremos el emulador VICE actual y arrancaremos otro, de modo que partamos de un mapa de memoria completamente limpio. Conectaremos la imagen DISCO.D64 al nuevo VICE.

Dado que “MIPROGRAMA” es un programa en ensamblador, y no BASIC, la carga no debe producirse en la zona BASIC, sino en la dirección indicada por la cabecera del fichero ($c102 = 49410). Es decir, no deberemos usar LOAD “MIPROGRAMA”, 8, sino LOAD “MIPROGRAMA”, 8, 1 (el ,1 del final es fundamental):

Carga BASIC

Si hacemos LIST no veremos nada, porque no hemos cargado un programa en BASIC, sino un programa en ensamblador. Y si hacemos SYS 49410 lo ejecutaremos:

Ejecución BASIC

Como se puede observar, el programa que se ha cargado y ejecutado es el mismo que se grabó (ver fichero MiPrograma.asm), como no podía ser de otro modo.

En la siguiente entrada se verá cómo hacer esta carga también desde un programa en ensamblador.


Programa de ejemplo: Prog59

Grabación y carga de ficheros; imágenes D64

En las entradas anteriores hemos revisado cómo hacer E/S con diversos dispositivos: teclado, joysticks, dispositivos conectados al puerto serie, dispositivos conectados al puerto paralelo, etc. Pero esta discusión sobre E/S no estaría completa sin revisar dos de las operaciones de E/S más frecuentes: grabar y cargar ficheros, ya sea con cinta o disco.

Las próximas entradas versarán sobre grabación y carga de ficheros. En ambos casos usaremos las rutinas del Kernal, ya que hacerlo desde cero, por ejemplo, usando el puerto serie, sería demasiado duro.

Pero antes de adentrarnos en los detalles del ensamblador y las rutinas del Kernal, vamos a ver cómo hacer en VICE todo lo que sigue:

  • Crear una imagen D64 nueva.
  • Grabar un fichero desde BASIC.
  • Cargar un fichero desde BASIC.

Para crear una imagen D64 nueva, es decir, un disco vacío, debemos seleccionar la opción File > Attach Disk Image > Drive 8 (o el número de unidad que queramos usar). Dentro de la ventana que emerge debemos rellenar el nombre del fichero (ej. DISCO.D64), el nombre del disco (ej. entradasalida), el tipo (d64), y pulsar “Create Image”.

Aparecerá la imagen D64 nueva en el recuadro superior y, si la seleccionamos, veremos que es un disco vacío:

Crear D64

Pulsando “Attach” dejaremos el disco conectado a nuestro C64 como unidad 8.

Repasar cómo se graban y cargan ficheros desde BASIC es interesante porque, como veremos en las próximas entradas, hacerlo desde ensamblador es muy similar, al menos conceptualmente.

Para grabar un fichero (un programa) desde BASIC, lo primero es definir el citado programa. Teclearemos un programa sencillo como el que sigue:

10 PRINT “HOLA”

20 GOTO 10

Para grabar el programa a cinta, se puede usar SAVE o SAVE “NOMBRE”. Si se quiere grabar a disco lo habitual es hacer SAVE “NOMBRE”, 8.

La sintaxis general es SAVE “NOMBRE”, DISPOSITIVO, DIRECCIÓN, donde:

  • NOMBRE es el nombre del fichero.
  • DISPOSITIVO normalmente es 1 (cinta) u 8 (disco).
  • DIRECCIÓN normalmente no existe, en cuyo caso el programa se graba sin información de dirección, o 1, en cuyo caso el programa se graba con una cabecera que indica la dirección de carga del programa.

La cabecera de dirección, cuando está presente, puede usarse a la hora de cargar el fichero. Pero no es obligatorio respetarla, todo depende del usuario o del programa que haga la carga.

Probemos a hacer SAVE “MIPROGRAMA”, 8 para grabar nuestro programa. Si ahora volvemos a File > Attach Disk Image > Drive 8, y seleccionamos la imagen DISCO.D64, veremos que tiene un fichero de tipo PRG:

MiPrograma

Igualmente, podemos ver el contenido de la imagen/disco con LOAD “$”, 8, ya que “$” representa el directorio del disco:

MiPrograma2

Finalmente, para cargar “MIPROGRAMA” desde BASIC tendríamos que hacer LOAD, LOAD “MIPROGRAMA”, LOAD “MIPROGRAMA”, 8, etc., en función de que queramos cargar un programa concreto o no, queramos hacerlo desde cinta o disco, etc. Otra opción habitual es usar el “*” para representar el primer programa.

Nuevamente, la sintaxis general es LOAD “NOMBRE”, DISPOSITIVO, DIRECCIÓN, donde:

  • NOMBRE es el nombre del fichero.
  • DISPOSITIVO normalmente es 1 (cinta) u 8 (disco).
  • DIRECCIÓN normalmente no existe, en cuyo caso el programa se carga en la zona de BASIC (porque ahora estamos hablando de BASIC), o 1, en cuyo caso el programa se carga en la dirección indicada en la cabecera del fichero.

Si probamos a hacer LOAD “MIPROGRAMA”, 8 recuperaremos el programa:

MiPrograma3

Todo lo que hemos visto en esta entrada (crear imágenes D64 nuevas, grabar ficheros desde BASIC, y cargar ficheros desde BASIC), nos servirá de base para las entradas que siguen, ya dedicadas a la grabación y carga de ficheros desde ensamblador.

Registros del CIA2

El CIA2 es un chip idéntico al CIA1. Por tanto, tiene los mismos registros que el CIA1, pero ubicados en una zona de memoria más alta ($dd00 – $dd0f):

REGISTRO DIRECCIÓN FUNCIÓN
CI2PRA $dd00 Puerto de datos A.
CI2PRB $dd01 Puerto de datos B.
C2DDRA $dd02 Registro de dirección de datos A:

Bit 0…7 = 0: entrada.

Bit 0…7 = 1: salida.

C2DDRB $dd03 Registro de dirección de datos B:

Bit 0…7 = 0: entrada.

Bit 0…7 = 1: salida.

TI2ALO $dd04 Contador A. Parte menos significativa (low).
TI2AHI $dd05 Contador A. Parte más significativa (high).
TI2BLO $dd06 Contador B. Parte menos significativa (low).
TI2BHI $dd07 Contador B. Parte más significativa (high).
TO2TEN $dd08 Time of day. Décimas de segundo.
TO2SEC $dd09 Time of day. Segundos.
TO2MIN $dd0a Time of day. Minutos.
TO2HRS $dd0b Time of day. Horas.
CI2SDR $dd0c Envío y recepción de datos en modo serie por el puerto de usuario.
CI2ICR $dd0d Registro de control de interrupciones.
CI2CRA $dd0e Registro de control del contador A.
CI2CRB $dd0f Registro de control del contador B.

Las principales diferencias entre el CIA2 y el CIA1 son los dispositivos a los que da acceso y las interrupciones.

Respecto a los dispositivos, el CIA2 da acceso al puerto serie y al puerto de usuario. El puerto serie es un puerto que funciona bit a bit (en serie) y al que típicamente se conecta la unidad de disco 1541. El puerto de usuario, por el contrario, es un puerto que funciona en paralelo, es decir, permite intercambiar bits de ocho en ocho.

CIA2

Respecto a las interrupciones, el pin de interrupciones del CIA1 está conectado al pin IRQ del 6510, lo que significa que sus interrupciones son enmascarables con la instrucción “sei”. Sin embargo, el pin de interrupciones del CIA2 está conectado al pin NMI del 6510, lo que significa que sus interrupciones son no enmascarables (non-maskable interrupts).

Los detalles sobre cómo utilizar los registros CI2PRA y CI2PRB y resto de registros asociados para realizar E/S por el puerto serie y al puerto de usuario quedan fuera del alcance de este blog, al menos de momento. Pero pueden consultarse estos detalles en el libro “Mapping the Commodore 64”, en sus páginas 186 y siguientes.

CIA1: Reloj TOD

El C64 dispone de un reloj llamado TOD – Time of Day. No se trata de un reloj como el de los ordenadores modernos, que si se apaga y enciende el ordenador sigue en hora gracias a una batería. Es más bien un contador que, partiendo de cero, se va incrementando. Si se pone en hora, a partir de ese momento marcará la hora del día con horas, minutos, segundos y décimas de segundo.

El reloj TOD consta de cuatro registros:

REGISTRO DIRECCIÓN FUNCIÓN
TODTEN $dc08 Time of day. Décimas de segundo.
TODSEC $dc09 Time of day. Segundos.
TODMIN $dc0a Time of day. Minutos.
TODHRS $dc0b Time of day. Horas.

Si se leen los registros se leerán respectivamente las horas, los minutos, los segundos y las décimas de segundo. Si se escriben los registros el reloj se pondrá en hora. En particular, al escribir en el registro TODTEN se arranca el reloj.

Tanto al leer como al escribir los datos podría ocurrir el siguiente problema. Supongamos que son las 11:59. Se leen las horas (11h) y, al leer los minutos ya son las 12:00. Por tanto, se lee 00 para los minutos, es decir, las 11:00, cuando se empezó a las 11:59 y se terminó a las 12:00. Para evitar estos problemas, el reloj tiene un sistema de bloqueo (“latching”), de modo que al leer las horas el valor a leer se bloquea hasta que se lean las décimas de segundo, aunque el reloj se sigue actualizando internamente. Esto también ocurre con las escrituras.

Los registros del TOD están codificados en BCD, lo que significa que cada registro (8 bits) tiene codificado un dígito decimal (del 0 al 9) en cada nibble. Esto es suficiente para codificar las horas (de 00 a 12), los minutos (de 00 a 59), los segundos (de 00 a 59), y las décimas (de 0 a 9), y facilita la presentación de los datos al usuario.

Es más, el bit 7 de TODHRS, que en principio no se utiliza porque la hora más alta posible no lo necesita (12=%0001-0010), se utiliza para marcar si la hora es AM (bit 7 = 0) o PM (bit 7 = 1).

Además de lo anterior, el registro TOD tiene una alarma, de modo que es posible configurar una hora que, una vez alcanzada, se genera una interrupción. Esto se consigue grabando una hora en los registros TOD a la vez que el bit 7 del registro CIACRB está a 1. De este modo, la operación de grabación (“sta”) no cambia la hora del reloj, sino que configura la alarma.

El C64 tiene un reloj alternativo en las posiciones $a0-$a1-$a2 (Software Jiffy Clock), pero este reloj se actualiza por software (mediante interrupciones) y, por tanto, no es tan preciso como el TOD, que está actualizado por hardware (mediante registros específicos).

Dos usos interesantes del TOD son generar números pseudo aleatorios y, simplemente, saber el tiempo que ha transcurrido desde un determinado momento.

Reloj TOD


Programa de ejemplo: Prog58

CIA1: Temporización

Una forma sencilla de conseguir un “cronómetro” es mediante un contador. Un contador es un registro que, partiendo de cero, va incrementando su valor a intervalos regulares. De este modo, cuando termine el intervalo de tiempo a medir, se para el contador y se analiza su valor. Y dado que el período de tiempo entre incrementos será conocido, esto permite calcular fácilmente el tiempo transcurrido: tiempo = valor del contador x período entre incrementos.

Otra variante de lo anterior es la “cuenta atrás”. El mecanismo es muy parecido. En este caso, el contador se carga con un valor inicial y se va decrementando a intervalos regulares. Cuando el contador llega a 0, se activa algún “flag” o se genera una interrupción.

Esta forma de esperar un tiempo es mucho más interesante que el típico bucle de espera implementado por programa:

Retardo

En primer lugar, el tiempo de espera de un bucle es difícil de conocer y controlar con precisión. Habría que analizar el número de ciclos de reloj que consume todo el bucle (o cada iteración), y multiplicarlo por el tiempo que dura cada ciclo de reloj (el inverso de la frecuencia de reloj). Pero es que, además, mientras el 6510 ejecuta el bucle de espera no puede ejecutar otra cosa; son ciclos de reloj desperdiciados durante la espera.

Sin embargo, si la espera o cuenta atrás la controla otro registro (un registro del CIA1), el 6510 puede seguir ejecutando otras tareas útiles y, cuando termine el tiempo establecido, se ejecutará una rutina de interrupción. Por tanto, es un enfoque mucho más interesante.

De hecho, este es el mecanismo que utiliza el sistema operativo del C64. Un registro genera interrupciones 60 veces por segundo, y esas interrupciones se utilizan para escanear el teclado y realizar otras tareas.

Por claridad, repetimos aquí los registros del CIA1 que tienen que ver con temporización:

REGISTRO DIRECCIÓN FUNCIÓN
TIMALO $dc04 Contador A. Parte menos significativa (low).
TIMAHI $dc05 Contador A. Parte más significativa (high).
TIMBLO $dc06 Contador B. Parte menos significativa (low).
TIMBHI $dc07 Contador B. Parte más significativa (high).
CIAICR $dc0d Registro de control de interrupciones.

Bit 0: 1=contador A llega a 0.

Bit 1: 1=contador B llega a 1.

Bit 2: 1=alarma TOD.

Bit 7: 1=cualquier flag activo.

CIACRA $dc0e Registro de control del contador A.

Bit 0: 1=arrancar; 0=parar.

Bit 3: 1=one-shot; 0=free-running.

Bit 5: 1=reloj externo; 0=reloj interno.

CIACRB $dc0f Registro de control del contador B.

Bit 0: 1=arrancar; 0=parar.

Bit 3: 1=one-shot; 0=free-running.

Bit 6,5: 01=reloj externo; 00=reloj interno; 10=contador A pasa por 0.

Bit 7: 1=alarma TOD.

Contadores A y B:

En primer lugar, tenemos los contadores A y B, ambos de 16 bits (TIMAHI-TIMALO y TIMBHI-TIMBLO). Sirven para controlar intervalos de tiempo como hemos descrito, funcionando en modo “cuenta atrás”.

Se cargan con el valor inicial deseado mediante la instrucción “sta”, y se van decrementando una vez arrancados. Se pueden leer con la instrucción “lda”, en cuyo caso devuelven por qué valor van.

Registros de control de los contadores A y B:

En segundo lugar, tenemos los registros de control asociados a esos contadores (CIACRA y CIACRB), que sirven para arrancar y parar los contadores, seleccionar los diferentes modos de funcionamiento, seleccionar la señal de tiempo que produce los decrementos, etc.

Para arrancar un contador hay que poner a 1 el bit 0 de CIACRA/CIACRB. Para parar un contador hay que poner a 0 ese mismo bit.

Respecto a los modos de funcionamiento, tenemos el modo “one shot” (bit 3 = 1) y el modo “free running” (bit 3 = 0). Con el primero el contador sólo cuenta una vez y se para al llegar a 0. Con el segundo, el contador cuenta una y otra vez, hasta que se cambie su configuración.

Por último, respecto a la señal de reloj que produce los decrementos, ésta puede ser el reloj interno del C64 (bit 5 = 0) o una señal de reloj externa (bit 5 = 1) que se aplique al pin CNT del puerto de usuario.

El reloj interno del C64 tiene una frecuencia de 0,985 MHz en el modelo PAL (Europa) y de 1,023 MHz en el modelo NTSC (América). Es decir, los ciclos de reloj son de 1,02 y 0,97 microsegundos, respectivamente.

En el caso del contador B, y sólo en este caso, es posible configurarlo para que se decremente cada vez que el contador A pase por 0. Es decir, es posible concatenar los dos contadores A y B como si fueran un único contador de 32 bits. De este modo se consigue contabilizar intervalos de tiempo muy largos, algo más de una hora si la fuente de reloj utilizada es la interna (2^32 intervalos de 1,02 microsegundos = 1,21 horas).

Esta configuración se consigue con bit 6 = 1 y bit 5 = 0 del CIACRB.

Registro de control de las interrupciones:

En tercer lugar, tenemos el registro de control de interrupciones (CIAICR), que es donde están los “flags” que señalizan que los contadores A o B han llegado a 0, y donde se configuran las interrupciones deseadas.

Cuando el contador A llega a 0 se activa el bit 0 de CIAICR, y cuando el contador B llega a 0 se activa el bit 1. Para detectar estos flags habrá que ejecutar la instrucción “lda CIAICR”, en cuyo caso todos los flags se vuelven a poner a 0 automáticamente.

El bit 7 de CIAICR se activa siempre que cualquiera de los “flags” (los otros bits de CIAICR) esté activado.

Para configurar las interrupciones deseadas, habrá que ejecutar la instrucción “sta CIAICR”, donde, en función del valor del acumulador:

  • Si el bit 7 vale 1: los bits 0…6 que estén a 1 activarán las interrupciones cuando los flags correspondientes de CIAICR pasen a estar activados.
  • Si el bit 7 vale 0: los bits 0…6 que estén a 1 desactivarán las interrupciones de los flags correspondientes de CIAICR.

En ambos casos (bit 7 = 0 o 1) los restantes bits a 0 a cargar en CIAICR ni activan ni desactivan interrupciones. Simplemente, no modifican la configuración de las interrupciones.

Contador segundos-1


Programa de ejemplo: Prog57

CIA1: Lectura de los joysticks

El C64 admite conectar dos joysticks, uno al puerto de control 1 y otro al puerto de control 2:

Joysticks

El joystick conectado al puerto de control 1 se puede leer con el puerto CIAPRB del CIA1, y el joystick conectado al puerto de control 2 se puede leer con el puerto CIAPRA del CIA1. Sí, aunque no lo parezca, es correcto: joystick1 => CIAPRB y joystick2 => CIAPRA.

En realidad, el uso del joystick ya lo vimos en la entrada dedicada a las colisiones de sprites y, más en particular, en su programa de ejemplo (programa 37). Entonces no entramos a explicar los detalles y simplemente presentamos un código como éste:

LibJoy

Ahora ya sabemos que CIAPRA y CIAPRB son registros del CIA1. Y en realidad sería más adecuado llamar a las rutinas «leeJoystick1» (CIAPRB) y «leeJoystick2» (CIAPRA), ya que los usuarios identifican los joysticks en función del puerto de control al que están conectados (1 o 2), y no en función del puerto de datos con los que se leen.

Un joystick es un dispositivo electrónico que consta de cinco interruptores:

  • Arriba.
  • Abajo.
  • Izquierda.
  • Derecha.
  • Disparo.

Cuando el usuario empuja el joystick hacia arriba, ese interruptor se cierra, lo que hace que el voltaje de determinado pin del puerto de control se ponga a 0 voltios. Y lo mismo con el resto de interruptores.

Por supuesto, es posible cerrar varios interruptores a la vez. Por ejemplo, el disparo se puede combinar con cualquiera de las direcciones. Incluso hay direcciones que se puede combinar, como arriba y derecha/izquierda, o abajo y derecha/izquierda. Por último, hay combinaciones de interruptores que no tienen sentido, como arriba y abajo o derecha e izquierda a la vez.

Al final, cada pin del puerto de control se lee mediante un bit del puerto de datos correspondiente. Si se trata del puerto de control 1, se lee con el puerto CIAPRB; y si se trata del puerto de control 2 se lee con el puerto CIAPRA.

Tanto los puertos de control (las conexiones físicas) como los puertos de datos (los registros CIAPRA y CIAPRB) tienen ocho bits, pero como un joystick sólo tiene cinco interruptores, sólo se utilizan los cinco bits menos significativos:

  • %00011111 = nada pulsado.
  • %00001111 = disparo.
  • %00010111 = derecha.
  • %00011011 = izquierda.
  • %00011101 = abajo.
  • %00011110 = arriba.

Por último, en la entrada anterior ya se comentó que el puerto CIAPRB se utiliza para leer el teclado. Esto significa que puede haber confusiones entre teclado y joystick1. Cuando un programa lee CIAPRB, no tiene manera fácil de determinar si lo que lee ahí proviene del teclado o del joystick1, aunque hay algunos trucos como desactivar temporalmente el teclado mientras se lee el joystick1.

Y este es el motivo por el que muchos juegos del C64 utilizaban el joystick2. El joystick2 se lee con el puerto CIAPRA y, al no usarse este puerto para leer el teclado, se evitaba la confusión.


Programa de ejemplo: Prog56

CIA1: Lectura del teclado

El teclado se puede leer con la rutina SCNKEY del Kernal. No obstante, también se puede leer de forma directa utilizando el CIA1. Esto es lo que vamos a ver en esta entrada.

Las teclas del teclado están conectadas a una matriz de conexiones de modo que, cuando se pulsa una tecla (o varias a la vez), se “activan” determinados bits del puerto B del CIA1. Esos bits sirven para indicar la tecla o teclas que se han pulsado.

Matriz teclado

Si se compara la matriz anterior con un teclado del C64 se verá que faltan dos de las 66 teclas:

  • La tecla SHIFT LOCK.
  • La tecla RESTORE.

La tecla SHIFT LOCK, en realidad, no necesita ser leída de forma independiente. Siempre que esté pulsada se leerá como que está pulsada la tecla LEFT SHIFT.

Y la tecla RESTORE está conectada a la patilla “NMI” del 6510. Es decir, cuando se pulsa genera interrupciones no enmascarables (tipo NMI).

La forma de leer el teclado implica hacer un uso un poco singular del CIA1. De una forma global, el CIA1 se está utilizando para leer el teclado (digamos que como puerto de entrada), pero la forma de conseguirlo es configurando el puerto A como puerto de salida (CIDDRA = $ff) y el puerto B como puerto de entrada (CIDDRB = $00). Esta es la configuración por defecto del CIA1.

Para saber si una determinada tecla está pulsada, por ejemplo, la M, hay que escribir un 0 en la fila correspondiente (bit 4 del puerto A) y un 1 en el resto de filas (resto de bits del puerto A). De este modo, digamos que “centramos la atención” en esa fila. Posteriormente, leemos el puerto B. Esta lectura nos dirá qué tecla o teclas están pulsadas (de la fila en cuestión, es decir, N, O, K, M, 0, J, I o 9). En particular, si el bit 4 del puerto B está a 0, entonces la tecla M está pulsada.

Tecla pulsada

Lógicamente, si al programador no le interesa una tecla específica, por ejemplo, la M, sino que le interesa cualquier tecla que se haya podido pulsar, el programa tendrá que recorrer todas las filas de la matriz anterior y, para cada fila, determinar la tecla o teclas que se hayan pulsado.

Esta información (la tecla pulsada) se puede entregar al programa usuario tan pronto como se detecte la primera tecla pulsada, y volver a empezar el barrido, o hacer barridos completos de toda la matriz y entregar todas las teclas pulsadas mediante un buffer.


Programa de ejemplo: Prog55