惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
博客园 - 司徒正美
Last Week in AI
Last Week in AI
Recent Announcements
Recent Announcements
Y
Y Combinator Blog
博客园 - 聂微东
M
MIT News - Artificial intelligence
博客园_首页
Jina AI
Jina AI
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
Vercel News
Vercel News
The Cloudflare Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG
N
Netflix TechBlog - Medium
B
Blog
Google DeepMind News
Google DeepMind News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

dimden

Japan Spring 2026 WebTiles: It's fine to accept user-supplied code, actually Eastern Europe Trip (Oct 2025) Optimizing rendering of 100,000+ HTML nodes Custom profile CSS in OldTwitter Making Tetris in Twitter with Node.js brainfuck.js - DIMDEN The Puzzle 3 and stuff Multi Line Task∞: Hello World Hello World! - DIMDEN
chip8js - my first emulator
dimden · 2019-05-12 · via dimden

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