diff --git a/ls8/README.md b/ls8/README.md index d9170d968..fb5cb142e 100644 --- a/ls8/README.md +++ b/ls8/README.md @@ -30,21 +30,21 @@ then prints it out: The binary numeric value on the left in the `print8.ls8` code above is either: -* the machine code value of the instruction (e.g. `10000010` for `LDI`), also +- the machine code value of the instruction (e.g. `10000010` for `LDI`), also known as the _opcode_ or -* one of the opcode's arguments (e.g. `00000000` for `R0` or `00001000` for the +- one of the opcode's arguments (e.g. `00000000` for `R0` or `00001000` for the value `8`), also known as the _operands_. This code above requires the implementation of three instructions: -* `LDI`: load "immediate", store a value in a register, or "set this register to +- `LDI`: load "immediate", store a value in a register, or "set this register to this value". -* `PRN`: a pseudo-instruction that prints the numeric value stored in a +- `PRN`: a pseudo-instruction that prints the numeric value stored in a register. -* `HLT`: halt the CPU and exit the emulator. +- `HLT`: halt the CPU and exit the emulator. See [the LS-8 spec](../LS8-spec.md) for more details. @@ -59,11 +59,15 @@ but you'll have to implement those three above instructions first! ## Step 0: IMPORTANT: inventory what is here! -* Make a list of files here. -* Write a short 3-10-word description of what each file does. -* Note what has been implemented, and what hasn't. -* Read this whole file. -* Skim the spec. +- Make a list of files here + +1. cpu.py - Defines a CPU class that emulates a LS-8 Microcomputer +2. ls8.py - Uses a CPU instance to run a program + +- Write a short 3-10-word description of what each file does. +- Note what has been implemented, and what hasn't. +- Read this whole file. +- Skim the spec. ## Step 1: Add the constructor to `cpu.py` @@ -134,7 +138,7 @@ name instead of by numeric value. In `run()` in your if-else block, exit the loop if a `HLT` instruction is encountered, regardless of whether or not there are more lines of code in the -LS-8 program you loaded. +LS-8 program you loaded. We can consider `HLT` to be similar to Python's `exit()` in that we stop whatever we are doing, wherever we are. @@ -151,8 +155,8 @@ value. This is a very similar process to adding `LDI`, but the handler is simpler. See the LS-8 spec. -*At this point, you should be able to run the program and have it print `8` to -the console!* +_At this point, you should be able to run the program and have it print `8` to +the console!_ ## Step 7: Un-hardcode the machine code @@ -194,7 +198,7 @@ so you can look in `sys.argv[1]` for the name of the file to load. > expect, and print an error and exit if they didn't. In `load()`, you will now want to use those command line arguments to open a -file, read in its contents line by line, and save appropriate data into RAM. +file, read in its contents line by line, and save appropriate data into RAM. As you process lines from the file, you should be on the lookout for blank lines (ignore them), and you should ignore everything after a `#`, since that's a @@ -296,10 +300,10 @@ a high address) and grows _downward_ as things are pushed on. The LS-8 is no exception to this. Implement a system stack per the spec. Add `PUSH` and `POP` instructions. Read - the beginning of the spec to see which register is the stack pointer. - -* Values themselves should be saved in the ***portion of RAM*** _that is allocated for the stack_. - - Use the stack pointer to modify the correct block of memory. +the beginning of the spec to see which register is the stack pointer. + +- Values themselves should be saved in the **_portion of RAM_** _that is allocated for the stack_. + - Use the stack pointer to modify the correct block of memory. - Make sure you update the stack pointer appropriately as you `PUSH` and `POP` items to and from the stack. If you run `python3 ls8.py examples/stack.ls8` you should see the output: @@ -320,21 +324,21 @@ enables you to create reusable functions. Subroutines have many similarities to functions in higher-level languages. Just as a function in C, JavaScript or Python will jump from the function call, to its definition, and then return back to the line of code following the call, -subroutines will also allow us to execute instructions non-sequentially. +subroutines will also allow us to execute instructions non-sequentially. The stack is used to hold the return address used by `RET`, so you **must** implement the stack in step 10, first. Then, add subroutine instructions `CALL` and `RET`. -* For `CALL`, you will likely have to modify your handler call in `cpu_run()`. +- For `CALL`, you will likely have to modify your handler call in `cpu_run()`. The problem is that some instructions want to execute and move to the next instruction like normal, but others, like `CALL` and `JMP` want to go to a specific address. > Note: `CALL` is very similar to the `JMP` instruction. However, there is one - > key difference between them. Can you find it in the specs? + > key difference between them. Can you find it in the specs? - * In **any** case where the instruction handler sets the `PC` directly, you + - In **any** case where the instruction handler sets the `PC` directly, you _don't_ want to advance the PC to the next instruction. So you'll have to set up a special case for those types of instructions. This can be a flag you explicitly set per-instruction... but can also be computed from the diff --git a/ls8/cpu.py b/ls8/cpu.py index 9a307496e..752a17bad 100644 --- a/ls8/cpu.py +++ b/ls8/cpu.py @@ -1,51 +1,117 @@ """CPU functionality.""" - import sys - +import os.path +HLT = 0b00000001 +LDI = 0b10000010 +PRN = 0b01000111 +MUL = 0b10100010 +PUSH = 0b01000101 +POP = 0b01000110 +CALL = 0b01010000 +RET = 0b00010001 +ADD = 0b10100000 class CPU: """Main CPU class.""" - def __init__(self): """Construct a new CPU.""" - pass - + # 256-byte RAM, each element is 1 byte (can only store integers 0-255) + self.ram = [0] * 256 + + # R0-R7: 8-bit general purpose registers, R5 = interrupt mask (IM), + # R6 = interrupt status (IS), R7 = stack pointer (SP) + self.reg = [0] * 8 + # Internal Registers + self.pc = 0 # Program Counter: address of the currently executing instruction + self.ir = 0 # Instruction Register: contains a copy of the currently executing instruction + self.mar = 0 # Memory Address Register: holds the memory address we're reading or writing + self.mdr = 0 # Memory Data Register: holds the value to write or the value just read + self.fl = 0 # Flag Register: holds the current flags status + self.halted = False + # Initialize the Stack Pointer + # SP points at the value at the top of the stack (most recently pushed), or at address F4 if the stack is empty. + self.reg[7] = 0xF4 # 244 # int('F4', 16) + # Setup Branch Table + self.branchtable = {} + self.branchtable[HLT] = self.execute_HLT + self.branchtable[LDI] = self.execute_LDI + self.branchtable[PRN] = self.execute_PRN + self.branchtable[MUL] = self.execute_MUL + self.branchtable[PUSH] = self.execute_PUSH + self.branchtable[POP] = self.execute_POP + self.branchtable[CALL] = self.execute_CALL + self.branchtable[RET] = self.execute_RET + self.branchtable[ADD] = self.execute_ADD + + # Property wrapper for SP (Stack Pointer) + + @property + def sp(self): + return self.reg[7] + + @sp.setter + def sp(self, a): + self.reg[7] = a & 0xFF + + # Computed Properties + + @property + def operand_a(self): + return self.ram_read(self.pc + 1) + + @property + def operand_b(self): + return self.ram_read(self.pc + 2) + + @property + def instruction_size(self): + return ((self.ir >> 6) & 0b11) + 1 + + def instruction_sets_pc(self): + return ((self.ir >> 4) & 0b0001) == 1 + + # CPU Methods + + def ram_read(self, mar): + if mar >= 0 and mar < len(self.ram): + return self.ram[mar] + else: + print(f"Error: Attempted to read from memory address: {mar}, which is outside of the memory bounds.") + return -1 + def ram_write(self, mar, mdr): + if mar >= 0 and mar < len(self.ram): + self.ram[mar] = mdr & 0xFF + else: + print(f"Error: Attempted to write to memory address: {mar}, which is outside of the memory bounds.") def load(self): """Load a program into memory.""" - address = 0 - - # For now, we've just hardcoded a program: - - program = [ - # From print8.ls8 - 0b10000010, # LDI R0,8 - 0b00000000, - 0b00001000, - 0b01000111, # PRN R0 - 0b00000000, - 0b00000001, # HLT - ] - - for instruction in program: - self.ram[address] = instruction - address += 1 - - + file_name = "ls8/examples/call.ls8" + # file_path = os.path.join(os.path.dirname(__file__), file_name) + try: + with open(file_name) as f: + for line in f: + num = line.split("#")[0].strip() # "10000010" + try: + instruction = int(num, 2) + self.ram[address] = instruction + address += 1 + except: + continue + except: + print(f'Could not find file named: {file_name}') + sys.exit(1) def alu(self, op, reg_a, reg_b): """ALU operations.""" - if op == "ADD": self.reg[reg_a] += self.reg[reg_b] #elif op == "SUB": etc else: raise Exception("Unsupported ALU operation") - def trace(self): """ Handy function to print out the CPU state. You might want to call this from run() if you need help debugging. """ - print(f"TRACE: %02X | %02X %02X %02X |" % ( self.pc, #self.fl, @@ -54,12 +120,72 @@ def trace(self): self.ram_read(self.pc + 1), self.ram_read(self.pc + 2) ), end='') - for i in range(8): print(" %02X" % self.reg[i], end='') - print() - + # Run Loop def run(self): """Run the CPU.""" - pass + while not self.halted: +# Fetch the next instruction (it is decoded lazily using computed properties) + self.ir = self.ram_read(self.pc) + operand_a = self.ram_read(self.pc + 1) + operand_b = self.ram_read(self.pc + 2) + if not self.instruction_sets_pc(): + self.pc += self.instruction_size + #self.execute_instruction(operand_a, operand_b) + # Execute the instruction + print(bin(self.ir)) + print(self.pc) + if self.ir in self.branchtable: + self.branchtable[self.ir]() + else: + print(f"Error: Could not find instruction: {self.ir} in branch table.") + sys.exit(1) + + # Increment the program counter if necessary + if not self.instruction_sets_pc: + self.pc += self.instruction_size + + def execute_instruction(self, operand_a, operand_b): + if self.ir in self.branchtable: + self.branchtable[self.ir](operand_a, operand_b) + else: + print(f"Error: Could not find instruction: {self.ir} in branch table.") + sys.exit(1) + + # Define operations to be loaded into the branch table + def execute_HLT(self): + self.halted = True + + def execute_LDI(self): + self.reg[self.operand_a] = self.operand_b + + def execute_PRN(self): + print(self.reg[self.operand_a]) + + def execute_MUL(self): + self.reg[self.operand_a] *= self.reg[self.operand_b] + + def execute_PUSH(self): + self.sp -= 1 + self.mdr = self.reg[self.operand_a] + self.ram_write(self.sp, self.mdr) + + def execute_POP(self): + self.mdr = self.ram_read(self.sp) + self.reg[self.operand_a] = self.mdr + self.sp += 1 + + def execute_CALL(self): + self.sp -= 1 + self.ram_write(self.sp, self.pc + self.instruction_size) + print(self.instruction_size) + self.pc = self.reg[self.operand_a] + + def execute_RET(self): + self.pc = self.ram_read(self.sp) + self.sp += 1 + + def execute_ADD(self): + self.reg[self.operand_a] += self.reg[self.operand_b] diff --git a/ls8/ls8.py b/ls8/ls8.py index 74128d36b..c6eadf1d3 100755 --- a/ls8/ls8.py +++ b/ls8/ls8.py @@ -4,8 +4,11 @@ import sys from cpu import * +import os.path + cpu = CPU() + cpu.load() -cpu.run() \ No newline at end of file +cpu.run()