¡Gracias por venir! Vamos a estar ~5 horas programando
Si algo no funciona bien o no se entiende, no duden en preguntar
Presentación en: emuladores.gzalo.com/diapositivas
Licencia: Creative Commons Attribution-NonCommercial 4.0 International LicenseEmulador: permite ejecutar programas en una plataforma distinta, modelándola de forma precisa
Simulador: reproduce el comportamiento del programa usando modelos matemáticos o lógicos
Ejemplo: emulador de nokia 1100 vs simulador de nokia 1100
Hay muchas formas de emular una plataforma de hardware:
Dependiendo del nivel de precisión (y performance) deseado se suele elegir una.
uint8_t miVariable = 0x10; // Variable de 1 byte
miVariable = miVariable + 0x08;
LD V0, 0x10 ; Registro
ADD V0, 0x08
60107008
PC=0 · V0 = xxxx
PC=2 · V0 = 0x10
PC=4 · V0 = 0x18
En varios lugares necesitaremos trabajar a nivel de bits
Las variables numéricas se pueden pensar como una lista de bits
Aplican bit a bit (bitwise), no confundir con operaciones lógicas:
Máscara con 0s en todos los lugares excepto uno (o más), usar OR:
a = a | 0x08
a = a | (1<<3)
Máscara con 1s en todos los lugares excepto uno (o más), usar AND:
a = a & 0xDF
a = a & ~(1<<5)
Máscara con 0s en todos los lugares excepto uno (o más), usar XOR:
a = a ^ 0x10
a = a ^ (1<<4)
Máscara que tenga 1 en un solo lugar, usar AND:
Si el resultado es distinto de 0, el bit estaba setteado
if ((a & 0x40) != 0) ...
if ((a & (1<<6)) != 0) ...
8 bits, Von Neumann. Creada por Joseph Weisbecker en 1977
Empezó como un interprete para la computadora COSMAC VIP
0x0000-0x0FFF
uint8_t memory[0x1000];
uint8_t v[16];
uint16_t pc = 0x200;
uint16_t index = 0x0000;
uint16_t stack[24];
uint8_t stackPointer = 0;
void push(uint16_t valor){
stack[stackPointer++] = valor;
}
uint16_t pop(){
return stack[--stackPointer];
}
uint8_t delayTimer = 0;
uint8_t soundTimer = 0;
uint8_t screen[64*32];
void setPixel(uint8_t x, uint8_t y, uint8_t value) {
screen[ y * 64 + x ] = value;
}
uint8_t getPixel(uint8_t x, uint8_t y) {
return screen[ y * 64 + x ];
}
0 a F
)1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F
1 2 3 4
Q W E R
A S D F
Z X C V
cargarRom();
while (!fin) {
manejarEventos();
if(delayTimer > 0) delayTimer--;
if(soundTimer > 0) soundTimer--;
avanzarEmulacion();
dibujarPantalla();
esperar(16 ms);
}
avanzarEmulacion
debería correr ~500 veces por segundo. Asumiendo 60 fps, podemos correrlo 8 veces por frame como una aproximaciónEstadot+1 = f(Estadot)
No recomentable para emuladores más complejos, ya que usa bastante memoria y procesamiento para calcular el nuevo estado
uint8_t keys[16];
...
while (PollEvent(&event)) {
if (event.type == QUIT) {
fin = true;
} else if(event.type == KEYDOWN){
if(event.key == KEY_1) keys[1] = 1;
if(event.key == KEY_2) keys[2] = 1;
if(event.key == KEY_3) keys[3] = 1;
...
} else if(event.type == KEYUP){
if(event.key == KEY_1) keys[1] = 0;
if(event.key == KEY_2) keys[2] = 0;
if(event.key == KEY_3) keys[3] = 0;
...
}
}
setPixel(x,y,value)
y getPixel(x,y)
o si no, acceso a memoria de videoblanco = 0xFFFFFFFF
negro = 0xFF000000
Se usa para mostrar puntajes y otros números
uint8_t font[80] = {
0x60, 0xa0, 0xa0, 0xa0, 0xc0,
0x40, 0xc0, 0x40, 0x40, 0xe0,
0xc0, 0x20, 0x40, 0x80, 0xe0,
0xc0, 0x20, 0x40, 0x20, 0xc0,
0x20, 0xa0, 0xe0, 0x20, 0x20,
0xe0, 0x80, 0xc0, 0x20, 0xc0,
0x40, 0x80, 0xc0, 0xa0, 0x40,
0xe0, 0x20, 0x60, 0x40, 0x40,
0x40, 0xa0, 0x40, 0xa0, 0x40,
0x40, 0xa0, 0x60, 0x20, 0x40,
0x40, 0xa0, 0xe0, 0xa0, 0xa0,
0xc0, 0xa0, 0xc0, 0xa0, 0xc0,
0x60, 0x80, 0x80, 0x80, 0x60,
0xc0, 0xa0, 0xa0, 0xa0, 0xc0,
0xe0, 0x80, 0xc0, 0x80, 0xe0,
0xe0, 0x80, 0xc0, 0x80, 0x80
};
for(int i=0;i<80;i++) {
memory[i] = font[i];
}
Para cargar las ROMs, debemos abrir un archivo en modo binario y copiar cada byte a la memoria, desde la posición 0x200
Podemos intentar leer el tamaño completo, asumiendo que no falla si se acaba antes
FILE *input = fopen("game.ch8", "rb");
fread(&memory[0x200], 0xE00, 1, input);
fclose(input);
Rust: File::open
, luego read_to_end
Python: with open(..., "rb")
, luego readinto
Java: new FileInputStream(...)
, luego read
6010 LD V0, 0x10
6105 LD V1, 0x05
00E0 CLS
A214 LD I, 0x214
D015 DRW V0, V1, 5
A21A LD I, 0x219
7008 ADD V0, 0x08
D015 DRW V0, V1, 5
7008 ADD V0, 0x08
1204 JP 0x204
A4 AA EA AA A4 ; H O
84 8A 8E 8A EA ; L A
Es el core de la emulación, se puede dividir en 3 partes:
Lee la instrucción a ejecutar desde la memoria. Cada opcode indica qué instrucción se deberá realizar.
En CHIP-8 son todos son de 2 bytes, big endian:
uint16_t opcode = (memory[pc]<<8) | memory[pc+1];
pc += 2;
El primer nibble (medio byte) define el tipo general de instrucción. Hay varios subtipos con operandos de distintos largos, por ejemplo:
uint8_t nibble1 = opcode >> 12;
uint8_t nibble2 = (opcode >> 8) & 0xF;
uint8_t nibble3 = (opcode >> 4) & 0xF;
uint8_t nibble4 = opcode & 0xF;
uint16_t address = opcode & 0xFFF;
uint8_t byte2 = opcode & 0xFF;
Básicamente un montón de ifs o un switch-case (o una branch table)
00E0
· CLS
00EE
· RET
pc = pop()
0NNN
· SYS addr
1NNN
· JP addr
pc = NNN;
2NNN
· CALL addr
push(pc); pc = NNN;
3XNN
· SE Vx, byte
if(Vx == NN) pc += 2;
4XNN
· SNE Vx, byte
if(Vx != NN) pc += 2;
5XY0
· SE Vx, Vy
if(Vx == Vy) pc += 2;
6XNN
· LD Vx, byte
7XNN
· ADD Vx, byte
8XY0
· LD Vx, Vy
8XY1
· OR Vx, Vy
8XY2
· AND Vx, Vy
8XY3
· XOR Vx, Vy
8XY4
· ADD Vx, Vy
(V[x]+V[y]>0xFF)
8XY5
· SUB Vx, Vy
(V[x] >= V[y])
8XY6
· SHR Vx
(V[x] & 1)
8XY7
· SUBN Vx, Vy
(V[y] >= V[x])
8XYE
· SHL Vx
(Vx >> 7)
9XY0
· SNE Vx, Vy
if(Vx != Vy) pc += 2;
ANNN
· LD I, addr
BNNN
· JP V0, addr
CXNN
· RND Vx, byte
EX9E
· SKP Vx
if(isDown(Vx)) pc += 2
EXA1
· SKNP Vx
if(!isDown(Vx)) pc += 2
DXYN
· DRW Vx, Vy, nibble
draw(Vx, Vy, N)
dibuja un sprite en la posición Vx, Vy, 8 píxeles de ancho y N de alto.
Cada fila de 8 pixeles se lee a partir de la posición I.V[0xF] = 0;
for (int y=0;y<N;y++) {
uint8_t actual = memory[index+y];
for (int x=0;x<8;x++) {
if ((actual&(0x80)) != 0 && x+V[X]<64 && y+V[Y]<32) {
if (getPixel(x+V[X],y+V[Y])) {
setPixel(x+V[X],y+V[Y],0);
V[0xF] = 1;
} else {
setPixel(x+V[X],y+V[Y],0xFFFFFF);
}
}
actual <<= 1;
}
}
FX07
· LD Vx, DT
FX0A
· LD Vx, K
if(ningunaTecla) pc -= 2; else Vx = teclaPresionada;
FX15
· LD DT, Vx
FX18
· LD ST, Vx
FX1E
· ADD I, Vx
FX29
· LD F, Vx
index = Vx * 5;
FX33
· LD B, Vx
memory[index] = Vx/100;
memory[index+1] = (Vx/10)%10;
memory[index+2] = Vx%10;
FX55
· LD [I], Vx
for(i=0;i<=x;i++) memory[index+i] = Vi;
FX65
· LD Vx, [I]
for(i=0;i<=x;i++) Vi = memory[index+i];
Si salió todo bien, al cargar la ROM deberían ver esto: