├── LICENSE ├── README.md ├── build.sh ├── eas.py ├── include └── cpu.hpp ├── programs ├── add.asm ├── add.bin ├── fib.asm └── fib.bin ├── screenshot.png ├── screenshot1.png └── src ├── cpu.cpp └── main.cpp /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 0xhh 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 | 2 | # TinyE8 - Ben Eater's 8 Bit CPU Emulator 3 | 4 | TinyE8 emulates [Ben Eater's](https://www.youtube.com/channel/UCS0N5baNlQWJCUrhCEo8WlA) 8 bit breadboard CPU. Implemented all the Instructions ~~except `JC` and `JZ`, I need to watch the whole [playlist](https://www.youtube.com/playlist?list=PLowKtXNTBypGqImE405J2565dvjafglHU) before implementing these two instructions.~~. 5 | 6 | Here are some screenshots: 7 | 8 | ![screenshot](https://github.com/0xhh/TinyE8/blob/main/screenshot.png) 9 | 10 | ![screenshot1](https://github.com/0xhh/TinyE8/blob/main/screenshot1.png) 11 | 12 | Source code is extensively commented, so that it is readable. 13 | 14 | ## Instruction set 15 | ``` 16 | ------------------------------------------------------------------------------------------------------------------------------- 17 | | 0000 - NOP --> No Operation 18 | | 0001 - LDA --> Load contents of a memory address XXXX into A register 19 | | 0010 - ADD --> Load contents of a memory address XXXX into B register, then performs A+B and stores the result in A register 20 | | 0011 - SUB --> Load contents of a memory address XXXX into B register, then performs A-B and stores the result in A register 21 | | 0100 - STA --> Store contents of A register at memory address XXXX 22 | | 0101 - LDI --> Load 4 bit immediate value into A register 23 | | 0110 - JMP --> Unconditional jump: sets program counter to XXXX and executes from there 24 | | 0111 - JC --> Jump if carry: sets program counter to XXXX when carry flag is set and executes from there 25 | | 1000 - JZ --> Jump if zero: sets program counter to XXXX when zero flag is set and executes from there 26 | | 1110 - OUT --> Output contents of A register to 7 segment display, in our case, we'll print it on console 27 | | 1111 - HLT --> Halts the execution 28 | ------------------------------------------------------------------------------------------------------------------------------- 29 | ``` 30 | 31 | In the mean time feel free to fork, implement these instructions and start a pull request. I'll verify changes and merge it to the main branch. 32 | 33 | #### Example: Add 7 and 3 34 | ``` 35 | ----------------------------------------------------------------------------------------------------------- 36 | | LDA 0000 ;0000 is the memory address of 7 in the RAM. 7 is present at 0000 37 | | ADD 0001 ;0001 is the memory address 0f 3 in the RAM. 3 is present at 0001 38 | | OUT XXXX ;we don't care about XXXX, all we want is whether the last 4 bits are OUT instruction's or not 39 | | HLT XXXX ;similarly, for HLT, we only check the last 4 bits 40 | | 41 | | ;In Memory or RAM, the values are stored as follows 42 | | 43 | | ;at 0000 we store 7 or in C syntax, we can represent it as memory[0] = 7; 44 | | ;similarly at 0001, we store 3, or memory[1] = 3; 45 | | ;The output should be 10 46 | ----------------------------------------------------------------------------------------------------------- 47 | ``` 48 | 49 | #### Implemented Instructions: 50 | - [x] NOP 51 | - [x] LDA 52 | - [x] ADD 53 | - [x] SUB 54 | - [x] STA 55 | - [x] LDI 56 | - [x] JMP - ah, fixed it 57 | - [x] JC - implemented 58 | - [x] JZ - implemented 59 | - [x] OUT 60 | - [x] HLT 61 | 62 | ## Loading your own program 63 | 64 | #### Loading your own program from `.bin` file 65 | Create .asm file and write your assembly program in it. 66 | The program should contain atleast 16 lines and each line should consist of 1 or 2 instructions 67 | 68 | For example: If you want to write a program that adds 7 and 3 and then subtracts 2 from it, it should something like this 69 | ``` 70 | LDA 8 71 | ADD 9 72 | SUB 10 73 | OUT 0 74 | NOP 75 | NOP 76 | NOP 77 | NOP 78 | 7 79 | 3 80 | 2 81 | NOP 82 | NOP 83 | NOP 84 | NOP 85 | NOP 86 | ``` 87 | 88 | At `line 0`, we can see `LDA 8`, this means that, it loads value from `address 8` into `A register`. This `address 8` is at `line 8` which has a value of `7`. So, it Loads `7` into `A register`. 89 | 90 | At `line 1`, we have `ADD 9`, this will store contents from `address 9` into `B register`, then adds this value to contents of `A register`, So, the final value of `A register` will be `10` and `B register` will be `3`. 91 | 92 | At `line 2`, we have `SUB 10`, this will store contents from `address 10` int `B register`, then subtracts this value from contents of `A register`, the final value of `A register` will be `8` and `B register` will be `2` 93 | 94 | At `line 3`, we have `OUT 0`, this will `OUTPUT` the contents of `A register` on to the console. 95 | 96 | Rest of the lines contain `NOP` instructions. 97 | 98 | At `line 8`, there is `7`, this value will be used when `line 0` gets executed. 99 | At `line 9`, there is `3`, this value will be used when `line 1` gets executed. 100 | At `line 10`, there is `2`, this value will be used when `line 3` gets executed. 101 | 102 | 103 | In this manner, you have to write your assembly program. 104 | And make sure that there are no extra spaces other than spaces between instruction and memory addresses 105 | 106 | Once, you write your assembly program, save it. For example, if you have saved it with a name of `add.asm`, then run the following command to generate `.bin` file. 107 | 108 | `python3 eas.py add.asm -o add.bin` 109 | 110 | This will generate a `.bin` file. You can run this using the emulator with the following command 111 | 112 | `./main add.bin 1` where `1` will print the debug info and `0` will omit it. 113 | 114 | ## Building 115 | Clone the repo using `git clone https://github.com/0xhh/TinyE8.git` and then `cd TinyE8`. 116 | 117 | If you are on Linux, make sure you have `g++` working. Then run `sh build.sh` from the terminal. 118 | It will create `main` binary file. Run it using `./main `. 119 | 120 | If you are on Windows, install `C++` compiler using `MinGW` and then run the following command on the `CMD` or `Powershell` 121 | 122 | `g++ -c src/*.cpp -std=c++14 -g -Wall -I include && g++ *.o -o ./main.exe ` and then run `main.exe` file from `CMD` or `Powershell` 123 | 124 | ## TODO 125 | - [x] JC - Implement jump if carry 126 | - [x] JZ - Implement jump if zero 127 | - [x] load program from `.bin` file 128 | - [ ] did I miss something? 129 | 130 | ## Contributing 131 | Feel free to fork and contribute to this repo. Make sure that your code is readable and add comments wherever they are needed. 132 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | g++ -c src/*.cpp -std=c++14 -g -Wall -I include && g++ *.o -o ./main && rm *.o -------------------------------------------------------------------------------- /eas.py: -------------------------------------------------------------------------------- 1 | #eater assembler 2 | import sys 3 | 4 | def assemble(inFilename, outFilename): 5 | #instruction set 6 | instructions = { 7 | "NOP" : 0x0, 8 | "LDA" : 0x1, 9 | "ADD" : 0x2, 10 | "SUB" : 0x3, 11 | "STA" : 0x4, 12 | "LDI" : 0x5, 13 | "JMP" : 0x6, 14 | "JC" : 0x7, 15 | "JZ" : 0x8, 16 | "OUT" : 0xE, 17 | "HLT" : 0xF 18 | } 19 | 20 | #open the asm file 21 | try: 22 | f = open(inFilename, "r") 23 | except: 24 | print(f"Error: {inFilename} file not found.") 25 | 26 | #copy the whole file into a buffer and close the file 27 | buffer = f.read() 28 | f.close() 29 | 30 | #split the buffer based on new lines, we will have a list of instructions 31 | tokens = buffer.split("\n") 32 | 33 | #output buffer 34 | output = [] 35 | 36 | #iterate through the tokens, convert them to hexadecimal 37 | #values based on instruction set and append it to output 38 | for i in range(16): 39 | try: 40 | ins = tokens[i].split(" ") 41 | if(ins[0] in instructions): 42 | if(len(ins)==1): 43 | output.append(hex(instructions[ins[0]]<<4 | 0 )) 44 | #print(hex(instructions[ins[0]]<<4 | 0 )) 45 | else: 46 | output.append(hex(instructions[ins[0]]<<4 | int(ins[1]))) 47 | #print(hex(instructions[ins[0]]<<4 | int(ins[1]))) 48 | else: 49 | if(len(ins)==1): 50 | output.append(hex(int(ins[0]))) 51 | #print(hex(int(ins[0]))) 52 | except Exception as e: 53 | output.append(hex(0)) 54 | 55 | 56 | #write the output buffer to a bin file by converting it into to bytes 57 | with open(outFilename, "wb") as f: 58 | for i in output: 59 | print(i) 60 | f.write(bytes((int(i,16),))) 61 | f.close() 62 | 63 | 64 | 65 | if(len(sys.argv) != 4): 66 | print("Usage: python3 eas.py -o ") 67 | exit() 68 | 69 | inFilename = sys.argv[1] 70 | outFilename = sys.argv[3] 71 | assemble(inFilename, outFilename) 72 | -------------------------------------------------------------------------------- /include/cpu.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | ----------------------------------------------------------------------------------------------- 5 | |Instruction set - Each Instruction is 4 bits long and each opcode will be 8 bits or 1 byte long| 6 | ----------------------------------------------------------------------------------------------- 7 | ------------------------------------------------------------------------------------------------------------------------------- 8 | | 0000 - NOP --> No Operation | 9 | | 0001 - LDA --> Load contents of a memory address XXXX into A register | 10 | | 0010 - ADD --> Load contents of a memory address XXXX into B register, then performs A+B and stores the result in A register | 11 | | 0011 - SUB --> Load contents of a memory address XXXX into B register, then performs A-B and stores the result in A register | 12 | | 0100 - STA --> Store contents of A register at memory address XXXX | 13 | | 0101 - LDI --> Load 4 bit immediate value into A register | 14 | | 0110 - JMP --> Unconditional jump: sets program counter to XXXX and executes from there | 15 | | 0111 - JC --> Jump if carry: sets program counter to XXXX when carry flag is set and executes from there | 16 | | 1000 - JZ --> Jump if zero: sets program counter to XXXX when zero flag is set and executes from there | 17 | | 1110 - OUT --> Output contents of A register to 7 segment display, in our case, we'll print it on console | 18 | | 1111 - HLT --> Halts the execution | 19 | ------------------------------------------------------------------------------------------------------------------------------- 20 | 21 | --------------------------------- 22 | | Example: Add two numbers 7 and 3| 23 | --------------------------------- 24 | ----------------------------------------------------------------------------------------------------------- 25 | | LDA 0000 ;0000 is the memory address of 7 in the RAM. 7 is present at 0000 | 26 | | ADD 0001 ;0001 is the memory address 0f 3 in the RAM. 3 is present at 0001 | 27 | | OUT XXXX ;we don't care about XXXX, all we want is whether the last 4 bits are OUT instruction's or not | 28 | | HLT XXXX ;similarly, for HLT, we only check the last 4 bits | 29 | | | 30 | | ;In Memory or RAM, the values are stored as follows | 31 | | | 32 | | ;at 0000 we store 7 or in C syntax, we can represent it as memory[0] = 7; | 33 | | ;similarly at 0001, we store 3, or memory[1] = 3; | 34 | | ;The output should be 10 | 35 | ----------------------------------------------------------------------------------------------------------- 36 | 37 | */ 38 | 39 | class CPU{ 40 | public: 41 | uint8_t pc; //Program Counter starts from 0x00 42 | 43 | uint8_t A; //A register 44 | uint8_t B; //B register 45 | uint8_t IR; //Instruction Register 46 | 47 | uint8_t memory[16]; //16 bytes of memory or RAM 48 | 49 | uint16_t OUT; //output 50 | 51 | bool CF; //Carry Flag 52 | bool ZF; //Zero Flag 53 | bool HALT; //halt flag 54 | 55 | //constructor and destructor 56 | CPU(); 57 | ~CPU(); 58 | 59 | void init(); //initializes the CPU by resetting all the components 60 | int loadProgram(const char *filename); //loads a program into the memory[] 61 | void execute(); // Fetch->Decode->Execute opcode cycle 62 | }; 63 | -------------------------------------------------------------------------------- /programs/add.asm: -------------------------------------------------------------------------------- 1 | LDA 8 2 | ADD 9 3 | SUB 10 4 | OUT 0 5 | HLT 6 | NOP 7 | NOP 8 | NOP 9 | 7 10 | 3 11 | 2 12 | NOP 13 | NOP 14 | NOP 15 | NOP 16 | NOP 17 | -------------------------------------------------------------------------------- /programs/add.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/previouslyanywhere/TinyE8/31af308f42af3f2120e3ba9875f6999c6df7dafa/programs/add.bin -------------------------------------------------------------------------------- /programs/fib.asm: -------------------------------------------------------------------------------- 1 | LDI 1 2 | STA 14 3 | LDI 0 4 | STA 15 5 | OUT 6 | LDA 14 7 | ADD 15 8 | STA 14 9 | OUT 10 | LDA 15 11 | ADD 14 12 | JC 13 13 | JMP 3 14 | HLT 15 | 0 16 | 1 17 | -------------------------------------------------------------------------------- /programs/fib.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/previouslyanywhere/TinyE8/31af308f42af3f2120e3ba9875f6999c6df7dafa/programs/fib.bin -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/previouslyanywhere/TinyE8/31af308f42af3f2120e3ba9875f6999c6df7dafa/screenshot.png -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/previouslyanywhere/TinyE8/31af308f42af3f2120e3ba9875f6999c6df7dafa/screenshot1.png -------------------------------------------------------------------------------- /src/cpu.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "cpu.hpp" 3 | 4 | CPU::CPU(){ 5 | std::cout << "Booting up...\n"; 6 | } 7 | 8 | CPU::~CPU(){ 9 | 10 | } 11 | 12 | void CPU::init(){ 13 | std::cout << "Resetting Registers...\n"; 14 | 15 | //reset all the registers, counters, memory, flags 16 | pc = 0; //set program counter to 0 17 | 18 | //reset registers 19 | A = 0; 20 | B = 0; 21 | IR = 0; 22 | 23 | //reset memory 24 | for(uint8_t i = 0; i<16; i++){ 25 | memory[i] = 0; 26 | } 27 | 28 | //set output register to 0 29 | OUT = 0; 30 | 31 | //set flags to false 32 | CF = false; 33 | ZF = false; 34 | HALT = false; 35 | } 36 | 37 | int CPU::loadProgram(const char *filename){ 38 | init(); 39 | 40 | //load program into memory. 41 | //in our case, we want to add 7 and 3 42 | //load 7 at 0110 43 | //and load 3 at 0111 44 | 45 | /* 46 | memory[0b0000] = 0b00011000; //LDA 1000 or load 7 into A register 47 | memory[0b0001] = 0b00101001; //ADD 1001 or load 3 into B register, then ADD and store it in A 48 | memory[0b0010] = 0b11100000; //OUT 0000 or OUTPUT A register contents 49 | memory[0b0011] = 0b00111011; //SUB 1011 or subtract value stored on memory address 0111 50 | memory[0b0100] = 0b11100000; //OUT 0000 or OUTPUT A register contents 51 | memory[0b0101] = 0b11110000; //HLT 0000 or HALT execution 52 | 53 | //store data in the memory 54 | memory[0b1000] = 7; 55 | memory[0b1001] = 3; 56 | memory[0b1011] = 2; 57 | */ 58 | 59 | //This program will print Triangular numbers until 255 60 | /*memory[0x0] = 0x1F; 61 | memory[0x1] = 0x2E; 62 | memory[0x2] = 0x79; 63 | memory[0x3] = 0xE0; 64 | memory[0x4] = 0x4F; 65 | memory[0x5] = 0x1E; 66 | memory[0x6] = 0x2D; 67 | memory[0x7] = 0x4E; 68 | memory[0x8] = 0x60; 69 | memory[0x9] = 0x50; 70 | memory[0xA] = 0x4F; 71 | memory[0xB] = 0x1D; 72 | memory[0xC] = 0x4E; 73 | memory[0xD] = 1; 74 | memory[0xE] = 1; 75 | memory[0xF] = 0;*/ 76 | 77 | FILE *fp = fopen(filename, "rb"); 78 | 79 | if(fp == nullptr){ 80 | std::cout << "File not found!\n"; 81 | exit(0); 82 | } 83 | 84 | fseek(fp, 0, SEEK_END); // Jump to the end of the file 85 | int filesize = ftell(fp); // Get the current byte offset in the file 86 | rewind(fp); // Jump back to the beginning of the file 87 | 88 | uint8_t *buffer = (uint8_t *) malloc((filesize + 1) * sizeof(uint8_t)); 89 | 90 | fread(buffer, filesize, 1, fp); // Read in the entire file 91 | fclose(fp); 92 | 93 | for (int i = 0; i < filesize; i++) { 94 | memory[i] = buffer[i]; 95 | } 96 | 97 | 98 | return 1; 99 | } 100 | 101 | 102 | void CPU::execute(){ 103 | //every instruction is 8 bits long 104 | //higher 4 bits represent instruction 105 | //and lower 4 bits represent memory address 106 | 107 | //from an 8 bit instruction, Higher 4 bits can be extracted by performing & operation with 0xF0 and right shifting it with 4 bits 108 | //and Lower 4 bits can be extracted by performing & operation with 0x0F 109 | 110 | //the following code extracts the instruction from the higher 4 bits 111 | uint8_t opcode = (IR & 0xF0) >> 4; 112 | switch(opcode){ 113 | //NOP 114 | case 0b0000: 115 | break; 116 | 117 | //LDA - load contents from lower 4 bits memory address into A register 118 | case 0b0001: 119 | A = memory[(IR & 0x0f)]; 120 | break; 121 | 122 | //ADD 123 | case 0b0010: 124 | //set carry flag if addition is overflow. As this is 8 bit, 2^8-1 = 255; 125 | //when, the addition result is greater than 255, set the carry flag 126 | CF = (uint16_t(A) + uint16_t(memory[(IR & 0x0f)])) > 255; 127 | 128 | //normal addition 129 | B = memory[(IR & 0x0f)]; 130 | A = A + B; 131 | 132 | //set zero flag only if A==0 133 | ZF = A == 0; 134 | break; 135 | 136 | //SUB 137 | case 0b0011: 138 | //set carry flag if subtraction is overflow, just like in addition 139 | CF = (uint16_t(A) - uint16_t(memory[(IR & 0x0f)])) > 255; 140 | 141 | //normal subtraction 142 | B = memory[(IR & 0x0f)]; 143 | A = A - B; 144 | 145 | //set zero flag only if A==0 146 | ZF = A == 0; 147 | break; 148 | 149 | //STA 150 | case 0b0100: 151 | memory[(IR&0x0f)] = A; 152 | break; 153 | 154 | //LDI 155 | case 0b0101: 156 | A = IR & 0x0f; 157 | break; 158 | 159 | //JMP 160 | case 0b0110: 161 | //jump to xxxx instruction and do not increment the PC 162 | pc = (IR & 0x0f) - 1; 163 | break; 164 | 165 | //JC 166 | case 0b0111: 167 | //jump if carry flag is set 168 | if(CF){ 169 | pc = (IR & 0x0f) - 1; 170 | } 171 | break; 172 | 173 | //JZ 174 | case 0b1000: 175 | //jump if zero flag is set 176 | if(ZF){ 177 | pc = (IR & 0x0f) - 1; 178 | } 179 | break; 180 | 181 | //OUT 182 | case 0b1110: 183 | OUT = (uint16_t)A; 184 | std::cout << OUT << "\n"; 185 | break; 186 | 187 | //HLT - set HALT flag to true 188 | case 0b1111: 189 | HALT = true; 190 | break; 191 | 192 | default: 193 | std::cout << "Illegal opcode : " << IR << std::endl; 194 | exit(0); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "cpu.hpp" 3 | 4 | int main(int argc, const char *argv[]){ 5 | if(argc != 3){ 6 | std::cout << "Usage : ./main \n\n"; 7 | std::cout << " - binary file compiled using eas.py assembler\n"; 8 | std::cout << " - 1 will print debug info and 0 will omit it\n"; 9 | exit(1); 10 | } 11 | 12 | int debug = std::stoi(argv[2]); 13 | 14 | CPU cpu; //create the CPU object 15 | 16 | cpu.loadProgram(argv[1]); //load the program into the memory 17 | 18 | //execute until HALT flag is set to true 19 | while(!cpu.HALT){ 20 | //fetch instruction using the program counter from memory 21 | //into the instruction register IR 22 | cpu.IR = cpu.memory[cpu.pc]; 23 | 24 | //execute the instruction 25 | cpu.execute(); 26 | //increment the program counter by 1 27 | cpu.pc += 0b0001; 28 | } 29 | 30 | //Debug output 31 | if(debug == 1){ 32 | std::cout << " ----------------- \n"; 33 | std::cout << "| DEBUG |\n"; 34 | std::cout << " ----------------- \n"; 35 | printf("| PC : 0x%X |\n", cpu.pc); 36 | printf("| A : 0x%X |\n", cpu.A); 37 | printf("| B : 0x%X |\n", cpu.B); 38 | printf("| IR : 0x%X |\n", cpu.IR); 39 | 40 | std::cout << "| CF : " << cpu.CF << " |\n"; 41 | std::cout << "| ZF : " << cpu.ZF << " |\n"; 42 | std::cout << "| OUT : " << cpu.OUT << " |\n"; 43 | std::cout << " ----------------- \n"; 44 | } 45 | else{ 46 | std::cout << "OUT : " << cpu.OUT << "\n"; 47 | } 48 | 49 | 50 | return 0; 51 | } 52 | --------------------------------------------------------------------------------