# NONA assembler - compiles QIXOTE-1 assembly instructions into machine code. # Also includes a streaming output option to communicate directly with the QIXOTE-1 built-in UART/bootloader import sys import serial import time SERIAL_DELAY = 0.004 class Nona(): # Niner Instruction Set KEYW = {'GET':(0o0), 'PUT':(0o1), 'MOV':(0o2), 'JMP':(0o3), 'BUF':(0o0), 'NOT':(0o1), 'ADD':(0o2), 'SUB':(0o3), 'AND':(0o4), 'OR':(0o5), "INC":(0o6), "DEC":(0o7), 'ACC':(0o0), 'MMI':(0o2), 'MEM':(0o3), 'IMM':(0o4), 'IOS':(0o5), 'UNC':(0o0), 'ZER':(0o1), 'NTZ':(0o2) } def __init__(self, file, outfile): self.file = file self.outfile = outfile self.labels = {} self.instructions = [] def run(self): self.lines = [] with open(self.file) as fp: print (f'Compiling {self.file}...') line = fp.readline(); while line: toks = line.strip().split(' ') # skip comments if (len(toks) == 0) or (toks[0].startswith("#")): line = fp.readline() continue toks = [t for t in toks if t] #print (f'tokens: {toks}') self.lines.append(toks) line = fp.readline() fp.close() def scan(self): pc = 0 for line in self.lines: if len(line) == 0: continue # define a const name if line[0].startswith("$"): self.labels[line[0]] = line[1] continue instruction = '' fmt_pc = "{:03o}".format(pc) #starting line label if line[0].endswith(":"): # numeric label is an absolute octal addr if line[0][:-1].isnumeric(): pc = int(line[0][:-1], 8) fmt_pc = "{:03o}".format(pc) else: self.labels[line[0]] = fmt_pc line = line[1:] #print(f'instr: {line}') for t in line: #inline comment if t.startswith('#'): break elif t in self.KEYW: instruction = instruction + str(self.KEYW[t]) elif t.endswith(":") | t.isnumeric() | t.startswith("$"): #print(f"Machine Code: >{instruction}<") if len(instruction) == 0: self.instructions.append([fmt_pc,t,t]) pc = pc + 1 fmt_pc = "{:03o}".format(pc) break self.instructions.append([fmt_pc,instruction," ".join(line)]) instruction = t pc = pc + 1 fmt_pc = "{:03o}".format(pc) else: print(f"ERROR: unknown keyword: {t}") if len(instruction) == 0: continue #print(f"Op Code: >{instruction}<") self.instructions.append([fmt_pc, instruction, "OP: "+instruction]) pc = pc + 1 #print (f'Instructions: {self.instructions}') # convert any stored labels into bumeric values. def elaborate(self) : for inst in self.instructions: if inst[1] in self.labels: inst[1] = self.labels[inst[1]] print(f'Elaborated Instructions: ') for i in self.instructions: print (i) print(f'\nLabels:') for k in self.labels.keys(): print(f'{k}: {self.labels[k]}') # save based on output param. (None=stream via bootloader) def save(self): if self.outfile is None: self.serial_load() elif self.outfile.endswith(".coe"): self.save_coe() elif self.outfile.endswith(".mem"): self.save_mem() # XILINX Coefficient file save format def save_coe(self): o2b = {'0':'000', '1':'001', '2':'010', '3':'011', '4':'100', '5':'101', '6':'110', '7':'111'} with open(self.outfile, 'w') as fp: fp.write("mem_initialization_radix=2;\n") fp.write("mem_initialization_vector=\n") start = False for pc,inst, comment in self.instructions: if start == True: fp.write(",\n") start = True for i in range(0,len(inst)): fp.write(o2b[inst[i]]) fp.write(";\n") fp.close() # readmemb data file format. def save_mem(self): o2b = {'0': '000', '1': '001', '2': '010', '3': '011', '4': '100', '5': '101', '6': '110', '7': '111'} with open(self.outfile, 'w') as fp: fp.write("// NONA $readmemb() MEM file compiled from "+self.file+"\n// \n") file_pc =0 for pc, inst, comment in self.instructions: dec_pc = int(pc,8) if dec_pc > file_pc: for p in range(file_pc,dec_pc): fmt_pc = "{:03o}".format(file_pc) fp.write(f'000000000 // [{fmt_pc}] fill\n') file_pc = dec_pc for i in range(0, len(inst)): fp.write(o2b[inst[i]]) fmt_pc = "{:03o}".format(file_pc) fp.write(f" // [{fmt_pc}] "+comment+"\n") file_pc = file_pc + 1; fp.close() def serial_load(self): checksum = 0; s = serial.Serial('COM4') #s.baudrate = 9600 s.baudrate = 19200 s.stopbits = 1 print ("streaming on COM4...") file_pc = 0 FILL_BYTE = 0 for pc, inst, comment in self.instructions: dec_pc = int(pc, 8) # fill in memory for empty areas between program, data if dec_pc > file_pc: for p in range(file_pc, dec_pc): s.write(FILL_BYTE.to_bytes(1, 'big')) time.sleep(SERIAL_DELAY) s.write(FILL_BYTE.to_bytes(1, 'big')) time.sleep(SERIAL_DELAY) fmt_fill_addr = "{:03o}".format(p) print(f'[{fmt_fill_addr}:{FILL_BYTE}] # FILL') file_pc = dec_pc i = int(inst, 8) checksum = checksum ^ i; #print (f'inst: {inst} checksum: {checksum}') upper = int(i & 0b100000000 != 0) lower = 0b011111111 & i #print (f'{pc}/{inst}: {upper}, {lower}') s.write(upper.to_bytes(1, 'big')) time.sleep(SERIAL_DELAY) s.flushOutput() s.write(lower.to_bytes(1, 'big')) time.sleep(SERIAL_DELAY) s.flushOutput() #fmt_pc = "{:03o}".format(pc) fmt_data = "{:03o}".format(i) print(f'[{pc}:{fmt_data}] #{comment}') file_pc = file_pc + 1; s.close() print ("Complete.") fmt_checksum = "{:09b}".format(checksum) print (f'Binary Checksum: {fmt_checksum}') if __name__ == "__main__": args = sys.argv if len(args)<2: prog = f'programs/{input ("program name: ")}.nas' nona = Nona(prog,None) elif len(args)<3: nona = Nona(args[1],None) else: nona = Nona(args[1],args[2]) nona.run() nona.scan() nona.elaborate() nona.save() print("EOF")