Los modos de direccionamiento más complejos

Por último, están los modos de direccionamiento indirectos e indexados (a la vez), que son los más complejos:

  • Direccionamiento indirecto – indexado. Es una mezcla del modo de direccionamiento indirecto y del indexado. Primero se produce una indirección (es decir, hay una dirección M1 –que tiene que estar en página cero– que apunta a otra dirección M2), y luego se suma el índice, que en este caso tiene que ser el Y. Por poner un símil, sería como tener un puntero a una tabla. El puntero apunta al comienzo de la tabla y con el índice se puede recorrer la tabla.

Modos dir4

  • Direccionamiento indexado – indirecto. Es como el anterior, pero al revés. Es decir, primero se aplica el índice, que en este caso tiene que ser el X, y luego se aplica la indirección. Por poner un símil, sería como una tabla de punteros (la tabla tiene que estar en la página cero). Con el índice X se selecciona el puntero, y luego se sigue el puntero.

Modos dir5

El modo de direccionamiento indirecto – indexado, es muy útil para recorrer grandes zonas de memoria. El modo de direccionamiento indexado (indexado sin más) se queda un poco escaso para este propósito, ya el índice sólo puede recorrer un máximo de 256 posiciones (porque los registros X e Y son de 8 bits). Sin embargo, con el modo indirecto – indexado hay una alternativa: se deja quieto el índice Y, por ejemplo, con el valor $00, y se va incrementando el puntero en página cero que, al ser de 16 bits, permite recorrer zonas de memoria más amplias. Por este motivo, se utiliza mucho.

El modo indexado – indirecto, en cambio, no es tan práctico y se usa mucho menos.


Programa de ejemplo: Prog10

Otros modos de direccionamiento no derivados de los básicos

Hay algunos modos de direccionamiento más complejos y que no guardan relación directa con los modos de direccionamiento básicos ya presentados:

  • Direccionamiento relativo. Las instrucciones de salto condicional (“beq”, “bne”, “bcc”, “bcs”, “bmi”, “bpl”, “bvc” y “bvs”) saltan a una posición de memoria cuando se da una condición. Pero esa posición de memoria no se expresa de forma absoluta (ej. $c100), sino como offset o diferencia respecto de la posición siguiente a la instrucción. Por eso se habla de direccionamiento “relativo”, porque lo que indica la instrucción es el salto que debe dar el contador de programa hacia delante o hacia atrás. El offset sólo ocupa un byte, y por tanto sólo permite saltos de 127 posiciones hacia delante o 128 posiciones hacia atrás.
  • Direccionamiento indirecto. La instrucción de salto incondicional “jmp” permite que el contador de programa salte a una dirección de memoria (direccionamiento absoluto). Pero además permite saltar a una dirección de memoria M2 almacenada en otra posición de memoria M1. En cierto modo, M1 es como un “puntero” o “vector” que apunta a M2. De ahí el nombre de “indirecto”, porque hay un nivel más de indirección. En realidad, dado que todas las direcciones (y en particular M2) ocupan dos bytes, no es posible almacenar M2 en M1. En M1 se almacenará el byte LSB de M2, y en M1+1 (la posición siguiente) se almacenará el byte MSB de M2.

El Kernal del C64 utiliza este modo de direccionamiento para ofrecer sus rutinas a través de una tabla de saltos (o “jump table”), de modo que si en el futuro (ya pasado) cambiara la ubicación en memoria de esas rutinas, el programador no tendría que cambiar sus programas; sólo se cambiarían las direcciones de la tabla de saltos.
Modos dir3


Programa de ejemplo: Prog09

Otros modos de direccionamiento derivados de los básicos

Hay otros modos de direccionamiento que, en cierto modo, son variantes o pequeñas complicaciones de los anteriores:

  • Direccionamiento de página cero. En el fondo es muy parecido al direccionamiento absoluto (el dato está en una posición de memoria), con la salvedad de que la posición de memoria referenciada está en la página cero y, por tanto, es suficiente el byte LSB para especificarla (dando por hecho que el byte MSB será $00). Gracias a esto se consigue que la instrucción ocupe un byte menos y se ejecute más rápido.
  • Direccionamiento acumulador. Hay instrucciones como “asl”, “lsr”, “rol” y “ror” que permiten manipular los bits del acumulador. Por tanto, los datos en cierto modo están implícitos en la instrucción; de ella se deduce que están en el acumulador. Entonces, ¿por qué se habla de otro modo de direccionamiento diferente (implícito vs acumulador)? Sinceramente, no lo tengo muy claro. No veo mucha diferencia en la práctica. La principal diferencia que aprecio es que las instrucciones que soportan el modo implícito (“clc”, “sec”, “cli”, “sei”, “cld”, “sed”, “clv”, etc.) no soportan otros modos de direccionamiento, mientras que las que soportan el modo acumulador (“asl”, “lsr”, “rol” y “ror”) sí tienen otros modos de direccionamiento, por ejemplo, para actuar sobre los bits de una posición de memoria (direccionamiento absoluto). Por tanto, podría ser una forma de diferenciar cuándo la instrucción actúa sobre una posición de memoria de cuándo actúa sobre el acumulador. Pero igualmente se le podría haber llamado “modo implícito” a mi entender.
  • Direccionamiento indexado (también llamado “absoluto, X” y “absoluto, Y”). Nuevamente, es parecido al direccionamiento absoluto, en el sentido de que el dato está en una posición de memoria. La novedad es que esa posición de memoria se determina sumando la posición incluida en la instrucción (bytes 2 y 3), más el registro X o Y. Como ya se comentó al revisar los registros, un uso muy importante de los registros X e Y es su uso como índice. Aquí se puede ver. Este modo de direccionamiento permite recorrer cómodamente zonas de memoria incrementando el valor del registro X o Y.
  • Direccionamiento indexado, pero relativo a posiciones de memoria en página cero (también llamado “página cero, X” y “página cero, Y”). Igual que el anterior, pero con una dirección de base en la página cero.

Modos dir2


Programa de ejemplo: Prog08

Los modos de direccionamiento básicos

Las instrucciones operan sobre datos. Por tanto, el microprocesador debe acceder a los datos en memoria de algún modo. Y eso son precisamente los modos de direccionamiento: las diferentes maneras que tiene el microprocesador de acceder a los datos.

Recordemos que las instrucciones del 6510 pueden ocupar:

  • Un byte. En este caso, ese único byte tiene que ser el “opcode” de la instrucción. Es decir, la instrucción no manejará datos, o estarán implícitos.
  • Dos bytes. En este caso, el primer byte será el “opcode” de la instrucción y el segundo byte será un dato o la forma de acceder a él.
  • Tres bytes. En este caso, el primer byte será el “opcode” de la instrucción, y los bytes dos y tres serán los datos o la forma de acceder a ellos.

De una forma intuitiva, las maneras más o menos obvias de acceder a los datos son:

  • Hay instrucciones que no requieren datos. Por ejemplo, las instrucciones que modifican los flags del registro de estado (“clc”, “sec”, “cli”, “sei”, “cld”, “sed” y “clv”) no necesitan datos. En la propia instrucción está implícito que operan sobre el registro de estado, y en qué sentido lo modifican (qué flag activan o desactivan).
  • La instrucción aporta el dato de forma directa, es decir, el dato aparece en el byte 2. Por ejemplo, en la instrucción “adc #$ff” el dato a sumar al acumulador ($ff) aparece directamente en el byte 2.
  • La instrucción aporta una dirección de memoria donde están los datos, es decir, los bytes 2 y 3 lo que aportan no es el dato, sino una dirección de memoria donde está el dato. Por ejemplo, en la instrucción “adc $c100” el dato a sumar al acumulador está en la posición de memoria $c100.

Los tres modos de direccionamiento recién presentados son, en este orden:

  • Direccionamiento implícito. En este modo de direccionamiento los datos están implícitos, es decir, de la propia instrucción se deduce dónde están los datos.
  • Direccionamiento inmediato. En este modo de direccionamiento los datos forman parte de la instrucción, entendiendo ahora “instrucción” en sentido amplio (“opcode” + operando).
  • Direccionamiento absoluto. En este modo de direccionamiento la instrucción aporta una dirección de memoria donde están los datos. Y como toda dirección de memoria en el C64 requiere dos bytes, primero irá el byte menos significativo de la dirección (LSB) y después el más significativo (MSB). Esto es así porque el 6510 sigue una arquitectura “Little-endian”. El microprocesador tendrá que acceder a esa posición de memoria para obtener los datos.

Modos dir1

Es importante recordar que una misma instrucción, por ejemplo “adc” o “lda”, tiene un “opcode” (código hexadecimal que identifica la instrucción) diferente para cada modo de direccionamiento soportado. Esta es la forma que tiene el microprocesador de identificar la instrucción y su modo de direccionamiento: mediante el “opcode”. Es decir, en el fondo es como si fueran instrucciones diferentes (relacionadas pero diferentes, porque cambia la forma de acceder a los datos).

En la página http://www.6502.org/tutorials/6502opcodes.html se pueden consultar todas las instrucciones del 6510 con sus modos de direccionamiento soportados, sus “opcodes” asociados, y otros datos (memoria ocupada, ciclos de reloj consumidos, etc.).


Programa de ejemplo: Prog07

El juego de instrucciones del 6510

El microprocesador 6510 tiene un juego de 56 instrucciones:

Instrucciones2

En realidad, la cosa tiene truco, ya que un mismo nemónico, por ejemplo “lda”, admite varios modos de direccionamiento distintos, y para cada modo de direccionamiento hay un “opcode” diferente:

Lda

Es decir, en realidad es como si “lda” fueran 8 instrucciones, una por cada modo de direccionamiento, y cada una con su “opcode” (código hexadecimal). Pero como todas ellas guardan relación con la carga de datos en el acumulador, los programadores conocen a todas como “lda”.

Pero a efectos del microprocesador es como si fueran instrucciones distintas. De hecho, lo que carga y ejecuta el microprocesador son los “opcodes” (bytes en memoria), no los nemónicos. Los nemónicos no son más que una ayuda para que los programadores identifiquen más fácilmente las instrucciones.

Lo bueno es que gracias a las dos entradas anteriores (“Los registros del microprocesador 6510 (parte I)” y «Los registros del microprocesador 6510 (parte II)«) ya hemos presentado la mayoría de esas 56 instrucciones, ya que casi todas ellas operan de un modo u otro con algún registro.

Según mis cuentas, sólo faltaría por presentar las instrucciones que no operan con registros, sino con posiciones de memoria:

  • “bit”. Similar a “and” pero sin modificar el acumulador.
  • “dec”. Decrementa el valor de una posición de memoria.
  • “inc”. Incrementa el valor de una posición de memoria.
  • “nop”. No hace nada.

En realidad, las instrucciones “bit”, “dec” e “inc” también actúan sobre registros ya que, como poco, casi todas las instrucciones actúan de un modo u otro sobre el registro de estado.

Que sepa, la única instrucción que no actúa sobre ningún registro es “nop” que, como su nombre indica, no hace nada (“no operar”). Y por tanto tampoco modifica ningún registro. Básicamente sirve para “rellenar memoria”.

En las entradas que siguen se irán estudiando las 56 instrucciones por familias. Y aportando ejemplos de uso.

Los registros del microprocesador 6510 (parte II)

El registro de estado (P)

Ningún programa se ejecuta de inicio a fin de forma incondicional. Normalmente, los programas tienen bifurcaciones del tipo IF – THEN que se ejecutan o no en función de los datos aportados o en función de las circunstancias que se den.

En el caso del ensamblador del 6510 esto se consigue gracias al registro de estado, que tiene una serie de bits llamados “flags” que señalizan la ocurrencia o no ocurrencia de determinadas circunstancias.

Registro P

Los bits/flags del registro P son:

  • Flag C o carry. Se activa cuando como consecuencia de una operación aritmética se produce acarreo, es decir, el “me llevo una”. Por ejemplo, si al sumar el acumulador A más el contenido de una posición de memoria M, la suma supera la capacidad del acumulador ($ff), entonces se activará este flag para indicar que ha habido acarreo y actuar en consecuencia.
  • Flag Z o zero. Se activa cuando el resultado de una operación es cero. Por ejemplo, si se compara el acumulador A con el contenido de una posición de memoria M, y son iguales, se activará el flag Z. Esto es así porque la comparación se instrumenta como una resta.
  • Flag I o interrupt. Se activa para deshabilitar las interrupciones de tipo IRQ. Más adelante se presentará en detalle qué son las interrupciones; de momento baste decir que las hay de dos tipos (IRQ y NMI), y que son alteraciones en el flujo normal de ejecución de un programa, normalmente para ejecutar código del sistema (rutinas de interrupción).
  • Flag D o decimal. Se activa cuando se habilita la aritmética decimal (BCD). Normalmente el microprocesador 6510 trabaja con aritmética binaria, pero también puede trabajar con aritmética decimal.
  • Flag B o break. Las interrupciones de tipo IRQ pueden tener su origen en el hardware (cuando se activa el pin IRQ del microprocesador) o en un programa (cuando se ejecuta una instrucción “brk”). En este último caso, se activa el flag B, de modo que la rutina de interrupción puede saber si su origen es hardware o software analizando este flag.
  • Flag V o overflow. Como se describió en la entrada “Números negativos”, es posible trabajar con bytes positivos (hasta 127) y negativos (hasta -128). Se da la paradoja de que, al sumar dos bytes positivos, por ejemplo, 127 y 127, el resultado podría ser negativo: 127+127=254=%11111110. Para señalizar estas situaciones (una suma que supera 127) y tratar el resultado de forma correcta se activa el flag V.
  • Flag N o negative (a veces también llamado S – sign). Siguiendo con los números negativos, si el resultado de una instrucción de carga o aritmética da lugar a un número negativo (bit 7 a 1), se activará el flag N.

Las instrucciones de salto condicional (“beq”, “bne”, “bcc”, “bcs”, “bmi”, “bpl”, “bvc” y “bvs”) producen un salto o no en el contador de programa en función de los valores de estos flags. Además, hay instrucciones específicas para activar/desactivar algunos de estos flags. Las instrucciones “sec”, “sed” y “sei” ponen el flag correspondiente a 1; las instrucciones “clc”, “cld”, “cli” y “clv” ponen el flag correspondiente a 0.

El puntero de la pila (S)

La pila o stack ocupa la página uno, es decir, desde la posición $0100 hasta la posición $01ff. Se va llenando de datos de arriba abajo, es decir, desde la posición $01ff hacia la posición $0100.

El puntero de la pila es un registro del microprocesador 6510 que apunta a la primera posición libre de la pila. Sólo necesita 8 bits, ya que se presupone que su byte más significativo es siempre $01 (página uno).

Pila

Cada vez que se llama a una subrutina con “jsr” la dirección de retorno de la subrutina se mete en la pila y se decrementa el puntero de la pila. La dirección de retorno se mete así: primero, es decir, en la posición más alta, el byte más significativo de la dirección de retorno, y luego, es decir, en la posición más baja, el byte menos significativo de la dirección de retorno.

Cuando se llega al final de la subrutina, es decir, cuando se ejecuta la instrucción “rts” se procede de manera contraria. Primero se extrae la parte menos significativa del nuevo contador de programa, y luego se extrae la parte más significativa. De este modo puede continuar la ejecución en el sitio de partida justo tras llamar a la subrutina.

La pila también se utiliza para salvaguardar la dirección de retorno y algunos registros cuando se ejecutan rutinas de interrupción. Las rutinas de interrupción terminan con la instrucción “rti”.

Por último, el programador también puede utilizar la pila para guardar y recuperar información, para lo cual dispone de las instrucciones “pha”, “pla”, “php” y “plp”. También puede intercambiar los valores de los registros X y el puntero de la pila (S) mediante las instrucciones “txs” y “tsx”.

El registro de dirección de datos (DDR) y el puerto de entrada/salida (IOP)

Un “puerto” es una conexión con el exterior que le permite al C64 intercambiar datos con su entorno. A efectos del microprocesador, el puerto suele ser una posición de memoria, de modo que escribiendo en ella se envían datos al exterior (salida) y leyendo datos de ella se leen datos desde el exterior (entrada).

Acompañando al puerto de entrada/salida (IOP) siempre hay un registro de dirección de datos (DDR) que permite configurar si cada bit del puerto es de entrada o salida. Hay una correspondencia uno a uno entre los 8 bit del IOP y los 8 bits del DDR. Si un bit del DDR está a cero, el bit correspondiente del IOP es de entrada de datos; y si está a uno, el bit correspondiente es de salida.

El microprocesador 6510 tiene un DDR y un IOP incorporados. Se manejan mediante las posiciones de memoria $0000 (DDR) y $0001 (IOP), y lo que permiten es configurar los diferentes mapas de memoria que se pueden configurar en el C64, activando y desactivando las zonas de ROM del mapa de memoria convencional que ya se describió.


Programa de ejemplo: Prog06

Los registros del microprocesador 6510 (parte I)

El microprocesador 6510 es un circuito integrado, un chip. Y como tal tiene una serie de componentes electrónicos internos. Y los componentes más importantes para el programador son sus “registros”, que vienen a ser como variables internas en las que el microprocesador puede leer información (bytes) que proviene de memoria, manipularla (ej. operaciones aritméticas, operaciones lógicas, etc.), y volver a escribirla a memoria. En definitiva, los registros permiten ejecutar los programas y procesar los datos.

Los registros del microprocesador 6510 son:

  • El acumulador (A).
  • El registro X (X).
  • El registro Y (Y).
  • El contador de programa (PC).
  • El registro de estado (P).
  • El puntero de la pila (S).
  • El registro de dirección de datos (DDR).
  • El puerto de entrada/salida (IOP).

Estos registros se describen a continuación.

El acumulador (A)

El acumulador es el registro de propósito más general. Tiene 8 bits.

Sirve para cosas como:

  • Operaciones de lectura de datos desde memoria (instrucción “lda”).
  • Operaciones aritméticas (instrucciones “adc” y “sbc”).
  • Operaciones lógicas (instrucciones “and”, “ora” y “eor”).
  • Operaciones de manipulación de bits (instrucciones “asl”, “lsr”, “ror” y “rol”).
  • Operaciones de comparación de datos (instrucción “cmp”).
  • Operaciones de escritura de datos en memoria (instrucción “sta”).

En el ensamblador del 6510, siempre que se hacen operaciones aritméticas o lógicas, uno de los operandos está en el acumulador y el otro operando está en memoria; el resultado se vuelve a guardar en el acumulador.

El registro X

El registro X también tiene 8 bits. Comparte algunas funciones con el acumulador (leer datos desde memoria, escribir datos en memoria, etc.) pero, además, tiene una función muy importante como índice. Es decir, sirve para hacer bucles y leer/escribir en el acumulador de forma consecutiva varias posiciones de memoria desde una posición de base. Esas posiciones de memoria se van recorriendo según se va incrementando/decrementando X.

Sirve para cosas como:

  • Operaciones de lectura datos desde memoria (instrucción “ldx”).
  • Operaciones para incrementar/decrementar el índice (instrucciones “inx” y “dex”).
  • Operaciones de comparación de datos (instrucción “cpx”).
  • Operaciones de escritura de datos en memoria (instrucción “stx”).
  • Operaciones de movimiento de datos con el acumulador (instrucciones “txa” y “tax”).
  • Operaciones de movimiento de datos con el puntero de la pila (instrucciones “txs” y “tsx”).

El registro Y

El registro Y también tiene 8 bits. Es muy similar al registro X, es decir, permite leer/escribir datos desde memoria y, además, tiene una función muy importante como índice.

Sirve para cosas como:

  • Operaciones de lectura datos desde memoria (instrucción “ldy”).
  • Operaciones para incrementar/decrementar el índice (instrucciones “iny” y “dey”).
  • Operaciones de comparación de datos (instrucción “cpy”).
  • Operaciones de escritura de datos en memoria (instrucción “sty”).
  • Operaciones de movimiento de datos con el acumulador (instrucciones “tya” y “tay”).

El registro Y no es exactamente igual al registro X. Las diferencias más importantes son:

  • Permite algunos modos de direccionamiento distintos. El registro X tiene el modo de direccionamiento indexado indirecto, y el registro Y el indirecto indexado; se verán en detalle más adelante.
  • El registro X permite intercambio de datos con el puntero de la pila (instrucciones “txs” y “tsx”), cosa que no es posible con el registro Y.

Por lo demás, son registros muy parecidos, y que se pueden usar indistintamente con frecuencia.

El contador de programa (PC)

El contador de programa es el registro del microprocesador 6510 que almacena la posición de memoria donde reside la siguiente instrucción que se va a cargar y ejecutar. Por tanto, al ser la memoria del C64 de 64K, necesita tener 16 bits.

El contador de programa normalmente se va incrementando de forma secuencial, salvo cuando se ejecuta alguna instrucción de salto incondicional (ej. instrucciones “jmp” y “jsr”/”rts”) o de salto condicional (ej. instrucciones “beq”, “bne”, “bcc”, “bcs”, “bmi”, “bpl”, “bvc” o “bvs”), en cuyo caso el contador de programa parece saltar de una dirección a otra.

Programas en ensamblador vs programas en código máquina

En las entradas anteriores se ha descrito el hardware del C64 (microprocesador, memoria ROM, memoria RAM y dispositivos de E/S) y el mapa de memoria más habitual. Se ha comentado, también, que en la memoria RAM se almacenan programas y datos de usuario, y en la memoria ROM se almacenan programas y datos del sistema.

Entonces… ¿en qué consiste exactamente un programa en ensamblador? ¿Y un programa en código máquina?

Un programa, en términos generales, es una secuencia de instrucciones que opera sobre unos datos para conseguir un objetivo. Cuando ese programa está en su formato original, o fuente, entonces se dice que es un programa en ensamblador (si es que está escrito en lenguaje ensamblador, claro). La apariencia que tiene un programa en ensamblador es la que se vio en la entrada “Un primer programa en ensamblador para el Commodore 64”. Es decir, se trata de una secuencia de instrucciones, o nemónicos, que operan sobre unos operandos (valores, posiciones de memoria, etc.).

Prog01

El programa fuente se ensambla mediante una aplicación llamada “ensamblador” (ej. CBM prg Studio o Kick Assembler), y entonces se convierte en un programa en código máquina. Es un proceso similar a la compilación de los lenguajes de alto nivel. El código máquina también es un programa, pero tiene otra forma: una secuencia de bytes almacenados en memoria (o en un fichero).

Código máquina

En definitiva, son las dos caras de la misma moneda. Cuando el programa está en su formato fuente, que es de fácil comprensión para el programador, se habla de “ensamblador”. Y cuando ya se ha ensamblado y es una secuencia de bytes en memoria se habla de “código máquina” o «lenguaje máquina».

El código máquina es el lenguaje que entiende de forma nativa el microprocesador y lo que puede ejecutar. Para ejecutar el código máquina, el microprocesador va leyendo el programa desde la memoria y va ejecutando sus instrucciones (bytes en memoria). Las instrucciones operan sobre datos, que son otros bytes en memoria.

El proceso de ensamblado traduce el programa en ensamblador (uno o varios ficheros *.asm) en el código máquina (una secuencia de bytes en memoria o fichero). En el caso particular del microprocesador 6510, toda instrucción ocupa 1, 2 o un máximo de 3 bytes en memoria.

El juego de instrucciones completo del microprocesador 6510 se puede consultar aquí.

Lda

Dada una instrucción, por ejemplo “lda”, es importante notar que tiene diferentes “modos de direccionamiento”. Los modos de direccionamiento son las formas de acceder a los operandos o datos. Y, para cada modo de direccionamiento:

  • La instrucción tiene una sintaxis diferente.
  • Se traduce a un byte diferente ($A9, $A5, $B5, …). Estos bytes se llaman “opcodes”.
  • Tiene un tamaño diferente (1, 2 o 3 bytes). Este tamaño incluye los operandos.
  • Tarda en ejecutarse más o menos ciclos de reloj.

Lógicamente el microprocesador tarda algunos ciclos de reloj en leer la instrucción y sus operandos. Y también tarda otros ciclos de reloj en ejecutar la instrucción. El reloj del C64 funciona a 1 MHz aproximadamente, es decir, tiene un millón de ciclos en un segundo.

El mapa de memoria del Commodore 64

Como ya se ha dicho, el C64 se llama así precisamente porque es capaz de gestionar 64K de memoria. Efectivamente, esto es así, ya que el C64 tiene un bus de direcciones de 16 bits. Por tanto, dado que cada línea del bus puede tomar 2 valores (0 o 1), eso nos da 2^16 o 65.536 direcciones o, lo que es lo mismo, desde la dirección 0 ($0000 en hexadecimal) hasta la dirección 65.535 ($ffff en hexadecimal).

Para programar con el C64, y especialmente en ensamblador, es fundamental conocer bien ese mapa de memoria ya que, como veremos más adelante, el 90% de la programación básicamente consiste en leer y/o escribir valores en memoria.

64K de memoria puede parecer algo sencillo de manejar, pero la realidad es algo más compleja, ya que:

  • Muchas de esas 64K posiciones de memoria tienen propósitos específicos. Pueden ser registros del chip de vídeo (VIC), registros del chip de sonido (SID), registros para entrada/salida, etc. Y lo primero para poder usarlas es conocerlas y saber dónde están. Por ejemplo, para manejar sprites hay que conocer los registros del VIC dedicados a este propósito.
  • Dentro del mismo espacio de memoria (esas 65.536 posiciones) hay zonas de memoria RAM, de lectura y escritura, y zonas memoria ROM, de sólo lectura. Por ejemplo, están en ROM el intérprete de BASIC, el mapa de caracteres estándar, y las rutinas del Kernal.
  • Es más, hay zonas donde la ROM y la RAM se solapan (comparten direcciones), y hay que saber qué hacer para leer o escribir de una o de otra. De hecho, se suele decir que el C64 aplica un modelo de “RAM bajo ROM”.
  • El microprocesador 6510 es básicamente igual que el 6502, con el mismo juego de instrucciones, pero con flexibilidad para manejar distintos mapas de memoria. Es decir, en el C64 es posible configurar diferentes mapas de memoria, por ejemplo, desactivando determinadas zonas de ROM cuando no son necesarias.

El mapa de memoria clásico al arrancar el C64, si no se configura ninguna otra opción, es como sigue:

Memoria

En este mapa de memoria se pueden observar:

  • $0002-$00ff: La página cero.
  • $0100-$01ff: La pila (página uno).
  • $0200-$9fff: 40K de RAM. En esta zona se incluyen casi 1K de RAM de pantalla ($0400-$07e7) y 38K para programas en BASIC ($0801-$9fff; estos son los famosísimos “38.911 BASIC BYTES FREE”).
  • $a000-$bfff: El intérprete de BASIC (ROM).
  • $c000-$cfff: 4K de RAM, donde con mucha frecuencia se ha ubicado la programación en ensamblador.
  • $d000-$dfff: Los chips de vídeo (VIC), sonido (SID), RAM de color (para la pantalla), y dispositivos de entrada/salida.
  • $e000-$ffff: Las rutinas del Kernal (ROM).

Lo cual nos da pie a comentar el concepto de “página”. Una página es un segmento de 256 posiciones de memoria consecutivas. Pero no pueden ser arbitrarias, tienen que ir desde una posición $xx00 hasta una posición $xxff. Por ejemplo, la página cero va de la $0000 hasta la $00ff, y la pila/página uno desde la $0100 hasta la $01ff. Dicho de otro modo, los 64K de memoria del C64 se organizan en 256 páginas (desde la página $00 hasta la $ff) de 256 bytes cada una (desde el byte $00 hasta el $ff).

La página cero es especial porque sus posiciones se pueden especificar con sólo un byte, el byte menos significativo, dando por hecho que el byte más significativo es $00. Además, el micro requiere menos ciclos de reloj para acceder a esas posiciones. Tanto es así que hay un modo de direccionamiento específico para esta página, que se llama direccionamiento de página cero. Más adelante se revisarán el concepto y los tipos de modos de direccionamiento; baste adelantar ahora que son las formas de acceder a la memoria.

Y la página uno también es especial, porque en ella se guarda la pila, es decir, la secuencia de llamadas a subrutinas o, con más precisión, las direcciones en las que seguir ejecutando el programa cuando terminen esas subrutinas.

En el fondo, todas las zonas de memoria son “especiales”, porque cada zona tiene su uso. Quitando quizás las zonas de 40K+4K de RAM, que tienen un propósito más general (almacenar datos y programas de usuario), todo lo demás tiene unos u otros usos específicos. Por eso es importante conocer el mapa, sus zonas, y sus usos.

Y siempre sin olvidar que el C64 admite configurar diferentes mapas de memoria. El comentado en detalle aquí es el mapa de uso más general. Esta configuración de mapas se realiza la modificando los valores de las posiciones $0000 y $0001.


Programa de ejemplo: Prog05

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.