Alyx P. Hacker

Creando un Emulador de Chip8 en C

Instrucciones de alto nivel para implementar un emulador de chip8 completamente funcional, con sus 36 instrucciones, y descripciones detalladas sobre cada una.

Creando un Emulador de Chip8 en C

Creando un Emulador de Chip8 en C

Esta entrada no va a ser una guia completa para desarrollar un emulador de chip8 desde cero, con solamente leer esta entrada no vas a tener un emulador completamente funcional, que podras compilar y ejecutar, sino que mas bien te podras hacer una idea general de como funciona el chip8, y de como sus instrucciones podrian ser implementadas en C.

Importante: Antes de continuar con esta entrada, es importante tener sdl2 instalado, y tener los headers para usarlo en programas de C.

Chip8

Primero, vamos a ver los detalles tecnicos de como funciona el Chip8, antes de que miremos su implementacion, para asi saber que es lo que estamos haciendo y por que.

Pueden encontrar una referencia mas completa (en ingles) aqui.

Distribucion de Memoria ATTACH

El Chip8 tiene 4095 bytes de memoria, que estan representados de la siguiente manera:

La direccion 0x00 es donde se marca el inicio de la RAM del Chip8.

Desde las direcciones 0x00 a 0x1FF (511 bytes) es un espacio reservado para que Chip8 almacene algunos datos, como el conjunto de caracteres. Todos los programas del Chip8 se cargan en la direccion de memoria 0x200, y tienen desde 0x200 y 0xfff para almacenar todas sus instrucciones y sus propios sprites.

El Chip8 tiene un stack que es un array de 16 diferentes valores de 16 bits, estos se utilizan para almacenar las direcciones en las que el Chip8 debe volver cuando se termine una subrutina, lo que quiere decir que pueden 16 niveles de subrutinas anidadas. Puesto en palabras mas simples, cuando una subrutina se llama, la direccion de memoria de la instruccion donde esta fue llamada debe ser pusheada al stack, entonces si tenemos 3 llamadas de subrutinas anidadas, van a haber 3 diferentes entradas en nuestro stack, cuando alguna de estas subrutinas finalice su ejecucion, se le va a hacer pop a la ultima entrada, y volveremos a esa instruccion.

El Conjunto de Caracteres

El Chip8 contiene algunos sprites por defectos que se llaman “conjunto de caracteres”, ellos son un conjunto de sprites que representan los digitos hexadecimales desde 0 a F. Estos ocupan 5 bytes (son sprites 8x5) y son almacenados en el area reservada para el chip8 (0x00 - 0x1ff).

Los siguientes son ejemplos de algunos caracteres:

+----+--------+----+  +----+--------+----+
|"0" |Binario |Hex |  |"1" |Binario |Hex |
+----+--------+----|  +----+--------+----+
|****|11110000|0xF0|  |  1 |00100000|0x20|
|*  *|10010000|0x90|  | 11 |01100000|0x60|
|*  *|10010000|0x90|  |  1 |00100000|0x20|
|*  *|10010000|0x90|  |  1 |00100000|0x20|
|****|11110000|0xF0|  | 111|01110000|0x70|
+----+--------+----+  +----+--------+----+

Registros

El Chip8 tiene 16 registros que pueden almacenar valores de 8 bits, es decir, unicamente pueden almacenar un byte de informacion, los registros son los siguientes:

Es importante tener en cuenta, que el registro VF no debe ser usado por un programa, ya que algunas instrucciones van a manipular este registro a manera de asignar flags cuando ciertas condiciones se cumplan, pero los demas 15 registros pueden ser utilizados.

El Chip8 tambien tiene algunos registros adicionales:

El Display

La resolucion del display del Chip8 es de 64x32 pixeles y es monocromatico, significa que puede mostrar unicamente los colores blanco y negro, pueden ser representados como booleanos: 0 para negro, 1 para blanco.

Cuando dibujamos algo en la pantalla, estamos dibujando sprites y no pixeles individuales. Si un sprite le hace overflow a uno de los lados de la pantalla, sera dibujado en el otro lado.

Los sprites tienen un maximo de longitud de 8 bits de ancho, y 15 bits de alto.

El Teclado

El teclado del Chip8 tiene 16 teclas: desde los numeros 0 a F.

Asi es como se veria:

1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F

Set de Instrucciones

El conjunto de instrucciones del Chip8 es reducido, unicamente tiene 16 instrucciones diferentes. Estas instrucciones son para operaciones matematicas, dibujar en pantalla, y manipulacion de registros.

La Implementacion

Como referencia, vamos a tomar uno de mis codigos antiguos (de cuando usaba github) aqui.

Estructura Principal

El nucleo del emulador es una estructura que se llama struct chip8 ubicada en include/chip8, esa estructura es bastante simple, tiene los siguientes campos:

struct chip8
{
  struct chip8_memory memory;
  struct chip8_registers registers;
  struct chip8_stack stack;
  struct chip8_keyboard keyboard;
  struct chip8_screen screen;
};

Esa estructura contiene todos los elementos que previamente fueron mencionados, la memoria, los registros, el stack, el teclado y la pantalla.

La estructura chip8_memory (definida en include/mem.h) es una estructura que tiene una variable de 4095 bytes unsigned char memory, y tiene ciertas funciones como la de obtener un valor en cierto indice, insertar un valor en cierto indice, y obtener un short (dos bytes) en cierto indice.

La estructura chip8_registers (definida en include/registers.h) contiene todos los registros que fueron mecionados anteriormente, tiene un array de unsigned char llamado V con un tamano de 16, tambien tiene otros unsigned char como el delay timer, el stack pointer, y otros registros como I y PC que son del tipo unsigned short.

La structura chip8_stack (definida en include/stack.h) tiene una variable de tipo unsigned short llamada stack, con un tamano de 16, y algunas funciones para hacer push y pop.

chip8_keyboard y chip8_screen son estructuras simples, que unicamente contienen arrays.

Tambien hay un archivo adicional include/config.h que se usa unicamente para usar macros y no magic numbers a lo largo del codigo.

La Funcion Principal

La funcion principal del emulador (definida en src/main.c) se hacen varias cosas:

Ejecutando Instrucciones

Hay una funcion que se llama en cada iteracion del bucle principal llamada chip8_exec (definida en src/chip8.c) que recibe como argumento la estructura del chip8 y el opcode que esta en la posicion 0x200 + PC.

Esta funcion es una coleccion inmensa de switches anidados que prueban todas las posibles instrucciones que pueden ser pasadas al emulador, las instrucciones se pueden ver la siguiente manera:

Las letras n, nnn, kk, x y y significan cosas distintas:

Ahora, usando como ejemplo las instrucciones mostradas antes, pueden ser transformadas de la siguiente manera:

Sabiendo eso, debemos extraer 5 cosas de cada opcode que se pasa a la funcion para ejecutarlas para poder interpretarlas, necesitamos saber la operacion (los 4 bits mas significantes del opcode), nnn, x, y, kk, y n. podemos hacer esto usando operaciones con bits, vamos a revisar cada una de ellas (consideremos que tenemos una variable opcode con una instruccion de 2 bytes.)

unsigned short nnn = opcode & 0x0fff;
unsigned char x = (opcode >> 8) & 0x000f;
unsigned char y = (opcode >> 4) & 0x000f;
unsigned char kk = opcode & 0x00ff;
unsigned char n = opcode & 0x000f;

Podemos hacerlo de esa manera ya que estos elementos siempre van a estar en la misma posicion, por eso es que shifteamos x por 8 bits, y y por 4, y asi con los demas.

Ahora, tenemos tres funciones en src/chip8.c que ejecutan opcodes:

La primera funcion chip8_exec tiene switch similar al siguiente:

switch (opcode)
  {
    /* los casos vienen aqui. */

  default:
    chip8_exec_extended (chip8, opcode);
    break;
  }

Esta va a ejecutar algunas instrucciones simples (unicamente 0x00E0 y 0x00EE) , y si el opcode que se paso no es ninguna de esas, ahora se va a llamar chip8_exec_extended, que tiene un switch similar a este:

switch (opcode & 0x0f000)
  {
    /* los casos vienen aqui */

  case 0xF000:
    chip8_exec_extended_F (chip8, opcode);
    break;
  }

En ese bloque de codigo estamos haciendo el switch por los 4 bits mas significantes (diciendo que pueden ser valores entre 0 - F), y los compararemos como: 0x1000, 0x2000, 0x3000, y asi suscesivamente.

Finalmente, chip8_exec_extended_F, como ya sabemos que empieza con F, unicamente necesitamos comparar los 4 bits menos significantes:

switch (opcode)
  {
    /* los casos vienen aqui */
  }

Sabiendo como se comparan todas las instrucciones, podemos implementar el conjunto de instrucciones de chip8.

Conjunto de Instrucciones