Instrucciones para operaciones aritméticas (suma)

Las instrucciones para hacer operaciones aritméticas son:

  • “adc”.
  • “sbc”.

La primera instrucción vale para sumar y la segunda para restar.

“adc” es la instrucción que permite sumar un número al acumulador, dejando la suma nuevamente en el acumulador. Y un detalle muy importante es la “c” del final; la instrucción no se llama “add”, se llama “adc”. Esto es así porque en realidad lo que se suma son tres cosas: el acumulador, el operando, y el flag C – carry.

Esto es fuente de muchísimos errores, porque uno hace un programa para sumar el acumulador (supongamos que tiene el valor 10) y otro valor (supongamos que otro 10), y a veces obtiene 20 y otras veces 21, y no se explica por qué. La explicación es el flag C – carry, que a veces vale 0 y a veces vale 1, en función de la situación previa del registro de estado. Para evitar esto, siempre hay que preceder toda (o casi toda) instrucción “adc” con una instrucción “clc”, que lo que hace es borrar el acarreo.

Pero el hecho de sumar el flag C tiene su razón de ser. No está ahí para molestar. Al sumar dos bytes (el acumulador y otro) es posible que la suma exceda la capacidad del acumulador (ej. $ff + $ff = $1fe) y esto se señaliza poniendo el flag C a uno. En cierto modo el flag C nos está señalizando ese “1” extra que tenemos por la izquierda. Pero es que, además, si a uno no le llega con un byte para la suma, lo lógico es que use dos bytes. Y aquí es donde entra el acarreo: en la suma multi-byte.

Para sumar dos sumandos de dos bytes, lo que hay que hacer es:

  • Borrar el acarreo que pudiera venir heredado de antes (“clc”), para que así no moleste.
  • Sumar los bytes menos significativos de ambos sumandos. Uno tendrá que estar en el acumulador, y el otro estará en memoria según el modo de direccionamiento que se utilice. También se sumará el acarreo (“adc”), pero ya hemos dicho que lo habremos puesto a cero. El resultado se llevará al byte menos significativo de la suma.
  • Sumar los bytes más significativos de ambos sumandos. Nuevamente, uno tendrá que estar en el acumulador. Y también se sumará el acarreo derivado de la suma de los bytes menos significativos (“adc”). El resultado se llevará al byte más significativo de la suma.

Obsérvese que entre la suma de los bytes menos significativos y la suma de los bytes más significativos no se borra el acarreo (“clc”). Esto es deliberado; interesa tener en cuenta ese acarreo para que la suma sea correcta.

El procedimiento anterior se puede generalizar fácilmente a N bytes. Y ahora se le ve la razón de ser al acarreo: no sólo señalizar un desbordamiento de capacidad; también facilitar la suma multi-byte.

La instrucción “adc” soporta muchos modos de direccionamiento: inmediato, absoluto, página cero, absoluto indexado, página cero indexado, indirecto – indexado e indexado – indirecto. Y afecta a los flags: S – sign, V – overflow, Z – zero y C – carry.

Además, debe tenerse en cuenta que las reglas de la suma no son las mismas si se está usando codificación binaria (lo más habitual) o si se está usando codificación BCD (menos habitual). En el segundo caso habrá que informar al microprocesador mediante la instrucción “sed”.

Los detalles de esta instrucción se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#ADC.


Programa de ejemplo: Prog13

Instrucciones de transferencia entre registros

Las instrucciones de transferencia entre registros son:

  • “tax”.
  • “txa”.
  • “tay”.
  • “tya”.

Las dos primeras mueven el contenido del acumulador al registro X y viceversa. Las dos segundas mueven el contenido del acumulador al registro Y y viceversa.

Todas ellas utilizan el modo de direccionamiento implícito (porque los datos sobre los que se operan están implícitos) y afectan a los flags S – sign y Z – zero.

Los detalles de estas instrucciones se pueden consultar aquí: http://www.6502.org/tutorials/6502opcodes.html#TAX.


Programa de ejemplo: Prog12

Instrucciones de escritura de datos

Las instrucciones que permiten escribir o grabar datos en memoria son:

  • “sta”.
  • “stx”.
  • “sty”.

“sta” permite escribir el acumulador en una posición de memoria. Esa posición de memoria se puede especificar mediante los modos de direccionamiento absoluto, absoluto indexado, página cero, página cero indexado (usando como índice sólo X), indirecto – indexado, e indexado – indirecto.

Análogamente, “stx” permite escribir el registro X en la memoria. Los modos de direccionamiento soportados son: absoluto, página cero, y página cero indexado (usando como índice Y).

Finalmente, “sty” permite escribir el registro Y en la memoria. Los modos de direccionamiento soportados son los mismos que “stx”, si bien el modo página cero indexado utiliza el índice X.

Ninguna de estas instrucciones afecta a los flags del registro de estado. Esto es así porque no cargan datos ni modifican los registros, sino que graban datos en memoria.

Los detalles de estas instrucciones se pueden consultar aquí: http://www.6502.org/tutorials/6502opcodes.html#STA.


Programa de ejemplo: Prog11

Instrucciones de lectura de datos

Las instrucciones que permiten leer o cargar datos desde la memoria son:

  • “lda”.
  • “ldx”.
  • “ldy”.

“lda” permite cargar datos de la memoria en el acumulador. Soporta casi todos los modos de direccionamiento: inmediato, absoluto, página cero, absoluto indexado, página cero indexado, indirecto – indexado, e indexado – indirecto.

Análogamente, “ldx” permite cargar datos de la memoria en el registro X. Soporta menos modos de direccionamiento que “lda”, concretamente: inmediato, absoluto, página cero, absoluto indexado (usando como índice Y), y página cero indexado (usando como índice Y). No soporta los modos indirecto – indexado ni indexado – indirecto.

Finalmente, “ldy” permite cargar datos de la memoria en el registro Y. Soporta los mismos modos de direccionamiento que “ldx”, aunque lógicamente los modos indexados son usando como índice X.

Todas ellas afectan a los flags S – sign y Z – zero.

Los detalles de estas instrucciones (modos de direccionamiento soportados, “opcodes”, consumos de memoria, consumos de reloj, etc.) se pueden consultar aquí: http://www.6502.org/tutorials/6502opcodes.html#LDA.

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