Volver a Página Principal

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

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
(Agradecimientos a Ivan Dario Ospina Acosta por encontrar detalles en este capítulo que he corregido)

Índice | Siguiente Lección

La idea

En esta primera parte aprenderemos lo aspectos básicos para poder programar un emulador, como lo son, las operaciones básicas con bits, shifting, como extraer bits de un dirección de varios bytes y como unir bits.

Herramientas Utilizadas

  • Lenguaje de programación C# 2.0 con Visual Studio 2008.
  • Multimedia (gráficos, sonido, eventos de teclado) usaré SDL.NET 6.1.
    NOTA: este no es un tutorial de SDL.NET, si quieren saber más de esta librería recomiendo que visiten mi tutorial de desarrollo de juegos con SDL .NET que está en este sitio.
  • La Rom de PONG.

    Operaciones básicas con Bits

    Operaciones lógicas

    Para nuestro emulador usaremos estás operaciones que se aplican sobre bits (hay más):

  • AND: && y &: todos los elementos a comparar deben ser 1(true) para que el resultado sea 1(true). La regla general del AND es:
    -True AND True = True (1 AND 1 = 1)
    -True AND False = False (1 AND 0 = 0)
    -False AND True = False (0 AND 1 = 0)
    -False AND False = False (0 AND 0 = 0)

  • OR: || y |: si un elemento es 1 (true), el resultado es 1 (true). La regla general del OR es:
    -True OR True = True (1 Xor 1 = 1)
    -True OR False = True (1 Xor 0 = 1)
    -False OR True = True (0 Xor 1 = 1)
    -False OR False = False (0 Xor 0 = 0)

  • XOR: ^ : Es conocido como un "Or exclusivo", o para entender mas fácil es como un "or" diferente. La regla es: si los 2 valores a evaluar son 1 (true) o 0 (false) el resultado es 0 (false), pero si solo uno es 1(true), el resultado es 1(true). La regla general del XOR es:
    -True XOR True = False (1 XOR 1 = 0)
    -True XOR False = True (1 XOR 0 = 1)
    -False XOR True = True (0 XOR 1 = 1)
    -False XOR False = False (0 XOR 0 = 0)

    Los operadores && y || se diferencia de & y | en que los primeros realizan la evaluación "perezosa" y los segundos no. La evaluación perezosa consiste en que si el resultado de evaluar el primer operando permite deducir el resultado de la operación, entonces no se evalúa el segundo y se devuelve dicho resultado directamente, mientras que la evaluación no perezosa consiste en evaluar siempre ambos operandos.
    A modo de ejemplo, si el primer operando de una operación && es falso se devuelve false directamente, sin evaluar el segundo; y si el primer operando de una || es cierto se devuelve true directamente, sin evaluar el otro.

    Ejemplos generales:
             0011 (3)
       OR(|) 0101 (5)
      -----------
             0111 (7)
     
             0011 (3)
      AND(&) 0101 (5)
      -----------
             0001 (1)
     
             0011 (3)
      Xor(^) 1010 (10)
      -----------
             1001 (9)
      	   
             0101 (5)
      Xor(^) 0001 (1)
      -----------
             0100 (4)
    	   
             0000 (0)
      Xor(^) 0001 (1)
      -----------
             0001 (1)
    
    Ejemplos en C#:
      int a = 0xAF;       //175 = 10101111
      int b = 0x05;       //  5 = 00000101
    
      int EjAND = a & b;  //   10101111 
                          // & 00000101 
                          //   --------
                          //   00000101  = 5
    
      int EjOR  = a | b;  //   10101111 
                          // | 00000101 
                          //   --------
                          //   10101111  = 175
    
      int EjXOR = a ^ b;  //   10101111 
                          // & 00000101 
                          //   --------
                          //   10101010  = 170
    

    Operadores Shift

    Estos operadores sirven para multiplicar por 2 bits (<<) o dividir por 2 bits (>>), pero en nuestro emuladores los usaremos para "extraer" valores de bytes. Por ejemplo si tenemos 0x6A0B, podemos con los Operadores Shift obtener en una variable opcodeA = 0x6A y otra opcodeB = 0x0B.

      << : Shift a la izquierda, sirve para multiplicar por 2 el valor
             Fórmula: x = y << z      => es lo mismo que: x = y * 2z

      >> : Shift a la derecha, sirve para dividir por 2 el valor
             Fórmula: x = y >> z      => es lo mismo que: x = y / 2z

        Ejemplos Shift a la izquierda:
    uint valor = 3;             // binario:    00000011
    uint valor2 = valor << 1;   // Resultado = 00000110 = 6  (3 * 2^1)
    uint valor3 = valor << 4;   // Resultado = 00110000 = 48 (3 * 2^4)
    uint valor4 = 8 << 3;   // Resultado = 64 (8* 2^3)
    
    //Overflow de bit, cuando al mover a la derecha o izquierda no hay espacio:
    uint opcode = 240;   // 11110000
    uint opcode = opcode << 1;  // se pueden dar 2 cosas: 
                                // si opcode solo almacena valores de 8 bits = 11100000 (se pierde un 1) = 224
                                // si opcode solo almacena valores mayores de 8 bits = 111100000 (se agrega un 1) = 480
    
    Fijarse que Shift a la izquierda "movemos" todos los bits a la izquierda.

        Ejemplos Shift a la derecha:
    uint valor = 240; // 11110000
    uint valor2 = valor >> 1;  // Resultado = 01111000 = 120 (240 / 2^1)
    uint valor3 = valor >> 4;  // Resultado = 00001111 = 15  (240 / 2^4)
    
    int opcode = 10;
    opcode = opcode >> 0; // resultado opcode = 10;
    
    //Overflow de bit, cuando al mover a la derecha no hay espacio, se pueden producir inconsistencias en los resultados:
    uint opcode = 15;           // 00001111
    uint opcode = opcode >> 1;  // 00001111 se corre a la derecha y se "pierde un 1" = 00000111 = 7
    
    Fijarse que con Shift a la derecha "movemos" todos los bits a la derecha.

        Ejemplo real

    Analicemos la primera instrucción de PONG. Dicha ROM carga en memoria estos datos:

    Memoria[512] = 106;
    Memoria[513] = 2;

    Luego tenemos en el código:
      int PC = 0x200;  
      int instruccion = memoria[PC] << 8 | memoria[PC+1];
      int opcode = (instruccion & 0x0F00) >> 8;
    
    < ¿que valor tendrá la variable opcode al final?

    Sabemos que PC = 0x200 = 512. Entonces vamos por parte y escribamos instruccion:

    int instruccion = 106 << 8 | 2;

    Ahora resolvamos 106 << 8 = 01101010 << 8 = 27136

    Luego 27136 | 2 = 0110101000000000 | 00000010 = 0110 1010 0000 0010 = 27138 => convertimos a hex = 0x6A02
    Entonces instruccion = 0x6A02.

    Ahora vamos con la parte que falta:

    int opcode = (0x6A02 & 0x0F00) >> 8;

    0x6A02 & 0x00F0 = 0110101000000010 & 111100000000 = 101000000000 = 2560

    2560 >> 8 = 101000000000 >> 8 = corremos 8 veces los bits a la derecha:

    101000000000 inicio
    010100000000 1 vez
    001010000000 2
    000101000000 3
    000010100000 4
    000001010000 5
    000000101000 6
    000000010100 7
    000000001010 8 = > 10

    Finalmente queda:
    int opcode = 10

    Como extraer bits de una dirección

    Una cosa muy comun en un emulador es que hagas cáculos con ciertas partes de un una dirección y no con la dirección completa. Por ejemplo, si la dirección es 0xAAB0 y queremos revisar que los bytes mayores sean mayores a cero, esto sería algo como 0xAA > 0. Como dije arriba, para poder extraer opcodes de una una dirección (en chip-8 se opera con 12 bits) se debe usar Shift a la derecha o izquierda. Otra cosa a tener en cuenta es que en CHIP-8 las direcciones pueden ser de 16 bits aunque nunca se hacen operaciones con estos 16 bits, sinó que el intérprete (registro I) a los más, usa los 12 bits menores. Los 4 bits superiores se usan para la carga de Fuentes.

    Casos posibles
    Dirección (16 bits)  = 0000 0000 0000 0000
         Podemos sacar:
                           A    B    C    D   (A B C y D por separado, cada uno de 4 bits)
                           N    N    K    K   (N y K, cada uno de 8 bits)
                                Z    Z    Z   (Z de 12 bits)
    
    Esta es una forma de obtener cada uno de esas letras por separado usando C# es usar &(AND) y >> (Shift a la derecha):
    A = (Dirección & 0xF000) >> 12 = (Dirección AND 1111 0000 0000 0000) >> 12   -> movemos todo a la derecha 12 espacios
    B = (Dirección & 0x0F00) >> 8  = (Dirección AND 0000 1111 0000 0000) >> 8    -> movemos todo a la derecha 8 espacios
    C = (Dirección & 0x00F0) >> 4  = (Dirección AND 0000 0000 1111 0000) >> 4    -> movemos todo a la derecha 4 espacios
    D = (Dirección & 0x000F)       = (Dirección AND 0000 0000 0000 FFFF)         -> no se mueve
    N = (Dirección & 0xFF00) >> 8  = (Dirección AND 1111 1111 0000 0000) >> 8    -> movemos todo a la derecha 8 espacios
    K = (Dirección & 0x00FF)       = (Dirección AND 0000 0000 1111 1111)         -> no se mueve
    Z = (Dirección & 0x0FFF)       = (Dirección AND 0000 1111 1111 1111)         -> no se mueve
    
    Ahora calculemos cada letra tomando como base la dirección anterior 0x6A02 = 0110 1010 0000 0010
    A = (0x6A02 & 0xF000) >> 12 = (0110 1010 0000 0010 & 1111 0000 0000 0000) >> 12 = 0110 = 6
    B = (0x6A02 & 0x0F00) >> 8  = 1010 = 10
    C = (0x6A02 & 0x00F0) >> 4  = 0000 = 0
    D = (0x6A02 & 0x000F)       = 0010 = 2 
    N = (0x6A02 & 0xFF00) >> 8  = (0110 1010 0000 0010 & 1111 1111 0000 0000) >> 8 = 0110 1010  = 106
    K = (0x6A02 & 0x00FF)       = (0110 1010 0000 0010 & 0000 0000 1111 1111) =  0000 0010      = 2
    Z = (0x6A02 & 0x0FFF)       = (0110 1010 0000 0010 & 0000 1111 1111 1111) =  1010 0000 0010 = 2562
    
    Leer y unir BITS

    Para mostrar como "Leer" bits, imagina que tenemos un arreglo miArreglo con 2 elementos: miArreglo[1] = 106 y miArreglo[2] = 3
    Podemos crear una variable X que contenga el elemento de miArreglo[1]:
      int x = miArreglo[1];  //x valdría 106
    
    Para leer bits usaremos arreglos.
    
    Para mostrar como "Unir" se utiliza la instrucción | (OR). Ejemplo:
    La variable x contendrá el contenido del miArreglo[1]= 106 junto con miArreglo[2] = 3:
      int x = miArreglo[1] | miArreglo[2];   //x valdría 107
    
      Internamente tenemos:
      x = 106 | 3 = 0110 1010 | 0000 0011 = 0110 1010 = 0110 1011 = 107
    
    CADA vez que se unen bits, NO SE SUMAN, sinó que se "pegan" uno al lado de otro
    
    Índice | Siguiente Lección
    Volver a Página Principal

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