Chip 8 Emulator

Project Repo

Tech Used:

C++ / SDL2 / Make / Binary operations




What does the project do?

Emulate a virtual machine from the 70s and allow ROMS for that machine to be read.





pic

The application will accept a Chip8 ROM as a parameter during execution of the binary. The ROM file itself is just a file containing a long list of numbers. The emulator is able to read the entire file, decode the numbers into valid instructions for the CPU to process. From a single file, an animation like the one above can be displayed or a simple game of Pong can be played.




            
              //instructions
              void op0x00(uint16_t op);
              void op0x00e0(uint16_t op);
              void op0x00ee(uint16_t op);
              void op0x01(uint16_t op);
              void op0x02(uint16_t op);
              void op0x03(uint16_t op);
              void op0x04(uint16_t op);
              void op0x05(uint16_t op);
              void op0x06(uint16_t op);
              void op0x07(uint16_t op);
              void op0x08(uint16_t op);
              void op0x080(uint16_t op);
              void op0x081(uint16_t op);
              void op0x082(uint16_t op);
              void op0x083(uint16_t op);
              void op0x084(uint16_t op);
              void op0x085(uint16_t op);
              void op0x086(uint16_t op);
              void op0x087(uint16_t op);
              void op0x08e(uint16_t op);
              void op0x09(uint16_t op);
              void op0x0a(uint16_t op);
              void op0x0b(uint16_t op);
              void op0x0c(uint16_t op);
              void op0x0d(uint16_t op);
              void op0x0e(uint16_t op);
              void op0x0e9(uint16_t op);
              void op0x0ea(uint16_t op);
              void op0x0f(uint16_t op);
              void op0x0f07(uint16_t op);
              void op0x0f0A(uint16_t op);
              void op0x0f15(uint16_t op);
              void op0x0f18(uint16_t op);
              void op0x0f1E(uint16_t op);
              void op0x0f29(uint16_t op);
              void op0x0f33(uint16_t op);
              void op0x0f55(uint16_t op);
              void op0x0f65(uint16_t op);

            
          

The Emulator has a CPU that is capable of decoding 38 unique instructions. This means that the emulator can detect up to 38 different instructions from a file containing valid Chip8 instructions. A file containing random numbers will produce odd results if read using the emulator, but a chip8 ROM file will either result in a game or some picture being drawn to the 64 by 32 pixel display.




          
            void chip8::op0x01(uint16_t op) 
            {
              //jump to nnn
              uint16_t addr = op & 0x0FFF; // extract memory address

              if(getPC() == addr) 
              {
                  cout << "Error trying to hault!" << '\n';
                  exit(1);
              }
              setPC(addr); // perform jump
          }
          
        

The above is an example of a chip8 instruction or opcode. This instruction is supposed to take in the entire opcode, perform binary math to extract a memory address, check if that memory address is equal to the program counter register, if it is then the emulation cycle is haulted, otherwise it sets the program counter register equal to the extracted memory address. That will mean that the address in memory the CPU will next read from will be the address just set by this instruction, meaning the jump to nnn has concluded.

An emulator in general has a cycle it runs through in order to "run". This emulator is the "hello world" to emulation because of how simple that cycle is. Each instruction is exactly the same size in bytes, meaning the CPU simply needs to read 2 bytes at a time. Since instructions are stored in big endian, meaning the most significant byte of the instruction is stored at the smallest memory address and the least significant byte is stored at the largest memory address, the emulator will first look at the first byte, determine which instruction it is, and then use the remaining byte for any processing done by that instruction, the program counter might be increased by the instruction or set to a specific address specified within the second byte of the instruction. This loop is done until the user ends the program.