mirror of
https://github.com/rzzzwilson/pymlac.git
synced 2025-06-10 09:32:41 +00:00
571 lines
20 KiB
Python
Executable File
571 lines
20 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
"""
|
|
The Python Imlac emulator (pymlac).
|
|
|
|
Console-run version
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import wx
|
|
|
|
from Globals import *
|
|
import Memory
|
|
import Ptr
|
|
import Ptp
|
|
import TtyIn
|
|
import TtyOut
|
|
import Kbd
|
|
import MainCPU
|
|
import Display
|
|
import DisplayCPU
|
|
import Trace
|
|
|
|
|
|
# map ROM name to type
|
|
RomTypeMap = {'ptr': ROM_PTR,
|
|
'tty': ROM_TTY,
|
|
'none': ROM_NONE}
|
|
|
|
count = 1
|
|
|
|
# display window
|
|
WindowTitleHeight = 22
|
|
#DefaultAppSize = (600, 600+WindowTitleHeight)
|
|
DefaultAppSize = (1024, 1024+WindowTitleHeight)
|
|
|
|
|
|
def abort(msg):
|
|
"""Handle a terminal problem."""
|
|
|
|
print(msg)
|
|
sys.exit(1)
|
|
|
|
def check_int(value, msg):
|
|
"""Check that a value is a valid 16 bit positive value.
|
|
|
|
We allow decimal and octal number representations.
|
|
"""
|
|
|
|
try:
|
|
if str(value)[0] == '0':
|
|
data = int(value, 8)
|
|
else:
|
|
data = int(value)
|
|
except:
|
|
abort(msg)
|
|
if data < 0 or data > 0177777:
|
|
abort(msg)
|
|
|
|
return data
|
|
|
|
def get_next(options, index, msg):
|
|
"""If possible, get next arg in list."""
|
|
|
|
if index < len(options):
|
|
next_arg = options[index]
|
|
else:
|
|
abort(msg)
|
|
|
|
return next_arg
|
|
|
|
def handle_options():
|
|
"""Routine to analyse command line options.
|
|
|
|
Returns a list of operation objects which are tuples:
|
|
(OPERATION, ARGS)
|
|
"""
|
|
|
|
options = sys.argv[1:]
|
|
|
|
index = 0
|
|
result = []
|
|
boot_rom = 'ptr'
|
|
|
|
while index < len(options):
|
|
arg = options[index]
|
|
index += 1
|
|
|
|
if arg[0] == '-':
|
|
if arg == '-b':
|
|
next_arg = get_next(options, index,
|
|
"-b option must be 'ptr' or 'tty' or 'none'")
|
|
index += 1
|
|
next_arg = next_arg.lower()
|
|
#next_arg = RomTypeMap.get(next_arg, False)
|
|
|
|
if next_arg:
|
|
op = ('boot', next_arg)
|
|
boot_rom = next_arg
|
|
else:
|
|
abort("-b option must be 'ptr', 'tty' or 'none'")
|
|
elif arg == '-c':
|
|
op = ('clear', boot_rom)
|
|
elif arg == '-d':
|
|
next_arg = get_next(options, index,
|
|
"-d option must be data switch value, positive & max 16 bits")
|
|
index += 1
|
|
data = check_int(next_arg,
|
|
"-d option must be data switch value, positive & max 16 bits")
|
|
op = ('data', data)
|
|
elif arg == '-h':
|
|
usage()
|
|
elif arg == '-ptp':
|
|
next_arg = get_next(options, index,
|
|
"-ptp option must be papertape punch file")
|
|
index += 1
|
|
if not os.path.isfile(next_arg):
|
|
abort("File %s isn't a papertape file" % next_arg)
|
|
op = ('load_ptp', next_arg)
|
|
elif arg == '-ptr':
|
|
next_arg = get_next(options, index,
|
|
"-ptr option must be papertape reader file")
|
|
index += 1
|
|
if not os.path.isfile(next_arg):
|
|
abort("File %s isn't a papertape file" % next_arg)
|
|
op = ('load_ptr', next_arg)
|
|
elif arg == '-r':
|
|
next_arg = get_next(options, index,
|
|
"-r option must be 'PC' or run address, positive & max 16 bits")
|
|
index += 1
|
|
next_arg = next_arg.lower()
|
|
if next_arg == 'pc':
|
|
op = ('run', None)
|
|
else:
|
|
address = check_int(next_arg,
|
|
"-r option must be 'PC' or run address, positive & max 16 bits")
|
|
op = ('run', address)
|
|
elif arg == '-s':
|
|
next_arg = get_next(options, index,
|
|
"-s option must be set file")
|
|
index += 1
|
|
if not os.path.isfile(next_arg):
|
|
abort("File %s isn't a 'set' file" % next_arg)
|
|
op = ('set', next_arg)
|
|
elif arg == '-t':
|
|
next_arg = get_next(options, index,
|
|
"-t option must be trace range")
|
|
index += 1
|
|
addr = next_arg.split(',')
|
|
if len(addr) == 1:
|
|
trace1 = addr[0]
|
|
if trace1.lower() == 'off':
|
|
trace1 = None
|
|
else:
|
|
trace1 = check_int(trace1,
|
|
"-t option must be trace range")
|
|
trace2 = None
|
|
elif len(addr) == 2:
|
|
trace1 = check_int(addr[0], "-t option must be trace range")
|
|
trace2 = check_int(addr[1], "-t option must be trace range")
|
|
else:
|
|
abort("-t option must be trace range")
|
|
op = ('trace', (trace1, trace2))
|
|
elif arg == '-ttyin':
|
|
next_arg = get_next(options, index,
|
|
"-ttyin option must be teletype input file")
|
|
index += 1
|
|
if not os.path.isfile(next_arg):
|
|
abort("File %s isn't a teletype file" % next_arg)
|
|
op = ('load_ttyin', next_arg)
|
|
elif arg == '-ttyout':
|
|
next_arg = get_next(options, index,
|
|
"-ttyout option must be teletype output file")
|
|
index += 1
|
|
if not os.path.isfile(next_arg):
|
|
abort("File %s isn't a teletype file" % next_arg)
|
|
op = ('load_ttyout', next_arg)
|
|
elif arg == '-v':
|
|
next_arg = get_next(options, index,
|
|
"-v option must be view file")
|
|
index += 1
|
|
if not os.path.isfile(next_arg):
|
|
abort("File %s isn't a 'view' file" % next_arg)
|
|
op = ('view', next_arg)
|
|
elif arg == '-w':
|
|
next_arg = get_next(options, index,
|
|
"-W option must be ON|OFF")
|
|
index += 1
|
|
next_arg = next_arg.lower()
|
|
if next_arg in ['on', 'off']:
|
|
op = ('write', next_arg)
|
|
else:
|
|
abort("-w option must be 'ON' or 'OFF'")
|
|
elif arg == '-x':
|
|
next_arg = get_next(options, index,
|
|
"-x option must be dump range")
|
|
index += 1
|
|
addr = next_arg.split(',')
|
|
if len(addr) == 1:
|
|
abort("-x option must be dump range: lower,higher")
|
|
elif len(addr) == 2:
|
|
lower = check_int(addr[0], "-x option must be dump range: lower,higher")
|
|
higher = check_int(addr[1], "-x option must be dump range: lower,higher")
|
|
else:
|
|
abort("-t option must be dump range: lower,higher")
|
|
op = ('x', (lower, higher))
|
|
else:
|
|
usage()
|
|
else:
|
|
usage()
|
|
|
|
result.append(op)
|
|
return result
|
|
|
|
def usage():
|
|
"""Help for befuddled user."""
|
|
|
|
print('Usage:\n')
|
|
print('pymlac [-b (ptr|tty|none)] [-c] [-t [<a>[,<b>]]] [-r <address>] [<imlacfile>]\n')
|
|
print('where -b (ptr|tty|none) sets the bootstrap ROM code, if no device disable ROM')
|
|
print(' -c clear core, except bootstrap ROM')
|
|
print(' -d <value> set data switches')
|
|
print(' -h prints this help')
|
|
print(' -ptp <file> mount <file> on the papertape punch')
|
|
print(' -ptr <file> mount <file> on the papertape reader')
|
|
print(' -r (<address>|PC) execute from the <address>, no address runs from current PC')
|
|
print(' -s <setfile> sets memory values from file')
|
|
print(' -t (<addr1>[,<addr2>]|off) controls trace:')
|
|
print(' -t 100 trace from address 0100')
|
|
print(' -t 100,200 traces between addresses 100 and 200')
|
|
print(' -t off trace OFF')
|
|
print(' -ttyin <file> mount <file> on the teletype reader')
|
|
print(' -ttyout <file> mount <file> on the teletype writer')
|
|
print(' -v <viewfile> prints memory locations from file')
|
|
print(' -w (on|off) turn ROM writability ON or OFF')
|
|
print(' -x <low>,<high> dumps memory to file:')
|
|
print(" -x 100,200 dumps memory between 100 and 200 to 'x.dump'")
|
|
sys.exit()
|
|
|
|
|
|
def main():
|
|
"""Start of the emulator."""
|
|
|
|
# get operations list
|
|
ops = handle_options()
|
|
|
|
# Initialize the emulator.
|
|
boot_rom = 'ptr' # default ROM loader
|
|
Imlac.init(0, TRACE_FILENAME, None, None, boot_rom, CORE_FILENAME)
|
|
|
|
# now perform operations
|
|
for (operation, args) in ops:
|
|
if operation == 'boot':
|
|
Memory.set_ROM(args)
|
|
Trace.comment('Bootstrap ROM set to %s' % args.upper())
|
|
elif operation == 'clear':
|
|
Memory.clear_core()
|
|
Trace.comment('Core cleared')
|
|
elif operation == 'load_ptp':
|
|
Ptp.mount(args)
|
|
Trace.comment("File '%s' mounted on PTP" % args)
|
|
elif operation == 'load_ptr':
|
|
Ptr.mount(args)
|
|
Trace.comment("File '%s' mounted on PTR" % args)
|
|
elif operation == 'data':
|
|
MainCPU.DS = args
|
|
Trace.comment('Dataswitch value set to %06o' % args)
|
|
elif operation == 'run':
|
|
MainCPU.PC = args
|
|
Trace.comment('Running from address %06o' % args)
|
|
if Imlac.tracestart:
|
|
Trace.comment('DPC\tDisplay\t\tPC\tMain\t\tRegs')
|
|
Trace.comment('------ ------------- ------ -------------- '
|
|
'-----------------------')
|
|
Imlac.run()
|
|
Trace.comment('Imlac halted')
|
|
elif operation == 'set':
|
|
Trace.comment("Setting memory from file '%s'" % args)
|
|
elif operation == 'trace':
|
|
if args == 'off':
|
|
Imlac.tracestart = None
|
|
Imlac.traceend = None
|
|
else:
|
|
(start, end) = args
|
|
Imlac.tracestart = start
|
|
Imlac.traceend = end
|
|
tstart = Imlac.tracestart
|
|
if tstart is not None:
|
|
tstart = '%06o' % tstart
|
|
tend = Imlac.traceend
|
|
if tend is not None:
|
|
tend = '%06o' % tend
|
|
Trace.comment('Trace set to (%s, %s)' % (tstart, tend))
|
|
elif operation == 'ttyin':
|
|
TtyIn.mount(args)
|
|
Trace.comment("File '%s' mounted on TTYIN" % args)
|
|
elif operation == 'ttyout':
|
|
TtyOut.mount(args)
|
|
Trace.comment("File '%s' mounted on TTYOUT" % args)
|
|
elif operation == 'view':
|
|
Trace.comment("Viewing memory from file '%s'" % args)
|
|
elif operation == 'write':
|
|
if args == 'on':
|
|
Memory.using_rom = True
|
|
elif args == 'off':
|
|
Memory.using_rom = False
|
|
else:
|
|
abort("Invalid view arg: %s" % args)
|
|
Trace.comment('ROM write protect set %s' % args.upper())
|
|
elif operation == 'x':
|
|
(low, high) = args
|
|
Memory.dump(args)
|
|
Trace.comment('Memory dumped: %07o to %07o' % (low, high))
|
|
else:
|
|
abort('Invalid internal operation: %s' % operation)
|
|
|
|
Imlac.close(CORE_FILENAME)
|
|
|
|
|
|
class PymlacFrame(wx.Frame):
|
|
"""A frame containing the pymlac display."""
|
|
|
|
WIDTH_SCREEN = 1024
|
|
HEIGHT_SCREEN = 1024
|
|
WIDTH_CONSOLE = 330 # 400 # 256
|
|
HEIGHT_CONSOLE = HEIGHT_SCREEN
|
|
|
|
SCREEN_COLOUR = (0, 0, 0)
|
|
CONSOLE_COLOUR = (255, 223, 169)
|
|
PHOSPHOR_COLOUR = '#F0F000' # yellow
|
|
#PHOSPHOR_COLOUR = '#40FF40' # green
|
|
|
|
V_MARGIN = 20
|
|
CTL_MARGIN = 15
|
|
LED_MARGIN = 5
|
|
|
|
INC = 0.01
|
|
|
|
|
|
def __init__(self, title):
|
|
wx.Frame.__init__(self, None, size=DefaultAppSize,
|
|
title=('pymlac %s' % '0.1'))
|
|
self.SetMinSize(DefaultAppSize)
|
|
self.panel = wx.Panel(self, wx.ID_ANY)
|
|
self.panel.ClearBackground()
|
|
|
|
# consruct the Imlac machine
|
|
self.ptr = Ptr.Ptr()
|
|
self.ptp = Ptp.Ptp()
|
|
self.ttyin = TtyIn.TtyIn()
|
|
self.ttyout = TtyOut.TtyOut()
|
|
self.kbd = Kbd.Kbd()
|
|
self.memory = Memory.Memory()
|
|
self.display = Display.Display(self.panel)
|
|
self.display_cpu = DisplayCPU.DisplayCPU(self.display, self.memory)
|
|
self.cpu = MainCPU.MainCPU(self.memory, self.display, self.display_cpu,
|
|
self.kbd, self.ttyin, self.ttyout,
|
|
self.ptp, self.ptr)
|
|
|
|
# build the GUI
|
|
box = wx.BoxSizer(wx.VERTICAL)
|
|
box.Add(self.display, proportion=1, border=1, flag=wx.EXPAND)
|
|
self.panel.SetSizer(box)
|
|
self.panel.Layout()
|
|
self.Centre()
|
|
self.Show(True)
|
|
|
|
self.Bind(wx.EVT_SIZE, self.OnSize)
|
|
|
|
self.lock = False
|
|
|
|
self.Refresh()
|
|
|
|
# start running
|
|
self.run_machine()
|
|
|
|
def OnSize(self, event):
|
|
"""Maintain square window."""
|
|
|
|
if not self.lock:
|
|
self.lock = True
|
|
(w, h) = event.GetSize()
|
|
size = min(w, h)
|
|
self.SetSize((size-WindowTitleHeight, size))
|
|
self.lock = False
|
|
|
|
event.Skip()
|
|
|
|
def run_machine(self):
|
|
# get operations list
|
|
ops = handle_options()
|
|
|
|
# Initialize the emulator.
|
|
boot_rom = 'ptr' # default ROM loader
|
|
self.main_running = False
|
|
self.display_running = False
|
|
self.tracestart = None
|
|
self.traceend = None
|
|
self.DS = 0100000 # dataswitches
|
|
self.trace_filename = TRACE_FILENAME
|
|
self.core_filename = CORE_FILENAME
|
|
Trace.init(TRACE_FILENAME, self.cpu, self.display_cpu)
|
|
|
|
# now perform operations
|
|
for (operation, args) in ops:
|
|
if operation == 'boot':
|
|
self.memory.set_ROM(args)
|
|
Trace.comment('Bootstrap ROM set to %s' % args.upper())
|
|
self.Refresh()
|
|
elif operation == 'clear':
|
|
self.memory.clear_core()
|
|
Trace.comment('Core cleared')
|
|
self.Refresh()
|
|
elif operation == 'load_ptp':
|
|
self.ptp.mount(args)
|
|
Trace.comment("File '%s' mounted on PTP" % args)
|
|
self.Refresh()
|
|
elif operation == 'load_ptr':
|
|
self.ptr.mount(args)
|
|
Trace.comment("File '%s' mounted on PTR" % args)
|
|
self.Refresh()
|
|
elif operation == 'data':
|
|
self.cpu.DS = args
|
|
Trace.comment('Dataswitch value set to %06o' % args)
|
|
self.Refresh()
|
|
elif operation == 'run':
|
|
self.cpu.PC = args
|
|
Trace.comment('Running from address %06o' % args)
|
|
if self.tracestart:
|
|
Trace.comment('DPC\tDisplay\t\tPC\tMain\t\tRegs')
|
|
Trace.comment('------ ------------- ------ -------------- '
|
|
'-----------------------')
|
|
self.Refresh()
|
|
self.run()
|
|
self.Refresh()
|
|
Trace.comment('Imlac halted')
|
|
self.Refresh()
|
|
elif operation == 'set':
|
|
Trace.comment("Setting memory from file '%s'" % args)
|
|
self.Refresh()
|
|
elif operation == 'trace':
|
|
if args == 'off':
|
|
self.tracestart = None
|
|
self.traceend = None
|
|
else:
|
|
(start, end) = args
|
|
self.tracestart = start
|
|
self.traceend = end
|
|
tstart = self.tracestart
|
|
if tstart is not None:
|
|
tstart = '%06o' % tstart
|
|
tend = self.traceend
|
|
if tend is not None:
|
|
tend = '%06o' % tend
|
|
Trace.comment('Trace set to (%s, %s)' % (tstart, tend))
|
|
self.Refresh()
|
|
elif operation == 'ttyin':
|
|
self.ttyin.mount(args)
|
|
Trace.comment("File '%s' mounted on TTYIN" % args)
|
|
self.Refresh()
|
|
elif operation == 'ttyout':
|
|
self.ttyout.mount(args)
|
|
Trace.comment("File '%s' mounted on TTYOUT" % args)
|
|
self.Refresh()
|
|
elif operation == 'view':
|
|
Trace.comment("Viewing memory from file '%s'" % args)
|
|
self.Refresh()
|
|
elif operation == 'write':
|
|
if args == 'on':
|
|
self.memory.using_rom = True
|
|
elif args == 'off':
|
|
self.memory.using_rom = False
|
|
else:
|
|
abort("Invalid view arg: %s" % args)
|
|
Trace.comment('ROM write protect set %s' % args.upper())
|
|
self.Refresh()
|
|
elif operation == 'x':
|
|
(low, high) = args
|
|
self.memory.dump(low, high)
|
|
Trace.comment('Memory dumped: %07o to %07o' % (low, high))
|
|
else:
|
|
abort('Invalid internal operation: %s' % operation)
|
|
|
|
self.memory.savecore('after_%s.core' % operation)
|
|
|
|
# Imlac.close(CORE_FILENAME)
|
|
|
|
|
|
def close(self, corefile=None):
|
|
if corefile:
|
|
Memory.savecore(corefile)
|
|
sys.exit()
|
|
|
|
# def set_ROM(self, type):
|
|
# self.memory.set_ROM(type)
|
|
|
|
# def set_boot(self, romtype):
|
|
# pass
|
|
|
|
def __tick_all(self, cycles):
|
|
self.ptr.tick(cycles)
|
|
self.ptp.tick(cycles)
|
|
# self.ttyin.tick(cycles)
|
|
# self.ttyout.tick(cycles)
|
|
|
|
def set_trace(self, tstart, tend=None):
|
|
"""Set trace to required range of values."""
|
|
|
|
global tracestart, traceend
|
|
|
|
if tstart:
|
|
tracestart = tstart
|
|
traceend = tend
|
|
Trace.tracing = True
|
|
else:
|
|
Trace.tracing = False
|
|
|
|
def execute_once(self):
|
|
# decide if trace should be on/off
|
|
if self.traceend is None:
|
|
if self.cpu.PC == self.tracestart:
|
|
Trace.settrace(True)
|
|
else:
|
|
Trace.settrace(self.cpu.PC >= self.tracestart and
|
|
self.cpu.PC <= self.traceend)
|
|
|
|
# execute and trace the Display CPU
|
|
if self.display_cpu.ison():
|
|
Trace.trace('%6.6o' % self.display_cpu.DPC)
|
|
Trace.trace('\t')
|
|
|
|
instruction_cycles = self.display_cpu.execute_one_instruction()
|
|
|
|
# execute and trace the Main CPU
|
|
if self.cpu.running:
|
|
Trace.trace('%6.6o\t' % self.cpu.PC)
|
|
else:
|
|
Trace.trace('\t')
|
|
|
|
cpu_cycles = self.cpu.execute_one_instruction()
|
|
instruction_cycles += cpu_cycles
|
|
|
|
if cpu_cycles < 1:
|
|
Trace.trace('\t\t')
|
|
|
|
Trace.itraceend(self.display_cpu.ison())
|
|
|
|
self.__tick_all(instruction_cycles)
|
|
|
|
if not self.display_cpu.ison() and not self.cpu.running:
|
|
return 0
|
|
|
|
return instruction_cycles
|
|
|
|
def run(self):
|
|
"""Start the machine and run until it halts."""
|
|
|
|
self.cpu.running = True
|
|
|
|
while self.execute_once() > 0:
|
|
pass
|
|
|
|
self.cpu.running = False
|
|
|
|
|
|
# run it ...
|
|
app = wx.App()
|
|
PymlacFrame(title='pymlac 0.1').Show()
|
|
app.MainLoop()
|