diff options
| author | Jan Tuomi <jans.tuomi@gmail.com> | 2024-02-21 12:02:14 +0200 |
|---|---|---|
| committer | Jan Tuomi <jans.tuomi@gmail.com> | 2024-02-21 12:02:14 +0200 |
| commit | a0b4a1a6107e4dc905b21858d8ac76c47293cfb4 (patch) | |
| tree | f7ec4d592a6ee7bf6f1e61ef805643c83fedf740 /atk16_emu | |
| parent | 549901c85044b6ccd912688d6be007c2fdacc6c6 (diff) | |
Add emu
Diffstat (limited to 'atk16_emu')
| -rw-r--r-- | atk16_emu/__init__.py | 0 | ||||
| -rw-r--r-- | atk16_emu/cli.py | 50 | ||||
| -rw-r--r-- | atk16_emu/emu.py | 234 | ||||
| -rw-r--r-- | atk16_emu/opcodes.py | 38 | ||||
| -rw-r--r-- | atk16_emu/test/__init__.py | 0 | ||||
| -rw-r--r-- | atk16_emu/test/test_opcode_alr.py | 78 | ||||
| -rw-r--r-- | atk16_emu/test/test_opcode_hlt.py | 18 | ||||
| -rw-r--r-- | atk16_emu/test/utils.py | 25 |
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 |
