Few weeks ago I started to make my first emulator - chip8js.
I never did emulators before, and that was really a good experience!
Actually, I wanted to make NES emulator firstly, but It's too hard for me, because I never did emulators before.
CHIP-8 is perfect for first emulator project. Literally almost everyone doing it first. So, after some digging in google,
I decided to start. CHIP-8 has less than 50 opcodes, and It didn't take a while to make them.
I'm using JavaScript, but if you want to do emulator too, you can use any other language.
To make CHIP-8, we need to create input (keyboard), output (screen) and CPU.
So, let's start!
First off, we need memory. CHIP-8 can hold up to 4096 bytes. In memory there is interpreter itself, fonts, and loaded program.
In JS it looks like this:
let memory = new ArrayBuffer(0x1000); // 4096
this.memory = new Uint8Array(memory); // My emulator is class, so everything will be able to easily edit the memory.
CHIP-8 has 64x32 display, 16 key inputs and sound buzzer. The display is just array of pixels, that can be on or off.
this.screen = new Array(64*32);
let should_draw = false; // is draw requested or no
this.keypress = {};
this.keywait = false; // is waiting for key input
let buzz = new Audio('buzz.wav'); // you can find the buzz sound in my GitHub repo
The CHIP-8 has 16 8bit registers. They are used to store values for operations. VX, VY, VF. VF register is used for the flags, and should not be used in programs.
this.reg = new Array(16);
It also has 2 timer registers, that we'll decrement per cycle.
this.delay_t = 0;
this.sound_t = 0;
It has index register and program counter, and they are 16bit.
There is also a stack and stack pointer, which includes the address of the topmost stack element. The stack has at most 16 elements in it at any given time.
this.index = 0;
this.pc = 0x200;
this.stack = new Array(16);
this.sp = 0;
Why 0x200? It's because first 512 bytes are always was used by interpreter and fonts, and all programs start from 0x200.
For display, I use canvas, you can use anything that you want. I like canvas, and I can easily use it in web.
To make key inputs work, we need to make event listener for keypress, and write 1 on keydown, and 0 on keyup. Nothing really hard:
let keys = {
1: 0x1,
2: 0x2,
3: 0x3,
4: 0xc,
Q: 0x4,
W: 0x5,
E: 0x6,
R: 0xd,
A: 0x7,
S: 0x8,
D: 0x9,
F: 0xe,
Z: 0xa,
X: 0,
C: 0xb,
V: 0xf
};
document.addEventListener("keydown", e => {
if(keys[e.key.toUpperCase()] === undefined) return; // check is key exists
console.log("Key press: " + e.key);
this.keypress[keys[e.key.toUpperCase()]] = 1;
if(this.keywait) this.keywait = false;
});
document.addEventListener("keyup", e => {
if(keys[e.key.toUpperCase()] === undefined) return; // check is key exists
console.log("Key release: " + e.key);
this.keypress[keys[e.key.toUpperCase()]] = 0;
if(this.keywait) this.keywait = false;
});
Now we need run function:
this.run = (binary, options) => {
this.init();
// Now we need to load all ROM binary to memory:
for(let i = 0; i < binary.length; i++) this.memory[i+0x200] = binary[i].charCodeAt(0); // get char from symbol (ord)
let t = setInterval(() => {
if(this.pause) return;
if(this.exit) return clearInterval(t);
this.cycle(); // execute opcode
this.draw(); // draw on screen
}, 6 /*ms*/);
};
Let's look on the code. First off, we need to initialize our emulator - set everything to zero, reset everything.
We got the binary from the uploaded ROM, so we need to load content of ROM to memory (from 0x200).
And after that, we start the "loop". Every cycle we check is emulator paused and should it exit. You can add own checks there.
And we reach the cycle that executes the opcode and if we need to draw, we... draw on screen. And repeat, repeat, repeat!
CHIP-8 has built-in font. It should be loaded into memory, and before 0x200. Let's make an array, and put it in memory on init.
let fonts = [0xF0, 0x90, 0x90, 0x90, 0xF0,// 0
0x20, 0x60, 0x20, 0x20, 0x70, // 1
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
0x90, 0x90, 0xF0, 0x10, 0x10, // 4
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
0xF0, 0x10, 0x20, 0x40, 0x40, // 7
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
0xF0, 0x90, 0xF0, 0x90, 0x90, // A
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
0xF0, 0x80, 0x80, 0x80, 0xF0, // C
0xE0, 0x90, 0x90, 0x90, 0xE0, // D
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
0xF0, 0x80, 0xF0, 0x80, 0x80 // F
],
Yes, I know what you want to ask - how the hell it should work? I had same question, but with this table, you'll understand it:
"0"
binary - hex
11110000 0xF0
10010000 0x90
10010000 0x90
10010000 0x90
11110000 0xF0
Binary forms "0" symbol!
Let's make our init function, it just should reset everything and put fonts in memory.
this.init = () => {
clear(); // clear the canvas function
this.reg = new Array(16);
this.screen = new Array(64 * 32);
this.stack = new Array(16);
this.opcode = 0;
this.index = 0;
this.pc = 0x200;
this.sp = 0;
this.pause = document.getElementById("pause").checked; // my pause checkbox
delay_t = 0;
sound_t = 0;
should_draw = false;
// loading font into memory
for (let i = 0; i < 80; i++) this.memory[i] = fonts[i];
};
Now, to the hardest part - cycle.
It's biggest, and most boring part. Let's make "skeleton" of cycle, and only after that make opcodes.
Each cycle basically looks like this:
this.funcs = {}; // TODO: create opcodes
let that = this; // to access this from function
this.cycle = () => {
this.opcode = (that.memory[that.pc] << 8) | that.memory[that.pc + 1]; // getting the opcode from memory
let eop = this.opcode & 0xf000; // extracting opcode
console.log(`[${that.pc}] Opcode: ${op}`);
that.pc += 2; // going to the next instruction
// getting vx and vy registers
this.vx = (this.opcode & 0x0f00) >> 8;
this.vy = (this.opcode & 0x00f0) >> 4;
// executing!
try {
that.funcs[eop]();
} catch (e) {
throw new Error("Unknown instruction: " + eop);
};
// decrementing the timers
if(delay_t > 0) delay_t--;
if(sound_t > 0) {
sound_t--;
if(sound_t === 0) buzz.play(); // play sound
}
};
And now, our skeleton for cycle is done! It's time to make final steps - implementing opcodes and draw function!
Opcode - is basically instruction that we need to execute. Each opcode is 2 bytes long. We are moving our program counter
and executing all the opcodes. Now, we need to fill our this.funcs with all opcodes. It's pretty long, I won't show how I made
all the opcodes, you can see it in my GitHub repo, but I'll show few of these. Look at Cowgod's Chip-8 Technical Reference for all opcodes and CHIP-8 information.
Let's implement our first opcode!
CLS (00E0) - Clear screen opcode:
this.funcs = {
0x00e0: () => {
that.screen = new Array(64*32); // make screen blank again
should_draw = true;
}//,
// and now we just add here new and new opcodes.
};
For example, let's implement another opcode - 3xkk. It skips next instruction if VX == kk. Nothing too hard!
this.funcs = {
/* ... */
0x3000: () => {
if(that.reg[that.vx] === (that.opcode & 0x00ff)) that.pc += 2;
}
}
Now, it's time to make one of the hardest functions - draw function!
There is 2 opcodes that draws something on screen - FZ29 and DXYN. They both set "should_draw" to true, so let's make our draw function.
this.draw = () => {
if(!should_draw) return; // only on should_draw!
should_draw = false;
clear(); // clear the screen func
let i = 0;
while(i < 2048) {
if(this.screen[i] === 1) {
let x = (i%64), // 64 width
y = Math.floor((i / 32)/2); // 32 height
// Now, draw pixel. tx - context of canvas
tx.fillStyle = "rgb(255, 255, 255)";
tx.fillRect(x, y, 1, 1);
}
i++;
}
};
Done! Now, if you implemented all instructions it should work! If you will follow the documentation, implementing opcodes
won't be hard. Also, when you make upload ROM function, don't forget to read it as binary(!!!), because I was reading it
incorrectly, and I almost gave up after few days of trying to fix it.
There is some test ROMS, and they really help a lot!
Check out my online chip8 emulator - https://dimden.dev/chip8
And GitHub repo - https://github.com/dimdenGD/chip8js