El hardware del Commodore 64

Para programar en lenguajes de alto nivel apenas es necesario conocer nada sobre la arquitectura del ordenador que se utiliza. Para programar en ensamblador, en cambio, sí es muy conveniente, o incluso necesario, conocer el microprocesador y la arquitectura del ordenador, en este caso el C64. Por eso empezamos revisando el hardware del C64.

Placa base

De una manera muy esquemática, el C64 consta de:

  • Un microprocesador, concretamente el 6510.
  • Una o varias zonas de memoria ROM, es decir de sólo lectura, para almacenar datos y programas que no cambian, como el intérprete de BASIC, el mapa de caracteres estándar, o las rutinas del Kernal. La memoria ROM es no volátil, es decir, no se pierde con el apagado/encendido.
  • Una o varias zonas de memoria RAM, es decir de lectura/escritura, para almacenar datos y programas de usuario. La memoria RAM es volátil.
  • Dispositivos de entrada/salida que se encuentran accesibles como posiciones de memoria.

Todos estos componentes se interconectan entre sí mediante un bus de direcciones, un bus de datos, y un bus de control. El bus de direcciones marca qué dirección de memoria va a leer o escribir el microprocesador. El bus de datos mueve los datos. Si los mueve en el sentido memoria – microprocesador hablamos de “lectura”; si los mueve en el sentido microprocesador – memoria hablamos de “escritura”. Y el bus de control marca si la operación es de lectura o escritura, y cuándo debe realizarse (señal de reloj).

En definitiva, el microprocesador tiene acceso a un “mapa de memoria” donde están accesibles zonas de memoria ROM (datos y programas del sistema), zonas de memoria RAM (datos y programas de usuario), y dispositivos de entrada/salida. El microprocesador básicamente lo que hace es leer y ejecutar esos programas que están en memoria, leer datos de memoria, y generar y escribir otros datos.

En el caso del C64 el bus de direcciones es de 16 bits, lo que significa que se puede señalizar desde la dirección %0000000000000000 hasta la dirección %1111111111111111, es decir, desde la dirección $0000 hasta la $ffff en hexadecimal, o desde la 0 hasta la 65.535 en decimal. De ahí el nombre de Commodore 64, por las 64K posiciones de memoria direccionables.

Por otro lado, el bus de datos es de 8 bits, lo que significa que en cada una de esas 64K posiciones de memoria se almacena un byte. Como ya se ha comentado, los bits de un byte se numeran desde el 0 (menos significativo) hasta el 7 (más significativo): B7-B6-B5-B4-B3-B2-B1-B0.

A efectos de la memoria, los datos y las instrucciones o programas son indistinguibles. En ambos casos se trata de bytes almacenados. Es el microprocesador el que interpretará cada byte como instrucción o como dato, según corresponda.

Los dispositivos de entrada/salida (ej. teclado, unidad de cassete, unidad de disco, etc.) no se conectan directamente al microprocesador mediante los buses de datos y direcciones. Lo hacen a través de circuitos integrados (6526 CIA – Complex Interface Adapter) que hacen de interfaz entre los dispositivos y el microprocesador, adaptando las especificaciones eléctricas de unos y otro, y haciendo que el microprocesador “vea” los dispositivos como posiciones de memoria en las que puede leer y escribir.

El dispositivo de salida por excelencia es la TV (o el monitor). En este caso, la conexión del microprocesador con la TV es a través de otro chip, el 6567 VIC – Video Interface Chip. Para dispositivos de sonido y música se utiliza el chip 6581 SID – Sound Interface Device. Estos chips, el VIC y el SID, son casi tan famosos o más que el propio 6510.

En realidad, el microprocesador 6510 no sólo maneja un mapa de memoria. Es capaz de manejar varios mapas de memoria distintos en función de cómo se configure. En otro blog posterior se hablará de los mapas de memoria.

Números negativos

Hasta ahora, de forma implícita hemos asumido que todos los números con que trabajamos son enteros positivos. Sin embargo, en ocasiones interesará trabajar con enteros negativos o, incluso, con números con decimales.

En el sistema de numeración decimal son enteros negativos, por ejemplo, el -37 o el -64. Pues bien, en los sistemas binario y hexadecimal también es posible trabajar con números negativos.

La característica de todo número negativo (ej. -37) es que es el opuesto de otro positivo (ej. 37). Por tanto, dado que los números positivos ya están dominados, la clave está en localizar los opuestos.

Ahora fijémonos en el número 37 en binario: %00100101. Si cambiamos los 0’s por 1’s y los 1’s por 0’s obtenemos lo que se llama su complemento o complemento a uno: %11011010. Y si sumamos un número más su complemento, lo que se obtiene es todo 1’s, es decir, %11111111 o, lo que es lo mismo, 255 en decimal o $ff en hexadecimal.

Pues bien, si además de sumar el complemento también sumamos 1, lo que se obtiene es %100000000 (9 bits) o, lo que es lo mismo, 256 o $100 en hexadecimal. Y si ahora tenemos en cuenta que el C64 es una máquina de 8 bits, y que el bit más significativo (bit 8 contando desde 0) se perderá o, a lo sumo, irá a parar al flag C del registro de estado (esto se verá más adelante), la conclusión es que se obtiene %00000000 (8 bits) o, lo que es lo mismo, 0 en decimal o $00 en hexadecimal.

Es decir, hemos encontrado un número (llamado “complemento a dos”) que, sumado al número de partida (37), nos da cero. Por tanto, el complemento a dos viene a ser el opuesto del número de partida (-37). Y se obtiene fácilmente calculando el complemento y sumando 1.

De este modo, los opuestos o complementos a dos de los siguientes números son:

Complemento a dos

Obsérvese que todos los números considerados negativos, los complementos a dos, tienen un 1 en el bit más significativo, el bit 7, mientras que los otros números tienen un 0 en ese bit. Por eso al bit 7 se le suele llamar bit de signo.

El microprocesador 6510 puede sumar y restar, pero, curiosamente, la resta la implementa obteniendo el complemento a dos y sumándolo. Esto es así porque el complemento a dos es muy fácil de obtener: sólo hay que cambiar los 0’s por 1’s y viceversa, y luego sumar 1.

Por último, conviene notar que los bytes como tal no son positivos ni negativos. Sólo son eso: bytes o paquetes de bits. Es el programador el que debe saber si el número ahí almacenado debe interpretarlo como un número sin signo (del 0 al 255) o con signo (del -128 al 127), y actuar en consecuencia.


Programa de ejemplo: Prog04

BCD – Binary Coded Decimal

Los sistemas de numeración decimal, binario y hexadecimal son conceptos matemáticos. Son formas de representar los números. Un mismo número, por ejemplo, el 37, puede representarse como 37 (decimal), %00100101 (binario), o $25 (hexadecimal). Pero el número es el mismo: 37.

Por tanto, a la hora de programar, o a la hora de referirnos al número 37, indistintamente podemos usar 37, %00100101, o $25. Ahora bien, el ordenador lo que almacena en memoria son bytes, y los bytes son paquetes de 8 bits. Por tanto, si el ordenador tiene un 37 en una posición de memoria, lo que tendrá almacenado necesariamente será 00100101, indistintamente de cómo lo representemos en los programas o cómo nos refiramos a ese número.

Lo anterior es cierto si se utiliza la codificación binaria, que es la más habitual. Pero el C64 admite una codificación alternativa llamada BCD – Binary Coded Decimal, es decir, decimal codificado en binario. Esta forma de codificación permite codificar los dígitos decimales (del 0 al 9) usando 4 bits, lo que también se llama “nibble” (un byte tiene dos nibbles). Es decir, el número 37 se codificaría como 0011 en el nibble más significativo y 0111 en el nibble menos significativo. De este modo, con un byte es posible codificar desde el decimal 0 hasta el decimal 99.

El C64 no sólo permite esta codificación, es decir, almacenar los números decimales de esta manera. También permite hacer operaciones aritméticas sobre datos que están codificados así. Esto se consigue activando el flag D del registro de estado, como se verá en entradas posteriores.

La codificación BCD también está ampliamente descrita en Internet: https://es.wikipedia.org/wiki/Decimal_codificado_en_binario.


Programa de ejemplo: Prog03

Sistemas de numeración

En la vida cotidiana, y cuando se programa en lenguajes de alto nivel como BASIC u otros, lo normal es usar números en base 10. En cambio, cuando se programa en ensamblador, lo más cómodo es usar números en base 16 (hexadecimal) y, en ocasiones, en base 2 (binario).

Por ejemplo, para manejar direcciones de memoria del C64 resultan muy cómodos los números hexadecimales. En cambio, para operar con los bits de una posición de memoria resultan muy cómodos los números binarios.

Al final, todos los sistemas de numeración se rigen por unos principios comunes:

  • La base indica el número de dígitos que se utilizan. En base 10 se utilizan los dígitos del 0 al 9 (10 dígitos); en base 2 el 0 y el 1 (2 dígitos); y en base 16 los dígitos del 0 al 9 además de las letras a, b, c, d, e y f (16 dígitos).
  • Los números se escriben de izquierda a derecha, siendo los dígitos de la izquierda los que más peso tienen (los más significativos) y los de la derecha los que menos peso tienen (los menos significativos).
  • Dado un número de N dígitos en base B, su valor en decimal es D(N-1)*B^(N-1) + D(N-2)*B^(N-2) + … + D(1)*B^(1) + D(0)*B^(0). Obsérvese que lo dígitos se numeran desde 0 (el menos significativo) hasta N-1 (el más significativo). Por ejemplo, el número hexadecimal ffff, que se suele poner como $ffff para marcar que es hexadecimal, vale 15*16^3+15*16^2+15*16+15, es decir, 65.535.
  • Se puede operar con números binarios y hexadecimales de forma similar a cómo se opera con números decimales. Se puede sumar, restar, multiplicar y dividir. Las rutinas para sumar/restar/multiplicar/dividir son similares en todas las bases.

En todo caso, dado que hay mucha documentación en Internet relativa a los sistemas de numeración (ver por ejemplo https://es.wikipedia.org/wiki/Sistema_binario y https://es.wikipedia.org/wiki/Sistema_hexadecimal) no abundaremos más en ello.

Simplemente insistir en que, a la hora de programar en ensamblador, en general, lo que resulta cómodo es usar números hexadecimales y, en algunos casos particulares, números binarios.

En la mayoría de ensambladores, CBM prg Studio incluido, los números hexadecimales llevan el prefijo $, los números binarios el prefijo %, y los números decimales no llevan prefijo.


Programa de ejemplo: Prog02

Depuración de un programa en ensamblador

Tras programar un programa en ensamblador, hay que ensamblarlo. Al hacer esto, lo normal es que el ensamblador señale errores que habrá que corregir. En realidad, es un proceso iterativo: se programa, se ensambla, se corrige, se programa, se ensambla, se corrige, etc.

Llegará un punto en que todos los errores de ensamblado se hayan corregido y el programa ya se ensamble bien, lo cual tampoco significa que el programa sea correcto. Para determinar si el programa es correcto, es decir, que hace lo que se espera de él, hay que probarlo.

En ocasiones, lo probaremos y llegaremos a la conclusión de que el programa es correcto. En otras ocasiones, en cambio, detectaremos algún error de funcionamiento (no ya de ensamblado).

Si detectamos un error de funcionamiento, lo ideal es que sea obvio en qué parte del código está el error. En tal caso, volveremos al código fuente, lo cambiaremos, y repetiremos el proceso.

Lamentablemente, en muchas ocasiones no será fácil localizar el error de funcionamiento, y tendremos que recurrir al depurador (Debugger) de CBM prg Studio para localizarlo. Básicamente, lo que nos permite el depurador es hacer una ejecución paso a paso, viendo el impacto que cada instrucción tiene sobre el C64 (en la memoria, en los registros del microprocesador, en la pantalla, etc.).

Para ejecutar el depurador hay que ir al menú Debugger > Debug project. Se abrirán las siguientes ventanas:

Debugger

La ventana más importante es la llamada “Debugger”. En esa ventana se ven los registros del microprocesador con sus valores (el contador de programa, el acumulador, el registro X, el registro Y, etc.). También se ve una zona de memoria con el código máquina que tiene cargado. En ese código máquina, además de las instrucciones y sus operandos, se ven las etiquetas, lo que facilita la lectura del código.

Sobre esta ventana, pulsando F7, se puede ejecutar instrucción a instrucción, e ir observando su efecto sobre los registros y/o la memoria. También se puede cambiar la instrucción que se va a ejecutar (el contador de programa) pulsando F6, para ir directamente a la instrucción o la zona del código que sea de interés.

Otras ventanas de interés son la “Watch list” (lista de variables o posiciones de memoria cuyos valores se muestran ahí para facilitar su supervisión), la pila o “Stack” (puntos de retorno de las llamadas a subrutinas), los puntos de ruptura o “Breakpoints” (puntos donde se para la ejecución para facilitar la inspección), o la memoria de vídeo (para observar cómo va cambiando la pantalla).

En definitiva, el depurador de CBM prg Studio es una herramienta muy útil para analizar en detalle por qué un programa no funciona como esperamos.

Un primer programa en ensamblador para el Commodore 64

Según los objetivos esbozados en la entrada “Objetivos”, lo primero sería revisar el hardware del Commodore 64. No obstante, dado que acabamos de instalar y configurar el emulador VICE y el entorno de desarrollo CBM prg Studio, lo primero que vamos a hacer es una pequeña demostración de un programa sencillo en ensamblador.

El programa va a pintar la cadena “PROGRAMACION RETRO DEL COMMODORE 64” en la primera línea de la pantalla.

El proceso para programarlo y probarlo es como sigue:

  • Ejecutar CBM prg Studio.
  • Crear un nuevo proyecto, elegir la máquina C64, y darle un nombre al proyecto.
  • En el explorador del proyecto, crear un fichero en ensamblador llamado Prog01.asm. Esto se hace con el botón derecho del ratón y añadir nuevo fichero.
  • Ahora queda hacer el programa, ensamblarlo y probarlo.

El programa es algo tan sencillo como lo que sigue (el ; sirve para marcar los comentarios):

Prog01

En resumen:

  • Con * = $c000 se indica que la dirección de carga del programa es $c000 (hexadecimal), que en decimal es 49152.
  • “pantalla” es una constante que vale $0400 (hexadecimal), es decir, 1024. Esa posición de memoria es el comienzo de la RAM de pantalla. Cuando el ensamblador ensambla el programa, sustituye las constantes por su valor. Las constantes no ocupan memoria del C64.
  • “prog01”, “bucle”, “fin” y “cadena” son etiquetas. Las etiquetas representan posiciones de memoria del C64. Por ejemplo, “prog01” en este caso vale $c000 o 49152. Y siempre es más cómodo y flexible utilizar etiquetas antes que posiciones de memoria propiamente dichas. Te permiten reubicar el código en otra zona de memoria y que siga funcionando sin mayores modificaciones. Nuevamente, el ensamblador las sustituye por su valor al ensamblar.
  • “ldx”, “lda”, “beq”, “sta”, “inx”, “jmp” y “rts” son instrucciones en ensamblador del microprocesador 6510. De momento, con entender los comentarios a su derecha es suficiente.
  • Todo lo que va a la derecha de un ; es un comentario.
  • “text” y “byte” son tipos de datos del ensamblador. Sirven para definir variables, es decir, posiciones de memoria del C64 con una información almacenada. En este caso, hay dos cadenas de texto de 40 caracteres cada una (el ancho de pantalla del C64 es de 40 caracteres) y un byte 0 que sirve para marcar el final.

Y el algoritmo es bien sencillo:

  • Se inicializa el registro X a cero. Este registro funcionará como índice para recorrer las dos líneas de texto.
  • Se carga el carácter X-esimo en el acumulador.
  • Si el carácter cargado es 0, se salta al final.
  • Si el carácter cargado no es 0, se imprime en la pantalla.
  • Se incrementa el registro X, es decir, el índice.
  • Se continúa con el bucle hasta el final.

De momento, no estará muy claro qué son el acumulador o el registro X, pero lo iremos viendo. Valga el ejemplo para hacernos una idea de cómo es un programa sencillo en ensamblador.

Bueno, después de escribir el programa hay que ensamblarlo, corregir errores, y ejecutarlo. En este caso podemos hacerlo del tirón con Control + F5. Si todo está bien, se cargará el emulador y dentro de éste el programa:

Prog01-emulador

Por último, se puede ejecutar el programa con la sentencia BASIC SYS 49152, que es la posición de memoria en que se ha cargado el programa. El resultado es:

Prog01-ejecucion

Como se puede observar, las dos primeras líneas de la pantalla se han modificado con el contenido esperado.

Bueno, algo sencillo pero suficiente para hacerse una idea de cómo es un programa en ensamblador, cómo programarlo, ensamblarlo, y ejecutarlo en el emulador.


Programa de ejemplo: Prog01

Instalación y configuración de CBM prg Studio

La instalación y configuración de CBM prg Studio tampoco tienen mucho misterio. En este caso, lo que hay que hacer es:

  • Conectarse a la página de CBM prg Studio, cuya dirección es http://www.ajordison.co.uk/.
  • Entrar en la sección “Download”.
  • Descargar la última versión disponible, en este caso la 3.13.
  • Guardar el fichero comprimido en alguna carpeta, por ejemplo, el escritorio.
  • Extraer el contenido del fichero comprimido, que en este caso es el ejecutable de instalación, y ubicarlo donde nos interese.
  • Ejecutar el archivo de instalación. En este paso, es interesante seleccionar la opción de que se cree en el escritorio un acceso directo a la aplicación.
  • Ejecutar el acceso directo del escritorio.

Al ejecutar el acceso directo (ejecutable CBMPrgStudio.exe) se verá la pantalla de inicio de CBM prg Studio:

CBM-prg-Studio

En este caso, al contrario que con VICE, sí interesa hacer alguna configuración básica. Esto se hace en el menú Tools > Options:

  • En “Project” pondremos la máquina objetivo (C64) y la ubicación por defecto de los proyectos (ej. C:\Users\hvsw\Desktop\PRC64).
  • En “Emulator Control” podremos la ruta de VICE, por ejemplo, C:\Users\hvsw\Desktop\WinVICE-3.2-x86-r34842\x64.exe.

De este modo, será posible probar en VICE los programas que programemos en CBM prg Studio. Ni siquiera será necesario salir de CBM prg Studio. Desde el propio entorno de programación será posible ensamblar y ejecutar en el emulador.

También será posible depurar con el debugger de CBM prg Studio.