From 7aecc9c5762630d4be9c978309f36cf0063c9729 Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Tue, 14 Jul 2026 19:54:23 +1000 Subject: [PATCH] Initial commit --- emulator.cpp | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ emulator.h | 67 +++++++++++++++++++++++++ instruction.cpp | 5 ++ instruction.h | 32 ++++++++++++ main.cpp | 17 +++++++ meson.build | 8 +++ 6 files changed, 256 insertions(+) create mode 100644 emulator.cpp create mode 100644 emulator.h create mode 100644 instruction.cpp create mode 100644 instruction.h create mode 100644 main.cpp create mode 100644 meson.build diff --git a/emulator.cpp b/emulator.cpp new file mode 100644 index 0000000..b9c0ee9 --- /dev/null +++ b/emulator.cpp @@ -0,0 +1,127 @@ +#include "emulator.h" +#include "instruction.h" +#include +#include +#include +#include + +void Memory::addInstruction(const BytecodeInstruction& inst) { + // make sure we won't write an instruction out of bounds + constexpr size_t maxoffset = (GENERAL_PURPOSE_START - PROGRAM_ROM_START) / sizeof(BytecodeInstruction); + if (instructions >= maxoffset) { + throw std::runtime_error("out of space for program"); + } + + // get offset for next instruction + size_t offset = PROGRAM_ROM_START + (sizeof(Instruction) * instructions++); + uint8_t* memoffset = &this->memory[offset]; + std::memcpy(memoffset, &inst, sizeof(Instruction)); +} + +void Memory::addCall(const uint16_t address) { + // make sure we're not out of call stack space + constexpr size_t maxoffset = (GENERAL_PURPOSE_START - PROGRAM_ROM_START) / sizeof(Instruction); + if (instructions >= maxoffset) { + throw std::runtime_error("out of space for call stack"); + } + + size_t offset = sizeof(uint16_t) * calls++; + uint8_t* memoffset = &this->memory[offset]; + std::memcpy(memoffset, &address, sizeof(uint16_t)); +} + +uint8_t* Memory::operator[](const size_t& offset) { + if (offset >= MEMORY_SIZE) { + throw std::runtime_error("offset out of bounds"); + } + + return &memory[offset]; +} + +uint32_t combineLowAndHighBits(uint16_t low, uint16_t high) { + return (((uint32_t)high << 16) | low); +} + +void Emulator::updateFlags(int32_t opResult) { + if (opResult > std::numeric_limits::max()) { + FLAGS = FLAGS_CARRY; + } else if (opResult == 0) { + FLAGS = FLAGS_ZERO; + } else if (opResult < 0) { + FLAGS = FLAGS_NEGATIVE; + } +} + +Instruction Emulator::fetchNextInst() { + Instruction* insts = reinterpret_cast(memory.memory); \ + Instruction inst = (insts + PROGRAM_ROM_START)[PC++]; + return inst; +} + +void Emulator::start() { + halted = false; + + static const void* dispatchTable[] = { + &&MOV, + &&MVI, + &&ADD, + &&SUB, + &&LOAD, + &&STORE, + &&AND, + &&OR, + &&XOR, + &&SHF, + &&JMP, + &&JIZ, + &&CALL, + &&RET, + &&SYS, + &&HLT + }; + + Instruction inst; + + #define DISPATCH() if (halted) return; \ + inst = fetchNextInst(); \ + goto *dispatchTable[static_cast(inst.opcode)]; + + DISPATCH(); + + MOV: { + registers[inst.arg1] = registers[inst.arg2]; + DISPATCH(); + } + MVI: { + PC++; // the instruction reads the space where the next instruction would be as a number + registers[inst.arg1] = *reinterpret_cast(memory[PC]); + DISPATCH(); + } + ADD: { + int32_t result = registers[inst.arg1] + registers[inst.arg2]; + updateFlags(result); + registers[inst.arg1] = static_cast(result); + + DISPATCH(); + } + SUB: { + int32_t result = registers[inst.arg1] - registers[inst.arg2]; + updateFlags(result); + registers[inst.arg1] = static_cast(result); + + DISPATCH(); + } + LOAD: { + registers[inst.arg1] = *reinterpret_cast(memory[combineLowAndHighBits(inst.arg2, inst.arg3)]); + DISPATCH(); + } + STORE: { + *(reinterpret_cast(memory[combineLowAndHighBits(inst.arg1, inst.arg2)])) = registers[inst.arg3]; + DISPATCH(); + } + AND: { + + } + + +} diff --git a/emulator.h b/emulator.h new file mode 100644 index 0000000..49df9bc --- /dev/null +++ b/emulator.h @@ -0,0 +1,67 @@ +#pragma once +#include +#include +#include +#include "instruction.h" + +#define MEMORY_SIZE 0x00040000 // 256 kibibytes, or 262144 +#define PROGRAM_ROM_START 0x00001000 +#define GENERAL_PURPOSE_START 0x00010000 + +#define PC_REG_OFFSET 7 +#define IHP_REG_OFFSET 8 +#define SP_REG_OFFSET 9 +#define FLAGS_REG_OFFSET 10 + +#define PC registers[PC_REG_OFFSET] +#define IHP registers[PC_REG_OFFSET] +#define SP registers[PC_REG_OFFSET] +#define FLAGS registers[PC_REG_OFFSET] + +#define FLAGS_CARRY (1 << 0) +#define FLAGS_ZERO (1 << 1) +#define FLAGS_NEGATIVE (1 << 2) + +/* + * All-purpose memory used by the emulator + * 4kb for call stack (0x00000000 -> 0x00000FFF) + * 65kb for program ROM (0x00001000 -> 0x0000FFFF) + * Rest for general purpose (0x00010000 -> MEMORY_SIZE) + */ +struct Memory { + size_t instructions = 0; + size_t calls = 0; + + uint8_t memory[MEMORY_SIZE]{}; + + void addInstruction(const Instruction& inst); + void addCall(const uint16_t address); + + uint8_t* operator[](const size_t& offset); + + Memory() = default; + Memory(const std::vector& instructions) { + for (const auto& instruction : instructions) { + addInstruction(instruction.asBytecodeInstruction()); + } + } +}; + +using Program = std::vector; + +class Emulator { + private: + std::array registers = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + Memory memory = {}; + + bool halted = false; + + Instruction fetchNextInst(); + void updateFlags(int32_t opResult); + + public: + void start(); + Emulator() = delete; + Emulator(const Memory& memory) : memory(std::move(memory)) {} +}; diff --git a/instruction.cpp b/instruction.cpp new file mode 100644 index 0000000..c56b7ca --- /dev/null +++ b/instruction.cpp @@ -0,0 +1,5 @@ +#include "instruction.h" + +Instruction::Instruction(BytecodeInstruction bytecodeInst) { + opcode = (InstType)(bytecodeInst & 0b00001111); +} diff --git a/instruction.h b/instruction.h new file mode 100644 index 0000000..6f8710e --- /dev/null +++ b/instruction.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include + +enum class InstType : uint8_t { + MOV, MVI, ADD, SUB, LOAD, STORE, AND, OR, XOR, SHF, JMP, JIZ, CALL, RET, SYS, HLT +}; + +enum class OperandType { + ImmNone, Imm4, Imm8 +}; + +using BytecodeInstruction = uint16_t; + +static std::unordered_map> instructionOperandTypes = { + {InstType::MOV, {OperandType::Imm4, OperandType::Imm4, OperandType::Imm4}} +}; + +struct Instruction { + InstType opcode = InstType::MOV; + uint16_t arg1 = 0; + uint16_t arg2 = 0; + uint16_t arg3 = 0; + + BytecodeInstruction asBytecodeInstruction() const; + + Instruction() = default; + Instruction(InstType opcode, uint16_t arg1, uint16_t arg2, uint16_t arg3) + : opcode(opcode), arg1(arg1), arg2(arg2), arg3(arg3) {} + Instruction(BytecodeInstruction bytecodeInst); +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..36cbb3c --- /dev/null +++ b/main.cpp @@ -0,0 +1,17 @@ +#include "emulator.h" +#include + +// I'm too lazy to write these every time +using in = Instruction; +using it = InstType; + +int main() { + std::cout << "Starting VM..." << std::endl; + + Memory memory({ + in() + }); + + Emulator emulator(memory); + emulator.start(); +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..d508bf5 --- /dev/null +++ b/meson.build @@ -0,0 +1,8 @@ +project('mlcpu', 'cpp', version : '0.1.0') + +sources = files( + 'main.cpp', + 'emulator.cpp' +) + +program = executable('mlcpu', sources)