chip8js - my first emulator - DIMDEN

chip8js - my first emulator

Posted on Sunday, 12th of May 2019

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