Initial commit
This commit is contained in:
commit
2e6ebe201a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
cmake-build-debug/
|
||||
.idea/
|
||||
6
CMakeLists.txt
Normal file
6
CMakeLists.txt
Normal file
@ -0,0 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(nes)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
|
||||
add_executable(nes main.cpp src/Rom.cpp src/Rom.h src/Nes.cpp src/Nes.h src/Cpu.cpp src/Cpu.h src/Bus.cpp src/Bus.h)
|
||||
26
main.cpp
Normal file
26
main.cpp
Normal file
@ -0,0 +1,26 @@
|
||||
#include "src/Nes.h"
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
int main() {
|
||||
|
||||
nes::Nes device;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "A2 0A 8E 00 00 A2 03 8E 01 00 AC 00 00 A9 00 18 6D 01 00 88 D0 FA 8D 02 00 EA EA EA";
|
||||
uint16_t nOffset = 0x8000;
|
||||
while (!ss.eof())
|
||||
{
|
||||
std::string b;
|
||||
ss >> b;
|
||||
device.write(nOffset++, (uint8_t)std::stoul(b, nullptr, 16));
|
||||
}
|
||||
|
||||
// Set Reset Vector
|
||||
device.write(0xFFFC, 0x00);
|
||||
device.write(0xFFFD, 0x80);
|
||||
|
||||
device.reset();
|
||||
|
||||
return 0;
|
||||
}
|
||||
24
src/Bus.cpp
Normal file
24
src/Bus.cpp
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 12.08.2023.
|
||||
//
|
||||
|
||||
#include "Bus.h"
|
||||
|
||||
namespace nes {
|
||||
|
||||
Bus::Bus(): _rom{nullptr} {
|
||||
_ram = std::make_unique<uint8_t[]>(64*1024);
|
||||
}
|
||||
|
||||
uint8_t Bus::read(uint16_t address) {
|
||||
return _ram[address];
|
||||
}
|
||||
|
||||
void Bus::write(uint16_t address, uint8_t value) {
|
||||
_ram[address] = value;
|
||||
}
|
||||
|
||||
void Bus::connect(Rom *rom) {
|
||||
_rom = rom;
|
||||
}
|
||||
}
|
||||
29
src/Bus.h
Normal file
29
src/Bus.h
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 12.08.2023.
|
||||
//
|
||||
|
||||
#ifndef NES_BUS_H
|
||||
#define NES_BUS_H
|
||||
|
||||
#include "Rom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace nes {
|
||||
|
||||
class Bus {
|
||||
public:
|
||||
Bus();
|
||||
uint8_t read(uint16_t address);
|
||||
void write(uint16_t address, uint8_t value);
|
||||
void connect(Rom* rom);
|
||||
|
||||
private:
|
||||
std::unique_ptr<uint8_t[]> _ram;
|
||||
Rom* _rom;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif //NES_BUS_H
|
||||
180
src/Cpu.cpp
Normal file
180
src/Cpu.cpp
Normal file
@ -0,0 +1,180 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 11.08.2023.
|
||||
//
|
||||
|
||||
#include "Cpu.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace nes {
|
||||
|
||||
Cpu::Cpu(Bus* bus): _ticks{}, _bus{bus}, A{}, X{}, Y{}, PC{}, SP{}, flags{} {
|
||||
_instructions = std::vector<Instruction>(256);
|
||||
_instructions[0xA2] = {"LDX", &Cpu::LDX, &Cpu::IMM, 2, false};
|
||||
_instructions[0x8E] = {"STX", &Cpu::STX, &Cpu::ABS, 4, false};
|
||||
_instructions[0xAC] = {"LDY", &Cpu::LDY, &Cpu::ABS, 4, false};
|
||||
_instructions[0xA9] = {"LDA", &Cpu::LDA, &Cpu::IMM, 2, false};
|
||||
_instructions[0x18] = {"CLC", &Cpu::CLC, &Cpu::IMP, 2, false};
|
||||
_instructions[0x6D] = {"ADC", &Cpu::ADC, &Cpu::ABS, 4, false};
|
||||
_instructions[0x88] = {"DEY", &Cpu::DEY, &Cpu::IMP, 2, false};
|
||||
_instructions[0xD0] = {"BNE", &Cpu::BNE, &Cpu::REL, 2, false};
|
||||
_instructions[0x8D] = {"STA", &Cpu::STA, &Cpu::ABS, 4, false};
|
||||
_instructions[0xEA] = {"NOP", &Cpu::NOP, &Cpu::IMP, 2, false};
|
||||
}
|
||||
|
||||
void Cpu::reset() {
|
||||
A = 0;
|
||||
X = 0;
|
||||
Y = 0;
|
||||
SP = 0xFF;
|
||||
flags = 0;
|
||||
|
||||
uint16_t lo = _bus->read(0xFFFC);
|
||||
uint16_t hi = _bus->read(0xFFFD);
|
||||
|
||||
PC = (hi << 8) | lo;
|
||||
}
|
||||
|
||||
void Cpu::tick() {
|
||||
if(_ticks == 0) {
|
||||
uint8_t opcode = _bus->read(PC++);
|
||||
auto instruction = _instructions[opcode];
|
||||
|
||||
if(instruction.getAddress == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto args = (this->*instruction.getAddress)();
|
||||
(this->*instruction.process)(args);
|
||||
|
||||
std::cout << instruction.name << ", A: " << (int)A << ", X: " << (int)X << ", Y: " << (int)Y << std::endl;
|
||||
|
||||
_ticks = instruction.cycles;
|
||||
if(instruction.variableCycles) {
|
||||
_ticks += args.cycles;
|
||||
}
|
||||
}
|
||||
|
||||
_ticks--;
|
||||
}
|
||||
|
||||
void Cpu::setFlag(CpuFlags flag, bool value) {
|
||||
if(value) {
|
||||
flags |= flag;
|
||||
} else {
|
||||
flags &= ~flag;
|
||||
}
|
||||
}
|
||||
|
||||
bool Cpu::getFlag(CpuFlags flag) {
|
||||
return flags & flag;
|
||||
}
|
||||
|
||||
// CPU instructions
|
||||
|
||||
void Cpu::LDA(Cpu::InstructionArgs args) {
|
||||
A = _bus->read(args.address);
|
||||
setFlag(Negative, A & 0x80);
|
||||
setFlag(Zero, A == 0);
|
||||
}
|
||||
|
||||
void Cpu::LDX(InstructionArgs args) {
|
||||
X = _bus->read(args.address);
|
||||
setFlag(Negative, X & 0x80);
|
||||
setFlag(Zero, X == 0);
|
||||
}
|
||||
|
||||
void Cpu::LDY(Cpu::InstructionArgs args) {
|
||||
Y = _bus->read(args.address);
|
||||
setFlag(Negative, Y & 0x80);
|
||||
setFlag(Zero, Y == 0);
|
||||
}
|
||||
|
||||
void Cpu::STA(Cpu::InstructionArgs args) {
|
||||
_bus->write(args.address, A);
|
||||
}
|
||||
|
||||
void Cpu::STX(InstructionArgs args) {
|
||||
_bus->write(args.address, X);
|
||||
}
|
||||
|
||||
void Cpu::CLC(Cpu::InstructionArgs args) {
|
||||
setFlag(Carry, false);
|
||||
}
|
||||
|
||||
void Cpu::ADC(Cpu::InstructionArgs args) {
|
||||
uint16_t value = _bus->read(args.address);
|
||||
uint16_t result = A + value + getFlag(Carry);
|
||||
|
||||
setFlag(Carry, result > 255);
|
||||
setFlag(Zero, (result & 0xFF) == 0);
|
||||
setFlag(Negative, result & 0x80);
|
||||
setFlag(Overflow, (~(A^value) & (A^result)) & 0x80);
|
||||
|
||||
A = result & 0xFF;
|
||||
}
|
||||
|
||||
void Cpu::SBC(Cpu::InstructionArgs args) {
|
||||
uint16_t value = _bus->read(args.address) ^ 0xFF;
|
||||
uint16_t result = A + value + getFlag(Carry);
|
||||
|
||||
setFlag(Carry, result > 255);
|
||||
setFlag(Zero, (result & 0xFF) == 0);
|
||||
setFlag(Negative, result & 0x80);
|
||||
setFlag(Overflow, (~(A^value) & (A^result)) & 0x80);
|
||||
|
||||
A = result & 0xFF;
|
||||
}
|
||||
|
||||
void Cpu::DEY(Cpu::InstructionArgs args) {
|
||||
Y -= 1;
|
||||
setFlag(Negative, Y & 0x80);
|
||||
setFlag(Zero, Y == 0);
|
||||
}
|
||||
|
||||
void Cpu::BNE(Cpu::InstructionArgs args) {
|
||||
if(!getFlag(Zero)) {
|
||||
_ticks++;
|
||||
|
||||
uint16_t address = PC + args.address;
|
||||
uint16_t srcPage = PC & 0xFF00;
|
||||
uint16_t dstPage = address & 0xFF00;
|
||||
|
||||
if(srcPage != dstPage) {
|
||||
_ticks++;
|
||||
}
|
||||
|
||||
PC = address;
|
||||
}
|
||||
}
|
||||
|
||||
void Cpu::NOP(Cpu::InstructionArgs args) {
|
||||
}
|
||||
|
||||
// Addressing modes
|
||||
|
||||
Cpu::InstructionArgs Cpu::IMM() {
|
||||
return {PC++, 0};
|
||||
}
|
||||
|
||||
Cpu::InstructionArgs Cpu::ABS() {
|
||||
uint8_t lo = _bus->read(PC++);
|
||||
uint8_t hi = _bus->read(PC++);
|
||||
uint16_t address = (hi << 8) | lo;
|
||||
return {address, 0};
|
||||
}
|
||||
|
||||
Cpu::InstructionArgs Cpu::IMP() {
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
Cpu::InstructionArgs Cpu::REL() {
|
||||
uint16_t address = _bus->read(PC++);
|
||||
|
||||
if (address & 0x80) {
|
||||
address |= 0xFF00;
|
||||
}
|
||||
|
||||
return {address, 0};
|
||||
}
|
||||
|
||||
}
|
||||
85
src/Cpu.h
Normal file
85
src/Cpu.h
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 11.08.2023.
|
||||
//
|
||||
|
||||
#ifndef NES_CPU_H
|
||||
#define NES_CPU_H
|
||||
|
||||
#include "Bus.h"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace nes {
|
||||
|
||||
enum CpuFlags: uint8_t {
|
||||
Carry = (1 << 0),
|
||||
Zero = (1 << 1),
|
||||
InterruptDisable = (1 << 2),
|
||||
DecimalMode = (1 << 3),
|
||||
Break = (1 << 4),
|
||||
Unused = (1 << 5),
|
||||
Overflow = (1 << 6),
|
||||
Negative = (1 << 7)
|
||||
};
|
||||
|
||||
class Cpu {
|
||||
public:
|
||||
struct InstructionArgs {
|
||||
uint16_t address;
|
||||
uint8_t cycles;
|
||||
};
|
||||
|
||||
struct Instruction {
|
||||
char name[4];
|
||||
void(Cpu::*process)(InstructionArgs);
|
||||
InstructionArgs(Cpu::*getAddress)();
|
||||
uint8_t cycles;
|
||||
bool variableCycles;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit Cpu(Bus* bus);
|
||||
void reset();
|
||||
void tick();
|
||||
void setFlag(CpuFlags flag, bool value);
|
||||
bool getFlag(CpuFlags flag);
|
||||
|
||||
private:
|
||||
size_t _ticks;
|
||||
Bus* _bus;
|
||||
std::vector<Instruction> _instructions;
|
||||
|
||||
private:
|
||||
// Registers
|
||||
uint8_t A; // Accumulator
|
||||
uint8_t X; // - Index registers
|
||||
uint8_t Y; // /
|
||||
|
||||
uint16_t PC; // Program Counter
|
||||
uint8_t SP; // Stack Pointer
|
||||
uint8_t flags;
|
||||
|
||||
private:
|
||||
void LDA(InstructionArgs args);
|
||||
void LDX(InstructionArgs args);
|
||||
void LDY(InstructionArgs args);
|
||||
void STA(InstructionArgs args);
|
||||
void STX(InstructionArgs args);
|
||||
void CLC(InstructionArgs args);
|
||||
void ADC(InstructionArgs args);
|
||||
void SBC(InstructionArgs args);
|
||||
void DEY(InstructionArgs args);
|
||||
void BNE(InstructionArgs args);
|
||||
void NOP(InstructionArgs args);
|
||||
|
||||
private:
|
||||
InstructionArgs IMM();
|
||||
InstructionArgs ABS();
|
||||
InstructionArgs IMP();
|
||||
InstructionArgs REL();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif //NES_CPU_H
|
||||
40
src/Nes.cpp
Normal file
40
src/Nes.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 11.08.2023.
|
||||
//
|
||||
|
||||
#include "Nes.h"
|
||||
#include "Rom.h"
|
||||
#include "Cpu.h"
|
||||
|
||||
namespace nes {
|
||||
|
||||
Nes::Nes() {
|
||||
_bus = std::make_unique<Bus>();
|
||||
_cpu = std::make_unique<Cpu>(_bus.get());
|
||||
}
|
||||
|
||||
void Nes::runRom(const fs::path &path) {
|
||||
_rom = std::make_unique<Rom>(path);
|
||||
_bus->connect(_rom.get());
|
||||
reset();
|
||||
}
|
||||
|
||||
uint8_t Nes::read(uint16_t address) {
|
||||
return _bus->read(address);
|
||||
}
|
||||
|
||||
void Nes::write(uint16_t address, uint8_t value) {
|
||||
_bus->write(address, value);
|
||||
}
|
||||
|
||||
void Nes::reset() {
|
||||
_cpu->reset();
|
||||
|
||||
size_t ticks = 5000;
|
||||
while (ticks > 0) {
|
||||
_cpu->tick();
|
||||
ticks--;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
34
src/Nes.h
Normal file
34
src/Nes.h
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 11.08.2023.
|
||||
//
|
||||
|
||||
#ifndef NES_NES_H
|
||||
#define NES_NES_H
|
||||
|
||||
#include "Cpu.h"
|
||||
#include "Rom.h"
|
||||
#include <filesystem>
|
||||
|
||||
namespace nes {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class Nes {
|
||||
public:
|
||||
Nes();
|
||||
void runRom(const fs::path& path);
|
||||
void reset();
|
||||
|
||||
public: // For debug
|
||||
uint8_t read(uint16_t address);
|
||||
void write(uint16_t address, uint8_t value);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Bus> _bus;
|
||||
std::unique_ptr<Cpu> _cpu;
|
||||
std::unique_ptr<Rom> _rom;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif //NES_NES_H
|
||||
31
src/Rom.cpp
Normal file
31
src/Rom.cpp
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 09.08.2023.
|
||||
//
|
||||
|
||||
#include "Rom.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
namespace nes {
|
||||
|
||||
Rom::Rom(const fs::path &path) {
|
||||
auto romSize = fs::file_size(path);
|
||||
_romData = std::make_unique<std::byte[]>(romSize);
|
||||
|
||||
std::ifstream rom(path);
|
||||
rom.read(reinterpret_cast<char*>(_romData.get()), static_cast<std::streamsize>(romSize));
|
||||
|
||||
// UB here
|
||||
// Should be std::start_lifetime_as<RomHeader>(_romData);
|
||||
// when it become available
|
||||
_header = reinterpret_cast<RomHeader*>(_romData.get());
|
||||
|
||||
const size_t prgSize = _header->prgChunks*PRG_CHUNK_SIZE;
|
||||
const size_t chrSize = _header->chrChunks*CHR_CHUNK_SIZE;
|
||||
|
||||
_prgRom = std::span<std::byte>(_romData.get() + sizeof(RomHeader), prgSize);
|
||||
_chrRom = std::span<std::byte>(_romData.get() + sizeof(RomHeader) + prgSize, chrSize);
|
||||
}
|
||||
|
||||
}
|
||||
40
src/Rom.h
Normal file
40
src/Rom.h
Normal file
@ -0,0 +1,40 @@
|
||||
//
|
||||
// Created by Selim Mustafaev on 09.08.2023.
|
||||
//
|
||||
|
||||
#ifndef NES_ROM_H
|
||||
#define NES_ROM_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
namespace nes {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
constexpr uint32_t ROM_MAGIC = 0x4E45531A;
|
||||
constexpr size_t PRG_CHUNK_SIZE = 16*1024;
|
||||
constexpr size_t CHR_CHUNK_SIZE = 8*1024;
|
||||
|
||||
struct RomHeader {
|
||||
uint32_t magic;
|
||||
uint8_t prgChunks;
|
||||
uint8_t chrChunks;
|
||||
uint8_t reserved[10];
|
||||
};
|
||||
|
||||
class Rom {
|
||||
public:
|
||||
explicit Rom(const fs::path& path);
|
||||
|
||||
private:
|
||||
std::unique_ptr<std::byte[]> _romData;
|
||||
RomHeader* _header;
|
||||
std::span<std::byte> _prgRom;
|
||||
std::span<std::byte> _chrRom;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif //NES_ROM_H
|
||||
Loading…
Reference in New Issue
Block a user