main.c

chip-8 emulator

main.c

16.54 KB
/*** includes ***/
#include <SDL2/SDL.h>
#include <SDL2/SDL_config-x86_64.h>
#include <bits/types/struct_FILE.h>
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>

/*** data ***/
// opcode store
// All opcodes are 2 bytes and so are unsigned short
unsigned short opcode;
// 4KB of RAM
unsigned char memory[4096];
// CPU Register
// 16 general purpose 8 bit registers
unsigned char V[16];
// Index or Address Register
unsigned short I;
// Program counter | A pseudo register that points to the next instruction on
// the stack Instruction pointer being my preferred nomenclature
unsigned short pc;
// Display containing 2048 pixels in an array
unsigned char gfx[64 * 32];
// Timer registers that countdown to 0 at 60Hz
unsigned char delay_timer;
unsigned char sound_timer;
// Sound flag
unsigned char sound_flag = 0;
// Draw flag
unsigned char draw_flag = 0;
// Stack and stack pointer
unsigned short stack[16];
unsigned short stack_pointer;

// Font set used by CHIP 8
// Also used to draw sprites using XOR
// DEC   HEX    BIN         RESULT    DEC   HEX    BIN         RESULT
// 240   0xF0   1111 0000    ****     240   0xF0   1111 0000    ****
// 144   0x90   1001 0000    *  *      16   0x10   0001 0000       *
// 144   0x90   1001 0000    *  *      32   0x20   0010 0000      *
// 144   0x90   1001 0000    *  *      64   0x40   0100 0000     *
// 240   0xF0   1111 0000    ****      64   0x40   0100 0000     *

unsigned char chip8_fontset[80] = {
    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
};

// Keypad
unsigned char keypad[16];

// Screen dimension constants
const int SCREEN_WIDTH = 64 * 20;
const int SCREEN_HEIGHT = 32 * 20;

/*** initialize CPU ***/
void chip8_initialize(void)
{
    // Seed for rand from time.h
    srand((unsigned int)time(NULL));
    pc = 0x200; // program counter starts at 0x200 or 512
    opcode = 0; // reset current opcode
    I = 0; // reset current index register
    stack_pointer = 0; // reset stack pointer
    printf("\n Chip 8 Initialized successfully \n");
    memcpy(memory, chip8_fontset, sizeof(chip8_fontset));
}

/*** load rom ***/
void chip8_load_program(char* filename)
{
    FILE* program = fopen(filename, "rb");

    // get file size
    fseek(program, 0, SEEK_END);
    long buf_len = ftell(program);
    rewind(program);

    // load data into buffer
    char* buf = malloc((buf_len + 1) * sizeof(char));
    fread(buf, buf_len, 1, program);
    printf("Rom loaded succesfully \n");
    printf("Rom size: %zu bytes \n", buf_len);
    fclose(program);

    // load ROM into memory
    for (int i = 0; i < buf_len; i++) {
        memory[0x200 + i] = buf[i];
        // Big endian so check last two values from the right
        printf("Byte successfully read: %08X \n", buf[i]);
    }
    // clear buffer
    free(buf);
}

/*** initialize display ***/

SDL_Window* window;
SDL_Renderer* renderer;

int chip8_init_display()
{
    SDL_Init(SDL_INIT_VIDEO);
    // Init Window struct

    // Init window
    window = SDL_CreateWindow("CHIP8",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        SCREEN_WIDTH,
        SCREEN_HEIGHT,
        0);
    // Init renderer struct
    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
        return -1;
    }
    // Check that the window was successfully created

    if (window == NULL) {
        // In the case that the window could not be made...
        printf("Could not create window: %s\n", SDL_GetError());
        return -1;
    }
    return 0;
}

void stop_display(void)
{
    SDL_DestroyWindow(window);
    SDL_Quit();
}

void chip8_draw(unsigned char* gfx)
{
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    // Init renderer
    SDL_RenderClear(renderer);
    // SDL Render color (background)

    // Clear the current color and use the one specified above
    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);

    for (int y = 0; y < 32; y++) {
        for (int x = 0; x < 64; x++) {
            if (gfx[x + (y * 64)]) {
                SDL_Rect rect;
                rect.x = x * 20;
                rect.y = y * 20;
                // size of each "pixel" keep it consistent with the scaling of the display in SCREEN_WIDTH/HEIGHT
                rect.w = 20;
                rect.h = 20;

                SDL_RenderFillRect(renderer, &rect);
            }
        }
    }
    SDL_RenderPresent(renderer);
}

void chip8_emulate_cycle()
{
    draw_flag = 0;
    sound_flag = 0;
    printf(" \n");

    // Fetch opcode present at program counter and program counter + 1
    // Since each opcode is supposed to be 2 bytes we fetch both pc and pc + 1
    // Shift the current 8 bytes in memory[pc] left by 8 points
    // We merge them using binary OR to get the entire opcode
    opcode = memory[pc] << 8 | memory[pc + 1];

    // Vx register, we are basically "grabbing" the x present in some
    // instructions like 3XNN
    unsigned short x = (opcode & 0x0F00) >> 8;

    // Vy register, we are basically "grabbing" the y present in some
    // instructions like 5XY0
    unsigned short y = (opcode & 0x00F0) >> 4;

    printf("0x%X \n", opcode);
    switch (opcode & 0xF000) {
    case 0x0000:
        switch (opcode & 0x00FF) {
        case 0x00E0:
            for (int i = 0; i < 64 * 32; i++) {
                gfx[i] = 0;
            }
            printf("00E0: Display Cleared\n");
            pc += 2;
            break;
        case 0x00EE:
            pc = stack[stack_pointer];
            stack_pointer--;
            printf("00EE: Retrurn from a subroutine\n");
            pc += 2;
            break;
        default:
            break;
        }
        break;

    // 1NNN: Jumps to address NNN
    case 0x1000:
        printf("1nnn: Jump to location nnn \n");
        pc = opcode & 0x0FFF;
        break;

    // 2nnn CALL subroutine at nnn
    case 0x2000:
        stack_pointer += 1;
        stack[stack_pointer] = pc;
        pc = opcode & 0x0FFF; // parse the NNN
        printf("2nnn: Calls subroutine at nnn \n");
        break;

    // 3XNN: Skip next instruction if Vx = kk
    case 0x3000:
        if (V[x] == (opcode & 0x00FF)) {
            pc += 2;
        }
        pc += 2;
        printf("3XNN: Skip next instruction if V[x] == kk \n");
        break;

    // 4xkk: Skip next instruction if V[x] != kk
    case 0x4000:
        if (V[x] != (opcode & 0x00FF)) {
            pc += 2;
        }
        pc += 2;
        printf("4xkk: Skip next instruction if V[x] != kk \n");
        break;

    // 5xy0 - SE Vx, Vy Skip next instruction if Vx = Vy.
    case 0x5000:
        if (V[x] == V[y]) {
            pc += 2;
        }
        pc += 2;
        printf("5xy0: Skip next instruction if V[x] != kk \n");
        break;

    // 6XNN: Sets Vx to NN
    case 0x6000:
        V[x] = (opcode & 0x00FF);
        pc += 2;
        printf("6xkk: The interpreter puts the value kk into register V[x] \n");
        break;

    // 7xkk: ADD Vx, byte
    case 0x7000:
        V[x] += opcode & 0x00FF;
        pc += 2;
        printf("7xkk: Adds the value kk ot the value of V[x] then stores it in "
               "V[x] \n");
        break;

    case 0x9000:
        if (V[x] != V[y]) {
            pc += 2;
        }
        pc += 2;
        break;

    // Instructions starting with 8
    case 0x8000:
        switch (opcode & 0x000F) {
        // 8xy0: Sets Vx to Vy
        case 0x0000:
            V[x] = V[y];
            pc += 2;
            break;

        // 8xy1: V[x] = V[x] | V[y]
        case 0x0001:
            V[x] = (V[x] | V[y]);
            pc += 2;
            break;

        // 8xy2: V[x] = V[x] | V[y]
        case 0x0002:
            V[x] = (V[x] & V[y]);
            pc += 2;
            break;

        // 8xy3: V[x] = [Vx] ^ V[y]
        case 0x0003:
            V[x] = (V[x] ^ V[y]);
            pc += 2;
            break;

        // 8xy4: V[x] = V[x] + V[y], set VF = carry.
        case 0x0004:
            if (V[x] + V[y] > 255) {
                printf("is this valid?");
                V[0xF] = 1;
            } else {
                V[0xF] = 0;
            }
            V[x] += V[y];
            pc += 2;
            break;

        // 8xy5: V[x] = V[x] - V[y], set VF = NOT BORROW
        case 0x0005:
            if (V[x] > V[y]) {
                V[0xF] = 1;
            } else {
                V[0xF] = 0;
            }
            V[x] -= V[y];
            pc += 2;
            break;

        // 8xy6: Sets Vx to Vy minus Vx. Vf is set to 0 when there's a borrow
        case 0x0006:
            V[0xF] = V[x] & 0x1;
            V[x] = (V[x] >> 1);
            pc += 2;
            break;

        // Set Vx = Vy - Vx, set VF = NOT borrow
        case 0x0007:
            if (V[y] > V[x]) {
                V[0xF] = 1;
            } else {
                V[0xF] = 0;
            }
            V[x] = V[y] - V[x];
            pc += 2;
            break;

        // 8xyE: Stores the most significant bit of V[x] and shifts V[x]
        case 0x000E:
            V[0xF] = (V[x] >> 7) & 0x1;
            V[x] = (V[x] << 1);
            pc += 2;
            break;

        default:
            printf("Unknown opcode 0x%X.\n", opcode);
            break;
        }
        break;

    // Display n-byte sprite starting at memory location I at (Vx, Vy)
    // Set VF = collision.
    case 0xD000:
        draw_flag = 1;
        unsigned short height = opcode & 0x000F;
        unsigned short pixel;
        // set collision flag to 0
        V[0xF] = 0;
        // loop over each row
        for (int row = 0; row < height; row++) {
            // fetch the pixel value from the memory starting at location I
            pixel = memory[I + row];
            // loop over 8 bits of one row
            for (int col = 0; col < 8; col++) {
                // check if current evaluated pixel is set to 1 (0x80 >>
                // col scnas throught the byte, one bit at the time)
                if ((pixel & (0x80 >> col)) != 0) {
                    // if drawing causes any pixel to be erased set the
                    // collision flag to 1
                    if (gfx[(V[x] + col + ((V[y] + row) * 64))] == 1) {
                        V[0xF] = 1;
                    }
                    // set pixel value by using XOR
                    gfx[V[x] + col + ((V[y] + row) * 64)] ^= 1;
                }
            }
        }
        printf(
            "Dxyn: Display n-byte sprite starting at memory location I at (Vx, "
            "Vy), set VF to collision \n");
        pc += 2;
        break;

    case 0xC000:
        V[x] = (rand() % 256) & (opcode & 0x00FF);
        pc += 2;
        printf("Cxkk: The value at V[x] is %d \n", V[x]);
        break;

    case 0xA000:
        I = opcode & 0x0FFF;
        pc += 2;
        printf("Annn: The value at register I is set to 0x%X \n",
            opcode & 0x0FFF);
        break;

    case 0xE000:
        switch (opcode & 0x00FF) {

        // Ex9e: Skips next instructuion if key stored in V[x]is pressed
        case 0x009E:
            if (keypad[V[x]]) {
                pc += 2;
            }
            pc += 2;
            break;

        // ExA1: Skips next instruction if key stroed in V[x] is not pressed
        case 0x00A1:
            if (!keypad[V[x]]) {
                pc += 2;
            }
            pc += 2;
            break;

        default:
            printf("Uknown opcode: 0x%X\n", opcode);
        }
        break;

    // Instructions starting with FX
    case 0xF000:
        switch (opcode & 0x00FF) {
        case 0x0007:
            printf("Fx07: Place value of delay_timer into V[x]\n");
            V[x] = delay_timer;
            pc += 2;
            break;

        case 0x000A:
            printf(
                "Fx0A: Stop all execution till key press, keypress is stored in "
                "V[x]\n");
            for (int i = 0; i < 16; i++) {
                if (keypad[i]) {
                    V[x] = i;
                    pc += 2;
                    break;
                }
            }
            break;

        case 0x0015:
            printf("Fx15: Sets the delay_timer value to V[x]\n");
            delay_timer = V[x];
            pc += 2;
            break;

        // FX18: Sets the sound timer to Vx
        case 0x0018:
            printf("Fx18: Sets the sound_timer value to V[x]\n");
            sound_timer = V[x];
            pc += 2;
            break;

        // FX1E: Adds Vx to I
        case 0x001E:
            printf("Fx1E:The values of I and Vx are added, and the results are "
                   "stored in I\n");
            I += V[x];
            pc += 2;
            break;

        // FX29: Sets I to the location of the sprite for the character
        // in Vx
        case 0x0029:
            printf("Fx29: Set location of I to mathching 0x sprite in V[x] \n");
            // Each digit contains 5 bytes
            I = V[x] * 5;
            pc += 2;
            break;

            /*
             * FX33:
             *
             * Stores the binary-coded decimal representation
             * of VX, with the most significant of three digits
             * at the address in I, the middle digit at I plus
             * 1, and the least significant digit at I plus 2.
             * (In other words, take the decimal representation
             * of VX, place the hundreds digit in memory
             * at location in I, the tens digit at
             * location I+1, and the ones digit at
             * location I+2.)
             * */

        case 0x0033:
            printf(
                "Fx33: Stores the binary-coded decimal representation of VX \n");
            memory[I] = (V[x] % 1000) / 100;
            memory[I + 1] = (V[x] % 100) / 10;
            memory[I + 2] = (V[x] % 10);

            pc += 2;
            break;

        case 0x0055:
            printf("Fx55: Stores V[0] to V[x] in memory starting at I\n");

            for (int i = 0; i <= x; i++) {
                V[i] = memory[I + i];
            }

            pc += 2;
            break;

        // Fills V0 through Vx (Vx included) with values from memory
        // starting at addr I.
        case 0x0065:
            printf("Read registers V0 through Vx from memory starting at "
                   "location I \n");
            for (int i = 0; i <= x; i++) {
                V[i] = memory[I + i];
            }
            pc += 2;
            break;

        default:
            printf("Unknown opcode 0x%X.\n", opcode);
            break;
        }
        break;
    default:
        printf("Unknown opcode 0x%X.\n", opcode);
        break;
    }

    // Update timers
    if (delay_timer > 0)
        --delay_timer;
    if (sound_timer > 0) {
        if (sound_timer == 1)
            printf("BEEP!\n");
        --sound_timer;
    }
}

SDL_Event event;
SDL_Scancode keymappings[16] = {
    SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
    SDL_SCANCODE_Q, SDL_SCANCODE_W, SDL_SCANCODE_E, SDL_SCANCODE_R,
    SDL_SCANCODE_A, SDL_SCANCODE_S, SDL_SCANCODE_D, SDL_SCANCODE_F,
    SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_C, SDL_SCANCODE_V
};

int usleep(unsigned int usec);
int main(int argc, char* argv[])
{
    (void)argc;
    const Uint8* state = SDL_GetKeyboardState(NULL);
    chip8_initialize();
    chip8_load_program(argv[1]);
    chip8_init_display();
    while (1) {
        chip8_emulate_cycle();

        if (draw_flag == 1) {
            chip8_draw(gfx);
            	// Delay to slow down the clock
        	usleep(45000);
        }
        while (SDL_PollEvent(&event) != 0) {
            switch (event.type) {
            case SDL_QUIT:
                SDL_DestroyWindow(window);
                SDL_Quit();
                return 0;
                break;
            default:
                if (state[SDL_SCANCODE_ESCAPE]) {
                    return 0;
                }
                // update keyboard state
                for (int keycode = 0; keycode < 16; keycode++) {
                    keypad[keycode] = state[keymappings[keycode]];
                }
            }
        }
    }
}