#!/usr/bin/env python3 """ simk16.py --- A simulator for the Kathleen16 Mk. 1 processor. The simulator takes as input two Logisim raw files, one of code for the ROM and one of data for the RAM of the Kathleen16. If only one file is given, it is used for the ROM and the RAM is uninitialized. The simulator functions under the assumption that the instruction: beq $r0, $r0, -1 signals the end of the program. Any other convention may lead to an infinite loop. The simulator displays the value of all the registers at the end of the computation. It is also possible to execute the code step-by-step with or without breakpoints after each instruction using the options `-t` and `-s`. Author: Frédéric Goualard Last modified: 2022-11-01/16:50:17/+0100 """ import sys import argparse import logging import array from dataclasses import dataclass @dataclass class Environment: PC: int rom: array.array('H') ram: array.array('i') registers: array.array('i') def setram(self,addr, v): self.ram[addr] = v & 0xffff def getram(self,addr): return self.ram[addr] def setreg(self,i,v): if i != 0: self.registers[i] = v & 0xffff def getreg(self,i): return self.registers[i] def setPC(self,v): self.PC = v & 0xffff def getPC(self): return self.PC def split_RRR(instruction): """ Return the fields from the RRR-type format of instruction. The function does not return the `opcode`. """ regA = (instruction >> 10) & 7 regB = (instruction >> 7) & 7 regC = instruction & 7 return (regA, regB, regC) def split_RRI(instruction): """ Return the fields from the RRI-type format of instruction. The function does not return the `opcode`. """ regA = (instruction >> 10) & 7 regB = (instruction >> 7) & 7 imm7 = instruction & 0x7f # Sign extension if imm7 >= 64: imm7 = imm7 - 128 return (regA, regB, imm7) def split_RI(instruction): """ Return the fields from the RI-type format of instruction. The function does not return the `opcode`. """ regA = (instruction >> 10) & 7 imm10 = instruction & 0x3ff return (regA, imm10) def add(instruction, env): (regA,regB,regC) = split_RRR(instruction) env.setPC(env.getPC() + 1) env.setreg(regA,env.getreg(regB) + env.getreg(regC)) return True def addi(instruction, env): (regA,regB,imm7) = split_RRI(instruction) env.setPC(env.getPC() + 1) env.setreg(regA, env.getreg(regB) + imm7) return True def nand(instruction, env): (regA,regB,regC) = split_RRR(instruction) env.setPC(env.getPC() + 1) x = ~(env.registers[regB] & env.registers[regC]) if x < 0: x = 65536 + x env.setreg(regA, x) return True def lui(instruction, env): (regA,imm10) = split_RI(instruction) env.setPC(env.getPC() + 1) env.setreg(regA, imm10 << 6) return True def sw(instruction, env): (regA,regB,imm7) = split_RRI(instruction) env.setPC(env.getPC() + 1) env.setram(env.getreg(regB) + imm7, env.getreg(regA)) return True def lw(instruction, env): (regA,regB,imm7) = split_RRI(instruction) env.setPC(env.getPC() + 1) env.setreg(regA, env.getram(env.getreg(regB) + imm7)) return True def beq(instruction, env): (regA,regB,imm7) = split_RRI(instruction) oldPC = env.getPC() env.setPC(env.getPC() + 1) if env.getreg(regA) == env.getreg(regB): env.setPC(env.getPC() + imm7) return oldPC != env.PC def jalr(instruction, env): (regA,regB,_) = split_RRI(instruction) env.setreg(regA, env.getPC() + 1) env.setPC(env.getreg(regB)) return True execute_instruction = [add, addi, nand, lui, sw, lw, beq, jalr] def display_registers(env): """ Prettry-print the contents of all the registers in `env` `env.PC` is not displayed """ for i in range(7,3,-1): hexa = "{:04x}".format(registers[i]) #deci = "{:>5}".format(registers[i]) print(f"reg{i}: {hexa}", end=" ") print() for i in range(3,-1,-1): hexa = "{:04x}".format(registers[i]) #deci = "{:>5}".format(registers[i]) print(f"reg{i}: {hexa}", end=" ") print() def execute_program(env): """ Execute the Kathleen machine code in the `env` `Environment` object. """ while True: instruction = env.rom[env.PC] if args.trace or args.step: print(f">>> {env.PC:04x}: {instruction:04x}") opcode = (instruction & 0xe000) >> 13 if not execute_instruction[opcode](instruction, env): return if args.trace or args.step: display_registers(env) if args.step: input("Press a key...") def load_raw_file(name): """ Return a list containing the values in a Logisim raw file whose name is given as a parameter. Raw file format =============== A Logisim raw file is a text file starting with the line `v2.0 raw`. It then contains a list of hexadecimal numbers separated by spaces and, optionally, newlines. The size of the numbers depends on the characteristics of the memory used. For the Kathleen16, each number has four digits. Large sections with `n` times the same value `v` are replaced by the notation `n*v`. Example ------- ```v2.0 raw 13*0 2400 2800 2c3c c983 482 2903 c07c c07f``` Raise an exception if the file cannot be read. """ try: with open(name) as rawfile: # Reading the header and checking we havfe a raw file header = rawfile.readline().rstrip() if header != 'v2.0 raw': print(f"{sys.argv[0]}: ERROR: {name} does not appear to be a raw file.", file=sys.stderr) exit(3) L = [] for line in rawfile: values = line.rstrip().split() for v in values: if '*' in v: # Value of the form `n*i`? (n,i) = v.split('*') L += [int(i,base=16)]*int(n) else: # Value is an hex number? L.append(int(v,base=16)) return L except IOError: print(f"{sys.argv[0]}: ERROR: could not open {name} for reading.",file=sys.stderr) exit(2) except ValueError as e: logging.error(f"{name}:{e}") if __name__ == "__main__": parser = argparse.ArgumentParser(prog="simk16.py", description="A simulator for the Kathleen16 Mk. 1") parser.add_argument("romfile",help="RAW file for the code") parser.add_argument("ramfile",nargs='?',help="RAW file for the data") parser.add_argument("-s","--step",action="store_true", help="perform a step by step execution. Break after each instruction") parser.add_argument("-t","--trace",action="store_true", help="trace the execution and the modification of registers") args = parser.parse_args() rom = array.array('H',load_raw_file(args.romfile)) if args.ramfile is not None: # There is also a RAM file ram = array.array('H',load_raw_file(args.ramfile)) else: ram = array.array('H',[]) # Empty RAM # We extend the RAM to 65536 words since some part not specified in the file # may be used at runtime (e.g., the stack). ram.extend([0]*(65536-len(ram))) # Definition of the register file with eight 16-bit register registers = array.array('H',[0]*8) env = Environment(rom=rom, ram = ram, registers = registers, PC = 0) execute_program(env) display_registers(env)