#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test pymlac CPU opcodes DIRECTLY. Usage: test_CPU.py [] where is a file of test instructions and is one or more of -h prints this help and stops """ import time import collections import filecmp # We implement a small interpreter to test the CPU. # The DSL is documented here: [github.com/rzzzwilson/pymlac]. import sys import os import json import collections from Globals import * import MainCPU import Memory import PtrPtp import TtyIn import TtyOut import Trace trace = Trace.Trace(TRACE_FILENAME) import log log = log.Log('test.log', log.Log.DEBUG) def octword_line(s, offset): """Generate one line of fdump output. s integer list to convert (8 words or less) offset offset from start of dump """ HEXLEN = 55 CHARLEN = 16 hex = '' char = '' for v in s: hex += '%06o ' % v hi = v >> 8 lo = v & 0xff if ord(' ') <= hi <= ord('~'): char += chr(hi) else: char += '.' if ord(' ') <= lo <= ord('~'): char += chr(lo) else: char += '.' hex = (hex + ' '*HEXLEN)[:HEXLEN] char = (char + '|' + ' '*CHARLEN)[:CHARLEN+1] return '%06o %s |%s' % (offset, hex, char) class TestCPU(object): # temporary assembler file and listfile prefix AsmFilename = '_#ASM#_' ProgressChar = '|\\-/' ProgressCount = 0 def __init__(self): """Initialize the test.""" pass def show_progress(self): """Show progress to stdout. A spinning line.""" print '%s\r' % self.ProgressChar[self.ProgressCount], self.ProgressCount += 1 if self.ProgressCount >= len(self.ProgressChar): self.ProgressCount = 0 sys.stdout.flush() def list2int(self, values): """Convert a string of multiple values to a list of ints. values a string like: '123;4;-2' (could be just '5') Returns a list of integers, for example: [123, 4, -2] """ values = values.split(';') result = [] for value in values: result.append(self.str2int(value)) return result def str2int(self, s): """Convert string to numeric value. s numeric string (decimal or octal) (could be None) Returns the numeric value. Returns None if value incorrect. """ if s is None: return None base = 10 if s[0] == '0': base = 8 try: value = int(s, base=base) except: return None return value def assemble(self, addr, opcodes): """Assemble a set of instructions, return opcode values. addr address to assemble at opcodes list of ASM lines to assemble Returns a list of integer opcodes. """ # split possible multiple instructions opcodes = opcodes.split('|') # create ASM file with instruction with open(self.AsmFilename+'.asm', 'wb') as fd: fd.write('\torg\t%07o\n' % addr) for line in opcodes: fd.write('\t%s\n' % line) fd.write('\tend\n') # now assemble file cmd = ('../iasm/iasm -l %s.lst %s.asm' % (self.AsmFilename, self.AsmFilename)) res = os.system(cmd) # read the listing file to get assembled opcode (second line) with open(self.AsmFilename+'.lst', 'rb') as fd: lines = fd.readlines() result = [] for line in lines[1:-1]: (opcode, _) = line.split(None, 1) result.append(int(opcode, base=8)) return result # Here we implement the DSL primitives. They all take two parameters which are # the DSL field1 and field2 values (lowercase strings). If one or both are # missing the parameters are None. # # setreg # setmem
# allreg # allmem # bootrom # romwrite # run [] # rununtil
# checkcycles # checkreg # checkmem # checkcpu # checkdcpu # mount # dismount # checkfile # dumpmem file begin,end # cmpmem file # trace [::...] # where ::= , def setreg(self, name, value): """Set register to a value. Remember value to check later. """ value = self.str2int(value) self.reg_values[name] = value if name == 'ac': self.cpu.AC = value elif name == 'l': self.cpu.L = value & 1 elif name == 'pc': self.cpu.PC = value elif name == 'ds': self.cpu.DS = value else: raise Exception('setreg: bad register name: %s' % name) def setmem(self, addr, values): """Set memory location to a value. addr address of memory location to set values value to store at 'addr' """ addr = self.str2int(addr) # check if we must assemble values if values[0] == '[': # assemble one or more instructions values = self.assemble(addr, values[1:-1]) else: values = self.list2int(values) for v in values: self.mem_values[addr] = v self.memory.put(v, addr, False) addr += 1 def allreg(self, value, ignore): """Set all registers to a value.""" new_value = self.str2int(value) if new_value is None: return 'allreg: bad value: %s' % str(value) self.reg_all_value = new_value self.cpu.AC = new_value self.cpu.L = new_value & 1 self.cpu.PC = new_value self.cpu.DS = new_value def allmem(self, value, ignore): """Set all of memory to a value. Remember value to check later. """ new_value = self.str2int(value) if new_value is None: return 'allmem: bad value: %s' % str(value) self.mem_all_value = new_value for mem in range(MEMORY_SIZE): self.memory.put(new_value, mem, False) def bootrom(self, loader_type, ignore): """Set bootloader memory range to appropriate bootloader code. loader_type either 'ptr' or 'tty' """ if loader_type not in ['ptr', 'tty']: return 'bootrom: invalid bootloader type: %s' % loader_type self.memory.set_ROM(loader_type) def romwrite(self, writable, ignore): """Set ROM to be writable or not.""" self.memory.rom_protected = writable def run(self, num_instructions, ignore): """Execute one or more instructions. num_instructions number of instructions to execute """ if num_instructions is None: # assume number of instructions is 1 number = 1 else: number = self.str2int(num_instructions) if number is None: return 'Invalid number of instructions: %s' % num_instructions self.used_cycles= 0 while number > 0: # for _ in range(number): (cycles, _) = self.cpu.execute_one_instruction() trace.itraceend(False) self.ptrptp.ptr_tick(cycles) self.ptrptp.ptp_tick(cycles) self.used_cycles += cycles number -= 1 def rununtil(self, address, ignore): """Execute instructions until PC == address. address address at which to stop We must handle the case where PC initial address is the actual stop address, that is, execute one instruction before checking for the stop address. We also stop if the CPU executes the HLT instruction. """ new_address = self.str2int(address) if new_address is None: return 'rununtil: invalid stop address: %s' % address self.used_cycles = 0 self.cpu.running = True while self.cpu.running: (cycles, tracestr) = self.cpu.execute_one_instruction() if tracestr: endstr = trace.itraceend(False) trace.comment('%s\t%s' % (tracestr, endstr)) trace.flush() self.ptrptp.ptr_tick(cycles) self.ptrptp.ptp_tick(cycles) self.used_cycles += cycles if self.cpu.PC == new_address: break def checkcycles(self, cycles, ignore): """Check that opcode cycles used is correct. cycles expected number of cycles used """ num_cycles = self.str2int(cycles) if num_cycles is None: return 'checkcycles: invalid number of cycles: %s' % cycles if num_cycles != self.used_cycles: return ('Run used %d cycles, expected %d' % (self.used_cycles, num_cycles)) def checkreg(self, reg, value): """Check register is as it should be.""" new_value = self.str2int(value) if new_value is None: return 'checkreg: bad value: %s' % str(value) if reg == 'ac': self.reg_values[reg] = self.cpu.AC if self.cpu.AC != new_value: return ('AC wrong, is %07o, should be %07o' % (self.cpu.AC, new_value)) elif reg == 'l': self.reg_values[reg] = self.cpu.L if self.cpu.L != new_value: return ('L wrong, is %02o, should be %02o' % (self.cpu.L, new_value)) elif reg == 'pc': self.reg_values[reg] = self.cpu.PC if self.cpu.PC != new_value: return ('PC wrong, is %07o, should be %07o' % (self.cpu.PC, new_value)) elif reg == 'ds': self.reg_values[reg] = self.cpu.DS if self.cpu.DS != new_value: return ('DS wrong, is %07o, should be %07o' % (self.cpu.DS, new_value)) else: return 'checkreg: bad register name: %s' % str(name) def checkmem(self, addr, value): """Check a memory location is as it should be.""" new_addr = self.str2int(addr) if new_addr is None: return 'checkmem: bad address: %s' % str(addr) new_value = self.str2int(value) if new_value is None: return 'checkmem: bad value: %s' % str(value) self.mem_values[new_addr] = new_value memvalue = self.memory.fetch(new_addr, False) if memvalue != new_value: return ('Memory wrong at address %07o, is %07o, should be %07o' % (new_addr, memvalue, new_value)) def checkcpu(self, state, ignore): """Check main CPU run state is as desired.""" if state not in ('on', 'off'): return 'checkcpu: bad state: %s' % str(state) cpu_state = str(self.cpu.running).lower() if ((state == "on" and cpu_state != "true") or (state == "off" and cpu_state != "false")): return ('Main CPU run state is %s, should be %s' % (str(self.cpu.running), str(state))) def checkdcpu(self, state, ignore): """Check display CPU run state is as desired.""" if state not in ('on', 'off'): return 'checkdcpu: bad state: %s' % str(state) dcpu_state = str(self.display.running).lower() if ((state == "on" and dcpu_state != "true") or (state == "off" and dcpu_state != "false")): return ('Display CPU run state is %s, should be %s' % (str(self.cpu.running), str(state))) def mount(self, device, filename): """Mount a file on a device. device name of device filename path to file to mount If the device is an input device, the file must exist. """ if device == 'ptr': if not os.path.exists(filename) or not os.path.isfile(filename): return "mount: '%s' doesn't exist or isn't a file" % filename self.ptrptp.ptr_mount(filename) elif device == 'ptp': self.ptrptp.ptp_mount(filename) elif device == 'ttyin': if not os.path.exists(filename) or not os.path.isfile(filename): return "mount: '%s' doesn't exist or isn't a file" % filename self.ttyin.mount(filename) elif device == 'ttyout': self.ttyout.mount(filename) else: print('mount: bad device: %s' % device) return 'mount: bad device: %s' % device def dismount(self, device, ignore): """Dismount a file from a device. device name of device """ if device == 'ptr': self.ptrptp.ptr_dismount() elif device == 'ptp': self.ptrptp.ptp_dismount() else: return 'dismount: bad device: %s' % device def checkfile(self, file1, file2): """Compare two files, error if different.""" cmd = 'cmp -s %s %s' % (file1, file2) if filecmp.cmp(file1, file2, shallow=False): return 'Files %s and %s are different' % (file1, file2) def dumpmem(self, filename, addresses): """Dump memory to a file. filename path to the file to create addresses string containing "begin,end" addresses """ # get address limits if addresses is None: # dump everything begin = 0 end = 0x3fff else: s = addresses.split(',') if len(s) != 2: return "dumpmem: dump limits are bad: %s" % addresses (begin, end) = s begin = self.str2int(begin) end = self.str2int(end) if begin is None or end is None: return "dumpmem: dump limits are bad: %s" % addresses # create octdump-like text file with required memory locations with open(filename, 'wb') as handle: addr = begin offset = addr chunk = [] while addr <= end: word = self.memory.fetch(addr, False) addr += 1 chunk.append(word) if len(chunk) == 8: line = octword_line(chunk, offset) handle.write(line + '\n') chunk = [] offset = addr if len(chunk): line = octword_line(chunk, offset) handle.write(line + '\n') def cmpmem(self, filename, ignore): """Compare a 'dumpmem' file with memory. filename path of the 'dumpmem' file A dumpmem file has the following format: 000100 100000 010104 000000 000000 004111 000000 000000 000000 |...D.....I......| """ # get file contents into memory try: with open(filename, 'rb') as handle: lines = handle.readlines() except IOError as e: return "Error opening file '%s': %s" % (filename, e.strerror) # read dumpmem file, get address and expected contents for line in lines: line = line.split('|', 1)[0] values = line.split() address = self.str2int(values[0]) for value in values[1:]: expected = self.str2int('0'+value) # force octal evaluation actual = self.memory.fetch(address, False) if actual != expected: return ('Address %06o is wrong, expected %06o, is %06o' % (address, expected, actual)) address += 1 def trace(self, ranges, ignore): """Set the trace range(s). ranges trace ranges of the form [::...] where ::= , Puts the trace range(s) into the Trace object. """ trace_map = collections.defaultdict(bool) for rng in ranges.split(':'): be = rng.split(',') if len(be) != 2: return("Trace ranges must have the form 'begin,end'") (begin, end) = be begin = self.str2int(begin) end = self.str2int(end) if begin > end: return("Trace begin address must be <= end address. Got: %s" % rng) for addr in range(begin, end+1): trace_map[addr] = True trace.set_trace_map(trace_map) # end of DSL primitives def check_all_mem(self): """Check memory for unwanted changes.""" result = [] for mem in range(MEMORY_SIZE): value = self.memory.fetch(mem, False) if mem in self.mem_values: if value != self.mem_values[mem]: result.append('Memory at %07o changed, is %07o, should be %07o' % (mem, value, self.mem_values[mem])) else: if value != self.mem_all_value: result.append('Memory at %07o changed, is %07o, should be %07o' % (mem, value, self.mem_all_value)) def check_all_regs(self): """Check registers for unwanted changes.""" result = [] if 'ac' in self.reg_values: if self.cpu.AC != self.reg_values['ac']: result.append('AC changed, is %07o, should be %07o' % (self.cpu.AC, self.reg_values['ac'])) else: if self.cpu.AC != self.reg_all_value: result.append('AC changed, is %07o, should be %07o' % (self.cpu.AC, self.reg_all_value)) if 'l' in self.reg_values: if self.cpu.L != self.reg_values['l']: result.append('L changed, is %02o, should be %02o' % (self.cpu.L, self.reg_values['l'])) else: if self.cpu.L != self.reg_all_value & 1: result.append('L changed, is %02o, should be %02o' % (self.cpu.L, self.reg_all_value & 1)) if 'pc' in self.reg_values: if self.cpu.PC != self.reg_values['pc']: result.append('PC changed, is %07o, should be %07o' % (self.cpu.PC, self.reg_values['pc'])) else: if self.cpu.PC != self.reg_all_value: result.append('PC changed, is %07o, should be %07o' % (self.cpu.PC, self.reg_all_value)) if 'ds' in self.reg_values: if self.cpu.DS != self.reg_values['ds']: result.append('DS changed, is %07o, should be %07o' % (self.cpu.DS, self.reg_values['ds'])) else: if self.cpu.DS != self.reg_all_value: result.append('DS changed, is %07o, should be %07o' % (self.cpu.DS, self.reg_all_value)) return result def setd(self, state, var2): """Set display state.""" if state == 'on': self.display_state = True elif state == 'off': self.display_state = False else: raise Exception('setd: bad state: %s' % str(state)) def execute(self, test, filename): """Execute test string in 'test'.""" # set globals self.reg_values = {} self.mem_values = {} self.reg_all_value = '0' self.mem_all_value = '0' result = [] self.memory = Memory.Memory() self.ptrptp = PtrPtp.PtrPtp() self.ttyin = TtyIn.TtyIn() self.ttyout = TtyOut.TtyOut() self.cpu = MainCPU.MainCPU(self.memory, None, None, None, self.ttyin, self.ttyout, self.ptrptp) # turn yrace OFF, initially trace_map = collections.defaultdict(bool) trace.set_trace_map(trace_map) self.cpu.running = True self.display_state = False # prepare the trace trace.add_maincpu(self.cpu) # clear registers and memory to 0 first self.allreg(self.reg_all_value, None) self.allmem(self.mem_all_value, None) # show the DSL we are about execute trace.comment('') trace.comment(test) trace.comment('-'*80) # interpret the test instructions suite = test.split(';') for instruction in suite: fields = instruction.split(None, 2) if not fields: continue; opcode = fields[0].strip().lower() if len(fields) == 1: fld1 = fld2 = None elif len(fields) == 2: fld1 = fields[1].strip().lower() fld2 = None elif len(fields) == 3: fld1 = fields[1].strip().lower() fld2 = fields[2].strip().lower() # cll the correct DSL primitive if opcode == 'setreg': r = self.setreg(fld1, fld2) elif opcode == 'setmem': r = self.setmem(fld1, fld2) elif opcode == 'run': r = self.run(fld1, fld2) elif opcode == 'rununtil': r = self.rununtil(fld1, fld2) elif opcode == 'checkcycles': r = self.checkcycles(fld1, fld2) elif opcode == 'checkreg': r = self.checkreg(fld1, fld2) elif opcode == 'checkmem': r = self.checkmem(fld1, fld2) elif opcode == 'allreg': r = self.allreg(fld1, fld2) elif opcode == 'allmem': r = self.allmem(fld1, fld2) elif opcode == 'checkcpu': r = self.checkcpu(fld1, fld2) elif opcode == 'checkdcpu': r = self.checkdcpu(fld1, fld2) elif opcode == 'setd': r = self.setd(fld1, fld2) elif opcode == 'bootrom': r = self.bootrom(fld1, fld2) elif opcode == 'romwrite': r = self.romwrite(fld1, fld2) elif opcode == 'mount': r = self.mount(fld1, fld2) elif opcode == 'dismount': r = self.dismount(fld1, fld2) elif opcode == 'checkfile': r = self.checkfile(fld1, fld2) elif opcode == 'dumpmem': r = self.dumpmem(fld1, fld2) elif opcode == 'cmpmem': r = self.cmpmem(fld1, fld2) elif opcode == 'trace': r = self.trace(fld1, fld2) else: print("Unrecognized opcode '%s' in: %s" % (opcode, test)) raise Exception("Unrecognized opcode '%s' in: %s" % (opcode, test)) if r is not None: result.append(r) # now check all memory and regs for changes r = self.check_all_mem() if r: result.append(r) r = self.check_all_regs() if r: result.extend(r) if result: print(test) print('\t' + '\n\t'.join(result)) def memdump(self, filename, start, number): """Dump memory from 'start' into 'filename', 'number' words dumped.""" with open(filename, 'wb') as fd: for addr in range(start, start+number, 8): a = addr llen = min(8, start+number - addr) line = '%04o ' % addr for _ in range(llen): line += '%06o ' % self.memory.fetch(a, False) a += 1 fd.write('%s\n' % line) def main(self, filename): """Execute CPU tests from 'filename'.""" # get all tests from file with open(filename, 'rb') as fd: lines = fd.readlines() # read lines, join continued, get complete tests tests = [] test = '' for line in lines: self.show_progress() line = line[:-1] # strip newline if not line: continue # skip blank lines if '#' in line: index = line.find('#') line = line[:index] # remove trailing spaces line = line.rstrip() if not line: continue # skip blank lines if line[0] in ('\t', ' '): # continuation if test: if not test.endswith(';'): test += ';' test += ' ' + line[1:].strip() else: # beginning of new test if test: tests.append(test) test = line # flush last test if test: tests.append(test) # now do each test for test in tests: self.show_progress() self.execute(test, filename) ################################################################################ if __name__ == '__main__': import sys import getopt def usage(msg=None): if msg: print('*'*60) print(msg) print('*'*60) print(__doc__) try: (opts, args) = getopt.gnu_getopt(sys.argv, "h", ["help"]) except getopt.GetoptError: usage() sys.exit(10) for opt, arg in opts: if opt in ("-h", "--help"): usage() sys.exit(0) if len(args) != 2: usage() sys.exit(10) filename = args[1] try: f = open(filename) except IOError: print("Sorry, can't find file '%s'" % filename) sys.exit(10) f.close() test = TestCPU() test.main(filename)