Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 26 additions & 22 deletions ls8/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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`

Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
188 changes: 157 additions & 31 deletions ls8/cpu.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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]
5 changes: 4 additions & 1 deletion ls8/ls8.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

import sys
from cpu import *
import os.path


cpu = CPU()


cpu.load()
cpu.run()
cpu.run()