Volver a Página Principal

Tutorial de creación de un Emulador sencillo de Chip-8 con VS 2008 y C# (Parte 3)

Lenguaje: C#
Para: VS 2008 con Sdl.Net 6.1
Por Dark-N: hernaldog@gmail.com
http://darknromhacking.com
Hilo del Foro: http://foro.romhackhispano.org/viewtopic.php?f=4&t=872

Lección Anterior | Índice | Siguiente Lección

La idea

La idea en este capítulo es hacer el core de emulador, es decir, la parte que se encarga de cargar la ROM, leer las instrucciones y luego ejecutarlas en el "ciclo fetch".
Solo veremos las 2 primeras instrucciones en este capítulo. Tampoco se verá el manejo de Timers.

Programa Principal y Ciclo de Ejecución

Lo primero que hacemos es instanciar la clase y lamar al método principal Run().
static void Main(string[] args)
{
	Emulador emulador = new Emulador();
	emulador.Run();
}
Luego definimos el método principal Run, donde a parte de realizar las tareas de Reseteo de Hardware y Carga del Juego, también se realiza la más importante tarea conocida como Ciclo de Ejecución o game-loop, que es la particularidad que tienen los juegos, con respecto a otras aplicaciones, que significa que es un ciclo infinito que no se detiene o cambia a menos que haya alguna acción interactiva. ¿Se han fijado que en cualquier juego no se detiene a menos que uno salga a algún menú o apague la consola? Incluso hay un ciclo infinito cuando el juego está en pausa, ya que muestra la misma imagen una y otra vez.
void Run()
{			
	ResetHardware();

	if (CargarJuego() == true)   
	{
		while (true) //game-loop   
		{
			EjecutaFech();
			ManejaTimers();
		}
	}
}
En Resumen, adentro del programa principal se tiene:

  • Reseteo de Hardware: simulamos que lo que hace la máquina, o el chip-8 en este caso, al partir.
  • Carga del Rom o juego
  • Ciclo del Juego o Game-Loop. Si solo el paso anterioe es ok, se realiza el 3er. paso que corresponde al ciclo del juego donde se tienen 2 etapas: 1. Ejecución del ciclo Fetch donde internamente se leen y ejecutan las instrucciones. 2. Manejo de los timers.

    Como se ve arriba, en C# se puede utilizar el while(true) para simular un ciclo infinito, donde no se detendrá a menos que se cierre la aplicación o se ejecute la sentencia break.

    Reseteo de Hardware

    Todos los emuladores deben simular el inicio de la máquina o chip que emulan, en nuestro caso debemos simular el reseteo del chip-8. Los pasos a realizar son:

  • Reseteo de los timers
  • Reseteo de Registros generales
  • Limpiado del Registro V
  • Limpiado de memoria
  • Limpiado de la Pila
  • Carga de Fuentes a Memoria
    void ResetHardware()
    {
    	// Reseteo de Timers.
    	delayTimer = 0x0;
    	soundTimer = 0x0;
    	
    	// Reseteo de Registros generales
    	opcode = 0x0;
    	PC = DIR_INICIO;
    	SP = 0x0;
    	I = 0x0;            
    	
    	// Limpiado del Registro V
    	for (int regActual = 0; regActual < CANT_REGISTROS; regActual++)
    	{
    	    memoria[regActual] = 0x0;
    	}
    	
    	// Limpiado de memoria
    	for (int dir = 0; dir < TAMANO_MEM; dir++)
    	{
    	    memoria[dir] = 0x0;
    	}
    	
    	// Limpiado de la Pila
    	for (int item = 0; item < TAMANO_PILA; item++)
    	{
    	    pila[item] = 0x0;
    	}
    	
    	// Carga de Fuentes a Memoria (eran 80 bytes, 5 byte por cada una de las 16 letras)
    	for (int i = 0; i < 80; i++)
    	{
    	    memoria[i] = arregloFuentes[i];
    	}
    }
    

    Carga de la ROM (y como se carga en la Memoria RAM)

    Como estamos simulando el verdadero comportamiento del Chip-8, debemos cargar la ROM leyéndola byte a byte con el método FileStream.ReadByte() para cargarla desde la dirección 0x200, que en nuestro código sería memoria[DIR_INICIO]+0, memoria[DIR_INICIO]+1, memoria[DIR_INICIO]+2, etc. La forma más práctica de abrir la ROM es con la clase FileStream ([archivo a abrir],[modo Open]):

    bool CargarJuego()
    {
    	string nombreRom = "PONG";
    	FileStream rom;
    	
    	try
    	{
    		rom = new FileStream(@nombreRom, FileMode.Open);
    		
    		if (rom.Length == 0)
    		{
    			Console.Write("Error: ROM dañada o vacía");
    			return false;
    		}
    		
    		// Comenzamos a cargar la rom a la memoria a partir de la dir 0x200
    		for (int i = 0; i < rom.Length; i++)
    			memoria[DIR_INICIO + i] = (byte)rom.ReadByte();
    		
    		rom.Close();
    		return true;
    	}
    	catch (Exception ex)
    	{
    		Console.Write("Error general al cargar la ROM. " + ex.Message);
    		return false;
    	}
    }
    
    Es muy importante entender como es la carga en la Memoria del Chip-8 (cuando hablamos de Memoria nos referimos a la Memoria RAM, por si acaso ya que la otra memoria, el juego, es la Memoria ROM o memoria de solo lectura).
    Nosotros lo cargamos "byte a byte" leyendo de la ROM y copiándolo a la Memoria. Ahora, la memoria del Chip-8 parte desde la dirección 0x000 (0000 0000 0000 binario) y termina en la posición 0xFFF (4095 decimal o 1111 1111 1111 Binario).

    Por otro lado, en C# el tipo de dato byte va del 0 al 255 o lo que es lo mismo del 00 al FF.

    Vamos paso a paso, primero revisando los códigos Hexadecimales de la ROM "PONG":



    Por otro lado, la descripción inferior muestra como está la Memoria inicialmente y como va quedando a medida que se carga con los bytes de la ROM "PONG":
    +---------------+= 0xFFF (4095) Fin RAM de Chip-8
    |               |
    |               |
    | 0x200 a 0xFFF |
    |  Programas de |
    |    Chip-8 o   |
    |    espacio    |
    |  para datos   |
    |               |
    +---------------+= 0x200 (512) Inicio de la mayoría de los programas de Chip-8
    | 0x000 a 0x1FF |
    | Reservado al  |
    |  interprete   |
    +---------------+= 0x000 (0) Inicio de la Memoria RAM de Chip-8
    
    Luego si cargamos el primer byte de la ROM, el 6A que se ve en la imagen superior, por lo tanto tenemos:
    +-------+= 0xFFF (4095)
    |       |
    |       |
    |       |
    |       |
    |       |
    |       |
    |       |
    |       |
    +--6 A -+= 0x200 (512)
    |       |
    |       |
    |       |
    +-------+= 0x000 (0)
    
    Luego si cargamos el 2do byte de la ROM, el 02 que se ve en la imagen superior:
    
    +-------+= 0xFFF (4095)
    |       |
    |       |
    |       |
    |       |
    |       |
    |       |
    |       |
    |  0 2  |  0x201   -> aquí queda el 02
    +--6 A -+= 0x200 (512)
    |       |
    |       |
    |       |
    +-------+= 0x000 (0)
    
    Luego si cargamos el 3er byte de la ROM, el 6B:
    
    +-------+= 0xFFF (4095)
    |       |
    |       |
    |       |
    |       |
    |       |
    |       |
    |  6 B  |  0x202 (514) -> aquí queda el 6B
    |  0 2  |  0x201
    +--6 A -+= 0x200 (512)
    |       |
    |       |
    |       |
    +-------+= 0x000 (0)
    
    Para finalizar el ejemplo cargamos el 4er byte de la ROM, el 0C:
    
    +-------+= 0xFFF (4095)
    |       |
    |       |
    |       |
    |       |
    |       |
    |  0 C  |  0x203    -> aquí queda el 0C
    |  6 B  |  0x202 (514)
    |  0 2  |  0x201
    +--6 A -+= 0x200 (512)
    |       |
    |       |
    |       |
    +-------+= 0x000 (0)
    

    Ejecución del Ciclo Fetch

    La implementación original del Chip-8 tiene un total de 36 instrucciones, incluyendo funciones matemáticas, gráficas y de control de flujo. El Super Chip-48 tiene 10 más, haciendo un total de 46.
    Cada instrucción es de 2 bytes y se almacena en la memoria con el byte más significante primero (por ejemplo, si tengo la instruccion ABC2, que es de 2 bytes, en la memoria se almacena primero el AB y luego el 02).

    El Ciclo Fetch se divide en 2 fases:
  • 1. Lectura de cada instrucción desde la memoria
  • 2. Ejecución de cada una de las Instrucciones

    Lectura de las Instrucciones desde la Memoria

    En este paso vamos a leer las diferentes instrucciones que están en memoria ya que se cargaron previamente desde la ROM. Las instrucciones que leeremos son por ejemplo "Salta a una Subrutina" (el caso del opcode 0NNN, después veremos que son los Opcodes) o "Suma el Registro VX con el registro KK" (el caso del opcode 7XKK). La instrucción se arma haciendo 2 lecturas a la Memoria, primero con el bytes más significativo y luego el menos significativo. Se utiliza el Program Counter para ir "moviendose" a través de la memoria.
    Estos son los pasos en un seudo código:

  • 1) leer el byes más significativo, es decir leer el valor de memoria[PC]
  • 2) leer el byes menos significativo, es decir leer el valor de memoria[PC+1]
  • 3) nuestra instrucción, que sabemos que es de 2 bytes, se arma de la forma: A. muevo 1) a la izquierda 8 pocisiones (1 byte) y luego B. se "une" lo anterior A. con 2), no me refiero a sumar, sino colocado uno al lado de otro.

    Vamos por partes, como realizamos el paso A:
    Para mover ciertos bits de 1) a la izquierda se debe recordar el Capítulo 1, por lo que usamos un shift a la izquierda de 8 valores, de forma quedaría:
        memoria[PC] << 8
    Lo que internamente según el capítulo 1 es lo mismo que memoria[PC] * 2^8.

    Como se realiza el paso B:
    La unica forma de "unir" byes es con el comando OR (en C# es con "|"). De esta forma queda:
        Instruccion = memoria[PC] << 8 | memoria[PC+1]

    Finalmente siempre luego de asignar la Instruccion se debe incrementar el Program Counter (PC) para el siguiente ciclo, por lo tanto se deja PC + 2. No es PC + 1 como uno comúnmente pudiera pensar, ya que el PC salta de 2 en 2 ya que como se dijo en el paso anterior, la Instrucción lee de a 2 filas de la memoria (2 bytes).
    void EjecutaFech()
    {
    	// leemos cada una de las instrucciones desde la memoria. 
    	// cada instruccion es de 2 bytes
    	instruccion = memoria[PC] << 8 | memoria[PC + 1];
    	
    	// dejamos incrementado el Program Counter en 2, lista para leer 
    	// la siguiente instruccion en el siguiente ciclo.
    	PC += 2;
    
    Ejemplo de lectura de Instrucción usando los 2 primeros ciclos de PONG

    Se sabe por la imagen de los Bytes que forman el juego PONG que los 4 primeros bytes son: 6A, 02, 6B y 0C.
    Ahora, ejecutemos el paso 1) para esto tomamos el primer btye 6A:

    000000001101010 bin = 6A = 106 dec en memoria[PC] y PC inicialmente es 512 = 0x200

    Luego ejecutemos el paso 2) para esto tomamos el 2do byte, 02:

    000000000000010 bin = 2 en memoria[PC+1] = en memoria[513]

    Luego ejecutamos el paso A) aplicando shift a la izquierda del paso 1) en 8 pociciones:

    000000001101010 bin << 8 = 110101000000000

    Ahora ejecutamos el paso B) que "une" 1) con 2):

    instruccion = 000000001101010 << 8 | 000000000000010
    instruccion = 110101000000010 = 27138 dec = 0x6A02.

    Finalmente el resultado es la unión de los 2 bytes.

    Ahora si ejecutamos el 2do ciclo, vamos más rápido:

    Tomanos el 3er byte, 6B, lo movemos 8 bits a la izquierda y luego lo unimos con el 4to byte, 0C, lo que nos da:
    instruccion = 0x6B0C

    Ejecución de las Instrucciones

    Esta etapa se divide en 2:

  • 1. "División" de dicha instrucción en distintos bloques de bits llamados Opcodes
  • 2. Ejecución de cada instrucción utilizando los Opcodes


    1. División de la Instrucción en Opcodes

    A partir de la Instrucción, se debe obtener el valor del Registro KK y de los 4 Opcodes de 4 bits cada uno. La idea de los opcodes es ayudarnos a leer la instrucción, de esta forma se separa la instrucción en 4 bloques de 4 bits cada uno. Cada conjunto de bits representa un Opcode:

  • Opcode1: represente la 4 bits mayores
  • Opcode X: representa los 4 bits menores del byte mayor
  • Opcode Y: representa los 4 bits mayores del byte menor
  • Opcode N: representa los 4 bits menores
  • Opcodes NNN: representan los 12 bits menores
    
    Instrucción (2 bytes): 6A02 = 0110 1010 0000 0010 binario
    +----------+-----------+----------+-----------+
    |          |           |          |           |
    | Opcode1  | Opcode X  | Opcode Y | Opcode N  |
    |  0110    |   1010    |  0000    |   0010    |
    +----------+-----------+----------+-----------+
    |          |         Opcode  NNN              |
    |          |         10100000010              |
    +----------+-----------+----------+-----------+
    
    Continuando con el código fuente, esta parte sería así:
    void EjecutaFech()
    {
    	...
    	
    	//obtengo el valor del registro KK, de largo 1 byte, el más chico de la instrucción
    	KK = (instruccion & 0x00FF);
    	
    	// cada opcode es de 4 bit
    	opcode1 = (instruccion & 0xF000) >> 12; //los 4 bits mayores de la instrucción
    	opcode2 = (instruccion & 0x0F00) >> 8;  //X
    	opcode3 = (instruccion & 0x00F0) >> 4;  //Y
    	opcode4 = (instruccion & 0x000F) >> 0;  //Opcode N = los 4 bits menores de la instrucción
    
    	//obtengo el valor del opcode NNN
    	NNN = (instruccion & 0x0FFF);
    
    Si no entiendes lo anterior, en el Capítulo 1 está claramente explicado como extraer los bits para cada uno de los 4 opcodes utilizando el AND (&) y el shift a la derecha.

    2. Ejecución de cada instrucción

    Esta es la parte más larga de realizar del emulador, ya que se debe simular cada una de las 35 instrucciones. Para ir en orden, iremos siguiendo las instrucciones según aparecen en Cowgod's Chip-8 ya que están muy ordenadas cada una de las instrucciones. Si miras dicha tabla de instrucciones se ve que primero está 0NNN, luego 00E0, luego 1NNN, 2NNN, 3XKK, y así sucesivamente.

    Instrucciones del tipo 0XXX (Opcode1 = 0)

    Auí tenemos las 2 primeras instrucciones:

  • 1. 00E0 - CLS: Limpiar pantalla (clear screen)
  • 2. 00EE - RET: Retorno de una subrutina (Return From Subroutine)

    Nota: No se implementará la instrucción 0XXX - SYS debido a que según Cowgod's Chip-8, esta es ignorada por la mayoría de los interpretes actuales.
    // Ejecutamos las instrucciones a través de los opcodes
    switch (opcode1)
    {
    	// opcodes del tipo 0xxx
    	case (0x0):
    	{
    		switch (instruccion)
    		{
    			// opcode 00E0: Clear Screen.
    			case (0x00E0):
    			{
    				ClearScreen();
    				break;
    			}
    			// opcode 00EE: Return From Subroutine.
    			case (0x00EE):
    			{
    				ReturnFromSub();
    				break;
    			}
    		}
    		break;
    	}
    ...
    }
    
    Notar que después de cada "switch" tiene que existir un "break" que le indica al compilador .Net que salga del Case actual y continúe su ejecución.
    La implementación de estos 2 métodos ClearScreen y ReturnFromSub es la sgte:

    1. 00E0 - CLS: Limpiar pantalla (clear screen)
    void ClearScreen()
    {
    	// No se limpia la pantalla directamente, sino el arreglo que la representa
    	// asignandole un valor 0
    	for (int p =0; p < RES_X; p++)
    		for (int q = 0; q < RES_Y; q++)
    			arregloPantalla[p,q] = 0;					
    }
    

    2. 00EE - RET: Retorno de una subrutina (Return From Subroutine)

    Aquí la acciones son 2: 1ro se debe decrementar el puntero de la Pila (Stack Pointer o SP) en 1, luego se debe rescatar el valor que está allí guardado y asignar a la dirección actual (PC) que se ejecutará.
    Recordar que cada vez que se lea una dirección desde la Pila, primero se decrementa el SP en 1 y luego lee de la Pila.
    Y para el caso que se quiera guardar un valor en la Pila, primero se guarda el valor en la Pila y luego se debe aumenta el SP en 1.
    void ReturnFromSub()
    {
    	// Se diminuye el SP en 1
    	SP--;
    	
    	// Apuntamos el PC (que apunta a la sgte. instrucción a ejecutar) a la
    	// posición salvada en la Pila
    	PC = pila[SP];
    }
    
    Bueno , esto es por ahora. Falta por explicar el resto de las instruciones, pero eso lo veremos en el siguiente capítulo.



    Código fuente de esta parte (C# 2.0):
    using System;
    using System.IO;
    using System.Runtime.InteropServices;  //para "clearscreen" y el "beep" de la Bios
    
    namespace Chip8_ConsoleMode
    {
        class Emulador
        {
            #region variables emulador
    
            //variables principales        
            const int RES_X = 64;  //resolucion de pantalla 64x32
            const int RES_Y = 32;        
            const int DIR_INICIO = 0x200;
            const int TAMANO_MEM = 0xFFF;
            const int TAMANO_PILA = 16;
            const int CANT_REGISTROS = 16;
            int[,] arregloPantalla = new int[RES_X, RES_Y];
    
            //arreglo que representa la memoria
            int[] memoria = new int[TAMANO_MEM];
    
            //Arreglo que representa los 16 Registros (V) 
            int[] V = new int[CANT_REGISTROS];
    
            //arreglo que representa la Pila
            int[] pila = new int[TAMANO_PILA];
    
            //variables que representan registros varios
            int instruccion;  //representa una instruccion del Chip-8. Tiene largo 2 byte
            int PC;
            int I;
            int SP;
            int KK;
            
            //variables para manejar los opcodes
            int opcode1 = 0;
            int opcode2 = 0;  //X
            int opcode3 = 0;  //Y
            int opcode4 = 0;
            int NNN = 0;
    
            //variables que representan los 2 timers: Delay Timer y Sound Timer
            int delayTimer;
            int soundTimer;
    
            //variables para el manejo de fuentes (80 bytes, ya que hay 5 bytes x caracter 
            //y son 16 caracteres o letras (5x16=80). Cada font es de 4x5 bits.
            int[] arregloFuentes = {
    		   0xF0, 0x90, 0x90, 0x90, 0xF0,	// valores para 0
    		   0x60, 0xE0, 0x60, 0x60, 0xF0,	// valores para 1
    		   0x60, 0x90, 0x20, 0x40, 0xF0,	// valores para 2
    		   0xF0, 0x10, 0xF0, 0x10, 0xF0,	// valores para 3
    		   0x90, 0x90, 0xF0, 0x10, 0x10,	// valores para 4
    		   0xF0, 0x80, 0x60, 0x10, 0xE0,	// valores para 5
    		   0xF0, 0x80, 0xF0, 0x90, 0xF0,	// valores para 6
    		   0xF0, 0x10, 0x10, 0x10, 0x10,	// valores para 7
    		   0xF0, 0x90, 0xF0, 0x90, 0xF0,	// valores para 8
    		   0xF0, 0x90, 0xF0, 0x10, 0x10,	// valores para 9
    		   0x60, 0x90, 0xF0, 0x90, 0x90,	// valores para A
    		   0xE0, 0x90, 0xE0, 0x90, 0xE0,	// valores para B
    		   0x70, 0x80, 0x80, 0x80, 0x70,	// valores para C
    		   0xE0, 0x90, 0x90, 0x90, 0xE0, 	// valores para D
    		   0xF0, 0x80, 0xF0, 0x80, 0xF0,	// valores para E
    		   0xF0, 0x90, 0xF0, 0x80, 0x80		// valores para F
    		};
    
            private bool[] teclasPresionadas = { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false };
    
            const int TECLA_1 = 0;
            const int TECLA_2 = 1;
            const int TECLA_3 = 2;
            const int TECLA_4 = 3;
            const int TECLA_Q = 4;
            const int TECLA_W = 5;
            const int TECLA_E = 6;
            const int TECLA_R = 7;
            const int TECLA_A = 8;
            const int TECLA_S = 9;
            const int TECLA_D = 10;
            const int TECLA_F = 11;
            const int TECLA_Z = 12;
            const int TECLA_X = 13;
            const int TECLA_C = 14;
            const int TECLA_V = 15;
    
            // mapeamos las 16 teclas de Chip8
            private byte[] MapeoTeclas = 
            {	       
                0x01,0x02,0x03,0x0C,
                0x04,0x05,0x06,0x0D,
                0x07,0x08,0x09,0x0E,
                0x0A,0x00,0x0B,0x0F 
            };
    
            //[DllImport("Kernel32.dll")] //para beep
            //public static extern bool Beep(UInt32 frequency, UInt32 duration);
            //En Framework 2.0 se puede usar directamente: Console.Beep(350, 50);
    
            [DllImport("msvcrt.dll")] //Framwork 1.1: para cls (clear screen) en DOS
            public static extern int system(string cmd);
            //En Framework 2.0 se puede usar directamente: Console.Clear();
    
            //Variable de tipo Random (para generar números aleatoios) utilizada por ciertas instrucciones
            Random rnd = new Random();
    
            #endregion
    
    
            static void Main(string[] args)
            {
                Emulador emulador = new Emulador();
                emulador.Run();
            }
    
            void Run()
            {
                ResetHardware();
    
                if (CargarJuego() == true)
                {
                    while (true) //game-loop
                    {
                        EjecutaFech();
                        //ManejaTimers(); //no implementado aun
                    }
                }
            }
    
    
            void ResetHardware()
            {
                // Reseteo de Timers
                delayTimer = 0x0;
                soundTimer = 0x0;
    
                // Reseteo de Registros generales
                instruccion = 0x0;
                PC = DIR_INICIO;
                SP = 0x0;
                I = 0x0;            
    
                // Limpiado del Registro V
                for (int regActual = 0; regActual < CANT_REGISTROS; regActual++)
                {
                    memoria[regActual] = 0x0;
                }
    
                // Limpiado de memoria
                for (int dir = 0; dir < TAMANO_MEM; dir++)
                {
                    memoria[dir] = 0x0;
                }
    
                // Limpiado de la Pila
                for (int item = 0; item < TAMANO_PILA; item++)
                {
                    pila[item] = 0x0;
                }
    
                // Carga de Fuentes a Memoria (eran 80 bytes, 5 byte por cada una de las 16 letras)
                for (int i = 0; i < 80; i++)
                {
                    memoria[i] = arregloFuentes[i];
                }
            }
    
            bool CargarJuego()
            {
                string nombreRom = "PONG";
                FileStream rom;
    
                try
                {
                    rom = new FileStream(@nombreRom, FileMode.Open);
    
                    if (rom.Length == 0)
                    {
                        Console.Write("Error: ROM dañada o vacía");
                        return false;
                    }
    
                    // Comenzamos a cargar la rom a la memoria a partir de la dir 0x200
                    for (int i = 0; i < rom.Length; i++)
                        memoria[DIR_INICIO + i] = (byte)rom.ReadByte();
    
                    rom.Close();
                    return true;
                }
                catch (Exception ex)
                {
                    Console.Write("Error general al cargar la ROM. " + ex.Message);
                    return false;
                }
            }
    
            void EjecutaFech()
            {
                #region lectura de instrucciones
    
                // leemos cada una de las instrucciones desde la memoria. 
                // cada instruccion es de 2 bytes
                instruccion = memoria[PC] << 8 | memoria[PC + 1];
    
                // dejamos incrementado el Program Counter en 2, lista para leer 
                // la siguiente instruccion en el siguiente ciclo.
                PC += 2;
    
                #endregion
    
                #region extracción de opcodes
    
                //obtengo el valor del registro KK, de largo 1 byte, el más chico de la instrucción
                KK = (instruccion & 0x00FF);
    
                // cada opcode es de 4 bit
                opcode1 = (instruccion & 0xF000) >> 12; //los 4 bits mayores de la instrucción
                opcode2 = (instruccion & 0x0F00) >> 8;  //X
                opcode3 = (instruccion & 0x00F0) >> 4;  //Y
                opcode4 = (instruccion & 0x000F) >> 0;  //Opcode N = los 4 bits menores de la instrucción
    
                //obtengo el valor del opcode NNN
                NNN = (instruccion & 0x0FFF);
    
                #endregion
    
                #region ejecución de instrucciones
    
                // Ejecutamos las instrucciones a travez de los opcodes
                switch (opcode1)
                {
                    // opcodes del tipo 0xxx
                    case (0x0):
                        {
                            switch (instruccion)
                            {
                                // opcode 00E0: Clear Screen.
                                case (0x00E0):
                                    {
                                        ClearScreen();
                                        break;
                                    }
                                // opcode 00EE: Return From Subroutine.
                                case (0x00EE):
                                    {
                                        ReturnFromSub();
                                        break;
                                    }
                            }
                            break;
                       }
                       
                    //....
                }
    
                #endregion
            }
    
            #region implementación de métodos para cada instrucción
    
            void ClearScreen()
            {
                // No se limpia la pantalla directamente, sino el arreglo que la representa
                // asignandole un valor 0
                for (int p = 0; p < RES_X; p++)
                    for (int q = 0; q < RES_Y; q++)
                        arregloPantalla[p, q] = 0;
            }
    
            void ReturnFromSub()
            {
                // Se diminuye el SP en 1
                SP--;
    
                // Apuntamos el PC (que apunta a la sgte. instrucción a ejecutar) a la
                // posición salvada en la Pila
                PC = pila[SP];
            }
    
            #endregion
    
        }
    }
    
    Lección Anterior | Índice | Siguiente Lección
    Volver a Página Principal

    blog comments powered by Disqus
    2003 - 2018    La Web de Dark-N