using System;
using System.IO; //Librería de .Net para manejar archivos
using System.Runtime.InteropServices; //para el "beep" de la Bios
using System.Drawing; //Librería de .Net para manejar Colores
using System.Windows.Forms; //Para enviar mensajes en ventanas de dialogos
using SdlDotNet.Core; //Eventos
using SdlDotNet.Graphics; //para Surfaces
using SdlDotNet.Graphics.Sprites; //para Sprites y textos en pantalla
using SdlDotNet.Input; //para manejo de teclado
namespace Chip8
{
class Emulador
{
#region variables emulador
//variables principales
const int DIR_INICIO = 0x200;
const int TAMANO_MEM = 0xFFF;
const int TAMANO_PILA = 16;
const int CANT_REGISTROS = 16;
//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;
//resolucion de pantalla 64x32 (mas alta que larga)
const int RES_X = 64;
const int RES_Y = 32;
int[,] arregloPantalla = new int[RES_X, RES_Y];
//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();
private Surface PixelNegro;
private Surface PixelBlanco;
const int tamanoPixel = 5; // tamaño de pixel, sirve para zoom de todo el emulador
public const int operPorSegundo = 600;
public const int operPorFrame = operPorSegundo / 60;
private int TimeUntilTimerUpdate; //para la simulación de timers
#endregion
///
/// Constructor de la clase
///
public Emulador()
{
try
{
Video.SetVideoMode(RES_X * tamanoPixel, RES_Y * tamanoPixel);
Video.Screen.Fill(Color.Black);
Events.Fps = 60;
Events.TargetFps = 60;
PixelNegro = Video.CreateRgbSurface(tamanoPixel, tamanoPixel);
PixelBlanco = Video.CreateRgbSurface(tamanoPixel, tamanoPixel);
PixelNegro.Fill(Color.Black);
PixelBlanco.Fill(Color.White);
//declaración de Eventos
Events.KeyboardDown += new EventHandler(this.Tecla_Presionada);
Events.KeyboardUp += new EventHandler(this.Tecla_Liberada);
Events.Quit += new EventHandler(this.Events_Salir);
Events.Tick += new EventHandler(this.Events_Tick);
ResetHardware();
CargarJuego("PONG");
}
catch (Exception ex)
{
MessageBox.Show("Error General: " + ex.Message + "-" + ex.StackTrace);
}
}
static void Main(string[] args)
{
Emulador emulador = new Emulador();
emulador.Run();
}
private void Events_Salir(object sender, QuitEventArgs e)
{
Events.QuitApplication();
}
private void Tecla_Presionada(object sender, KeyboardEventArgs e)
{
//Salimos del juego con tecla Escape
if (e.Key == Key.Escape)
{
Events.QuitApplication();
}
if (e.Key == Key.One) { teclasPresionadas[TECLA_1] = true; }
if (e.Key == Key.Two) { teclasPresionadas[TECLA_2] = true; }
if (e.Key == Key.Three) { teclasPresionadas[TECLA_3] = true; }
if (e.Key == Key.Four) { teclasPresionadas[TECLA_4] = true; }
if (e.Key == Key.Q) { teclasPresionadas[TECLA_Q] = true; }
if (e.Key == Key.W) { teclasPresionadas[TECLA_W] = true; }
if (e.Key == Key.E) { teclasPresionadas[TECLA_E] = true; }
if (e.Key == Key.R) { teclasPresionadas[TECLA_R] = true; }
if (e.Key == Key.A) { teclasPresionadas[TECLA_A] = true; }
if (e.Key == Key.S) { teclasPresionadas[TECLA_S] = true; }
if (e.Key == Key.D) { teclasPresionadas[TECLA_D] = true; }
if (e.Key == Key.F) { teclasPresionadas[TECLA_F] = true; }
if (e.Key == Key.Z) { teclasPresionadas[TECLA_Z] = true; }
if (e.Key == Key.X) { teclasPresionadas[TECLA_X] = true; }
if (e.Key == Key.C) { teclasPresionadas[TECLA_C] = true; }
if (e.Key == Key.V) { teclasPresionadas[TECLA_V] = true; }
}
private void Tecla_Liberada(object sender, KeyboardEventArgs e)
{
if (e.Key == Key.One) { teclasPresionadas[TECLA_1] = false; }
if (e.Key == Key.Two) { teclasPresionadas[TECLA_2] = false; }
if (e.Key == Key.Three) { teclasPresionadas[TECLA_3] = false; }
if (e.Key == Key.Four) { teclasPresionadas[TECLA_4] = false; }
if (e.Key == Key.Q) { teclasPresionadas[TECLA_Q] = false; }
if (e.Key == Key.W) { teclasPresionadas[TECLA_W] = false; }
if (e.Key == Key.E) { teclasPresionadas[TECLA_E] = false; }
if (e.Key == Key.R) { teclasPresionadas[TECLA_R] = false; }
if (e.Key == Key.A) { teclasPresionadas[TECLA_A] = false; }
if (e.Key == Key.S) { teclasPresionadas[TECLA_S] = false; }
if (e.Key == Key.D) { teclasPresionadas[TECLA_D] = false; }
if (e.Key == Key.F) { teclasPresionadas[TECLA_F] = false; }
if (e.Key == Key.Z) { teclasPresionadas[TECLA_Z] = false; }
if (e.Key == Key.X) { teclasPresionadas[TECLA_X] = false; }
if (e.Key == Key.C) { teclasPresionadas[TECLA_C] = false; }
if (e.Key == Key.V) { teclasPresionadas[TECLA_V] = false; }
}
private void Events_Tick(object sender, TickEventArgs e)
{
EmulaFrame();
}
// Inicializamos los Eventos
public void Run()
{
Events.Run();
}
void ResetHardware()
{
// Reseteamos los Timers
delayTimer = 0x0;
soundTimer = 0x0;
// Reseteamos variables
instruccion = 0x0;
PC = DIR_INICIO;
SP = 0x0;
I = 0x0;
// Limpiamos la memoria
for (int i = 0; i < TAMANO_MEM; i++)
memoria[i] = 0x0;
// Limpiamos registros
for (int i = 0; i < CANT_REGISTROS; i++)
V[i] = 0x0;
// Limpiamos el stack
for (int i = 0; i < TAMANO_PILA; i++)
pila[i] = 0x0;
// Cargamos las fuentes en la memoria por ej, los marcadores del juego PONG esos "[0-0]", "[0-2]"
for (int i = 0; i < 80; i++)
memoria[i] = arregloFuentes[i];
Video.Screen.Fill(Color.Black);
Video.Screen.Update();
}
void ManejaTimers()
{
if (delayTimer > 0)
delayTimer--;
if (soundTimer > 0)
soundTimer--;
}
void CargarJuego(string nombreRom)
{
FileStream rom;
try
{
rom = new FileStream(@nombreRom, FileMode.Open);
if (rom.Length == 0)
{
throw new Exception("Error en la Carga de la Rom: ROM dañada o vacía");
}
// 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();
}
catch (Exception ex)
{
throw new Exception("Error en la Carga de la Rom: " + ex.Message);
}
}
public void EmulaFrame() //representa un frame
{
//por cada Tick o Frame, se ejecutan 600/60=10 instrucciones
for (int i = 0; i < operPorFrame; i++)
{
EmulaOpcodes();
}
}
void EmulaOpcodes()
{
if (TimeUntilTimerUpdate == 0)
{
if (delayTimer > 0)
delayTimer--;
if (soundTimer > 0)
soundTimer--;
TimeUntilTimerUpdate = operPorFrame;
}
else
{
TimeUntilTimerUpdate--;
}
EjecutaOpcodes();
}
void EjecutaOpcodes()
{
#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;
}
// opcodes del tipo 1xxx
case (0x1):
{
// opcode 1NNN: Jump To Address NNN.
JumpToAddr();
break;
}
// opcodes del tipo 2xxx
case (0x2):
{
// opcode 2NNN: Call Subroutine At Address NNN.
CallSub();
break;
}
// opcodes del tipo 3xxx
case (0x3):
{
// opcode 4XKK: Skip Next Instruction If VX == KK
SkipIfEql();
break;
}
// opcodes del tipo 4xxx
case (0x4):
{
// opcode 4XKK: Skip Next Instruction If VX != KK
SkipIfNotEql();
break;
}
// opcodes del tipo 5xxx
case (0x5):
{
// opcode 5XY0: Skip Next Instruction If VX == VY
SkipIfRegEql();
break;
}
// opcodes del tipo 6xxx
case (0x6):
{
// opcode 6XKK: Assign Number KK To Register X.
AssignNumToReg();
break;
}
// opcodes del tipo 7xxx
case (0x7):
{
// opcode 7XKK: Add Number KK To Register X.
AddNumToReg();
break;
}
// opcodes del tipo 8xxx
case (0x8):
{
//Tenemos varios tipos
switch (opcode4)
{
// opcode 8XY0: Assign From Register To Register.
case (0x0):
{
AssignRegToReg();
break;
}
// opcode 8XY1: Bitwise OR Between Registers.
case (0x1):
{
RegisterOR();
break;
}
// opcode 8XY2: Bitwise AND Between Registers.
case (0x2):
{
RegisterAND();
break;
}
// opcode 8XY3: Bitwise XOR Between Registers.
case (0x3):
{
RegisterXOR();
break;
}
// opcode 8XY4: Add Register To Register.
case (0x4):
{
AddRegToReg();
break;
}
// opcode 8XY5: Sub Register From Register.
case (0x5):
{
SubRegFromReg();
break;
}
// opcode 8XY6: Shift Register Right Once.
case (0x6):
{
ShiftRegRight();
break;
}
// opcode 8XY7: Sub Register From Register (Reverse Order).
case (0x7):
{
ReverseSubRegs();
break;
}
// opcode 8XYE: Shift Register Left Once.
case (0xE):
{
ShiftRegLeft();
break;
}
}
break;
}
// opcodes del tipo 9xxx
case (0x9):
{
// opcode 9XY0: Skip Next Instruction If VX != VY
SkipIfRegNotEql();
break;
}
// opcodes del tipo AXXX
case (0xA):
{
// opcode ANNN: Set Index Register To Address NNN.
AssignIndexAddr();
break;
}
// opcodes del tipo BXXX
case (0xB):
{
// opcode BNNN: Jump To NNN + V0.
JumpWithOffset();
break;
}
// opcodes del tipo CXXX
case (0xC):
{
// opcode CXKK: Assign Bitwise AND Of Random Number & KK To Register X.
RandomANDnum();
break;
}
// opcodes del tipo DXXX
case (0xD):
{
// opcode DXYN: Draw Sprite To The Screen.
DrawSprite();
break;
}
// opcodes del tipo EXXX
case (0xE):
{
// Tenemos 2 tipos según EXKK
switch (KK)
{
// opcode EX9E: Skip Next Instruction If Key In VX Is Pressed.
case (0x9E):
{
SkipIfKeyDown();
break;
}
// opcode EXA1: Skip Next Instruction If Key In VX Is NOT Pressed.
case (0xA1):
{
SkipIfKeyUp();
break;
}
}
break;
}
// opcodes del tipo FXXX
case (0xF):
{
// tenemos varios tipos de Opcodes
switch (KK)
{
// opcode FX07: Assign Delay Timer To Register.
case (0x07):
{
AssignFromDelay();
break;
}
// opcode FX0A: Wait For Keypress And Store In Register.
case (0x0A):
{
StoreKey();
break;
}
// opcode FX15: Assign Register To Delay Timer.
case (0x15):
{
AssignToDelay();
break;
}
// opcode FX18: Assign Register To Sound Timer.
case (0x18):
{
AssignToSound();
break;
}
// opcode FX1E: Add Register To Index.
case (0x1E):
{
AddRegToIndex();
break;
}
// opcode FX29: Index Points At CHIP8 Font Char In Register.
case (0x29):
{
IndexAtFontC8();
break;
}
// opcode FX30: Index Points At SCHIP8 Font Char In Register.
case (0x30):
{
IndexAtFontSC8();
break;
}
// opcode FX33: Store BCD Representation Of Register In Memory.
case (0x33):
{
StoreBCD();
break;
}
// opcode FX55: Save Registers To Memory.
case (0x55):
{
SaveRegisters();
break;
}
// opcode FX65: Load Registers From Memory.
case (0x65):
{
LoadRegisters();
break;
}
}
break;
}
}
#endregion
}
#region implementación de métodos para cada instrucción
void ClearScreen()
{
Video.Screen.Fill(Color.Black);
Video.Screen.Update();
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];
}
void JumpToAddr()
{
// salta a la instrucción dada. No se salta directamente, sino que se le indica
// al PC que vaya a dicha direccion luego de salir del actual ciclo.
PC = NNN;
}
void CallSub()
{
// Salva la posicion actual del PC en la Pila, para volver a penas se ejecute la subrutina
pila[SP] = PC;
SP++;
// Saltamos a la subrutina indicada en NNN
PC = NNN;
}
void SkipIfEql()
{
// Recordar que Opcode2=X
if (V[opcode2] == KK)
{
// Salta a la siguiente instruccion
PC += 2;
}
}
void SkipIfNotEql()
{
if (V[opcode2] != KK)
{
// Salta a la siguiente instruccion
PC += 2;
}
}
void SkipIfRegEql()
{
if (V[opcode2] == V[opcode3])
{
// Salta a la siguiente instruccion
PC += 2;
}
}
void AssignNumToReg()
{
V[opcode2] = KK;
}
/**
entrega los 8 bits menores (char) de un numero de 16 bits (int)
@param number el numero de 16 bit
@return el numero de 8 bits
**/
private char getLower(int number)
{
return (char)(number&0xFF);
}
void AddNumToReg()
{
V[opcode2] += KK;
}
void AssignRegToReg()
{
//VX = VY
V[opcode2] = V[opcode3];
}
void RegisterOR()
{
// OR binario es |, entonces hacemos VX = VX | VY o mas elegante VX |= VY
V[opcode2] |= V[opcode3];
}
void RegisterAND()
{
// OR binario es &, entonces hacemos VX = VX & VY o mas elegante VX &= VY
V[opcode2] &= V[opcode3];
}
void RegisterXOR()
{
// XOR es ^, entonces hacemos VX = VX ^ VY o mas elegante VX ^= VY
V[opcode2] ^= V[opcode3];
}
void AddRegToReg()
{
//Con >> extraemos los 8 bits mayores de la suma, si el resultado supera
//los 8 bits el >> 8 dara 1 si no 0
V[0xF] = (V[opcode2] + V[opcode3]) >> 8;
//VX = VX + VY
V[opcode2] += V[opcode3];
}
void SubRegFromReg()
{
//seteamos F en 1 si VX > VY
if (V[opcode2] >= V[opcode3])
V[0xF] = 0x1;
else
V[0xF] = 0x0;
//VX = VX - VY
V[opcode2] -= V[opcode3];
}
void ShiftRegRight()
{
//VF = VX AND 1 (VF valdrá 1 o 0). Para este case es más optimo
V[0xF] = V[opcode2] & 0x1;
//Manera elegante de escribir un shift a la derecha para dividir por 2: V[opcode2] = V[opcode2] >> 1;
V[opcode2] >>= 1;
}
void ReverseSubRegs()
{
if (V[opcode2] <= V[opcode3])
{
V[0xF] = 0x1;
}
else
{
V[0xF] = 0x0;
}
V[opcode2] = V[opcode3] - V[opcode2];
}
void ShiftRegLeft()
{
//VF = VX AND 10 hex
V[0xF] = V[opcode2] & 0x10;
//Manera elegante de escribir un shift a la izquierda para multiplicar por 2: V[opcode2] = V[opcode2] << 1;
V[opcode2] <<= 1;
}
void SkipIfRegNotEql()
{
if (V[opcode2] != V[opcode3])
{
//Aumentamos el PC en 2 para saltar a la siguiente instrucción
PC += 2;
}
}
void AssignIndexAddr()
{
// se setea el Registro de Índice (I) a la dirección NNN.
I = NNN;
}
void JumpWithOffset()
{
PC = NNN + V[0x0];
}
void RandomANDnum()
{
//usamos el variable rnd seteada en la clase. Con el método Next se le puede dar el mínimo (0) y máximo (255)
int numeroRnd = rnd.Next(0,255);
V[opcode2] = numeroRnd & KK;
}
///
/// Screen is 64x62 pixels
/// Todos los drawings son hechos en modos XOR.
/// Cuando uno o mas pixels son borrados mientras un sprite es pintado,
/// el registro VF se setea en 01, sino queda en 00.
///
void DrawSprite()
{
// Reseteamos el registro que detecta colisiones
V[0xF] = 0x0;
if ((instruccion & 0x000F) == 0) //opcode & 0x000F =opcode4
{
// Dibujamos un Sprite de SCHIP8 de tamaño 16x16
// No implementado aún
}
else
{
// Bibujamos un Sprite de CHIP8 de tamaño 8xN
for (int spriY = 0; spriY < opcode4; spriY++)
{
for (int spriX = 0; spriX < 8; spriX++)
{
int x = (memoria[I + spriY] & (0x80 >> spriX));
if (x != 0)
{
// checkeamos por alguna colision
int xx = (V[opcode2] + spriX);
int yy = (V[opcode3] + spriY);
if (arregloPantalla[xx % 64, yy % 32] == 1)
{
arregloPantalla[xx % 64, yy % 32] = 0;
Video.Screen.Blit(PixelNegro, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
V[0xF] = 1; //colision activado
}
else
{
arregloPantalla[xx % 64, yy % 32] = 1;
Video.Screen.Blit(PixelBlanco, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
}
}
}
}
}
Video.Screen.Update();
}
void SkipIfKeyDown()
{
if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == true)
PC += 2;
}
void SkipIfKeyUp()
{
if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == false)
PC += 2;
}
void AssignFromDelay()
{
V[opcode2] = delayTimer;
}
void StoreKey()
{
for (int i = 0; i < teclasPresionadas.Length; i++)
{
if (teclasPresionadas[i] == true)
{
V[opcode2] = i;
}
}
}
void AssignToDelay()
{
delayTimer = V[opcode2];
}
void AssignToSound()
{
soundTimer = V[opcode2];
}
void AddRegToIndex()
{
I += V[opcode2];
}
void IndexAtFontC8()
{
I = (V[opcode2] * 0x5);
}
void IndexAtFontSC8()
{
// Not Implemanted yer, SCHIP8 Function.
}
void StoreBCD()
{
int vx = (int) V[opcode2];
memoria[I] = vx / 100; //centenas
memoria[I + 1] = (vx / 10) % 10; //decenas
memoria[I + 2] = vx % 10; //unidades
}
void SaveRegisters()
{
for (int i = 0; i <= opcode2; i++)
{
memoria[I++] = V[i];
}
//I += 1;
}
void LoadRegisters()
{
for (int i = 0; i <= opcode2; i++)
{
V[i] = memoria[I++];
}
//I += 1;
}
#endregion
}
}
|