Emuladores, Chip 8, e os Fundamentos da Computação – Parte 2 (Código Fonte)

Na Parte 1 deste artigo, falei sobre emuladores, e como sua análise e desenvolvimento nos força a compreender a fundo o funcionamento das CPUs, ou seja, as bases da computação. Se quiser conferir a primeira parte, acesse aqui:

Na Parte 2 teremos o código fonte do emulador, carregado com a ROM do jogo Tetris na memória. Implementando este projeto, você deverá ser capaz de executar o emulador diretamente no seu no navegador de internet.

A estrutura do projeto é composta por apenas 3 arquivos:

index.html
style.css
index.jsCode language: CSS (css)

A seguir, confira na integra o conteúdo de cada um deles.

index.html – A Estrutura

Este arquivo é responsável pela estrutura base de nosso projeto. Trata-se de um arquivo HTML simples, com algumas tags, sendo a mais importante:

<canvas id="canvas"></canvas>Code language: HTML, XML (xml)

É este canvas 2d que representará a tela do emulador, onde o jogo é renderizado. A seguir o arquivo completo:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chip-8</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div id="container">
        <h1 id="title">Chip-8 Emulator</h1>
        <canvas id="canvas"></canvas>
        <span id="signature">&reg; Rafael Valentim</span>
    </div>
    <script src="index.js"></script>
</body>
</html>Code language: HTML, XML (xml)

style.css – O formato

Este arquivo define a estrutura visual do nosso emulador. É ele quem faz centralização dos componentes na tela, define a fonte e as margens do projeto. Sua finalidade é apenas estética, sem impacto ao funcionamento de emulador.

h1,
span {
    font-family: Arial, Helvetica, sans-serif;
}

#container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

#canvas {
    border: 1px solid black;
}

#signature {
    margin-top: 20px;
}Code language: CSS (css)

index.js – A lógica

É aqui que magia acontece. O arquivo Javascript é onde o Emulador é implementado de ponta a ponta. A implementação do Chip-8 encontra-se dentro da class Chip8. É nela onde definimos as propriedades do canvas (tela), e a arquitetura da CPU (seus registradores, memória, teclado, conjunto de fontes). Nela também definimos os eventos de teclado, o ciclo de CPU, e o mais importante: a função de execução de Instruções.

A função executeInstruction é a responsável por receber uma instrução (opcode) e baseado em seu conteúdo definir qual operação será executada pela CPU. É nela onde emulamos a arquitetura do Chip-8. Emuladores a nível de CPU basicamente fazem isso, emulam em software as operações implementadas em hardware de um processador.

A seguir o arquivo na integra. Alguns comentários foram adicionados em certos pontos para melhor entendimento do comportamento.

const canvas = document.querySelector("#canvas")

class Chip8 {

    constructor(canvas) {
        // Canvas
        this.canvas = canvas
        this.pixelScale = 20
        this.canvas.width = 64 * this.pixelScale
        this.canvas.height = 32 * this.pixelScale
        this.context = canvas.getContext("2d")
        this.context.fillStyle = "black"
        this.context.fillRect(0, 0, this.canvas.width, this.canvas.height)

        //Chip8 architeture
        this.memory = new Uint8Array(4096)
        this.V = new Uint8Array(16)
        this.I = 0
        this.pc = 0x200
        this.stack = []
        this.sp = 0
        this.delayTimer = 0
        this.soundTimer = 0
        this.display = new Array(64 * 32).fill(0)
        this.keys = new Array(16).fill(false)
        this.paused = false
        this.speed = 3

        // Fontset
        this.fontset = [
            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

        ]

        // Load fontset into memory 0x050 to 0x09F
        for (let i = 0; i < this.fontset.length; i++) {
            this.memory[0x050 + i] = this.fontset[i]
        }

        // Key map - keypad
        this.keymap = {
            "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": 0x0, "c": 0xB, "v": 0xF
        }
    }

    // Mapped Key Down user input
    keyDown(event) {
        const key = this.keymap[event.key]
        if (key !== undefined) this.keys[key] = true
    }

    // Mapped Key Up user input
    keyUp(event) {
        const key = this.keymap[event.key]
        if (key !== undefined) this.keys[key] = false
    }

    // Load Rom to memory - Starting at 0x200
    loadRom(rom) {
        for (let i = 0; i < rom.length; i++) {
            this.memory[0x200 + i] = rom[i]
        }
    }

    // CPU Cycle
    cycle() {
        // Fetch Opcode - Combine first and second byte to generate Chip8 16 bits Opcode
        const opcode = (this.memory[this.pc] << 8) | this.memory[this.pc + 1]

        // Decode and execute corresponding instruction
        this.executeInstruction(opcode)

        if (this.delayTimer > 0) this.delayTimer--
        if (this.soundTimer > 0) this.soundTimer--

    }

    executeInstruction(opcode) {
        const x = (opcode & 0x0F00) >> 8
        const y = (opcode & 0x00F0) >> 4
        const n = opcode & 0x000F
        const nn = opcode & 0x00FF
        const nnn = opcode & 0x0FFF

        // Increment program counter
        this.pc += 2

        switch (opcode & 0xF000) {
            case 0x000:
                if (opcode === 0x00E0) {
                    // Clear the display
                    this.display.fill(0)
                } else if (opcode === 0x00EE) {
                    // Return from a subroutine
                    this.pc = this.stack.pop()
                }
                break
            case 0x1000: {
                // Jump to address NNN
                this.pc = nnn
                break
            }
            case 0x2000: {
                // Call subroutine at NNN
                this.stack.push(this.pc)
                this.pc = nnn
                break
            }
            case 0x3000: {
                // Skip next instruction if V[x] == NN
                if (this.V[x] === nn) this.pc += 2
                break
            }
            case 0x4000: {
                // Skip next instruction if V[x] !== NN
                if (this.V[x] !== nn) this.pc += 2
                break
            }
            case 0x5000: {
                if ((opcode & 0x000F) === 0x0) {
                    // Skip next instruction if V[x] = V[y]
                    if (this.V[x] === this.V[y]) this.pc += 2
                }
                break
            }
            case 0x6000: {
                // Set V[x] = NN
                this.V[x] = nn
                break
            }
            case 0x7000: {
                // Add NN to V[x]
                this.V[x] = (this.V[x] + nn) & 0xFF
                break
            }
            case 0x8000: {
                switch (opcode & 0x000F) {
                    case 0x0000: {
                        // V[x] = V[y]
                        this.V[x] = this.V[y]
                        break
                    }
                    case 0x0001: {
                        // V[x] Bitwise OR V[y]
                        this.V[x] |= this.V[y]
                        break
                    }
                    case 0x0002: {
                        // V[x] Bitwise AND V[y]
                        this.V[x] &= this.V[y]
                        break
                    }
                    case 0x0003: {
                        // V[x] Bitwise XOR V[y]
                        this.V[x] ^= this.V[y]
                        break
                    }
                    case 0x0004: {
                        // V[x] + V[y] -> Carry
                        const sum = this.V[x] + this.V[y]
                        this.V[0xF] = sum > 0xFF ? 1 : 0
                        this.V[x] = sum & 0xFF
                        break
                    }
                    case 0x0005: {
                        // V[x] - V[y] -> Borrow
                        const sub = this.V[x] - this.V[y]
                        this.V[0xF] = sub < 0x0 ? 0 : 1
                        this.V[x] = sub & 0xFF
                        break
                    }
                    case 0x0006: {
                        // V[x] Right Shift -> Least signficant bit
                        this.V[0xF] = this.V[x] & 0x1
                        this.V[x] >>= 0x1
                        break
                    }
                    case 0x0007: {
                        // V[y] - V[x] -> Borrow
                        const sub = this.V[y] - this.V[x]
                        this.V[0xF] = sub < 0x0 ? 0 : 1
                        this.V[x] = sub & 0xFF
                        break
                    }
                    case 0x000E: {
                        // V[x] Left Shift -> Most signficant bit
                        this.V[0xF] = (this.V[x] >> 7) & 0x1
                        this.V[x] = (this.V[x] << 0x1) & 0xFF
                        break
                    }
                }
                break
            }
            case 0x9000: {
                if ((opcode & 0x000F) === 0x0) {
                    // Skip next instruction if V[x] != V[y]
                    if (this.V[x] !== this.V[y]) this.pc += 2
                }
                break
            }
            case 0xA000: {
                // Ser I = NNN
                this.I = nnn
                break
            }
            case 0xB000: {
                // Jump to address nnn + V[0]
                this.pc = nnn + this.V[0]
                break
            }
            case 0xC000: {
                // V[x] = Random number between 0 and 255 AND nn
                const random = Math.floor(Math.random() * 256)
                this.V[x] = random & nn
                break
            }
            case 0xD000: {
                // Draw sprint at (V[x], V[y]) with width 8 and height N
                const vx = this.V[x]
                const vy = this.V[y]
                const height = n

                // Reset collision flag
                this.V[0xF] = 0

                for (let row = 0; row < height; row++) {
                    const sprite = this.memory[this.I + row]
                    for (let col = 0; col < 8; col++) {
                        const pixel = (sprite >> (7 - col)) & 1
                        const index = ((vy + row) % 32) * 64 + ((vx + col) % 64)

                        if (pixel && this.display[index] === 1) {
                            // Collision detected
                            this.V[0xF] = 1
                        }
                        this.display[index] ^= pixel
                    }
                }
                break
            }
            case 0xE000: {
                if ((opcode & 0x00FF) === 0x009E) {
                    // Skip next instruction if key stored in V[x] is pressed
                    if (this.keys[this.V[x]]) this.pc += 2
                    break

                } else if ((opcode & 0x00FF) === 0x00A1) {
                    // Skip next instruction if key stored in V[x] is not pressed
                    if (!this.keys[this.V[x]]) this.pc += 2
                    break
                }
                break
            }
            case 0xF000: {
                switch (opcode & 0x00FF) {
                    case 0x0007: {
                        // V[x] = Delay Timer
                        this.V[x] = this.delayTimer
                        break
                    }
                    case 0x000A: {
                        const keyPressed = this.keys.findIndex(k => k === 1)
                        if (keyPressed !== -1) {
                            this.V[x] = keyPressed
                        } else {
                            this.pc -= 2
                        }
                        break
                    }
                    case 0x0015: {
                        // Delay Timer = V[x]
                        this.delayTimer = this.V[x]
                        break
                    }
                    case 0x0018: {
                        // Sound Timer = V[x]
                        this.soundTimer = this.V[x]
                        break
                    }
                    case 0x001E: {
                        // I = I + V[x]
                        this.I = this.I + this.V[x]
                        break
                    }
                    case 0x0029: {
                        // I = V[x] * 5
                        this.I = 0x050 + (this.V[x] * 5)
                        break

                    }
                    case 0x0033: {
                        // BCD representation of V[x] in memory at I, I+1 and I+2
                        const value = this.V[x]

                        this.memory[this.I] = Math.floor(value / 100)
                        this.memory[this.I + 1] = Math.floor((value % 100) / 10)
                        this.memory[this.I + 2] = value % 10
                        break

                    }
                    case 0x0055: {
                        // Store registers V[0] through V[x] in memory starting in I
                        for (let i = 0; i <= x; i++) {
                            this.memory[this.I + i] = this.V[i]
                        }
                        break

                    }
                    case 0x0065: {
                        // Read registers V[0] through V[x] from memory starting at I
                        for (let i = 0; i <= x; i++) {
                            this.V[i] = this.memory[this.I + i]
                        }
                        break
                    }

                }
                break
            }
            default: {
                console.log(`Unknown opcode: 0x${opcode.toString(16).toUpperCase()}`);
                break
            }
        }
    }

    renderDisplay() {
        this.context.fillStyle = "black"
        this.context.fillRect(0, 0, this.canvas.width, this.canvas.height)

        this.context.fillStyle = "#33FF00"
        for (let y = 0; y < 32; y++) {
            for (let x = 0; x < 64; x++) {
                if (this.display[y * 64 + x] === 1) {
                    this.context.fillRect(
                        x * this.pixelScale,
                        y * this.pixelScale,
                        this.pixelScale,
                        this.pixelScale
                    )
                }
            }
        }
    }
}

const chip8 = new Chip8(canvas)

const rom = [
0xA2, 0xB4, 0x23, 0xE6, 0x22, 0xB6, 0x70, 0x01, 0xD0, 0x11, 0x30, 0x25, 0x12, 0x06, 0x71, 0xFF,0xD0, 0x11, 0x60, 0x1A, 0xD0, 0x11, 0x60, 0x25, 0x31, 0x00, 0x12, 0x0E, 0xC4, 0x70, 0x44, 0x70,0x12, 0x1C, 0xC3, 0x03, 0x60, 0x1E, 0x61, 0x03, 0x22, 0x5C, 0xF5, 0x15, 0xD0, 0x14, 0x3F, 0x01,0x12, 0x3C, 0xD0, 0x14, 0x71, 0xFF, 0xD0, 0x14, 0x23, 0x40, 0x12, 0x1C, 0xE7, 0xA1, 0x22, 0x72,0xE8, 0xA1, 0x22, 0x84, 0xE9, 0xA1, 0x22, 0x96, 0xE2, 0x9E, 0x12, 0x50, 0x66, 0x00, 0xF6, 0x15,0xF6, 0x07, 0x36, 0x00, 0x12, 0x3C, 0xD0, 0x14, 0x71, 0x01, 0x12, 0x2A, 0xA2, 0xC4, 0xF4, 0x1E,0x66, 0x00, 0x43, 0x01, 0x66, 0x04, 0x43, 0x02, 0x66, 0x08, 0x43, 0x03, 0x66, 0x0C, 0xF6, 0x1E, 0x00, 0xEE, 0xD0, 0x14, 0x70, 0xFF, 0x23, 0x34, 0x3F, 0x01, 0x00, 0xEE, 0xD0, 0x14, 0x70, 0x01,0x23, 0x34, 0x00, 0xEE, 0xD0, 0x14, 0x70, 0x01, 0x23, 0x34, 0x3F, 0x01, 0x00, 0xEE, 0xD0, 0x14,0x70, 0xFF, 0x23, 0x34, 0x00, 0xEE, 0xD0, 0x14, 0x73, 0x01, 0x43, 0x04, 0x63, 0x00, 0x22, 0x5C,0x23, 0x34, 0x3F, 0x01, 0x00, 0xEE, 0xD0, 0x14, 0x73, 0xFF, 0x43, 0xFF, 0x63, 0x03, 0x22, 0x5C,0x23, 0x34, 0x00, 0xEE, 0x80, 0x00, 0x67, 0x05, 0x68, 0x06, 0x69, 0x04, 0x61, 0x1F, 0x65, 0x10,0x62, 0x07, 0x00, 0xEE, 0x40, 0xE0, 0x00, 0x00, 0x40, 0xC0, 0x40, 0x00, 0x00, 0xE0, 0x40, 0x00,0x40, 0x60, 0x40, 0x00, 0x40, 0x40, 0x60, 0x00, 0x20, 0xE0, 0x00, 0x00, 0xC0, 0x40, 0x40, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x40, 0x40, 0xC0, 0x00, 0x00, 0xE0, 0x20, 0x00, 0x60, 0x40, 0x40, 0x00,0x80, 0xE0, 0x00, 0x00, 0x40, 0xC0, 0x80, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x40, 0xC0, 0x80, 0x00,0xC0, 0x60, 0x00, 0x00, 0x80, 0xC0, 0x40, 0x00, 0x00, 0x60, 0xC0, 0x00, 0x80, 0xC0, 0x40, 0x00,0x00, 0x60, 0xC0, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00,0xC0, 0xC0, 0x00, 0x00, 0x40, 0x40, 0x40, 0x40, 0x00, 0xF0, 0x00, 0x00, 0x40, 0x40, 0x40, 0x40,0x00, 0xF0, 0x00, 0x00, 0xD0, 0x14, 0x66, 0x35, 0x76, 0xFF, 0x36, 0x00, 0x13, 0x38, 0x00, 0xEE,0xA2, 0xB4, 0x8C, 0x10, 0x3C, 0x1E, 0x7C, 0x01, 0x3C, 0x1E, 0x7C, 0x01, 0x3C, 0x1E, 0x7C, 0x01, 0x23, 0x5E, 0x4B, 0x0A, 0x23, 0x72, 0x91, 0xC0, 0x00, 0xEE, 0x71, 0x01, 0x13, 0x50, 0x60, 0x1B,0x6B, 0x00, 0xD0, 0x11, 0x3F, 0x00, 0x7B, 0x01, 0xD0, 0x11, 0x70, 0x01, 0x30, 0x25, 0x13, 0x62,0x00, 0xEE, 0x60, 0x1B, 0xD0, 0x11, 0x70, 0x01, 0x30, 0x25, 0x13, 0x74, 0x8E, 0x10, 0x8D, 0xE0,0x7E, 0xFF, 0x60, 0x1B, 0x6B, 0x00, 0xD0, 0xE1, 0x3F, 0x00, 0x13, 0x90, 0xD0, 0xE1, 0x13, 0x94,0xD0, 0xD1, 0x7B, 0x01, 0x70, 0x01, 0x30, 0x25, 0x13, 0x86, 0x4B, 0x00, 0x13, 0xA6, 0x7D, 0xFF,0x7E, 0xFF, 0x3D, 0x01, 0x13, 0x82, 0x23, 0xC0, 0x3F, 0x01, 0x23, 0xC0, 0x7A, 0x01, 0x23, 0xC0,0x80, 0xA0, 0x6D, 0x07, 0x80, 0xD2, 0x40, 0x04, 0x75, 0xFE, 0x45, 0x02, 0x65, 0x04, 0x00, 0xEE, 0xA7, 0x00, 0xF2, 0x55, 0xA8, 0x04, 0xFA, 0x33, 0xF2, 0x65, 0xF0, 0x29, 0x6D, 0x32, 0x6E, 0x00,0xDD, 0xE5, 0x7D, 0x05, 0xF1, 0x29, 0xDD, 0xE5, 0x7D, 0x05, 0xF2, 0x29, 0xDD, 0xE5, 0xA7, 0x00,0xF2, 0x65, 0xA2, 0xB4, 0x00, 0xEE, 0x6A, 0x00, 0x60, 0x19, 0x00, 0xEE, 0x37, 0x23
]

chip8.loadRom(rom)

document.addEventListener("keydown", (event) => {
    chip8.keyDown(event)
})

document.addEventListener("keyup", (event) => {
    chip8.keyUp(event)
})


function run() {
    for (let i = 0; i < chip8.speed; i++) {
        chip8.cycle()
    }
    chip8.renderDisplay()
    requestAnimationFrame(run)
}
run()Code language: JavaScript (javascript)

Enfim, se você chegou até aqui, replicando os arquivos corretamente, ao abrir o arquivo index.html em seu navegador, você deverá visualizar o seguinte jogo em execução:

Para mais detalhes da implementação, visite o repositório completo no Github:

https://github.com/rmvalentim/CHIP-8-Emulator

Lá você conseguirá acessar outras ROMs, como o jogo Pong. Um exercício interessante é tentar mudar o jogo do emulador, substituindo o conteúdo Hexadecimal que é carregado na memória do emulador (função loadRom).

Nos próximos passos desse projeto, pretendo desenvolver novas versões do emulador com linguagens e ferramentas diferentes (a próxima deverá ser Python com a biblioteca PyGame).

Se você se interessou pelo tema, abaixo alguns links relevantes que utilizei na minha pesquisa durante o desenvolvimento do emulador, até a próxima.

https://tobiasvl.github.io/blog/write-a-chip-8-emulator

https://en.wikipedia.org/wiki/CHIP-8

https://johnearnest.github.io/Octo/docs/chip8ref.pdf