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 | |
| parent | 549901c85044b6ccd912688d6be007c2fdacc6c6 (diff) | |
Add emu
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | .vscode/launch.json | 2 | ||||
| -rw-r--r-- | .vscode/settings.json | 4 | ||||
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | atk16-utils/pad_bin.py | 59 | ||||
| -rwxr-xr-x | atk16-utils/ucode.py | 2 | ||||
| -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 | ||||
| -rw-r--r-- | resources/asm/just_hlt.atk16 | 1 |
15 files changed, 514 insertions, 3 deletions
@@ -5,3 +5,5 @@ out/ .DS_Store venv/ +__pycache__ +.pytest_cache diff --git a/.vscode/launch.json b/.vscode/launch.json index 22dd52d..a7efb5f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "program": "${file}", "console": "integratedTerminal", "justMyCode": true, - "args": ["src/test.atk16", "out/test.bin"] + "args": ["out/just_hlt.bin"] } ] }
\ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3516cb9..8d4fc35 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,5 @@ { - "python.formatting.provider": "autopep8" + "python.formatting.provider": "autopep8", + "editor.tabSize": 2, + "editor.detectIndentation": false }
\ No newline at end of file @@ -34,6 +34,10 @@ ast-compile: bytecode-compile: $(py) atk16-bytecode-compiler/compiler.py $(in) $(out) +# make emu in=rom.bin +emu: + $(py) atk16-emu/cli.py $(in) + # make test test: $(py) atk16-asm/test_alu.py digital-diagrams/atk16_alu.dig diff --git a/atk16-utils/pad_bin.py b/atk16-utils/pad_bin.py new file mode 100644 index 0000000..9f014d1 --- /dev/null +++ b/atk16-utils/pad_bin.py @@ -0,0 +1,59 @@ +import os + +def pad_binary(file_path, target_size) -> int: + """ + Pads a binary file with zeros until it reaches a specified size in KiB. + + :param file_path: Path to the binary file to be padded. + :param target_size: Target size in bytes. + """ + + # Check current file size + current_size = os.path.getsize(file_path) + + # Calculate needed padding + padding_size = target_size - current_size + + # Append zeros if needed + if padding_size > 0: + with open(file_path, 'ab') as file: + file.write(b'\x00' * padding_size) + + return padding_size + +def convert_size_to_bytes(size_str: str) -> int: + """ + Converts a size string with K, M, or G suffix to bytes. + + :param size_str: Size string (e.g., "64K", "1M", "2G"). + :return: Size in bytes. + """ + size_str = size_str.upper() + if size_str.endswith('K'): + return int(size_str[:-1]) * 1024 + elif size_str.endswith('M'): + return int(size_str[:-1]) * 1024 ** 2 + elif size_str.endswith('G'): + return int(size_str[:-1]) * 1024 ** 3 + else: + return int(size_str) + +if __name__ == "__main__": + import argparse + + # Setup argument parser + parser = argparse.ArgumentParser(description='Pad a binary file with zeros until it reaches a specified size.') + parser.add_argument('file', type=str, help='Path to the binary file to be padded.') + parser.add_argument('--to', dest='size', type=str, help='Target size (e.g., 64K, 1M, 2G).', required=True) + + args = parser.parse_args() + + # Convert size argument to bytes + target_size_bytes = convert_size_to_bytes(args.size) + + padded_bytes_n = pad_binary(args.file, target_size_bytes) + + if padded_bytes_n > 0: + print(f"File has been padded with {padded_bytes_n} zeros.") + else: + print("File is already equal to or larger than the target size. No padding added.") diff --git a/atk16-utils/ucode.py b/atk16-utils/ucode.py index 07d0554..22a2ce2 100755 --- a/atk16-utils/ucode.py +++ b/atk16-utils/ucode.py @@ -83,7 +83,7 @@ ucode = [ not_branch([IM_EN|PC_OE|IPC_IE, ISRA_OE|MAR_IE, MEM_OE|PC_IE, US_RS, 0, 0, 0, 0]), # ISRP1 1101 XXXX XXXX XXXX not_branch([IM_EN|PC_OE|IPC_IE, ISRA_OE|MAR_IE, MEM_OE|PC_IE, US_RS, 0, 0, 0, 0]), - # RTI 1110 XXXX XXXX XXXX + # RTI 1110 XXXX XXXX XXXX not_branch([*fetch, IM_DS|IPC_OE|PC_IE, US_RS, 0, 0, 0, 0]), # HLT 1111 XXXX XXXX XXXX not_branch([*fetch, HALT, 0, 0, 0, 0, 0]), 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 diff --git a/resources/asm/just_hlt.atk16 b/resources/asm/just_hlt.atk16 new file mode 100644 index 0000000..88320b9 --- /dev/null +++ b/resources/asm/just_hlt.atk16 @@ -0,0 +1 @@ +hlt |
