├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── include ├── backend.h ├── common.h ├── cpu.h ├── frontend.h ├── instructions.h ├── loader.h ├── memory_subsystem.h └── utils.h ├── program.asm ├── program2.asm ├── src ├── backend.cpp ├── common.cpp ├── cpu.cpp ├── frontend.cpp ├── instructions.cpp ├── loader.cpp ├── main.cpp ├── memory_subsystem.cpp └── utils.cpp └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-debug/ 2 | .idea 3 | main -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | project(cpu_emulator) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | 6 | add_executable(cpu_emulator src/main.cpp 7 | src/instructions.cpp 8 | include/instructions.h 9 | src/utils.cpp 10 | include/utils.h 11 | src/loader.cpp 12 | include/loader.h 13 | src/cpu.cpp 14 | include/cpu.h 15 | src/backend.cpp 16 | include/backend.h 17 | src/frontend.cpp 18 | include/frontend.h 19 | src/memory_subsystem.cpp 20 | include/memory_subsystem.h 21 | src/common.cpp 22 | include/common.h 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Peter Veentjer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # # CPU Emulator 2 | 3 | A C++ program that emulates a bogus CPU instruction set. The goal of this project is to 4 | upgrade my C++ programming skills while working on something I'm very passionate about. 5 | 6 | The idea is to implement a micro-architecture in software with many of the modern CPU 7 | techniques like pipelining, out of order execution, store buffering, speculative execution, 8 | register renaming. The actual instruction set for the time being is a fake one, but perhaps 9 | in the future I'll pick ARM or X86 or something else. 10 | 11 | ## Table of Contents 12 | - [License](#license) 13 | 14 | ## License 15 | 16 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 17 | -------------------------------------------------------------------------------- /include/backend.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | #ifndef CPU_EMULATOR_BACKEND_H 6 | #define CPU_EMULATOR_BACKEND_H 7 | 8 | 9 | #include "memory_subsystem.h" 10 | #include "frontend.h" 11 | 12 | enum ROB_Slot_State 13 | { 14 | ROB_SLOT_FREE, 15 | ROB_SLOT_EXECUTED 16 | }; 17 | 18 | struct RS; 19 | struct RS_Table; 20 | 21 | struct ROB_Slot 22 | { 23 | Instr *instr; 24 | int result; 25 | ROB_Slot_State state; 26 | RS *rs; 27 | }; 28 | 29 | struct ROB 30 | { 31 | uint64_t head, tail, reserved; 32 | uint16_t capacity; 33 | ROB_Slot *slots; 34 | 35 | ROB(uint16_t capacity) : capacity(capacity) 36 | { 37 | head = 0; 38 | tail = 0; 39 | reserved = 0; 40 | slots = new ROB_Slot[capacity]; 41 | for (int k = 0; k < capacity; k++) 42 | { 43 | ROB_Slot &rob_slot = slots[k]; 44 | rob_slot.instr = nullptr; 45 | rob_slot.rs = nullptr; 46 | rob_slot.result = 0; 47 | rob_slot.state = ROB_SLOT_FREE; 48 | } 49 | } 50 | 51 | ~ROB() 52 | { 53 | delete[] slots; 54 | } 55 | 56 | uint16_t empty_slots() 57 | { 58 | return capacity - size(); 59 | } 60 | 61 | uint16_t size() 62 | { 63 | return tail - head; 64 | } 65 | }; 66 | 67 | 68 | struct Backend; 69 | 70 | 71 | struct EU 72 | { 73 | bool busy; 74 | RS *rs; 75 | Backend *backend; 76 | // the input in_operands 77 | Operand in_operands[MAX_INPUT_OPERANDS]; 78 | // the result: probably this should also be an operand. 79 | int result; 80 | 81 | void execute(); 82 | 83 | void cycle(); 84 | }; 85 | 86 | struct EU_Table{ 87 | uint8_t count; 88 | EU *array; 89 | uint8_t *free_stack; 90 | uint8_t free_stack_size; 91 | 92 | EU_Table(uint8_t count); 93 | 94 | ~EU_Table(); 95 | 96 | void cycle(); 97 | 98 | optional allocate(); 99 | 100 | void deallocate(EU eu); 101 | }; 102 | 103 | 104 | struct RAT_Entry 105 | { 106 | uint16_t phys_reg; 107 | // True of this entry is currently in use. 108 | bool valid; 109 | }; 110 | 111 | // the register alias table used for register renaming 112 | struct RAT 113 | { 114 | RAT_Entry *entries; 115 | 116 | RAT(uint16_t arg_reg_cnt); 117 | 118 | ~RAT(); 119 | }; 120 | 121 | 122 | struct Phys_Reg_Struct 123 | { 124 | int value; 125 | bool has_value; 126 | }; 127 | 128 | struct Phys_Reg_File 129 | { 130 | uint16_t count; 131 | Phys_Reg_Struct *array; 132 | uint16_t *free_stack; 133 | uint16_t free_stack_size; 134 | 135 | Phys_Reg_File(uint16_t phys_reg_count); 136 | 137 | ~Phys_Reg_File(); 138 | 139 | // todo: return optional 140 | /** 141 | * Allocates a physical register. 142 | * 143 | * @return the physical register. 144 | */ 145 | uint16_t allocate(); 146 | 147 | /** 148 | * Deallocates a physical register. 149 | * 150 | * @param phys_reg the physical register to deallocate. 151 | */ 152 | void deallocate(uint16_t phys_reg); 153 | }; 154 | 155 | 156 | /** 157 | * The Backend is responsible for the actual execution of the instruction. 158 | * 159 | * It will take instructions from the InstrQueue. 160 | */ 161 | struct Backend 162 | { 163 | RS_Table *rs_table; 164 | Frontend *frontend; 165 | // when true, prints every instruction before being executed. 166 | bool trace; 167 | StoreBuffer *sb; 168 | vector *memory; 169 | InstrQueue *instr_queue; 170 | ROB *rob; 171 | EU_Table *eu_table; 172 | RAT *rat; 173 | Phys_Reg_File *phys_reg_file; 174 | int *arch_regs; 175 | EU eu; 176 | 177 | Backend(CPU_Config config, Frontend *frontend, InstrQueue *instrQueue, vector *memory, StoreBuffer *sb); 178 | 179 | ~Backend(); 180 | 181 | //todo: destructor 182 | 183 | void cycle(); 184 | 185 | bool is_idle(); 186 | 187 | void on_rs_ready(RS *rs); 188 | 189 | void retire(ROB_Slot *rob_slot); 190 | 191 | void cycle_retire(); 192 | 193 | void cycle_dispatch(); 194 | 195 | void cycle_issue(); 196 | 197 | void cdb_broadcast(uint16_t phys_reg, int result); 198 | 199 | }; 200 | 201 | enum RS_State 202 | { 203 | RS_FREE, 204 | // The RS is waiting for 1 or more of its src_phys_registers in_operands. 205 | RS_ISSUED, 206 | // The RS has all in_operands ready, but not yet submitted 207 | RS_READY, 208 | // not used yet; but as soon as we set a queue between the RS and EU, this is needed 209 | RS_SUBMITTED, 210 | // The instruction has executed. 211 | RS_COMPLETED, 212 | }; 213 | 214 | // A reservation station 215 | struct RS 216 | { 217 | 218 | RS_State state; 219 | 220 | uint16_t rs_index; 221 | 222 | Operand input_ops[MAX_INPUT_OPERANDS]; 223 | 224 | // number of required operands. 225 | int input_op_cnt; 226 | 227 | // the number of operands that are ready 228 | int input_opt_ready_cnt; 229 | 230 | Operand output_ops[MAX_OUTPUT_OPERANDS]; 231 | 232 | ROB_Slot *rob_slot; 233 | }; 234 | 235 | struct RS_Table 236 | { 237 | // the array with the reservation stations 238 | RS *array; 239 | // the number of reservation stations 240 | uint16_t count; 241 | 242 | // A stack of free RS. 243 | uint16_t *free_stack; 244 | uint16_t free_stack_size; 245 | 246 | // A circular queue with RS that are ready to be submitted 247 | uint16_t *ready_queue; 248 | uint64_t ready_tail, ready_head; 249 | 250 | RS_Table(uint16_t rs_count); 251 | 252 | ~RS_Table(); 253 | 254 | optional allocate(); 255 | 256 | void deallocate(RS *rs); 257 | 258 | }; 259 | 260 | #endif //CPU_EMULATOR_BACKEND_H 261 | -------------------------------------------------------------------------------- /include/common.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | #ifndef CPU_EMULATOR_COMMON_H 6 | #define CPU_EMULATOR_COMMON_H 7 | 8 | #include 9 | 10 | // currently just 2 stages; fetch + decode 11 | static const int PIPELINE_DEPTH = 10; 12 | struct CPU_Config 13 | { 14 | uint32_t cpu_frequency_Hz = 1; 15 | // the total available memory_addr in 'ints' the CPU can use. 16 | uint32_t memory_size_ints = 16; 17 | // the number of architectural registers 18 | uint16_t arch_reg_cnt = 8; 19 | // the number of physical registers 20 | uint16_t phys_reg_cnt = 64; 21 | // true if every instruction execution should be printed 22 | bool trace = false; 23 | // the capacity of the store buffer 24 | uint16_t sb_capacity = 4; 25 | // the capacity of the instruction queue between frontend and backend 26 | uint8_t instr_queue_capacity = 16; 27 | // the size of the reorder buffer 28 | uint8_t rob_capacity = 16; 29 | // the number of reservation stations 30 | uint16_t rs_count = 16; 31 | // the number of instructions that can be fetched/decoded in a single cycle. 32 | uint8_t frontend_n_wide = 8; 33 | 34 | bool debug = false; 35 | // the number of general purpose execution units 36 | uint8_t eu_count; 37 | }; 38 | #endif //CPU_EMULATOR_COMMON_H 39 | -------------------------------------------------------------------------------- /include/cpu.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 4/30/24. 3 | // 4 | 5 | #ifndef CPU_EMULATOR_CPU_H 6 | #define CPU_EMULATOR_CPU_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "common.h" 15 | #include "instructions.h" 16 | #include "utils.h" 17 | #include "memory_subsystem.h" 18 | #include "backend.h" 19 | #include "frontend.h" 20 | 21 | using namespace std; 22 | 23 | 24 | class CPU; 25 | 26 | 27 | class CPU 28 | { 29 | 30 | private: 31 | 32 | bool is_idle(); 33 | 34 | void cycle(); 35 | 36 | public: 37 | uint64_t cycles = 0; 38 | vector *memory; 39 | InstrQueue *instr_queue; 40 | StoreBuffer *sb; 41 | chrono::milliseconds cycle_period_ms; 42 | Frontend *frontend; 43 | Backend *backend; 44 | bool debug; 45 | 46 | CPU(CPU_Config config) : debug(config.debug) 47 | { 48 | memory = new vector(); 49 | for (int k = 0; k < config.memory_size_ints; k++) 50 | { 51 | memory->push_back(0); 52 | } 53 | sb = new StoreBuffer(config.sb_capacity, memory); 54 | 55 | double pause = 1.0 / config.cpu_frequency_Hz; 56 | cycle_period_ms = chrono::milliseconds(static_cast(pause * 1000)); 57 | 58 | 59 | instr_queue = new InstrQueue(config.instr_queue_capacity); 60 | 61 | frontend = new Frontend(&config, instr_queue); 62 | backend = new Backend(config, frontend, instr_queue, memory, sb); 63 | } 64 | 65 | // todo: destructor 66 | 67 | /** 68 | * Runs the program till completion (including writing the store buffer to memory_addr). 69 | */ 70 | void run(); 71 | 72 | void print_memory() const; 73 | 74 | }; 75 | 76 | #endif //CPU_EMULATOR_CPU_H 77 | -------------------------------------------------------------------------------- /include/frontend.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | #ifndef CPU_EMULATOR_FRONTEND_H 6 | #define CPU_EMULATOR_FRONTEND_H 7 | 8 | 9 | #include 10 | #include "instructions.h" 11 | #include "common.h" 12 | 13 | using namespace std; 14 | 15 | /** 16 | * The Frontend is responsible for fetching and decoding instruction 17 | * and then will place them on the InstrQueue for the backend. 18 | */ 19 | struct Frontend 20 | { 21 | uint8_t n_wide; 22 | vector *code; 23 | bool branch_in_pipeline; 24 | int32_t ip_next_fetch = -1; 25 | // used for inserting bubbles 26 | Instr *nop; 27 | InstrQueue *instr_queue; 28 | 29 | Frontend(CPU_Config *config, InstrQueue *instrQueue) 30 | : n_wide(config->frontend_n_wide) 31 | , instr_queue(instrQueue) 32 | { 33 | code = new vector(); 34 | ip_next_fetch = -1; 35 | branch_in_pipeline = false; 36 | nop = new Instr(); 37 | nop->opcode = OPCODE_NOP; 38 | } 39 | 40 | ~Frontend() 41 | { 42 | delete code; 43 | delete nop; 44 | } 45 | 46 | void cycle(); 47 | 48 | bool is_idle(); 49 | 50 | }; 51 | 52 | 53 | #endif //CPU_EMULATOR_FRONTEND_H 54 | -------------------------------------------------------------------------------- /include/instructions.h: -------------------------------------------------------------------------------- 1 | #ifndef CPU_EMULATOR_INSTRUCTIONS_H 2 | #define CPU_EMULATOR_INSTRUCTIONS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | static const int MAX_INPUT_OPERANDS = 3; 11 | static const int MAX_OUTPUT_OPERANDS = 1; 12 | 13 | enum Opcode 14 | { 15 | OPCODE_ADD, 16 | OPCODE_SUB, 17 | OPCODE_INC, 18 | OPCODE_DEC, 19 | OPCODE_AND, 20 | OPCODE_OR, 21 | OPCODE_XOR, 22 | OPCODE_NOT, 23 | OPCODE_LOAD, 24 | OPCODE_STORE, 25 | OPCODE_PRINTR, 26 | OPCODE_HALT, 27 | OPCODE_CMP, 28 | OPCODE_JNZ, 29 | OPCODE_MOV, 30 | OPCODE_NOP 31 | }; 32 | 33 | bool is_branch(Opcode opcode); 34 | 35 | static const std::unordered_map MNEMONIC_TO_OPCODE = { 36 | {"ADD", OPCODE_ADD}, 37 | {"SUB", OPCODE_SUB}, 38 | {"AND", OPCODE_AND}, 39 | {"OR", OPCODE_OR}, 40 | {"XOR", OPCODE_XOR}, 41 | {"NOT", OPCODE_NOT}, 42 | {"CMP", OPCODE_CMP}, 43 | {"MOV", OPCODE_MOV}, 44 | {"LOAD", OPCODE_LOAD}, 45 | {"STORE", OPCODE_STORE}, 46 | {"PRINTR", OPCODE_PRINTR}, 47 | {"INC", OPCODE_INC}, 48 | {"DEC", OPCODE_DEC}, 49 | {"JNZ", OPCODE_JNZ}, 50 | {"HALT", OPCODE_HALT}, 51 | {"NOP", OPCODE_NOP} 52 | }; 53 | 54 | enum OperandType { 55 | REGISTER, 56 | MEMORY, 57 | CODE, 58 | CONSTANT, 59 | }; 60 | 61 | struct Operand { 62 | OperandType type; 63 | union { 64 | uint16_t reg; 65 | uint64_t memory_addr; 66 | int constant; 67 | uint64_t code_addr; 68 | }; 69 | }; 70 | 71 | 72 | struct Instr 73 | { 74 | Opcode opcode; 75 | 76 | uint16_t input_ops_cnt; 77 | Operand input_ops[MAX_INPUT_OPERANDS]; 78 | 79 | uint16_t output_ops_cnt; 80 | Operand output_ops[MAX_OUTPUT_OPERANDS]; 81 | }; 82 | 83 | void print_instr(Instr *instr); 84 | 85 | 86 | /** 87 | * The InstrQueue sits between frontend and backend. 88 | */ 89 | struct InstrQueue 90 | { 91 | Instr **entries; 92 | uint16_t capacity; 93 | uint64_t head = 0; 94 | uint64_t tail = 0; 95 | 96 | InstrQueue(uint16_t capacity); 97 | 98 | ~InstrQueue(); 99 | 100 | [[nodiscard]] bool is_empty() const; 101 | 102 | [[nodiscard]] uint16_t size() const; 103 | 104 | [[nodiscard]] bool is_full() const; 105 | 106 | Instr *dequeue(); 107 | 108 | void enqueue(Instr *instr); 109 | }; 110 | 111 | 112 | #endif //CPU_EMULATOR_INSTRUCTIONS_H 113 | -------------------------------------------------------------------------------- /include/loader.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 4/30/24. 3 | // 4 | 5 | #include 6 | #include "cpu.h" 7 | 8 | #ifndef CPU_EMULATOR_LOADER_H 9 | #define CPU_EMULATOR_LOADER_H 10 | 11 | #endif //CPU_EMULATOR_LOADER_H 12 | 13 | using namespace std; 14 | 15 | void load_program(CPU* cpu, string path); -------------------------------------------------------------------------------- /include/memory_subsystem.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | #ifndef CPU_EMULATOR_MEMORY_SUBSYSTEM_H 6 | #define CPU_EMULATOR_MEMORY_SUBSYSTEM_H 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace std; 13 | 14 | struct StoreBufferEntry 15 | { 16 | int value; 17 | int addr; 18 | }; 19 | 20 | struct StoreBuffer 21 | { 22 | StoreBufferEntry *entries; 23 | uint16_t capacity; 24 | uint64_t head; 25 | uint64_t tail; 26 | vector *memory; 27 | 28 | StoreBuffer(uint16_t capacity, vector *memory); 29 | 30 | ~StoreBuffer(); 31 | 32 | optional lookup(int addr); 33 | 34 | bool is_empty(); 35 | 36 | void write(int addr, int value); 37 | 38 | void cycle(); 39 | }; 40 | 41 | 42 | #endif //CPU_EMULATOR_MEMORY_SUBSYSTEM_H 43 | -------------------------------------------------------------------------------- /include/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef CPU_EMULATOR_UTILS_H 2 | #define CPU_EMULATOR_UTILS_H 3 | 4 | template 5 | std::optional mapGet(const std::unordered_map& map, const Key& key) { 6 | auto it = map.find(key); 7 | if (it != map.end()) { 8 | return it->second; 9 | } else { 10 | return std::nullopt; 11 | } 12 | } 13 | 14 | std::string trim(const std::string &str) ; 15 | 16 | bool endsWith(const std::string &fullString, const std::string &ending) ; 17 | 18 | std::string removeLast(const std::string& s) ; 19 | 20 | #endif //CPU_EMULATOR_UTILS_H 21 | -------------------------------------------------------------------------------- /program.asm: -------------------------------------------------------------------------------- 1 | ; variables 2 | VAR A 10 3 | VAR B 100 4 | VAR C 100 5 | VAR D 0 6 | 7 | ; program 8 | LOAD 0 A 9 | again: 10 | PRINTR 0 11 | DEC 0 12 | JNZ 0 again 13 | LOAD 0 B 14 | LOAD 1 C 15 | ADD 2 0 1 16 | STORE 2 D 17 | HALT -------------------------------------------------------------------------------- /program2.asm: -------------------------------------------------------------------------------- 1 | ; variables 2 | VAR A 1 3 | VAR B 2 4 | VAR C 3 5 | VAR D 4 6 | 7 | ; program 8 | LOAD 0 A 9 | LOAD 1 B 10 | LOAD 2 C 11 | ;;LOAD 3 D 12 | 13 | ADD 3 0 1 14 | ADD 4 3 2 15 | PRINTR 4 16 | -------------------------------------------------------------------------------- /src/backend.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | #include "../include/backend.h" 6 | 7 | Backend::Backend(CPU_Config config, 8 | Frontend *frontend, 9 | InstrQueue *instrQueue, 10 | vector *memory, 11 | StoreBuffer *sb) 12 | : frontend(frontend), 13 | instr_queue(instrQueue), 14 | memory(memory), 15 | sb(sb) 16 | { 17 | arch_regs = new int[config.arch_reg_cnt]; 18 | for (int k = 0; k < config.arch_reg_cnt; k++) 19 | { 20 | arch_regs[k] = 0; 21 | } 22 | 23 | eu.backend = this; 24 | 25 | phys_reg_file = new Phys_Reg_File(config.phys_reg_cnt); 26 | trace = config.trace; 27 | rob = new ROB(config.rob_capacity); 28 | rat = new RAT(config.arch_reg_cnt); 29 | eu_table = new EU_Table(config.eu_count); 30 | rs_table = new RS_Table(config.rs_count); 31 | } 32 | 33 | 34 | Backend::~Backend() 35 | { 36 | delete[] arch_regs; 37 | delete phys_reg_file; 38 | delete rob; 39 | delete rat; 40 | delete eu_table; 41 | delete rs_table; 42 | } 43 | 44 | void Backend::cycle() 45 | { 46 | cycle_retire(); 47 | 48 | eu_table->cycle(); 49 | 50 | // send for execution units 51 | cycle_dispatch(); 52 | 53 | // select reservation stations 54 | cycle_issue(); 55 | 56 | // place instructions from the instruction queue into the rob. 57 | int cnt = std::min(rob->empty_slots(), instr_queue->size()); 58 | for (int k = 0; k < cnt; k++) 59 | { 60 | Instr *instr = instr_queue->dequeue(); 61 | //print_instr(instr); 62 | uint64_t i = rob->tail % rob->capacity; 63 | ROB_Slot *slot = &rob->slots[i]; 64 | slot->instr = instr; 65 | slot->state = ROB_SLOT_FREE; 66 | rob->tail++; 67 | 68 | //printf("Inserting into ROB "); 69 | //print_instr(slot->instr); 70 | } 71 | } 72 | 73 | // Add as many instructions to RS's 74 | void Backend::cycle_issue() 75 | { 76 | int unreserved_cnt = rob->tail - rob->reserved; 77 | for (int k = 0; k < unreserved_cnt; k++) 78 | { 79 | optional allocation = rs_table->allocate(); 80 | if (!allocation.has_value()) 81 | { 82 | // There are no free reservation stations, so we are done 83 | break; 84 | } 85 | 86 | RS *rs = allocation.value(); 87 | 88 | ROB_Slot *rob_slot = &rob->slots[rob->reserved % rob->capacity]; 89 | rob_slot->rs = rs; 90 | 91 | rob->reserved++; 92 | Instr *instr = rob_slot->instr; 93 | rs->rob_slot = rob_slot; 94 | 95 | 96 | printf("Issue "); 97 | print_instr(instr); 98 | 99 | // prepare the input operands. 100 | rs->input_op_cnt = instr->input_ops_cnt; 101 | rs->input_opt_ready_cnt = 0; 102 | for (int l = 0; l < instr->input_ops_cnt; l++) 103 | { 104 | Operand *input_op_instr = &instr->input_ops[l]; 105 | Operand *input_op_rs = &rs->input_ops[l]; 106 | switch (input_op_instr->type) 107 | { 108 | case OperandType::REGISTER: 109 | { 110 | // The rat_entry will determine if there is a physical register we should use or 111 | // if the architectural register should be used. 112 | uint16_t arch_reg = input_op_instr->reg; 113 | RAT_Entry *rat_entry = &rat->entries[arch_reg]; 114 | 115 | if (rat_entry->valid) 116 | { 117 | printf("RAT Valid\n"); 118 | // we need to use the physical register for the value 119 | Phys_Reg_Struct *phys_reg = &phys_reg_file->array[rat_entry->phys_reg]; 120 | if (phys_reg->has_value) 121 | { 122 | printf("Phys reg is valid\n"); 123 | // the physical register has the value, so use that 124 | input_op_rs->type = OperandType::CONSTANT; 125 | input_op_rs->constant = phys_reg->value; 126 | rs->input_opt_ready_cnt++; 127 | } 128 | else 129 | { 130 | printf("Phys reg is not valid\n"); 131 | 132 | // the physical register doesn't have the value. 133 | // the broadcast on the cdb will take care of setting the value 134 | input_op_rs->reg = rat_entry->phys_reg; 135 | input_op_rs->type = REGISTER; 136 | } 137 | } 138 | else 139 | { 140 | printf("Read from arch reg\n"); 141 | 142 | 143 | // there is no physical register, so we use the value in the architectural register 144 | input_op_rs->type = OperandType::CONSTANT; 145 | input_op_rs->constant = arch_regs[arch_reg]; 146 | rs->input_opt_ready_cnt++; 147 | } 148 | 149 | break; 150 | } 151 | case OperandType::CODE: 152 | input_op_rs->type = CONSTANT; 153 | input_op_rs->code_addr = input_op_instr->code_addr; 154 | rs->input_opt_ready_cnt++; 155 | break; 156 | case OperandType::CONSTANT: 157 | input_op_rs->type = CONSTANT; 158 | input_op_rs->constant = input_op_instr->constant; 159 | rs->input_opt_ready_cnt++; 160 | break; 161 | case OperandType::MEMORY: 162 | input_op_rs->type = MEMORY; 163 | input_op_rs->memory_addr = input_op_instr->memory_addr; 164 | rs->input_opt_ready_cnt++; 165 | break; 166 | default: 167 | throw std::runtime_error("Unhandled operand type while renaming"); 168 | } 169 | } 170 | 171 | // prepare the output operands 172 | for (int l = 0; l < instr->output_ops_cnt; l++) 173 | { 174 | Operand *output_op_instr = &instr->output_ops[l]; 175 | Operand *output_op_rs = &rs->output_ops[l]; 176 | output_op_rs->type = output_op_instr->type; 177 | switch (output_op_instr->type) 178 | { 179 | case OperandType::REGISTER: 180 | { 181 | uint16_t phys_reg = phys_reg_file->allocate(); 182 | uint16_t arch_reg = output_op_instr->reg; 183 | 184 | RAT_Entry &rat_entry = rat->entries[arch_reg]; 185 | rat_entry.phys_reg = phys_reg; 186 | rat_entry.valid = true; 187 | 188 | output_op_rs->reg = phys_reg; 189 | printf("Register rename from %d to %d\n", arch_reg, output_op_rs->reg); 190 | break; 191 | } 192 | // case OperandType::MEMORY: 193 | // output_op_rs->memory_addr = output_op_instr->memory_addr; 194 | // rs->input_opt_ready_cnt++; 195 | // break; 196 | default: 197 | throw std::runtime_error("Unhandled operand type while renaming"); 198 | } 199 | } 200 | 201 | 202 | if (rs->input_op_cnt == rs->input_opt_ready_cnt) 203 | { 204 | printf("Issue READY "); 205 | print_instr(rs->rob_slot->instr); 206 | 207 | 208 | rs->state = RS_READY; 209 | on_rs_ready(rs); 210 | } 211 | else 212 | { 213 | printf("Issue ISSUED "); 214 | print_instr(rs->rob_slot->instr); 215 | rs->state = RS_ISSUED; 216 | } 217 | 218 | //print_instr(rob_slot->instr); 219 | } 220 | } 221 | 222 | // The dispatch: so sending ready reservation stations to execution units. 223 | void Backend::cycle_dispatch() 224 | {// issue any rs that has all in_operands ready 225 | for (uint64_t k = rs_table->ready_head; k < rs_table->ready_tail; k++) 226 | { 227 | uint16_t rs_index = rs_table->ready_queue[k % rs_table->count]; 228 | 229 | RS *rs = &rs_table->array[rs_index]; 230 | if (trace) 231 | { 232 | printf("Dispatch (execute) "); 233 | print_instr(rs->rob_slot->instr); 234 | } 235 | 236 | eu.rs = rs; 237 | 238 | // prepare the in_operands 239 | Instr *instr = rs->rob_slot->instr; 240 | for (int op_index = 0; op_index < instr->input_ops_cnt; op_index++) 241 | { 242 | Operand *operand = &rs->input_ops[op_index]; 243 | eu.in_operands[op_index].type = operand->type; 244 | 245 | switch (operand->type) 246 | { 247 | case CONSTANT: 248 | eu.in_operands[op_index].constant = operand->constant; 249 | break; 250 | case REGISTER: 251 | // we loop up the physical register for the architectural register 252 | // and then load the value 253 | //eu.in_operands[op_index].constant = 254 | break; 255 | case MEMORY: 256 | eu.in_operands[op_index].memory_addr = operand->memory_addr; 257 | break; 258 | case CODE: 259 | eu.in_operands[op_index].code_addr = operand->code_addr; 260 | break; 261 | default: 262 | throw runtime_error("Backend::cycle: Unknown operand type"); 263 | } 264 | } 265 | 266 | eu.execute(); 267 | 268 | int result = eu.result; 269 | 270 | rs->rob_slot->result = result; 271 | rs->rob_slot->state = ROB_SLOT_EXECUTED; 272 | 273 | // todo: should become pending once the instruction is queued for an EU 274 | rs->state = RS_COMPLETED; 275 | 276 | for (int out_op_index = 0; out_op_index < instr->output_ops_cnt; out_op_index++) 277 | { 278 | Operand *out_op = &rs->output_ops[out_op_index]; 279 | if (out_op->type == OperandType::REGISTER) 280 | { 281 | // update the physical register. 282 | Phys_Reg_Struct *phys_reg = &phys_reg_file->array[out_op->reg]; 283 | phys_reg->has_value = true; 284 | phys_reg->value = result; 285 | 286 | // Broadcast the value to any RS that needs it. 287 | cdb_broadcast(out_op->reg, result); 288 | } 289 | } 290 | 291 | // should the phys register be invalidated here? 292 | } 293 | rs_table->ready_head = rs_table->ready_tail; 294 | } 295 | 296 | void Backend::cdb_broadcast(uint16_t phys_reg, int result) 297 | {// broadcast the value. 298 | // Iterate over all RS that are in RS_ISSUED (so waiting) 299 | 300 | for (int k = 0; k < rs_table->count; k++) 301 | { 302 | RS *rs = &rs_table->array[k]; 303 | 304 | if (rs->state != RS_ISSUED) 305 | { 306 | continue; 307 | } 308 | 309 | // iterate over all input operands of the rs 310 | for (int l = 0; l < rs->rob_slot->instr->input_ops_cnt; l++) 311 | { 312 | 313 | Operand *target_rs_in_op = &rs->input_ops[l]; 314 | if (target_rs_in_op->type != REGISTER || target_rs_in_op->reg != phys_reg) 315 | { 316 | continue; 317 | } 318 | 319 | // Directly update the value 320 | target_rs_in_op->type = CONSTANT; 321 | target_rs_in_op->constant = result; 322 | rs->input_opt_ready_cnt++; 323 | 324 | if (rs->input_op_cnt == rs->input_opt_ready_cnt) 325 | { 326 | rs->state = RS_READY; 327 | on_rs_ready(rs); 328 | } 329 | } 330 | } 331 | } 332 | 333 | void Backend::cycle_retire() 334 | {// retire any instruction that has been executed and hasn't been retired yet. 335 | // instructions can execute out of order, but will retire in order. 336 | for (uint64_t k = rob->head; k < rob->tail; k++) 337 | { 338 | ROB_Slot *slot = &rob->slots[k % rob->capacity]; 339 | if (slot->state == ROB_SLOT_EXECUTED) 340 | { 341 | printf("Retiring "); 342 | print_instr(slot->instr); 343 | 344 | // todo: retire 345 | retire(slot); 346 | rob->head++; 347 | } 348 | else 349 | { 350 | // As soon as we find an instruction that has not been executed, we stop 351 | break; 352 | } 353 | } 354 | } 355 | 356 | bool Backend::is_idle() 357 | { 358 | return rob->size() == 0; 359 | } 360 | 361 | void Backend::on_rs_ready(RS *rs) 362 | { 363 | uint64_t index = rs_table->ready_tail % rs_table->count; 364 | rs_table->ready_queue[index] = rs->rs_index; 365 | rs_table->ready_tail++; 366 | } 367 | 368 | void Backend::retire(ROB_Slot *rob_slot) 369 | { 370 | Instr *instr = rob_slot->instr; 371 | 372 | for (int out_op_index = 0; out_op_index < instr->output_ops_cnt; out_op_index++) 373 | { 374 | Operand *out_op = &rob_slot->rs->output_ops[out_op_index]; 375 | if (out_op->type == OperandType::REGISTER) 376 | { 377 | // update the architectural register 378 | uint16_t arch_reg = instr->output_ops[out_op_index].reg; 379 | 380 | // write the value to the architectural register 381 | arch_regs[arch_reg] = rob_slot->result; 382 | 383 | // deallocate the physical register 384 | phys_reg_file->deallocate(out_op->reg); 385 | 386 | // mark the rat entry as invalid 387 | rat->entries[arch_reg].valid = false; 388 | } 389 | } 390 | 391 | 392 | // hack to deal with updating the front-end 393 | if (instr->opcode == OPCODE_HALT) 394 | { 395 | frontend->ip_next_fetch = -1; 396 | } 397 | else if (instr->opcode == OPCODE_JNZ) 398 | { 399 | int v1 = arch_regs[instr->input_ops[0].reg]; 400 | if (v1 != 0) 401 | { 402 | printf("Take the branch\n"); 403 | frontend->ip_next_fetch = instr->input_ops[1].code_addr; 404 | frontend->branch_in_pipeline = false; 405 | } 406 | } 407 | 408 | // case OPCODE_STORE: 409 | // // write the result to memory 410 | // sb->write(instr->output_ops[0].memory_addr, rob_slot->result); 411 | // break; 412 | // case OPCODE_PRINTR: 413 | // break; 414 | 415 | // case OPCODE_JNZ: 416 | // { 417 | // int v1 = arch_regs->at(instr->code.JNZ.r_src); 418 | // if (v1 != 0) 419 | // { 420 | // cpu->frontend.ip_next_fetch = instr->code.JNZ.c_target; 421 | // } 422 | // break; 423 | // } 424 | 425 | RS *rs = rob_slot->rs; 426 | rob_slot->rs = nullptr; 427 | 428 | rs_table->deallocate(rs); 429 | } 430 | 431 | 432 | void EU::execute() 433 | { 434 | // todo: the result needs to be stored in the RS 435 | ROB_Slot *rob_slot = rs->rob_slot; 436 | Instr *instr = rob_slot->instr; 437 | switch (instr->opcode) 438 | { 439 | case OPCODE_ADD: 440 | result = in_operands[0].constant + in_operands[1].constant; 441 | break; 442 | case OPCODE_SUB: 443 | result = in_operands[0].constant - in_operands[1].constant; 444 | break; 445 | case OPCODE_AND: 446 | result = in_operands[0].constant & in_operands[1].constant; 447 | break; 448 | case OPCODE_OR: 449 | result = in_operands[0].constant | in_operands[1].constant; 450 | break; 451 | case OPCODE_XOR: 452 | result = in_operands[0].constant ^ in_operands[1].constant; 453 | break; 454 | case OPCODE_NOT: 455 | result = !in_operands[0].constant; 456 | break; 457 | // case OPCODE_CMP: 458 | // { 459 | // int res = src[0] == src[1]; 460 | // //array[rs_target].srcReady(rs_src_index, res); 461 | // break; 462 | // } 463 | case OPCODE_INC: 464 | result = in_operands[0].constant + 1; 465 | break; 466 | case OPCODE_DEC: 467 | result = in_operands[0].constant - 1; 468 | break; 469 | 470 | // case OPCODE_MOV: 471 | // { 472 | // arch_regs->at(instr->code.MOV.r_dst) = arch_regs->at(instr->code.MOV.r_src); 473 | // break; 474 | // } 475 | case OPCODE_LOAD: 476 | { 477 | // a primitive version of store to load forwarding. Because of the store buffer 478 | // we first need to look there before returning the value otherwise the CPU would 479 | // not be able to see some of its own writes and become incoherent. 480 | 481 | // todo: Load to store forwarding 482 | result = backend->memory->at(in_operands[0].memory_addr); 483 | // sb->lookup(instr->code.LOAD.m_src) 484 | // .value_or(memory_addr->at(instr->code.LOAD.m_src)); 485 | 486 | //arch_regs->at(instr->code.LOAD.r_dst) = value; 487 | break; 488 | } 489 | // case OPCODE_STORE: 490 | // { 491 | // sb->write(instr->code.STORE.m_dst, arch_regs->at(instr->code.STORE.r_src)); 492 | // break; 493 | // } 494 | case OPCODE_PRINTR: 495 | { 496 | Operand &operand = in_operands[0]; 497 | if (operand.type != CONSTANT) 498 | { 499 | printf("wtf\n"); 500 | } 501 | int i = operand.constant; 502 | printf(" R%d=%d\n", instr->input_ops[0].reg, i); 503 | break; 504 | } 505 | case OPCODE_JNZ: 506 | { 507 | // int v1 = arch_regs->at(instr->code.JNZ.r_src); 508 | // if (v1 != 0) 509 | // { 510 | // backend->frontend->ip_next_fetch = instr->code.JNZ.c_target; 511 | // } 512 | break; 513 | } 514 | case OPCODE_HALT: 515 | break; 516 | case OPCODE_NOP: 517 | break; 518 | default: 519 | throw runtime_error("Execute:Unrecognized opcode"); 520 | } 521 | 522 | 523 | rob_slot->state = ROB_Slot_State::ROB_SLOT_EXECUTED; 524 | } 525 | 526 | void EU::cycle() 527 | { 528 | 529 | } 530 | 531 | EU_Table::EU_Table(uint8_t count) : count(count) 532 | { 533 | array = new EU[count]; 534 | } 535 | 536 | EU_Table::~EU_Table() 537 | { 538 | delete[] array; 539 | delete[] free_stack; 540 | } 541 | 542 | void EU_Table::cycle() 543 | { 544 | for (uint8_t k = 0; k < count; k++) 545 | { 546 | EU eu = array[k]; 547 | if (eu.busy) 548 | { 549 | eu.cycle(); 550 | } 551 | } 552 | } 553 | 554 | optional EU_Table::allocate() 555 | { 556 | if (free_stack_size == 0) 557 | { 558 | return std::nullopt; 559 | } 560 | 561 | // get a free physical register. 562 | free_stack_size--; 563 | uint8_t free = free_stack[free_stack_size]; 564 | return array[free]; 565 | } 566 | 567 | void EU_Table::deallocate(EU eu) 568 | { 569 | 570 | } 571 | 572 | Phys_Reg_File::Phys_Reg_File(uint16_t phys_reg_count) 573 | { 574 | count = phys_reg_count; 575 | free_stack = new uint16_t[phys_reg_count]; 576 | for (uint16_t k = 0; k < phys_reg_count; k++) 577 | { 578 | free_stack[phys_reg_count - 1 - k] = k; 579 | } 580 | free_stack_size = phys_reg_count; 581 | array = new Phys_Reg_Struct[phys_reg_count]; 582 | for (uint16_t k = 0; k < phys_reg_count; k++) 583 | { 584 | Phys_Reg_Struct &slot = array[k]; 585 | slot.value = 0; 586 | slot.has_value = false; 587 | } 588 | } 589 | 590 | Phys_Reg_File::~Phys_Reg_File() 591 | { 592 | delete[] array; 593 | delete[] free_stack; 594 | } 595 | 596 | uint16_t Phys_Reg_File::allocate() 597 | { 598 | if (free_stack_size == 0) 599 | { 600 | throw std::runtime_error("Phys_Reg_File.allocate: There are no free physical registers."); 601 | } 602 | 603 | // get a free physical register. 604 | free_stack_size--; 605 | return free_stack[free_stack_size]; 606 | } 607 | 608 | void Phys_Reg_File::deallocate(uint16_t phys_reg) 609 | { 610 | if (free_stack_size == count) 611 | { 612 | throw std::runtime_error("Phys_Reg_File:Free: Too many deallocates."); 613 | } 614 | 615 | // invalidate the physical register 616 | Phys_Reg_Struct &slot = array[phys_reg]; 617 | slot.has_value = false; 618 | 619 | // return the physical register to the free stack 620 | free_stack[free_stack_size] = phys_reg; 621 | free_stack_size++; 622 | } 623 | 624 | RS_Table::RS_Table(uint16_t rs_count) : count(rs_count) 625 | { 626 | array = new RS[rs_count]; 627 | for (uint16_t k = 0; k < rs_count; k++) 628 | { 629 | RS &rs = array[k]; 630 | rs.rs_index = k; 631 | rs.state = RS_FREE; 632 | } 633 | free_stack_size = rs_count; 634 | free_stack = new uint16_t[rs_count]; 635 | for (uint16_t k = 0; k < rs_count; k++) 636 | { 637 | free_stack[k] = k; 638 | } 639 | 640 | ready_head = 0; 641 | ready_tail = 0; 642 | ready_queue = new uint16_t[rs_count]; 643 | } 644 | 645 | RS_Table::~RS_Table() 646 | { 647 | delete[] free_stack; 648 | delete[] array; 649 | delete[] ready_queue; 650 | } 651 | 652 | optional RS_Table::allocate() 653 | { 654 | if (free_stack_size == 0) 655 | { 656 | // There are no free reservation stations, so we are done 657 | return nullopt; 658 | } 659 | 660 | // get a free RS 661 | free_stack_size--; 662 | return &array[free_stack[free_stack_size]]; 663 | } 664 | 665 | void RS_Table::deallocate(RS *rs) 666 | { 667 | if (free_stack_size == count) 668 | { 669 | throw std::runtime_error("RS_Table: too many frees"); 670 | } 671 | 672 | rs->state = RS_FREE; 673 | rs->rob_slot = nullptr; 674 | rs->input_opt_ready_cnt = 0; 675 | free_stack[free_stack_size] = rs->rs_index; 676 | free_stack_size++; 677 | } 678 | 679 | RAT::RAT(uint16_t arg_reg_cnt) 680 | { 681 | entries = new RAT_Entry[arg_reg_cnt]; 682 | for (uint16_t k = 0; k < arg_reg_cnt; k++) 683 | { 684 | entries[k].phys_reg = k; 685 | entries[k].valid = false; 686 | } 687 | } 688 | 689 | RAT::~RAT() 690 | { 691 | delete[] entries; 692 | } 693 | 694 | -------------------------------------------------------------------------------- /src/common.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | #include "../include/common.h" 6 | -------------------------------------------------------------------------------- /src/cpu.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 4/30/24. 3 | // 4 | 5 | #include "../include/cpu.h" 6 | 7 | bool CPU::is_idle() 8 | { 9 | return frontend->is_idle() 10 | && instr_queue->is_empty() 11 | //&& backend.is_empty() 12 | && sb->is_empty(); 13 | } 14 | 15 | void CPU::cycle() 16 | { 17 | // todo: reverse order 18 | 19 | frontend->cycle(); 20 | 21 | backend->cycle(); 22 | 23 | sb->cycle(); 24 | 25 | cycles++; 26 | } 27 | 28 | void CPU::run() 29 | { 30 | while (!is_idle()) 31 | { 32 | this_thread::sleep_for(cycle_period_ms); 33 | 34 | if (debug) 35 | { 36 | printf("---------------------------------\n"); 37 | printf("cycle: %lu\n", cycles); 38 | printf("---------------------------------\n"); 39 | } 40 | 41 | cycle(); 42 | } 43 | } 44 | 45 | void CPU::print_memory() const 46 | { 47 | printf("------------------Memory----------------\n"); 48 | for (int k = 0; k < memory->size(); k++) 49 | { 50 | printf("%04d %04d\n", k, memory->at(k)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/frontend.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | #include "../include/frontend.h" 6 | #include "../include/common.h" 7 | 8 | 9 | void Frontend::cycle() 10 | { 11 | // under ideal conditions, the frontend can fetch/decode n instructions. 12 | for (uint8_t k = 0; k < n_wide; k++) 13 | { 14 | if (ip_next_fetch == -1 15 | || ip_next_fetch >= code->size() 16 | || instr_queue->is_full() 17 | || branch_in_pipeline) 18 | { 19 | return; 20 | } 21 | 22 | //printf("ip_next_fetch %d\n", ip_next_fetch); 23 | Instr *instr = &code->at(ip_next_fetch); 24 | 25 | if (is_branch(instr->opcode)) 26 | { 27 | branch_in_pipeline = true; 28 | } 29 | else 30 | { 31 | ip_next_fetch++; 32 | } 33 | instr_queue->enqueue(instr); 34 | } 35 | } 36 | 37 | bool Frontend::is_idle() 38 | { 39 | return ip_next_fetch == -1; 40 | } 41 | -------------------------------------------------------------------------------- /src/instructions.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 4/28/24. 3 | // 4 | 5 | #include "../include/instructions.h" 6 | 7 | 8 | void print_instr(Instr *instr) 9 | { 10 | switch (instr->opcode) 11 | { 12 | case OPCODE_ADD: 13 | printf("ADD R%d,R%d,R%d\n", instr->output_ops[0].reg, instr->input_ops[0].reg, 14 | instr->input_ops[1].reg); 15 | break; 16 | case OPCODE_SUB: 17 | printf("SUB R%d,R%d,R%d\n", instr->output_ops[0].reg, instr->input_ops[0].reg, 18 | instr->input_ops[1].reg); 19 | break; 20 | case OPCODE_AND: 21 | printf("AND R%d,R%d,R%d\n", instr->output_ops[0].reg, instr->input_ops[0].reg, 22 | instr->input_ops[1].reg); 23 | break; 24 | case OPCODE_OR: 25 | printf("OR R%d,R%d,R%d\n", instr->output_ops[0].reg, instr->input_ops[0].reg, 26 | instr->input_ops[1].reg); 27 | break; 28 | case OPCODE_XOR: 29 | printf("XOR R%d,R%d,R%d\n", instr->output_ops[0].reg, instr->input_ops[0].reg, 30 | instr->input_ops[1].reg); 31 | break; 32 | case OPCODE_NOT: 33 | printf("NOT R%d,R%d\n", instr->output_ops[0].reg, instr->input_ops[0].reg); 34 | break; 35 | // case OPCODE_CMP: 36 | // printf("CMP R%d R%d %d \n", instr->code.CMP.r_dst, instr->code.CMP.r_src1, instr->code.CMP.r_src2); 37 | // break; 38 | case OPCODE_INC: 39 | printf("INC R%d\n", instr->output_ops[0].reg); 40 | break; 41 | case OPCODE_DEC: 42 | printf("DEC R%d\n", instr->output_ops[0].reg); 43 | break; 44 | case OPCODE_MOV: 45 | printf("MOV R%d R%d\n", instr->input_ops[0].reg, instr->output_ops[0].reg); 46 | break; 47 | case OPCODE_LOAD: 48 | printf("LOAD R%d,[%lu]\n", instr->output_ops[0].reg, instr->input_ops[0].memory_addr); 49 | break; 50 | case OPCODE_STORE: 51 | printf("STORE R%d,[%lu]\n", instr->input_ops[0].reg, instr->output_ops[0].memory_addr); 52 | break; 53 | case OPCODE_PRINTR: 54 | printf("PRINTR R%d\n", instr->input_ops[0].reg); 55 | break; 56 | case OPCODE_JNZ: 57 | printf("JNZ R%d code[%lu]\n", instr->input_ops[0].reg, instr->input_ops[1].code_addr); 58 | break; 59 | case OPCODE_HALT: 60 | printf("HALT\n"); 61 | break; 62 | case OPCODE_NOP: 63 | printf("NOP\n"); 64 | break; 65 | default: 66 | throw std::runtime_error("print_instr:Unrecognized opcode"); 67 | } 68 | } 69 | 70 | bool is_branch(Opcode opcode) 71 | { 72 | switch (opcode) 73 | { 74 | case OPCODE_JNZ: 75 | return true; 76 | case OPCODE_HALT: 77 | // for the time being it is a branch instruction to fill the pipeline with nops. 78 | return true; 79 | default: 80 | return false; 81 | } 82 | } 83 | 84 | InstrQueue::InstrQueue(uint16_t capacity) : capacity(capacity) 85 | { 86 | head = 0; 87 | tail = 0; 88 | entries = new Instr *[capacity]; 89 | } 90 | 91 | InstrQueue::~InstrQueue() 92 | { 93 | delete[] entries; 94 | } 95 | 96 | bool InstrQueue::is_empty() const 97 | { 98 | return head == tail; 99 | } 100 | 101 | uint16_t InstrQueue::size() const 102 | { 103 | return tail - head; 104 | } 105 | 106 | bool InstrQueue::is_full() const 107 | { 108 | return size() == capacity; 109 | } 110 | 111 | Instr *InstrQueue::dequeue() 112 | { 113 | // todo: better handling when empty 114 | 115 | Instr *instr = entries[head % capacity]; 116 | head++; 117 | return instr; 118 | } 119 | 120 | void InstrQueue::enqueue(Instr *instr) 121 | { 122 | entries[tail % capacity] = instr; 123 | tail++; 124 | } 125 | -------------------------------------------------------------------------------- /src/loader.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 4/30/24. 3 | // 4 | 5 | #include "../include/loader.h" 6 | #include "../include/instructions.h" 7 | #include "../include/cpu.h" 8 | #include 9 | #include 10 | 11 | using namespace std; 12 | 13 | bool isValidLabel(const string &s) 14 | { 15 | for (char c: s) 16 | { 17 | if (!isalpha(c)) 18 | { 19 | return false; 20 | } 21 | } 22 | return true; 23 | } 24 | 25 | bool isValidVariable(const string &s) 26 | { 27 | for (char c: s) 28 | { 29 | if (!isalpha(c)) 30 | { 31 | return false; 32 | } 33 | } 34 | return true; 35 | } 36 | 37 | void load_program(CPU *cpu, string file) 38 | { 39 | ifstream infile(file); 40 | if (!infile.is_open()) 41 | { 42 | cerr << "Failed to open program file." << endl; 43 | throw std::runtime_error("Failed to open program file."); 44 | } 45 | 46 | unordered_map labels = unordered_map(); 47 | unordered_map variables = unordered_map(); 48 | string lineText; 49 | uint32_t line_nr = 0; 50 | int heapLimit = 0; 51 | while (getline(infile, lineText)) 52 | { 53 | line_nr++; 54 | 55 | size_t commentPos = lineText.find(';'); 56 | if (commentPos != string::npos) 57 | { 58 | // Remove the comment part 59 | lineText = lineText.substr(0, commentPos); 60 | } 61 | 62 | lineText = trim(lineText); 63 | 64 | if (lineText.empty()) 65 | { 66 | continue; 67 | } 68 | 69 | istringstream iss(lineText); 70 | 71 | string first_token; 72 | if (!(iss >> first_token)) 73 | { 74 | cerr << "Invalid instruction format at line " << line_nr << "." << endl; 75 | throw std::runtime_error("Invalid program"); 76 | } 77 | 78 | // check if it is a variable 79 | if (first_token == "VAR") 80 | { 81 | string name; 82 | int value; 83 | if (iss >> name >> value) 84 | { 85 | if (!isValidVariable(name)) 86 | { 87 | cerr << "Invalid VAR name [" << name << "] at line_nr " << line_nr << "." << endl; 88 | throw std::runtime_error("Invalid program"); 89 | } 90 | 91 | optional existing_variable = mapGet(variables, name); 92 | if (existing_variable.has_value()) 93 | { 94 | cerr << "Variable [" << name << "] not unique at line " << line_nr << "." << endl; 95 | throw std::runtime_error("Invalid program"); 96 | } 97 | 98 | int address = heapLimit; 99 | cpu->memory->at(address) = value; 100 | variables.insert({name, address}); 101 | heapLimit++; 102 | } 103 | else 104 | { 105 | cerr << "Invalid VAR format at line_nr " << line_nr << "." << endl; 106 | throw std::runtime_error("Invalid program"); 107 | } 108 | continue; 109 | } 110 | 111 | // check if it is a label 112 | if (endsWith(first_token, ":")) 113 | { 114 | string label_name = removeLast(first_token); 115 | if (!isValidLabel(label_name)) 116 | { 117 | cerr << "Invalid label_name [" << label_name << "]at line " << line_nr << "." << endl; 118 | throw std::runtime_error("Invalid program"); 119 | } 120 | 121 | optional existing_label = mapGet(labels, label_name); 122 | if (existing_label.has_value()) 123 | { 124 | cerr << "Label [" << label_name << "] not unique at line " << line_nr << "." << endl; 125 | throw std::runtime_error("Invalid program"); 126 | } 127 | 128 | labels.insert({label_name, cpu->frontend->code->size()}); 129 | continue; 130 | } 131 | 132 | Instr instr; 133 | auto it = MNEMONIC_TO_OPCODE.find(first_token); 134 | if (it != MNEMONIC_TO_OPCODE.end()) 135 | { 136 | instr.opcode = it->second; 137 | 138 | switch (instr.opcode) 139 | { 140 | case OPCODE_AND: 141 | { 142 | int r_src1, r_src2, r_dst; 143 | if (iss >> r_dst >> r_src1 >> r_src2) 144 | { 145 | instr.input_ops_cnt = 2; 146 | instr.input_ops[0].type = OperandType::REGISTER; 147 | instr.input_ops[0].reg = r_src1; 148 | instr.input_ops[1].type = OperandType::REGISTER; 149 | instr.input_ops[1].reg = r_src2; 150 | 151 | instr.output_ops_cnt = 1; 152 | instr.output_ops[0].type = OperandType::REGISTER; 153 | instr.output_ops[0].reg = r_dst; 154 | } 155 | else 156 | { 157 | cerr << "Invalid AND instruction format at line_nr " << line_nr << "." << endl; 158 | throw std::runtime_error("Invalid AND"); 159 | } 160 | break; 161 | } 162 | case OPCODE_OR: 163 | { 164 | int r_src1, r_src2, r_dst; 165 | if (iss >> r_dst >> r_src1 >> r_src2) 166 | { 167 | instr.input_ops_cnt = 2; 168 | instr.input_ops[0].type = OperandType::REGISTER; 169 | instr.input_ops[0].reg = r_src1; 170 | instr.input_ops[1].type = OperandType::REGISTER; 171 | instr.input_ops[1].reg = r_src2; 172 | 173 | instr.output_ops_cnt = 1; 174 | instr.output_ops[0].type = OperandType::REGISTER; 175 | instr.output_ops[0].reg = r_dst; 176 | } 177 | else 178 | { 179 | cerr << "Invalid OR instruction format at line_nr " << line_nr << "." << endl; 180 | throw std::runtime_error("Invalid OR"); 181 | } 182 | break; 183 | } 184 | case OPCODE_XOR: 185 | { 186 | int r_src1, r_src2, r_dst; 187 | if (iss >> r_dst >> r_src1 >> r_src2) 188 | { 189 | instr.input_ops_cnt = 2; 190 | instr.input_ops[0].type = OperandType::REGISTER; 191 | instr.input_ops[0].reg = r_src1; 192 | instr.input_ops[1].type = OperandType::REGISTER; 193 | instr.input_ops[1].reg = r_src2; 194 | 195 | instr.output_ops_cnt = 1; 196 | instr.output_ops[0].type = OperandType::REGISTER; 197 | instr.output_ops[0].reg = r_dst; 198 | } 199 | else 200 | { 201 | cerr << "Invalid XOR instruction format at line_nr " << line_nr << "." << endl; 202 | throw std::runtime_error("Invalid OR"); 203 | } 204 | break; 205 | } 206 | case OPCODE_NOT: 207 | { 208 | int r_src, r_dst; 209 | if (iss >> r_dst >> r_src) 210 | { 211 | instr.input_ops_cnt = 1; 212 | instr.input_ops[0].type = OperandType::REGISTER; 213 | instr.input_ops[0].reg = r_src; 214 | 215 | instr.output_ops_cnt = 1; 216 | instr.output_ops[0].type = OperandType::REGISTER; 217 | instr.output_ops[0].reg = r_src; 218 | } 219 | else 220 | { 221 | cerr << "Invalid NOT instruction format at line_nr " << line_nr << "." << endl; 222 | throw std::runtime_error("Invalid NOT"); 223 | } 224 | break; 225 | } 226 | case OPCODE_ADD: 227 | { 228 | int r_src1, r_src2, r_dst; 229 | if (iss >> r_dst >> r_src1 >> r_src2) 230 | { 231 | instr.input_ops_cnt = 2; 232 | instr.input_ops[0].type = OperandType::REGISTER; 233 | instr.input_ops[0].reg = r_src1; 234 | instr.input_ops[1].type = OperandType::REGISTER; 235 | instr.input_ops[1].reg = r_src2; 236 | 237 | instr.output_ops_cnt = 1; 238 | instr.output_ops[0].type = OperandType::REGISTER; 239 | instr.output_ops[0].reg = r_dst; 240 | } 241 | else 242 | { 243 | cerr << "Invalid ADD instruction format at line_nr " << line_nr << "." << endl; 244 | throw std::runtime_error("Invalid ADD"); 245 | } 246 | break; 247 | } 248 | case OPCODE_SUB: 249 | { 250 | int r_src1, r_src2, r_dst; 251 | if (iss >> r_dst >> r_src1 >> r_src2) 252 | { 253 | instr.input_ops_cnt = 2; 254 | instr.input_ops[0].type = OperandType::REGISTER; 255 | instr.input_ops[0].reg = r_src1; 256 | instr.input_ops[1].type = OperandType::REGISTER; 257 | instr.input_ops[1].reg = r_src2; 258 | 259 | instr.output_ops_cnt = 1; 260 | instr.output_ops[0].type = OperandType::REGISTER; 261 | instr.output_ops[0].reg = r_dst; 262 | } 263 | else 264 | { 265 | cerr << "Invalid SUB instruction format at line_nr " << line_nr << "." << endl; 266 | throw std::runtime_error("Invalid SUB"); 267 | } 268 | break; 269 | } 270 | case OPCODE_DEC: 271 | { 272 | int r_src; 273 | if (iss >> r_src) 274 | { 275 | instr.input_ops_cnt = 1; 276 | instr.input_ops[0].type = OperandType::REGISTER; 277 | instr.input_ops[0].reg = r_src; 278 | 279 | instr.output_ops_cnt = 1; 280 | instr.output_ops[0].type = OperandType::REGISTER; 281 | instr.output_ops[0].reg = r_src; 282 | } 283 | else 284 | { 285 | cerr << "Invalid DEC instruction format at line_nr " << line_nr << "." << endl; 286 | throw std::runtime_error("Invalid DEC"); 287 | } 288 | break; 289 | } 290 | case OPCODE_INC: 291 | { 292 | int r_src; 293 | if (iss >> r_src) 294 | { 295 | instr.input_ops_cnt = 1; 296 | instr.input_ops[0].type = OperandType::REGISTER; 297 | instr.input_ops[0].reg = r_src; 298 | 299 | instr.output_ops_cnt = 1; 300 | instr.output_ops[0].type = OperandType::REGISTER; 301 | instr.output_ops[0].reg = r_src; 302 | } 303 | else 304 | { 305 | cerr << "Invalid INC instruction format at line_nr " << line_nr << "." << endl; 306 | throw std::runtime_error("Invalid INC"); 307 | } 308 | break; 309 | } 310 | case OPCODE_LOAD: 311 | { 312 | string name; 313 | int r_dst; 314 | if (iss >> r_dst >> name) 315 | { 316 | optional variable = mapGet(variables, name); 317 | if (!variable.has_value()) 318 | { 319 | cerr << "Variable [" << name << "] not found at line " << line_nr << "." << endl; 320 | throw std::runtime_error("Invalid program"); 321 | } 322 | 323 | instr.input_ops_cnt = 1; 324 | instr.input_ops[0].type = OperandType::MEMORY; 325 | instr.input_ops[0].memory_addr = variable.value(); 326 | 327 | instr.output_ops_cnt = 1; 328 | instr.output_ops[0].type = OperandType::REGISTER; 329 | instr.output_ops[0].reg = r_dst; 330 | } 331 | else 332 | { 333 | cerr << "Invalid LOAD instruction format at line_nr " << line_nr << "." << endl; 334 | throw std::runtime_error("Invalid LOAD"); 335 | } 336 | break; 337 | } 338 | case OPCODE_STORE: 339 | { 340 | string name; 341 | int r_src; 342 | if (iss >> r_src >> name) 343 | { 344 | optional variable = mapGet(variables, name); 345 | if (!variable.has_value()) 346 | { 347 | cerr << "Variable [" << name << "] not found at line " << line_nr << "." << endl; 348 | throw std::runtime_error("Invalid STORE"); 349 | } 350 | 351 | instr.input_ops_cnt = 1; 352 | instr.input_ops[0].type = OperandType::REGISTER; 353 | instr.input_ops[0].reg = r_src; 354 | 355 | instr.output_ops_cnt = 1; 356 | instr.output_ops[0].type = OperandType::MEMORY; 357 | instr.output_ops[0].memory_addr = variable.value(); 358 | } 359 | else 360 | { 361 | cerr << "Invalid STORE instruction format at line_nr " << line_nr << "." << endl; 362 | throw std::runtime_error("Invalid STORE"); 363 | } 364 | break; 365 | } 366 | case OPCODE_JNZ: 367 | { 368 | int r_src; 369 | string label; 370 | if (iss >> r_src >> label) 371 | { 372 | optional labelAddr = mapGet(labels, label); 373 | if (labelAddr.has_value()) 374 | { 375 | instr.input_ops_cnt = 2; 376 | instr.input_ops[0].type = OperandType::REGISTER; 377 | instr.input_ops[0].reg = r_src; 378 | instr.input_ops[1].type = OperandType::CODE; 379 | instr.input_ops[1].code_addr = labelAddr.value(); 380 | } 381 | else 382 | { 383 | cerr << "Unknown target [" << label << "] at line_nr " << line_nr << "." 384 | << endl; 385 | throw std::runtime_error("Invalid JNZ"); 386 | } 387 | } 388 | else 389 | { 390 | cerr << "Invalid JNZ instruction format at line_nr " << line_nr << "." << endl; 391 | throw std::runtime_error("Invalid JNZ"); 392 | } 393 | break; 394 | } 395 | case OPCODE_PRINTR: 396 | { 397 | 398 | 399 | int r_src; 400 | if (iss >> r_src) 401 | { 402 | instr.input_ops_cnt = 1; 403 | instr.input_ops[0].type = OperandType::REGISTER; 404 | instr.input_ops[0].reg = r_src; 405 | 406 | instr.output_ops_cnt = 0; 407 | } 408 | else 409 | { 410 | cerr << "Invalid PRINTR instruction format at line_nr " << line_nr << "." << endl; 411 | throw std::runtime_error("Invalid PRINTR"); 412 | } 413 | break; 414 | } 415 | case OPCODE_HALT: 416 | instr.input_ops_cnt = 0; 417 | instr.output_ops_cnt = 0; 418 | break; 419 | case OPCODE_NOP: 420 | instr.input_ops_cnt = 0; 421 | instr.output_ops_cnt = 0; 422 | break; 423 | 424 | // todo: not all opcodes are handled. 425 | } 426 | 427 | cpu->frontend->code->push_back(instr); 428 | } 429 | else 430 | { 431 | cerr << "Unknown mnemonic: " << first_token << endl; 432 | throw std::runtime_error("Invalid program"); 433 | } 434 | } 435 | 436 | // todo: file is not closed on error. 437 | infile.close(); 438 | if (cpu->frontend->code->size() > 0) 439 | { 440 | cpu->frontend->ip_next_fetch = 0; 441 | } 442 | else 443 | { 444 | cpu->frontend->ip_next_fetch = -1; 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "../include/cpu.h" 2 | #include "../include/loader.h" 3 | 4 | int main() 5 | { 6 | CPU_Config *config = new CPU_Config(); 7 | config->trace = true; 8 | config->cpu_frequency_Hz = 5; 9 | config->memory_size_ints = 16; 10 | config->arch_reg_cnt = 16; 11 | config->phys_reg_cnt = 64; 12 | config->sb_capacity = 4; 13 | config->rs_count = 16; 14 | config->frontend_n_wide = 2; 15 | config->instr_queue_capacity = 64; 16 | config->rob_capacity = 32; 17 | config->debug = true; 18 | 19 | CPU *cpu = new CPU(*config); 20 | 21 | load_program(cpu, "program.asm"); 22 | 23 | cpu->run(); 24 | cpu->print_memory(); 25 | return 0; 26 | } 27 | -------------------------------------------------------------------------------- /src/memory_subsystem.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 5/3/24. 3 | // 4 | 5 | 6 | #include "../include/memory_subsystem.h" 7 | 8 | 9 | optional StoreBuffer::lookup(int addr) 10 | { 11 | // todo: instead of iterating over all values, there should be a directly-mapped hash-table 12 | // so that we can use the last 12 bits of the address and do a lookup. Then we also need 13 | // to handle the 4K aliasing problem. 14 | for (uint64_t k = tail; k < head; k++) 15 | { 16 | StoreBufferEntry &entry = entries[k % capacity]; 17 | if (entry.addr == addr) 18 | { 19 | return optional(entry.value); 20 | } 21 | } 22 | 23 | return nullopt; 24 | } 25 | 26 | bool StoreBuffer::is_empty() 27 | { 28 | return head == tail; 29 | } 30 | 31 | void StoreBuffer::write(int addr, int value) 32 | { 33 | StoreBufferEntry &entry = entries[tail % capacity]; 34 | entry.value = value; 35 | entry.addr = addr; 36 | tail++; 37 | } 38 | 39 | void StoreBuffer::cycle() 40 | { 41 | if (head != tail) 42 | { 43 | StoreBufferEntry &entry = entries[head % capacity]; 44 | memory->at(entry.addr) = entry.value; 45 | head++; 46 | } 47 | } 48 | 49 | StoreBuffer::StoreBuffer(uint16_t capacity, vector *memory) :memory(memory), capacity(capacity) 50 | { 51 | 52 | entries = new StoreBufferEntry[capacity]; 53 | head = 0; 54 | tail = 0; 55 | } 56 | 57 | StoreBuffer::~StoreBuffer() 58 | { 59 | delete[] entries; 60 | } 61 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by pveentjer on 4/29/24. 3 | // 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "../include/utils.h" 10 | 11 | 12 | std::string trim(const std::string &str) 13 | { 14 | auto start = std::find_if_not(str.begin(), str.end(), [](unsigned char c) { return std::isspace(c); }); 15 | auto end = std::find_if_not(str.rbegin(), str.rend(), [](unsigned char c) { return std::isspace(c); }).base(); 16 | return (start < end ? std::string(start, end) : ""); 17 | } 18 | 19 | bool endsWith(const std::string &fullString, const std::string &ending) 20 | { 21 | if (fullString.length() >= ending.length()) 22 | { 23 | return (fullString.compare(fullString.length() - ending.length(), ending.length(), ending) == 0); 24 | } 25 | else 26 | { 27 | return false; 28 | } 29 | } 30 | 31 | std::string removeLast(const std::string &s) 32 | { 33 | if (s.length() > 0) 34 | { 35 | return s.substr(0, s.length() - 1); 36 | } 37 | else 38 | { 39 | return ""; 40 | } 41 | } -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | TODO 2 | 3 | - store buffer and memory, should be part of memory subsystem 4 | 5 | - fix STORE 6 | 7 | - fix STLF 8 | 9 | - when printing cycle debug info: show some stats like total number of instructions issued, retired, ipc 10 | 11 | - introduce an architectural register file 12 | 13 | - better handling of the end of the program 14 | 15 | - better modelling of the execution unit. 16 | 17 | - super scalar (currently just 1 EU) 18 | 19 | - should RS_Table be called RST (T stands for Table; just like RAT) 20 | 21 | - program loader: should use R prefix for instructions 22 | 23 | - pass program to load from the command line 24 | 25 | - program loader: deal with loading literals 26 | 27 | - program loader: should properly close file on error 28 | 29 | - support speculative execution 30 | 31 | DONE 32 | 33 | - fix JNZ 34 | 35 | - the solution with the NOPs to deal with HALT or a branch is not going to work if 36 | the latency to get an instruction through the pipeline is larger than the pipeline 37 | depth. 38 | 39 | --------------------------------------------------------------------------------