Ejercicio obligatorio 4

Fecha de entrega: Domingo 20 de octubre

Introducción

El procesador MOS 6502 fue un procesador que era capaz de realizar operaciones en 8 bits pero que utilizaba 16 bits para indizar su memoria, pudiendo por esto operar hasta con 64 KB de memoria RAM.

Por diseño, el MOS 6502 requiere que haya siempre 64 KB de memoria RAM, la cual es compartida por el programa y los datos. Cuando un procesador utiliza una única memoria para ambas cosas se dice que es de arquitectura de Von Neumann.

El MOS 6502 era una arquitectura basada en acumulador. En estas arquitecturas existe un registro especial, el acumulador (A), sobre el cual se resuelven todas las operaciones. Por ejemplo, si se le dice al procesador que sume determinado valor (M) al ser una arquitectura de acumulador está implícito que lo que se computará es A += M, es decir, que el registro A será casi siempre un operando.

Además del acumulador, que se utiliza para todas las operaciones, muchos procesadores tienen registros especiales para trabajar con posiciones de memoria que complementan el acceso a RAM. El 6502 tenía 3 registros especiales para memoria.

Cuando se decide qué valores cargar en cualquiera de los registros intervienen los modos de direccionamiento de memoria. Por ejemplo, al cargar un dato en el acumulador, ese dato puede ser un valor constante, el valor de una posición de memoria determinada, el valor de determinada posición más un índice, etc.

Muchas instrucciones del MOS 6502 realizaban la misma operación pero eran diferentes en cuanto a los modos de direccionamiento de memoria. Por ejemplo, el mnemónico ADC (operación suma con carry, ADd with Carry) correspondía a 8 instrucciones diferentes del procesador según su modo de memoria, siendo la instrucción 0x69 la referida a sumar una constante al acumulador, o la instrucción 0x6D la correspondiente a sumar el valor contenido en una dirección de memoria dada. Asimismo no todas las variantes de la misma instrucción van a demorar el mismo tiempo, por ejemplo, el ADC de una constante se resuelve en 2 ciclos de máquina mientras que el ADC con una posición de memoria se resuelve en 4 ciclos.

La mayor parte de las instrucciones modifican el registro de status del procesador.

Los registros del MOS 6502

Internamente el CPU del MOS 6502 contaba con unos pocos registros:

El registro de status:

Registro de 8 bits que almacenaba los flags del procesador (ver EJ3).

El contador de programa (PC):

Registro de 16 bits que almacena la próxima instrucción a ser ejecutada.

Acumulador (A):

Registro de 8 bits sobre el cual se opera.

Índice X (X):

Registro de 8 bits para almacenar contadores u offsets para acceder a memoria.

Índice Y (Y):

Similar a X.

El puntero de stack (SP):

Registro de 8 bits para almacenar el byte bajo de la direción de memoria del stack.

Modos de direccionamiento de memoria

El MOS 6502 tenía 13 modos diferentes de direccionar memoria. Vamos a mencionar sólo algunos de ellos:

Implícito:

En este modo la instrucción ya sabe qué memoria afecta. Por ejemplo la operación CLC, limpieza de carry (CLear Carry flag) no necesita leer nada de ningún lado, está implícito que lo que hace es poner en cero el carry C.

Acumulador:

En este modo la instrucción se opera sobre el acumulador. Por ejemplo la instrucción LSR A, desplazamiento lógico a la derecha (Logical Shift Right) opera específicamente sobre el acumulador (la instrucción LSR puede actuar también directamente sobre memoria).

Inmediata:

En este modo se carga una constante de 8 bits que está en memoria a continuación de la instrucción que se está ejecutando. Por ejemplo la instrucción LDA #10, cargar en acumulador (LoaD Accumulator) carga el valor decimal 10 en el acumulador. En el ejemplo la operación LDA inmediata tiene el código 0xA9 por lo que LDA #10 en memoria ocupará dos bytes: {0xA9, 10} o {0xA9, 0x0A}.

Absoluta:

En este modo se carga el valor contenido en la posición de memoria de 16 bits que se especifica a continuación de la instrucción. Por ejemplo la instrucción LDA $ABCD carga en el acumulador el contenido de la posición de memoria 0xABCD. En el ejemplo la operación LDA absoluta tiene el código 0xAD por lo que LDA $ABCD se codificará {0xAD, 0xCD, 0XAB}. Cabe aclarar que el MOS 6502 es una arquitectura little endian, por lo que el byte menos pesado va primero.

Los modos restantes son similares a estos, por ejemplo el direccionamiento de página cero es como el absoluto sólo que se asume que el byte alto vale 0x00. Luego hay modos que buscan en posiciones relativas al PC o en posiciones que son una combinación de otros modos con X o Y. No se mencionarán en este ejercicio pero es importante destacar que existen.

Instrucciones

Las instrucciones de un microprocesador son atómicas, y generalmente pueden implementarse en muy pocas líneas de lenguaje de alto nivel. Más especificamente casi todas las operaciones pueden resolverse en apenas una línea, pero el código de un emulador es más complejo porque el resultado de las operaciones muchas veces implica modificar los flags del registro de status o muchas veces hay que hacer operaciones previas para poder acceder a los datos.

El MOS 6502 tenía 151 instrucciones codificadas en 56 mnemónicos. Es decir, el mismo mnemónico se utilizaba para instrucciones diferentes que representaban la misma operación pero que operaban con diferentes operandos.

Entonces cada operación va a tener diferentes códigos según su direccionamiento de memoria.

Como ya se vio entre las mismas operaciones con distintas instrucciones puede haber diferencias de tiempos de ejecución.

Advertencia

Se va a utilizar la siguiente convención de códigos y notación:

A:

Acumulador.

M:

El operando que diga el direccionamiento.

X, Y:

Los registros X e Y respectivamente.

PC:

El contador de programa.

C, Z, V, N:

Los flags Carry, Zero, oVerflow y Negative respectivamente.

operando = operación => flags afectados:

Es decir la operación modifica al operando y además el resultado de la operación se refleja en los flags específicos.

opcode: direccionamiento (ciclos):

Para esa operación con ese código de operación (instrucción) se utiliza determinado direccionamiento y demora tantos ciclos de máquina.

Presentamos algunas de las operaciones del MOS 6502:

ADC, ADd with Carry, sumar con carry:

A = A + M + C => Z, C, N

Opcodes: 0x69: inmediato (2), 0x6D: absoluto (4).

Suma A con la memoria con el bit de carry. El resultado se almacena en A. El registro de status debe registrar si el resultado fue cero o si hubo un carry o si fue un número negativo.

AND, logical AND, and lógico:

A = A & M => Z, N

Opcodes: 0x29: inmediato (2), 0x2D: absoluto (4).

Realiza el and de bits entre A y M.

ASL, Arithmetic Shift Left, shift izquierdo aritmético:

A = A << 1 => Z, C, N o M = M << 1 => Z, C, N

Opcodes: 0x0A: acumulador (2), 0x0E: absoluto (6).

Realiza un shift a izquierda. Ocurre un carry cuando estaba el bit 7 presente en el operando, o, lo que es lo mismo, en el resultado se encuentra el bit de carry.

CLC, CLear Carry flag, limpiar flag de carry:

C = 0

Opcodes: 0x18: implícito (2).

CMP, CoMPare, comparar:

A - M => Z, C, N

Opcodes: 0xC9: inmediato (2), 0xCD: absoluto (4).

Setea en C el resultado de A >= M, setea en Z el resultado de A = M, setea N si A - M es negativo. Notar que esta operación no almacena el resultado en ningún lado y notar que siempre se setean los tres flags según el resultado.

DEC, DECrement memory, decrementar memoria:

M = M - 1 => Z, N

Opcodes: 0xCE: absoluto (6).

DEX, DEcrement X register, decrementar registro X:

X = X - 1 => Z, N

Opcodes: 0xCA: implícito (2).

DEY, DEcrement Y register, decrementar registro Y:

Y = Y - 1 => Z, N

Opcodes: 0x88: implícito (2).

(Notar que las últimas 3 instrucciones no comparten mnemónico pero implícitamente son la misma operación, conviene emularla programando una función auxiliar y llamándola desde ellas.)

EOR, Exclusive OR, o exclusivo:

A = A ^ M => Z, N

Opcodes: 0x49: inmediato (2), 0x4D: absoluto (4).

JMP, JuMP, saltar:

PC = M

Opcodes: 0x4C: absoluto (3).

Setea el contador de programa en la posición de 16 bits dada por la memoria.

LDA, LoaD Accumulator, cargar acumulador:

A = M => Z, N

Opcodes: 0xA9: inmediato (2), 0xAD: absoluto (4).

LDX, LoaD X register, cargar registro X:

X = M => Z, N

Opcodes: 0xA2: inmediato (2), 0xAE: absoluto (4).

LDY, LoaD Y register, cargar registro Y:

Y = M => Z, N

Opcodes: 0xA0: inmediato (2), 0xAC: absoluto (4).

(Otra vez, las últimas 3 operaciones son idénticas pero cambia el operando.)

NOP, No Operation, no operar:

Opcodes: 0xEA: implícito (2).

No hace nada (pierde 2 ciclos de máquina).

SEC, SEt Carry flag, setear flag de carry:

C = 1

Opcodes: 0x38: implícito (2).

STA, STore Accumulator, guardar acumulador:

M = A

Opcodes: 0x8D: absoluto (4).

Guarda el contenido de A en M, no afecta los status.

STX, STore X register, guardar registro X:

M = X

Opcodes: 0x8E: absoluto (4).

STY, STore Y register, guardar registro Y:

M = Y

Opcodes: 0x8C: absoluto (4).

(Acá aclaramos lo contrario: Es más sencillo implementar una función diferente para cada una de estas 3 instrucciones que reutilizar.)

Cabe destacar que varias de las operaciones presentadas tiene muchos otros modos de direccionamiento, por ejemplo ADC, AND, CMP, EOR, LDA o STA tienen 8 opcodes diferentes.

Trabajo

Como se explicó ver el procesador tiene 151 instrucciones diferentes, pero en realidad se trata de menos de 50 operaciones que se repiten variando el modo de acceso a memoria. Para diseñar un emulador, aprovecharemos eso separando el direccionamiento de la operación.

Para esto desarrollaremos independientemente las funciones de direccionamiento de las operaciones y vincularemos ambas cosas utilizando diccionarios de punteros a funciones.

Estructuras

Se tiene la estructura mos6502_t la cual representa todos los registros del CPU del MOS 6502 más un contador de los ciclos transcurridos. Contiene a PC, SP, A, X, Y, el status y un puntero a la la memoria.

typedef struct {
    uint8_t a, x, y;    // Registros A, X e Y.
    uint16_t pc;        // Program counter.
    uint8_t status;     // Registro de status.
    uint8_t sp;         // Stack pointer.
    uint8_t *mem;       // Memoria.

    long ciclos;        // Cantidad de ciclos totales de ejecución.
} mos6502_t;

Como ya vimos, una instrucción tiene un código de operación y una cantidad de ciclos. Además, según el tipo de operación habrá un par más de parámetros. Uno de ellos tendrá un operando de 8 bits, el cual puede ser el acumulador o una posición arbitraria de la memoria, así que deberá ser un puntero a donde esté el operando (ver el caso de ASL). En otras operaciones además habrá una dirección asociada, la cual será un valor de 16 bits (ver el caso de JMP).

Para los cuatro direccionamientos propuestos en este trabajo el direccionamiento implícito no seteará nada, dado que es el operador el que sabe de dónde sacar sus operandos. Los demás todos apuntarán la posición del operando (que puede ser registro o memoria). Particularmente en el caso del direccionamiento absoluto además importa cuál es la dirección en la cual vive esa memoria. En los operadores del modo absoluto algunos utilizarán la memoria (por ejemplo DEC) y otros utilizarán la posición (por ejemplo JMP) por lo que necesitan estar las dos.

Se tiene la estructura instruccion_t que representa todos los aspectos de una instrucción. Contiene el opcode, la cantidad de ciclos, el operando y la dirección. Dependiendo del tipo de operación tal vez sólo se use el operando (ej. ASL) o la dirección (ej. JMP) o ninguna de los dos (ej. NOP). Si hay dirección deberá cargarse el operando porque no todas las operaciones absolutas utilizan la dirección.

typedef struct {
    uint8_t codigo;     // Opcode.
    short ciclos;       // Cantidad de ciclos de la instrucción.
    uint8_t *m;         // Puntero al operando (registro o memoria).
    uint16_t direccion; // Dirección del operando (si corresponde).
} instruccion_t;

Direccionamientos

Se debe programar una función para cada uno de los 4 tipos de direccionamiento mencionados.

La firma de las funciones será void f(mos6502_t *, instruccion_t *); y recibirán un puntero al procesador y un puntero a la instrucción que se está ejecutando.

La instrucción ya traerá seteados el código de operación ansi:codigo y la cantidad de ciclos ansi:ciclos.

De corresponder deben setear en la instrucción los parámetros necesarios. Adicionalmente pueden tener que modificar el estado del procesador (por ejemplo, una instrucción de tipo inmediato tendrá que incrementar en uno el PC, o una instrucción de tipo absoluto tendrá que incrementar en dos el PC).

Operaciones

Se debe programar una función para cada una de las 18 operaciones mencionadas.

La firma de las funciones será void f(moss6502_t *, instruccion_t *); y recibirán un puntero al procesador y un puntero a la instrucción que se está ejecutando.

La instrucción ya tendrá seteados todos los parámetros correspondientes al direccionamiento.

Las funciones podrán modificar las estructuras recibidas según corresponda.

Se deben reutilizar las funciones desarrolladas en el EJ3 para todo lo correspondiente al manejo del registro de status.

Diccionario

Se debe implementar un diccionario que relacione el código de operación de la instrucción con la función de direccionamiento, la función de resolución de la instrucción y la cantidad de ciclos de máquina que demorará la misma.

Si bien el MOS 6502 no emplea todos los códigos disponibles para 8 bits, para evitar iterar el diccionario para buscar el código de operación se propone implementar un diccionario de 256 posiciones donde en la posición correspondiente al código contenga los datos. Es decir, si el diccionario se llamara, por ejemplo, opcodes en opcodes[0x69] se almacenará el puntero a la función que implementa ADC, el direccionamiento correspondiente a inmediato y los 2 ciclos de máquina que la operación consume.

Ejecución

Se debe implementar una función void ejecutar_instruccion(mos6502_t *m); la cual ejecute una única instrucción del procesador.

El pseudocódigo de la ejecución de una instrucción es:

  1. Obtener el código de la instrucción a ejecutar.

  2. Incrementar PC.

  3. Generar el instruccion_t con el código y los ciclos.

  4. Ejecutar la función de direccionamiento.

  5. Ejecutar la función de la operación.

  6. Computar los ciclos del procesador.

Pruebas

Se proveen pruebas que permiten testear independientemente cada una de las operaciones a implementar.

Las mismas se encuentran ACÁ.

Entrega

Deberá entregarse el código fuente de las operaciones y direccionamiento del microprocesador, el diccionario creado y de la función de ejecutar_instruccion().

La entrega se realiza por correo a la dirección algoritmos9511entregas en gmail.com (reemplazar en por arroba).

El ejercicio es de entrega individual.

Referencias

Todo lo referido a las instrucciones del MOS 6502 volcado en este enunciado se construyó en base a las siguientes fuentes:

Este material puede servir de consulta para profundizar o contrastar lo aquí especificado.