aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/assembler.py263
-rw-r--r--src/test.atk1633
-rwxr-xr-xsrc/test_alu.py48
-rw-r--r--src/test_utils.py93
-rwxr-xr-xsrc/ucode.py103
5 files changed, 540 insertions, 0 deletions
diff --git a/src/assembler.py b/src/assembler.py
new file mode 100755
index 0000000..9e2d92f
--- /dev/null
+++ b/src/assembler.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python3
+# Assemble ATK16 assembly to bytecode
+
+import sys
+
+if len(sys.argv) != 3:
+ print("usage: assembler.py <infile> <outfile> # read from file")
+ print(" assembler.py - <outfile> # read from stdin")
+ sys.exit(1)
+
+infile_path = sys.argv[1]
+outfile_path = sys.argv[2]
+
+src = ""
+if (infile_path == "-"):
+ for line in sys.stdin:
+ src += line
+else:
+ with open(infile_path, "r") as f:
+ src = f.read()
+
+src_lines = src.splitlines()
+
+### Utils
+
+def parse(line: str) -> list[str]:
+ depth = 0
+ result = []
+ acc = ""
+ for c in line:
+ if c.isspace() and depth == 0:
+ result.append(acc)
+ acc = ""
+ elif c == "(":
+ depth += 1
+ acc += "("
+ elif c == ")":
+ depth -= 1
+ acc += ")"
+ else:
+ acc += c
+
+ result.append(acc)
+ return list(filter(lambda x: len(x) > 0, result))
+
+def eval_expr(expr: str) -> int:
+ expr = eval_symbol(expr)
+ return eval(expr, labels) # eval as Python expr
+
+def eval_symbol(c: str):
+ if c in labels:
+ return str(labels[c])
+
+ match c:
+ # Registers
+ case "rz": return "0"
+ case "ra": return "1"
+ case "rb": return "2"
+ case "rc": return "3"
+ case "rd": return "4"
+ case "pa": return "5"
+ case "pb": return "6"
+ # ALU instructions
+ case "al_clear": return "0"
+ case "al_b_minus_a": return "1"
+ case "al_a_minus_b": return "2"
+ case "al_a_plus_b": return "3"
+ case "al_a_xor_b": return "4"
+ case "al_a_or_b": return "5"
+ case "al_a_and_b": return "6"
+ case "al_preset": return "7"
+ case "al_logical_shift_right": return "8"
+ case "al_arithmetic_shift_right": return "9"
+ case "al_logical_shift_left": return "10"
+ # ALU flags
+ case "f_carry": return "0"
+ case "f_overflow": return "1"
+ case "f_zero": return "2"
+ case "f_sign": return "3"
+ case _: return c
+
+# 1st pass, gather labels
+
+labels: dict[str, int] = {}
+address = 0
+
+for (lineNo, line) in enumerate(src_lines):
+ line = line.strip()
+ if line == "": continue
+ keyword, *args = line.lower().split()
+ match keyword:
+ case "@address":
+ address = eval(args[0])
+ continue
+ case "@label":
+ labels[args[0]] = address
+ continue
+
+ address += 1
+
+print("labels:", labels)
+
+# 2nd pass
+
+address = 0
+nop = bytearray([0b1000_0000, 0])
+result = bytearray()
+# initially one nop
+result.extend(nop)
+
+for (lineNo, line) in enumerate(src_lines):
+ line = line.split(";")[0].strip()
+ if line == "": continue
+ keyword, *args = parse(line.lower())
+ match keyword:
+ # Directives
+ case "@address":
+ address = eval_expr(args[0])
+ continue
+ case "@label":
+ continue
+
+ # Instructions
+ case "alu":
+ word = (0b0000 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 6) + \
+ (eval_expr(args[2]) << 3) + \
+ (eval_expr(args[3]) << 0)
+ case "als":
+ word = (0b0001 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 6) + \
+ (eval_expr(args[2]) << 3) + \
+ (eval_expr(args[3]) << 0)
+ case "ldr":
+ word = (0b0010 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 6)
+ case "str":
+ word = (0b0011 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 6)
+ case "ldi":
+ word = (0b0100 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 0)
+ case "jmp":
+ word = (0b0101 << 12) + \
+ (eval_expr(args[0]) << 9)
+ case "br":
+ word = (0b0110 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 6)
+ case "hlt":
+ word = 0b1111 << 12
+
+ # Pseudoinstructions
+ case "add":
+ word = (0b0000 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 6) + \
+ (eval_expr(args[2]) << 3) + \
+ (eval_expr("al_a_plus_b") << 0)
+ case "sub":
+ word = (0b0000 << 12) + \
+ (eval_expr(args[0]) << 9) + \
+ (eval_expr(args[1]) << 6) + \
+ (eval_expr(args[2]) << 3) + \
+ (eval_expr("al_a_minus_b") << 0)
+ case "mov":
+ word = (0b0000 << 12) + \
+ (eval_expr("rz") << 9) + \
+ (eval_expr(args[0]) << 6) + \
+ (eval_expr(args[1]) << 3) + \
+ (eval_expr("al_a_plus_b") << 0)
+
+ # Default case: evaluate as is (e.g. data word)
+ case _:
+ try:
+ word = eval_expr(keyword)
+ except:
+ raise Exception(f"Invalid assembly at {infile_path}:{lineNo + 1}\n\n{line}")
+
+ if len(result) < 2 * address + 1:
+ result.extend((2 * address + 1 - len(result)) * nop)
+
+ print(f"{address:>08x} 0x{word:>04x} {line}")
+ result[2 * address + 0] = ((word >> 8) & 0xff)
+ result[2 * address + 1] = ((word >> 0) & 0xff)
+ address += 1
+
+with open(outfile_path, "wb") as f:
+ f.write(result)
+
+print(f"Wrote {len(result)} bytes to {outfile_path}")
+
+with open(f"{outfile_path}.logisim.txt", "w") as f:
+ f.write("v2.0 raw\n")
+ run_length = 0
+ run_last = ""
+ for i in range(len(result) // 2):
+ b0 = result[2 * i + 0]
+ b1 = result[2 * i + 1]
+ word = f"{b0:>02x}{b1:>02x}\n"
+
+ if word != run_last and run_length <= 1:
+ f.write(f"{run_last}")
+ run_length = 1
+ run_last = word
+ elif word != run_last and run_length > 1:
+ f.write(f"{run_length}*{run_last}")
+ run_length = 1
+ run_last = word
+ else:
+ run_length += 1
+
+ if run_length <= 1:
+ f.write(f"{run_last}")
+ else:
+ f.write(f"{run_length}*{run_last}")
+
+print(f"Wrote Logisim image format to {outfile_path}.logisim.txt")
+
+with open(f"{outfile_path}.ver.txt", "w") as f:
+ f.write("addr/data: 15 16")
+ run_length = 0
+ run_last = ""
+ written = -1
+
+ def update_layout():
+ if run_last == "": return
+ if written % 8 == 0:
+ f.write("\n")
+ else:
+ f.write(" ")
+
+ for i in range(len(result) // 2):
+ b0 = result[2 * i + 0]
+ b1 = result[2 * i + 1]
+ word = f"{b0:>02x}{b1:>02x}"
+
+ if word != run_last and run_length <= 1:
+ f.write(f"{run_last}")
+ run_length = 1
+ run_last = word
+ written += 1
+ update_layout()
+ elif word != run_last and run_length > 1:
+ f.write(f"{run_length}*{run_last}")
+ run_length = 1
+ run_last = word
+ written += 1
+ update_layout()
+ else:
+ run_length += 1
+
+ if run_length <= 1:
+ f.write(f"{run_last}")
+ else:
+ f.write(f"{run_length}*{run_last}")
+
+print(f"Wrote verification test format to {outfile_path}.ver.txt")
diff --git a/src/test.atk16 b/src/test.atk16
new file mode 100644
index 0000000..399bd7e
--- /dev/null
+++ b/src/test.atk16
@@ -0,0 +1,33 @@
+; Program: sum two values and store the result in RAM
+
+; ROM (and program execution) starts at offset 0x0
+@address 0x0
+ ldi RA program
+ jmp RA
+
+@label ram_offset
+ 0x8000 ; store ram offset for later memory access
+
+@label program
+ ldi RA 10 ; RA := 10
+ ldi RB 20 ; RB := 10
+ add RA RB RC ; RC := RA + RB
+ ldi RD ram_offset ; store address of ram_offset in RD
+ ldr RD RD ; dereference ram_offset address
+@label debug
+ str RD RC ; store RC in RAM
+
+; Check that 10 + 20 = 30
+ mov RC RA ; RA := result of sum
+ ldi RB 30 ; RB := 30
+ ldi RD success
+ sub RA RB RC ; RC := RA - RB
+ br f_zero RD ; if result is zero, jump to success
+
+ ldi RA 2 ; RA := 2 to signal failure
+ hlt
+
+; Else
+@label success
+ ldi RA 1 ; RA := 1 to signal success
+ hlt
diff --git a/src/test_alu.py b/src/test_alu.py
new file mode 100755
index 0000000..af30443
--- /dev/null
+++ b/src/test_alu.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+
+from test_utils import *
+
+def make_alu_table() -> str:
+ alu = ALU()
+ n = 10000
+ alu.generate_cases(n)
+ alu.process()
+ print(f"[test] running {n} ALU test cases")
+ return alu.to_table().strip()
+
+class ALU(TestGroup):
+ def __init__(self):
+ TestGroup.__init__(self, [
+ Param("A", 0x0, 0xffff),
+ Param("B", 0x0, 0xffff),
+ Param("S", 0, 7),
+ ], [
+ "Y",
+ "FLAGS",
+ ])
+
+ def process(self):
+ new_cases = []
+ for kase in self.cases:
+ S = kase["S"]
+ A = kase["A"]
+ B = kase["B"]
+ kase["FLAGS"] = "x"
+
+ match S:
+ case 0: Y = 0
+ case 1: Y = (B - A) & 0xFFFF
+ case 2: Y = (A - B) & 0xFFFF
+ case 3: Y = (A + B) & 0xFFFF
+ case 4: Y = (A ^ B) & 0xFFFF
+ case 5: Y = (A | B) & 0xFFFF
+ case 6: Y = (A & B) & 0xFFFF
+ case 7: Y = 0xffff
+
+ kase["Y"] = Y
+ new_cases.append(kase)
+
+ self.cases = new_cases
+
+table = make_alu_table()
+run_test("ALU", table)
diff --git a/src/test_utils.py b/src/test_utils.py
new file mode 100644
index 0000000..1db23b2
--- /dev/null
+++ b/src/test_utils.py
@@ -0,0 +1,93 @@
+import random
+from dataclasses import dataclass
+import tempfile
+import sys
+import os
+
+def make_test_file(name: str, table: str) -> str:
+ return f"""<?xml version="1.0" encoding="utf-8"?>
+<circuit>
+ <version>2</version>
+ <attributes/>
+ <visualElements>
+ <visualElement>
+ <elementName>Testcase</elementName>
+ <elementAttributes>
+ <entry>
+ <string>Label</string>
+ <string>{name}</string>
+ </entry>
+ <entry>
+ <string>Testdata</string>
+ <testData>
+ <dataString>{table}</dataString>
+ </testData>
+ </entry>
+ </elementAttributes>
+ <pos x="200" y="200"/>
+ </visualElement>
+ </visualElements>
+ <wires/>
+ <measurementOrdering/>
+</circuit>
+ """
+
+@dataclass
+class Param:
+ name: str
+ min: int
+ max: int
+
+class TestGroup:
+ def __init__(self, inputs: list[Param], outputs: list[str]):
+ self.inputs = inputs
+ self.outputs = outputs
+ self.cases = []
+
+ def generate_cases(self, n = 1000):
+ for _ in range(n):
+ result = {}
+ for param in self.inputs:
+ val = random.randrange(param.min, param.max + 1)
+ result[param.name] = val
+
+ self.cases.append(result)
+
+ def process(self):
+ raise NotImplemented()
+
+ def to_table(self) -> str:
+ names: list[str] = []
+ for param in self.inputs:
+ names.append(param.name)
+ for output in self.outputs:
+ names.append(output)
+
+ ret = " ".join(names)
+ ret += "\n"
+
+ for case in self.cases:
+ row_vals = []
+ for param_name in names:
+ row_vals.append(str(case[param_name]))
+
+ ret += " ".join(row_vals)
+ ret += "\n"
+
+ return ret
+
+def run_test(name: str, table: str):
+ if len(sys.argv) != 2:
+ print(f"usage: {sys.argv[0]} <circuit.dig>")
+ sys.exit(1)
+
+ circuit_path = sys.argv[1]
+ _, test_path = tempfile.mkstemp(".dig")
+
+ fc = make_test_file(name, table)
+ with open(test_path, "w") as f:
+ f.write(fc)
+
+ stream = os.popen(f"make run-single-test circ={circuit_path} tests={test_path}")
+ output = stream.read()
+ print(output)
diff --git a/src/ucode.py b/src/ucode.py
new file mode 100755
index 0000000..ea5c040
--- /dev/null
+++ b/src/ucode.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# Generate .bin file with ATK16 CPU microcode
+
+# Addressed by OOOO BUUU
+# where O = opcode, B = branch flag, U = microsequencer value
+
+import sys
+
+if len(sys.argv) != 2:
+ print("usage: ucode.py <outfile.bin>")
+ sys.exit(1)
+
+outfile_path = sys.argv[1]
+
+PC_CO = 0b0000_0000_0000_0001
+PC_IE = 0b0000_0000_0000_0010
+PC_OE = 0b0000_0000_0000_0100
+MAR_IE = 0b0000_0000_0000_1000
+RAM_IE = 0b0000_0000_0001_0000
+RAM_OE = 0b0000_0000_0010_0000
+R1_IE = 0b0000_0000_0100_0000
+R1_OE = 0b0000_0000_1000_0000
+R2_IE = 0b0000_0001_0000_0000
+R2_OE = 0b0000_0010_0000_0000
+IR_IE = 0b0000_0100_0000_0000
+L9_OE = 0b0000_1000_0000_0000
+L12_OE = 0b0001_0000_0000_0000
+ALU_OE = 0b0010_0000_0000_0000
+HALT = 0b0100_0000_0000_0000
+US_RS = 0b1000_0000_0000_0000
+
+BRANCH_FLAG_STATES_N = 2
+UCODE_N = 2**3
+CONTROL_WORD_SIZE = 2
+
+def not_branch(bs):
+ return BRANCH_FLAG_STATES_N * [bs]
+
+def branch(false_branch, true_branch):
+ return [false_branch, true_branch]
+
+fetch = [PC_OE|MAR_IE, RAM_OE|IR_IE|PC_CO]
+
+def nop():
+ return not_branch([*fetch, US_RS, 0, 0, 0, 0, 0])
+
+ucode = [
+ # ALU 0000 LLLR RRTT TSSS
+ not_branch([*fetch, ALU_OE, US_RS, 0, 0, 0, 0]),
+ # ALS 0001 LLLR RRTT TSSS
+ not_branch([*fetch, ALU_OE, US_RS, 0, 0, 0, 0]),
+ # LDR 0010 RRRT TTXX XXXX
+ not_branch([*fetch, R1_OE|MAR_IE, RAM_OE|R2_IE, US_RS, 0, 0, 0]),
+ # STR 0011 RRRT TTXX XXXX
+ not_branch([*fetch, R1_OE|MAR_IE, R2_OE|RAM_IE, US_RS, 0, 0, 0]),
+ # LDI 0100 RRRI IIII IIII
+ not_branch([*fetch, L9_OE|R1_IE, US_RS, 0, 0, 0, 0]),
+ # JMP 0101 RRRX XXXX XXXX
+ not_branch([*fetch, R1_OE|PC_IE, US_RS, 0, 0, 0, 0]),
+ # BR 0110 XFFR RRXX XXXX
+ branch([*fetch, US_RS, 0, 0, 0, 0, 0],
+ [*fetch, R2_OE|PC_IE, US_RS, 0, 0, 0, 0]),
+ # NOP 0111 XXXX XXXX XXXX
+ nop(),
+ # NOP 1000 XXXX XXXX XXXX
+ nop(),
+ # NOP 1001 XXXX XXXX XXXX
+ nop(),
+ # NOP 1010 XXXX XXXX XXXX
+ nop(),
+ # NOP 1011 XXXX XXXX XXXX
+ nop(),
+ # NOP 1100 XXXX XXXX XXXX
+ nop(),
+ # NOP 1101 XXXX XXXX XXXX
+ nop(),
+ # NOP 1110 XXXX XXXX XXXX
+ nop(),
+ # HLT 1111 XXXX XXXX XXXX
+ not_branch([*fetch, HALT, 0, 0, 0, 0, 0]),
+]
+
+INST_N = len(ucode)
+TOTAL_BYTEARRAY_SIZE = INST_N * BRANCH_FLAG_STATES_N * UCODE_N * CONTROL_WORD_SIZE
+
+res_b = bytearray(TOTAL_BYTEARRAY_SIZE)
+
+for i in range(INST_N):
+ for j in range(BRANCH_FLAG_STATES_N):
+ for k in range(UCODE_N):
+ for l in range(CONTROL_WORD_SIZE):
+ idx = l + \
+ k * CONTROL_WORD_SIZE + \
+ j * CONTROL_WORD_SIZE * UCODE_N + \
+ i * CONTROL_WORD_SIZE * UCODE_N * BRANCH_FLAG_STATES_N
+ cword = ucode[i][j][k]
+ assert len(ucode[i][j]) == 8
+ cbyte = (cword >> (8 * (CONTROL_WORD_SIZE - l - 1))) & 0xff
+ print(f"inst: {i:>04b}, idx: {idx:>04x}, cbyte: {cbyte:>08b}")
+ res_b[idx] = cbyte
+
+with open(outfile_path, "wb") as f:
+ f.write(res_b)