aboutsummaryrefslogtreecommitdiffstats
path: root/atk16_emu
diff options
context:
space:
mode:
Diffstat (limited to 'atk16_emu')
-rw-r--r--atk16_emu/__init__.py0
-rw-r--r--atk16_emu/cli.py50
-rw-r--r--atk16_emu/emu.py234
-rw-r--r--atk16_emu/opcodes.py38
-rw-r--r--atk16_emu/test/__init__.py0
-rw-r--r--atk16_emu/test/test_opcode_alr.py78
-rw-r--r--atk16_emu/test/test_opcode_hlt.py18
-rw-r--r--atk16_emu/test/utils.py25
8 files changed, 443 insertions, 0 deletions
diff --git a/atk16_emu/__init__.py b/atk16_emu/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atk16_emu/__init__.py
diff --git a/atk16_emu/cli.py b/atk16_emu/cli.py
new file mode 100644
index 0000000..dd06f67
--- /dev/null
+++ b/atk16_emu/cli.py
@@ -0,0 +1,50 @@
+import sys
+from dataclasses import dataclass
+from emu import Machine
+
+@dataclass
+class Options:
+ rom_image_path: str | None
+ debugger_enabled: bool
+
+options = Options(
+ rom_image_path=None,
+ debugger_enabled=False,
+)
+
+def print_help():
+ print("Usage: emu_cli.py [-d] [-h|--help] <rom_path>")
+ print("")
+ print("Options:")
+ print(" <rom_path>: path to the rom image")
+ print(" -h: print help")
+ print(" -d: enable debugger")
+
+for arg in sys.argv[1:]:
+ if arg == "-d":
+ options.debugger_enabled = True
+ elif arg == "-h" or arg == "--help" or arg == "-?":
+ print_help()
+ sys.exit(0)
+ elif arg[0] == "-":
+ print(f"Unknown option: {arg}")
+ print_help()
+ sys.exit(1)
+ else:
+ options.rom_image_path = arg
+
+if options.rom_image_path is None:
+ print_help()
+ sys.exit(1)
+
+def load_rom_image_from_path(path: str) -> bytearray:
+ with open(path, "rb") as f:
+ return bytearray(f.read())
+
+rom_image = load_rom_image_from_path(options.rom_image_path)
+
+machine = Machine()
+machine.load_rom_image(rom_image)
+machine.reset()
+machine.run_until_halted()
+print(machine.get_system_state())
diff --git a/atk16_emu/emu.py b/atk16_emu/emu.py
new file mode 100644
index 0000000..3c16116
--- /dev/null
+++ b/atk16_emu/emu.py
@@ -0,0 +1,234 @@
+import random
+from typing import Literal
+from dataclasses import dataclass
+from atk16_emu.opcodes import *
+
+class Register:
+ def __init__(self, bits: int):
+ self.bits = bits
+ self.value = random.randint(0, 2 ** bits)
+
+ def set_value(self, value: int):
+ if value < 0 or value >= 2 ** self.bits:
+ raise ValueError(f"Value {value} is out of range for {self.bits}-bit register")
+
+ self.value = value
+
+class Counter:
+ def __init__(self, bits: int):
+ self.bits = bits
+ self.value = random.randint(0, 2 ** bits)
+
+ def step(self):
+ self.value = (self.value + 1) % (2 ** self.bits)
+
+ def reset(self):
+ self.value = 0
+
+class ROM:
+ def __init__(self, addr_bits: int, data_bits: int):
+ self.addr_bits = addr_bits
+ self.data_bits = data_bits
+ self.memory = [random.randint(0, 2 ** data_bits) for _ in range(2 ** addr_bits)]
+
+ def read(self, addr: int) -> int:
+ if addr < 0 or addr >= 2 ** self.addr_bits:
+ raise ValueError(f"Address {addr} is out of range for {self.addr_bits}-bit ROM")
+
+ return self.memory[addr]
+
+class RAM:
+ def __init__(self, addr_bits: int, data_bits: int):
+ self.addr_bits = addr_bits
+ self.data_bits = data_bits
+ self.memory = [random.randint(0, 2 ** data_bits) for _ in range(2 ** addr_bits)]
+
+ def read(self, addr: int) -> int:
+ if addr < 0 or addr >= 2 ** self.addr_bits:
+ raise ValueError(f"Address {addr} is out of range for {self.addr_bits}-bit RAM")
+
+ return self.memory[addr]
+
+ def write(self, addr: int, value: int):
+ if addr < 0 or addr >= 2 ** self.addr_bits:
+ raise ValueError(f"Address {addr} is out of range for {self.addr_bits}-bit RAM")
+
+ if value < 0 or value >= 2 ** self.data_bits:
+ raise ValueError(f"Value {value} is out of range for {self.data_bits}-bit RAM")
+
+ self.memory[addr] = value
+
+@dataclass
+class ALUFlags:
+ carry: bool
+ overflow: bool
+ zero: bool
+ sign: bool
+
+@dataclass
+class ALUResult:
+ value: int
+ flags: ALUFlags
+
+class ALU:
+ def __init__(self):
+ pass
+
+ def process(self, S: int, L: int, R: int):
+ if S < 0 or S >= 8:
+ raise ValueError(f"Invalid ALU S: {S}")
+
+ if L < 0 or L >= 2 ** 16:
+ raise ValueError(f"Invalid ALU L: {L}")
+
+ if R < 0 or R >= 2 ** 16:
+ raise ValueError(f"Invalid ALU R: {R}")
+
+ match S:
+ case 0: # L + R
+ py_sum = L + R
+ result = py_sum & 0xFFFF
+ return ALUResult(result, ALUFlags(
+ carry = py_sum >= 2 ** 16,
+ overflow = (L & 0x8000) == (R & 0x8000) and (L & 0x8000) != (result & 0x8000),
+ zero = result == 0,
+ sign = (result & 0x8000) != 0
+ ))
+
+ raise NotImplementedError(f"{S}")
+
+
+class Machine:
+ def __init__(self):
+ self.rom = ROM(16, 16)
+ self.ram = RAM(16, 16)
+ self.alu = ALU()
+
+ self.ra = Register(16)
+ self.rb = Register(16)
+ self.rc = Register(16)
+ self.rd = Register(16)
+ self.re = Register(16)
+ self.rf = Register(16)
+ self.rg = Register(16)
+ self.rh = Register(16)
+
+ self.pc = Counter(16)
+ self.fr = ALUFlags(
+ carry = False,
+ overflow = False,
+ zero = False,
+ sign = False,
+ )
+
+ self.running = False
+
+ def mem_read(self, addr: int):
+ if addr < 2 ** 15:
+ return self.rom.read(addr)
+ else:
+ # TODO: Implement memory-mapped I/O
+ return self.ram.read(addr)
+
+ def get_nth_register(self, n: int) -> Register:
+ if n < 0 or n >= 8:
+ raise ValueError(f"Invalid register number: {n}")
+
+ s = chr(ord("a") + n)
+ return self.__getattribute__(f"r{s}")
+
+ def load_rom_image(self, bytes: bytearray):
+ if len(bytes) != 2 ** 16:
+ raise ValueError("ROM image must be 64 KiB")
+
+ # Loop over bytes, constructing two-byte words and storing them in ROM
+ i = 0
+ while i < len(bytes):
+ high_byte = bytes[i]
+ low_byte = bytes[i + 1]
+ word = (high_byte << 8) | low_byte
+ self.rom.memory[i // 2] = word
+ i += 2
+
+ def reset(self):
+ self.pc.reset()
+ self.running = False
+
+ def run(self):
+ "Set running = True."
+ self.running = True
+
+ def run_until_halted(self):
+ "Run the machine until HLT instruction is encountered"
+ self.running = True
+ while self.running:
+ self.step()
+
+ def step(self):
+ if not self.running:
+ raise RuntimeError("Machine is not running")
+
+ pc_addr = self.pc.value
+ self.pc.step()
+
+ instr = self.mem_read(pc_addr)
+ instruction = self.decode(instr)
+
+ match instruction:
+ case ALR(target, left, right, alu_code):
+ alu_result = self.alu.process(
+ S = alu_code,
+ L = self.get_nth_register(left).value,
+ R = self.get_nth_register(right).value,
+ )
+ self.fr = alu_result.flags
+
+ target_reg = self.get_nth_register(target)
+ target_reg.value = alu_result.value
+
+ case ALI(target, left, imm, alu_code):
+ alu_result = self.alu.process(
+ S = alu_code,
+ L = self.get_nth_register(left).value,
+ R = imm,
+ )
+ self.fr = alu_result.flags
+
+ target_reg = self.get_nth_register(target)
+ target_reg.value = alu_result.value
+
+ case HLT():
+ self.running = False
+
+ def decode(self, instr: int):
+ opcode = (instr & 0xF000) >> 12
+ opdata = instr & 0x0FFF
+ match opcode:
+ case 0b0000: return ALR(target=(opdata & 0b111000000000) >> 9,
+ left=(opdata & 0b000111000000) >> 6,
+ right=(opdata & 0b000000111000) >> 3,
+ alu_code=opdata & 0b000000000111)
+ case 0b1111: return HLT()
+ # TODO: Implement other opcodes
+
+ raise ValueError(f"Invalid instruction: {instr:>16b}")
+
+ def get_system_state(self):
+ return {
+ "running": self.running,
+
+ "ra": self.ra.value,
+ "rb": self.rb.value,
+ "rc": self.rc.value,
+ "rd": self.rd.value,
+ "re": self.re.value,
+ "rf": self.rf.value,
+ "rg": self.rg.value,
+ "rh": self.rh.value,
+
+ "pc": self.pc.value,
+ "fr": self.fr,
+
+ #"rom": self.rom.memory,
+ #"ram": self.ram.memory,
+ }
diff --git a/atk16_emu/opcodes.py b/atk16_emu/opcodes.py
new file mode 100644
index 0000000..268c873
--- /dev/null
+++ b/atk16_emu/opcodes.py
@@ -0,0 +1,38 @@
+from typing import Literal
+from dataclasses import dataclass
+
+Opcode = Literal[
+ "ALR",
+ "ALI",
+ "LDR",
+ "STR",
+ "LDI",
+ "JPR",
+ "JPI",
+ "BRR",
+ "BRI",
+ "LPC",
+ "NOP",
+ "ISRP0",
+ "ISRP1",
+ "RTI",
+ "HLT",
+]
+
+@dataclass
+class ALR():
+ target: int
+ left: int
+ right: int
+ alu_code: int
+
+@dataclass
+class ALI():
+ target: int
+ left: int
+ imm: int
+ alu_code: int
+
+@dataclass
+class HLT():
+ pass
diff --git a/atk16_emu/test/__init__.py b/atk16_emu/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atk16_emu/test/__init__.py
diff --git a/atk16_emu/test/test_opcode_alr.py b/atk16_emu/test/test_opcode_alr.py
new file mode 100644
index 0000000..c54d70e
--- /dev/null
+++ b/atk16_emu/test/test_opcode_alr.py
@@ -0,0 +1,78 @@
+from atk16_emu.emu import Machine
+from atk16_emu.test.utils import make_rom
+
+def test_alr_s0_small_unsigned():
+ machine = Machine()
+
+ rom_image = make_rom([0b0000_010_000_001_000]) # ALR target=RC left=RA right=RB alu_code=0
+ machine.load_rom_image(rom_image)
+ machine.reset()
+
+ machine.ra.value = 10
+ machine.rb.value = 20
+
+ machine.run()
+ machine.step()
+
+ assert machine.rc.value == 30
+ assert machine.fr.carry == 0
+ assert machine.fr.overflow == 0
+ assert machine.fr.zero == 0
+ assert machine.fr.sign == 0
+
+def test_alr_s0_large_unsigned():
+ machine = Machine()
+
+ rom_image = make_rom([0b0000_010_000_001_000]) # ALR target=RC left=RA right=RB alu_code=0
+ machine.load_rom_image(rom_image)
+ machine.reset()
+
+ machine.ra.value = 0xFFFF
+ machine.rb.value = 20
+
+ machine.run()
+ machine.step()
+
+ assert machine.rc.value == 19
+ assert machine.fr.carry == 1
+ assert machine.fr.overflow == 0
+ assert machine.fr.zero == 0
+ assert machine.fr.sign == 0
+
+def test_alr_s0_small_signed():
+ machine = Machine()
+
+ rom_image = make_rom([0b0000_010_000_001_000]) # ALR target=RC left=RA right=RB alu_code=0
+ machine.load_rom_image(rom_image)
+ machine.reset()
+
+ machine.ra.value = 0xFFFF # -1
+ machine.rb.value = 0x0001 # 1
+
+ machine.run()
+ machine.step()
+
+ assert machine.rc.value == 0
+ assert machine.fr.carry == 1
+ assert machine.fr.overflow == 0
+ assert machine.fr.zero == 1
+ assert machine.fr.sign == 0
+
+def test_alr_s0_large_signed():
+ machine = Machine()
+
+ rom_image = make_rom([0b0000_010_000_001_000]) # ALR target=RC left=RA right=RB alu_code=0
+ machine.load_rom_image(rom_image)
+ machine.reset()
+
+ machine.ra.value = 0x7FFF # 32767
+ machine.rb.value = 0x0001 # 1
+
+ machine.run()
+ machine.step()
+
+ assert machine.rc.value == 0x8000 # -32768
+ assert machine.fr.carry == 0
+ assert machine.fr.overflow == 1
+ assert machine.fr.zero == 0
+ assert machine.fr.sign == 1 \ No newline at end of file
diff --git a/atk16_emu/test/test_opcode_hlt.py b/atk16_emu/test/test_opcode_hlt.py
new file mode 100644
index 0000000..5cae888
--- /dev/null
+++ b/atk16_emu/test/test_opcode_hlt.py
@@ -0,0 +1,18 @@
+from atk16_emu.emu import Machine
+from atk16_emu.test.utils import make_rom
+
+def test_hlt():
+ machine = Machine()
+
+ rom_image = make_rom([0b1111_0000_0000_0000]) # HLT
+ machine.load_rom_image(rom_image)
+ machine.reset()
+ machine.run()
+
+ assert machine.pc.value == 0
+ assert machine.running == True
+
+ machine.step()
+
+ assert machine.pc.value == 1
+ assert machine.running == False
diff --git a/atk16_emu/test/utils.py b/atk16_emu/test/utils.py
new file mode 100644
index 0000000..0d40d54
--- /dev/null
+++ b/atk16_emu/test/utils.py
@@ -0,0 +1,25 @@
+def pad_bytearray(to_length: int, ba: bytearray) -> bytearray:
+ """
+ Pads a bytearray with zeros until it reaches a specified length.
+
+ :param to_length: Target length in bytes.
+ :param ba: The bytearray to be padded.
+ :return: The padded bytearray.
+ """
+ padding_size = to_length - len(ba)
+ if padding_size > 0:
+ ba.extend(b'\x00' * padding_size)
+ return ba
+
+def make_rom(words: list[int]) -> bytearray:
+ """
+ Constructs a ROM image from a list of 16-bit words.
+
+ :param words: The list of 16-bit words.
+ :return: The ROM image as a bytearray.
+ """
+ bytes = []
+ for word in words:
+ bytes.append((word >> 8) & 0xFF)
+ bytes.append(word & 0xFF)
+ return pad_bytearray(64 * 1024, bytearray(bytes)) \ No newline at end of file