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