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.

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