diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/assembler.py | 263 | ||||
| -rw-r--r-- | src/test.atk16 | 33 | ||||
| -rwxr-xr-x | src/test_alu.py | 48 | ||||
| -rw-r--r-- | src/test_utils.py | 93 | ||||
| -rwxr-xr-x | src/ucode.py | 103 |
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) |
