Added handler for 0x1000 and added documentation for most functions and classes

main
Tiger 2023-09-09 21:35:24 +02:00
parent 632873078b
commit fce922f6c6
10 changed files with 294 additions and 11 deletions

View File

@ -7,21 +7,70 @@
class InstructionHandler; class InstructionHandler;
/**
* The main Chip8 used to emulate/interpret the Chip8 program
*/
class Chip8 { class Chip8 {
private: private:
/**
* The memory of the Chip8 program
*/
unsigned char memory[0xFFF] = {}; unsigned char memory[0xFFF] = {};
/**
* The 16 general purpose 8-bit registers, generally used to store memory addresses
*/
unsigned char v[0x10] = {}; unsigned char v[0x10] = {};
/**
* Stack of 16-bit values, used to store addresses the interpreter should return to when finished with a subroutine
*/
std::stack<unsigned short> stack{}; std::stack<unsigned short> stack{};
/**
* The program counter which holds the currently executing address
*/
unsigned short pc; unsigned short pc;
/**
* The Display object holding information and functions used for the display
*/
Display display; Display display;
/**
* The instruction handler used to handle instructions
*/
InstructionHandler* instructionHandler; InstructionHandler* instructionHandler;
public: public:
Chip8(); Chip8();
~Chip8(); ~Chip8();
void loadRom(const std::string&);
/**
* Loads a rom in the memory
* @param rom The rom to be loaded in the memory
*/
void loadRom(const std::string& rom);
/**
* Gets the Display object
* @return
*/
[[nodiscard]] Display getDisplay() const; [[nodiscard]] Display getDisplay() const;
/**
* Cycle to handle the next instruction
*/
void emulateCycle(); void emulateCycle();
/**
* Pops a 16-bit value from the stack
* @return a unsigned 16-bit integer
*/
[[nodiscard]] unsigned short popFromStack(); [[nodiscard]] unsigned short popFromStack();
void setProgramCounter(unsigned short);
/**
* Sets the program counter (PC) to the given value
*/
void setProgramCounter(unsigned short value);
}; };
#endif //CHIP8_CHIP8_H #endif //CHIP8_CHIP8_H

View File

@ -3,12 +3,32 @@
#include <chrono> #include <chrono>
/**
* Class used to work with dates and times
*/
class Date { class Date {
public: public:
/**
* Creates a new Date object with the given time_point
* @param tp the time_point object
*/
explicit Date(std::chrono::system_clock::time_point tp); explicit Date(std::chrono::system_clock::time_point tp);
/**
* Gets a Date object with the current time as time_point
* @return The Date object
*/
static Date now(); static Date now();
std::string to_string() const;
/**
* Converts the time_point to a string with format "Y-%m-%d %H:%M:%S"
* @return the formatted datetime string
*/
[[nodiscard]] std::string to_string() const;
private: private:
/**
* The time_point to use
*/
std::chrono::system_clock::time_point tp; std::chrono::system_clock::time_point tp;
}; };

View File

@ -6,13 +6,36 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
/**
* The display class used to handle drawing pixels to the SDL renderer
*/
class Display { class Display {
private: private:
/**
* Flag whether the display should be redrawn
*/
bool flagRedraw = false; bool flagRedraw = false;
/**
* The 64x32-pixel monochrome display
*/
bool display[64 * 32] = {}; bool display[64 * 32] = {};
public: public:
void draw(SDL_Renderer*); /**
* Draws the current display to the SDL renderer
* @param renderer the SDL renderer to draw to
*/
void draw(SDL_Renderer* renderer);
/**
* Gets whether the display should be redrawn
* @return true if the display should be redrawn, false otherwise
*/
[[nodiscard]] bool needsRedraw() const; [[nodiscard]] bool needsRedraw() const;
/**
* Clears the display
*/
void clear(); void clear();
}; };
#endif //CHIP8_DISPLAY_H #endif //CHIP8_DISPLAY_H

View File

@ -1,14 +1,45 @@
#ifndef CHIP8_INSTRUCTION_H #ifndef CHIP8_INSTRUCTION_H
#define CHIP8_INSTRUCTION_H #define CHIP8_INSTRUCTION_H
/**
* The instruction class stores the opcode and allows easier access to the Chip8 values
*/
class Instruction { class Instruction {
private: private:
/**
* The basic opcode
*/
const unsigned short opcode; const unsigned short opcode;
public: public:
explicit Instruction(unsigned short); explicit Instruction(unsigned short opcode);
/**
* Gets the NNN Chip8 value
* @return
*/
[[nodiscard]] unsigned short nnn() const; [[nodiscard]] unsigned short nnn() const;
[[nodiscard]] unsigned short n() const;
/**
* Gets the N Chip8 value
* @return
*/
[[nodiscard]] unsigned char n() const;
/**
* Gets the KK/Byte/NN Chip8 value
* @return
*/
[[nodiscard]] unsigned short kk() const; [[nodiscard]] unsigned short kk() const;
/**
* Gets the X Chip8 value
* @return
*/
[[nodiscard]] unsigned char x() const; [[nodiscard]] unsigned char x() const;
/**
* Gets the Y Chip8 value
* @return
*/
[[nodiscard]] unsigned char y() const; [[nodiscard]] unsigned char y() const;
}; };
#endif //CHIP8_INSTRUCTION_H #endif //CHIP8_INSTRUCTION_H

View File

@ -5,13 +5,39 @@
#include "chip8.h" #include "chip8.h"
#include "instruction.h" #include "instruction.h"
/**
* The InstructionHandler class holds the Chip8 instruction set and all handlers
*/
class InstructionHandler { class InstructionHandler {
private: private:
/**
* The typedef for the handler functions
*/
typedef void (*Handler)(Chip8&, Instruction); typedef void (*Handler)(Chip8&, Instruction);
/**
* Map which stores all instructions with their handler
*/
std::map<unsigned short, Handler> handlers = {}; std::map<unsigned short, Handler> handlers = {};
/**
* Handles Chip8 instruction 0x0000, as well as 0x00E0 and 0x00EE
*/
static void handle0000(Chip8&, Instruction); static void handle0000(Chip8&, Instruction);
/**
* Handles Chip8 instruction 0x1000
*/
static void handle1000(Chip8&, Instruction);
public: public:
InstructionHandler(); InstructionHandler();
void handleInstruction(unsigned short, Chip8&, Instruction);
/**
* Attempts to handle a chip8 instruction
* @param instructionNumber the 16-bit value to determine the instruction
* @param chip8 the Chip8 instance
* @param instruction the Instruction instance
*/
void handleInstruction(unsigned short instructionNumber, Chip8& chip8, Instruction instruction);
}; };
#endif //CHIP8_INSTRUCTION_HANDLER_H #endif //CHIP8_INSTRUCTION_HANDLER_H

View File

@ -5,14 +5,26 @@
#include <string> #include <string>
#include "date.h" #include "date.h"
/**
* The LogType of a log
*/
enum LogType { enum LogType {
INFO, INFO,
DEBUG, DEBUG,
WARN, WARN,
ERROR ERROR
}; };
/**
* The logger class allows logging to either the output stream or the error stream
*/
class Logger { class Logger {
private: private:
/**
* Converts the LogType enum to a string to display it in the log
* @param logType the LogType enum to convert
* @return the LogType as a string
*/
static const char* logTypeToString(LogType logType) { static const char* logTypeToString(LogType logType) {
switch (logType) { switch (logType) {
case LogType::INFO: case LogType::INFO:
@ -28,6 +40,13 @@ private:
return ""; return "";
} }
public: public:
/**
* Writes log to either the 'cout' or 'cerr' streams
*
* @tparam Args Variadic template representing any number and type of arguments that
* @param logType The type of the log. If logType is LogType::ERROR, output to 'cerr' instead of 'cout'
* @param args The contents of the log, this can be any number of arguments
*/
template<typename... Args> template<typename... Args>
static void log(LogType logType, Args&&... args) { static void log(LogType logType, Args&&... args) {
std::ostream& stream = (logType == LogType::ERROR) ? std::cerr : std::cout; std::ostream& stream = (logType == LogType::ERROR) ? std::cerr : std::cout;
@ -36,21 +55,45 @@ public:
stream << std::endl; stream << std::endl;
} }
/**
* Writes an info log to the cout stream
*
* @tparam Args @see self::log
* @param args @see self::log
*/
template<typename... Args> template<typename... Args>
static void info(Args&&... args) { static void info(Args&&... args) {
log(LogType::INFO, std::forward<Args>(args)...); log(LogType::INFO, std::forward<Args>(args)...);
} }
/**
* Writes an debug log to the cout stream
*
* @tparam Args @see self::log
* @param args @see self::log
*/
template<typename... Args> template<typename... Args>
static void debug(Args&&... args) { static void debug(Args&&... args) {
log(LogType::DEBUG, std::forward<Args>(args)...); log(LogType::DEBUG, std::forward<Args>(args)...);
} }
/**
* Writes an warn log to the cout stream
*
* @tparam Args @see self::log
* @param args @see self::log
*/
template<typename... Args> template<typename... Args>
static void warn(Args&&... args) { static void warn(Args&&... args) {
log(LogType::WARN, std::forward<Args>(args)...); log(LogType::WARN, std::forward<Args>(args)...);
} }
/**
* Writes an error log to the cerr stream
*
* @tparam Args @see self::log
* @param args @see self::log
*/
template<typename... Args> template<typename... Args>
static void error(Args&&... args) { static void error(Args&&... args) {
log(LogType::ERROR, std::forward<Args>(args)...); log(LogType::ERROR, std::forward<Args>(args)...);

View File

@ -13,6 +13,13 @@ Chip8::~Chip8() {
delete instructionHandler; delete instructionHandler;
} }
/**
* Reads all bytes from 0x200 onward and put them in the memory.
* If the file does not exist, the program is exited.
*
* @param file The Chip8 rom file
* @todo Check if the Chip8 rom file is a valid rom file
*/
void Chip8::loadRom(const std::string& file) { void Chip8::loadRom(const std::string& file) {
FILE* game = fopen(("roms/" + file).c_str(), "rb"); FILE* game = fopen(("roms/" + file).c_str(), "rb");
if (game == nullptr) { if (game == nullptr) {
@ -28,12 +35,19 @@ Display Chip8::getDisplay() const {
return this->display; return this->display;
} }
/**
* This emulates one cycle by getting the opcode and handling the instruction corresponding that opcode
*/
void Chip8::emulateCycle() { void Chip8::emulateCycle() {
unsigned short opcode = (this->memory[this->pc] << 8 | this->memory[this->pc + 1]); unsigned short opcode = (this->memory[this->pc] << 8 | this->memory[this->pc + 1]);
this->pc += 2; this->pc += 2;
this->instructionHandler->handleInstruction(opcode & 0xF000, *this, Instruction(opcode)); this->instructionHandler->handleInstruction(opcode & 0xF000, *this, Instruction(opcode));
} }
/**
* Gets the value from the top of the stack, pop the top of the stack and returns the value
* @return A 16-bit value from the stack
*/
unsigned short Chip8::popFromStack() { unsigned short Chip8::popFromStack() {
unsigned short value = this->stack.top(); unsigned short value = this->stack.top();
this->stack.pop(); this->stack.pop();

View File

@ -1,5 +1,12 @@
#include "../include/display.h" #include "../include/display.h"
/**
* Draws the display to the SDL renderer
* If the bool in the display is true, a white pixel will be written on that position
* The redraw flag is set to false afterwards
*
* @param renderer The SDL renderer
*/
void Display::draw(SDL_Renderer * renderer) { void Display::draw(SDL_Renderer * renderer) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer); SDL_RenderClear(renderer);
@ -7,7 +14,7 @@ void Display::draw(SDL_Renderer * renderer) {
for (int y = 0; y < 32; y++) { for (int y = 0; y < 32; y++) {
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
if (display[(y * 64) + x] != STATE_OFF) { if (display[(y * 64) + x] == STATE_ON) {
SDL_Rect r; SDL_Rect r;
r.x = x + 10; r.x = x + 10;
r.y = y + 10; r.y = y + 10;
@ -26,7 +33,10 @@ bool Display::needsRedraw() const {
return this->flagRedraw; return this->flagRedraw;
} }
/**
* Clears the display by setting all pixels to false
*/
void Display::clear() { void Display::clear() {
std::fill(display, display + 64 * 32, false); std::fill(display, display + 64 * 32, STATE_OFF);
this->flagRedraw = true; this->flagRedraw = true;
} }

View File

@ -4,22 +4,59 @@ Instruction::Instruction(unsigned short opcode) : opcode(opcode) {
} }
/**
* Gets the NNN Chip8 value, also known as the addr.
* This is a 12-bit value, the lowest 12 bits of the instruction.
* @return
*/
unsigned short Instruction::nnn() const { unsigned short Instruction::nnn() const {
return this->opcode & 0x0FFF; return this->opcode & 0x0FFF;
} }
unsigned short Instruction::n() const { /**
* Gets the N Chip8 value, also known as the nibble.
* This is a 4-bit value, the lowest 4 bits of the instruction.
* @return
*/
unsigned char Instruction::n() const {
return this->opcode & 0x000F; return this->opcode & 0x000F;
} }
/**
* Gets the KK Chip8 value, also known as byte (or NN by Wikipedia).
* The naming is inconsistent, as Wikipedia has it as NN
* and http://devernay.free.fr/hacks/chip8/C8TECH10.HTM has it as KK (with byte as alternative name).
* This is a 8-bit value, the lowest 8 bits of the instruction.
* @return
*/
unsigned short Instruction::kk() const { unsigned short Instruction::kk() const {
return this->opcode & 0x00FF; return this->opcode & 0x00FF;
} }
/**
* Gets the X Chip8 value.
* This is a 4-bit value, the lower 4 bits of the high byte of the instruction.
* The high byte is the second nibble.
* Example:
*
* 0101 1010 1100 0011
* Here, the 1010 is the X value.
* @return
*/
unsigned char Instruction::x() const { unsigned char Instruction::x() const {
return (this->opcode & 0x0F00) >> 8; return (this->opcode & 0x0F00) >> 8;
} }
/**
* Gets the Y Chip8 value.
* This is a 4-bit value, the upper 4 bits of the low byte of the instruction.
* The low byte is the second nibble.
* Example:
*
* 0101 1010 1100 0011
* Here, the 1100 is the Y value.
* @return
*/
unsigned char Instruction::y() const { unsigned char Instruction::y() const {
return (this->opcode & 0x00F0) >> 4; return (this->opcode & 0x00F0) >> 4;
} }

View File

@ -1,11 +1,19 @@
#include "../include/instruction_handler.h" #include "../include/instruction_handler.h"
#include "../include/logger.h" #include "../include/logger.h"
#include "../include/display.h"
InstructionHandler::InstructionHandler() { InstructionHandler::InstructionHandler() {
this->handlers[0x0000] = handle0000; this->handlers[0x0000] = handle0000;
this->handlers[0x1000] = handle1000;
} }
/**
* Attempts to handle the Chip8 instruction by the given instruction number.
* If the handler doesn't exist for the instruction, an error is logged, but the program resumes.
*
* @param instructionNumber the 16-bit value to determine the instruction
* @param chip8 the Chip8 instance
* @param instruction the Instruction instance
*/
void InstructionHandler::handleInstruction(unsigned short instructionNumber, Chip8& chip8, Instruction instruction) { void InstructionHandler::handleInstruction(unsigned short instructionNumber, Chip8& chip8, Instruction instruction) {
if (!this->handlers.contains(instructionNumber)) { if (!this->handlers.contains(instructionNumber)) {
Logger::error("Unregistered instruction", instructionNumber); Logger::error("Unregistered instruction", instructionNumber);
@ -15,6 +23,17 @@ void InstructionHandler::handleInstruction(unsigned short instructionNumber, Chi
this->handlers[instructionNumber](chip8, instruction); this->handlers[instructionNumber](chip8, instruction);
} }
/**
* Handles the 0x0000 Chip-8 instruction.
* This handles 2 other instructions:
* - 0x00E0 which is used to clear the screen.
* - 0x00EE which is used to return from a subroutine.
*
* In older interpreters, 0x0NNN was also used to jump to a machine code at NNN, but this shouldn't be implemented.
*
* @param chip8 The Chip8 instance
* @param instruction The Instruction instance
*/
void InstructionHandler::handle0000(Chip8& chip8, Instruction instruction) { void InstructionHandler::handle0000(Chip8& chip8, Instruction instruction) {
switch (instruction.kk()) { switch (instruction.kk()) {
case 0x00E0: case 0x00E0:
@ -26,3 +45,14 @@ void InstructionHandler::handle0000(Chip8& chip8, Instruction instruction) {
} }
} }
/**
* Handles the 0x1NNN Chip8 instruction which is used to jump to location NNN.
* This will set the program counter to the NNN value.
*
* @param chip8 The Chip8 instance
* @param instruction The Instruction instance
*/
void InstructionHandler::handle1000(Chip8& chip8, Instruction instruction) {
chip8.setProgramCounter(instruction.nnn());
}