aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Tuomi <jans.tuomi@gmail.com>2024-02-21 12:02:14 +0200
committerJan Tuomi <jans.tuomi@gmail.com>2024-02-21 12:02:14 +0200
commita0b4a1a6107e4dc905b21858d8ac76c47293cfb4 (patch)
treef7ec4d592a6ee7bf6f1e61ef805643c83fedf740
parent549901c85044b6ccd912688d6be007c2fdacc6c6 (diff)
Add emu
-rw-r--r--.gitignore2
-rw-r--r--.vscode/launch.json2
-rw-r--r--.vscode/settings.json4
-rw-r--r--Makefile4
-rw-r--r--atk16-utils/pad_bin.py59
-rwxr-xr-xatk16-utils/ucode.py2
-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
-rw-r--r--resources/asm/just_hlt.atk161
15 files changed, 514 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index 951eb43..1566ed3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/Makefile b/Makefile
index cc2267b..8658fe1 100644
--- a/Makefile
+++ b/Makefile
@@ -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