1033 lines
40 KiB
Python
1033 lines
40 KiB
Python
# MIT License
|
|
#
|
|
# Copyright (c) 2023 Neil Webber
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
|
|
import logging
|
|
from types import SimpleNamespace
|
|
|
|
from pdptraps import PDPTrap, PDPTraps
|
|
from mmu import MemoryMgmt
|
|
from unibus import UNIBUS, UNIBUS_1170
|
|
from breakpoints import StepsBreakpoint, PCBreakpoint, XInfo
|
|
|
|
from op4 import op4_dispatch_table
|
|
|
|
|
|
# A note about the various opNxxx files:
|
|
#
|
|
# Conceptually all of those are part of the PDP11 class. But having one
|
|
# monolithic/large class file seemed less than ideal. Python does not
|
|
# allow multiple files for a single class.
|
|
#
|
|
# Some parts of the implementation, like mmu and mmio, and various devices,
|
|
# made sense as separate component classes. The opcodes, however, are
|
|
# basically additional methods in separate files. Since they are not real
|
|
# methods they get passed a "cpu" argument manually instead of "self".
|
|
#
|
|
# Was there a better way? Just give in and have one huge file??
|
|
#
|
|
# The opcode parsing/dispatch starts with the top 4 bits of the opcode;
|
|
# thus the names "op4" and "op4_dispatch_table". Further decoding from
|
|
# there is as defined by the pdp11 operation code encoding tree.
|
|
#
|
|
# A note about octal
|
|
#
|
|
# Octal notation is everywhere in the world of the PDP-11; it especially
|
|
# pervades the manuals when talking about I/O page addresses. The layout
|
|
# of special purpose CPU registers (e.g., MMU registers and the like)
|
|
# is often most sensical viewed as octal. This is especially true of
|
|
# instruction format decoding, where for example the MOV instruction
|
|
# is: 0o01<src><dst> where <src> is two octal digits (3 bits mode, 3 bits
|
|
# register number) and so is <dst>.
|
|
#
|
|
# Thus MOV R2,R3 is 0o010203 ... no one wants to think of that as 0x1083
|
|
#
|
|
# Given that octal therefore appears all over in low-level processor detail,
|
|
# it seems to make the most sense to just go with it, well, (almost)
|
|
# everywhere. Thus, for example, "MASK16 = 0o177777" not "MASK16 = 0xFFFF"
|
|
#
|
|
# Complaints about this decision should be written on the back
|
|
# of an eight dollar bill and deposited appropriately.
|
|
#
|
|
|
|
|
|
class PDP11:
|
|
|
|
# Architectural constants, generally common across the whole family
|
|
|
|
SIGN8 = 0o200 # sign bit mask for 8 bits
|
|
SIGN16 = 0o100000 # sign bit mask for 16 bits
|
|
SIGN32 = 0x80000000 # for 32 bits; no one wants to see this in octal
|
|
|
|
# index this by opsize (1 or 2) to get corresponding sign bit mask
|
|
SIGN816 = (None, SIGN8, SIGN16)
|
|
|
|
MASK8 = 0o377
|
|
MASK16 = 0o177777
|
|
MASK32 = 0xFFFFFFFF
|
|
|
|
# index this by opsize (1 or 2) to get corresponding byte/word mask
|
|
MASK816 = (None, MASK8, MASK16)
|
|
|
|
# I/O page size is 8K (bytes), and equivalent mask
|
|
IOPAGE_SIZE = 8192 # bytes
|
|
IOPAGE_MASK = IOPAGE_SIZE - 1
|
|
|
|
UNIBUS_MASK = 0o777777 # 18 bits
|
|
UNIBUS_SIZE = UNIBUS_MASK + 1
|
|
|
|
# General register(s) block(s), relative to I/O page base
|
|
IOPAGE_REGSETS_OFFS = 0o17700
|
|
IOPAGE_REGSET_SIZE = 0o10 # 11/70 overrides w/more registers
|
|
|
|
# PSW modes, though not all processors implement all
|
|
KERNEL = 0 # 00 in PSW bits
|
|
SUPERVISOR = 1 # 01 in PSW bits
|
|
UNDEFINED_MODE = 2 # 10 is undefined and will cause a trap
|
|
USER = 3 # 11 in PSW bits
|
|
|
|
# sometimes nice to use these for clarity; r6 == SP, r7 == PC
|
|
SP = 6
|
|
PC = 7
|
|
|
|
# the processor status word I/O page offset
|
|
PS_OFFS = 0o17776
|
|
|
|
# the stack limit register I/O page offset
|
|
STACKLIM_OFFS = 0o17774
|
|
|
|
# the console switches (read) and LEDs (write)
|
|
SWLEDS_OFFS = 0o17570
|
|
|
|
# this is a superb hack for controlling the logging level for debug
|
|
# this is in the unibus address range reserved for "testers" -- not
|
|
# sure what that really is but this is as good a place for it as any
|
|
LOGGING_OFFS = 0o17000
|
|
|
|
# Lower Size Register (memory size)
|
|
LOWERSIZE_OFFS = 0o17760
|
|
|
|
# Upper Size Register (memory size) ... ALWAYS ZERO
|
|
UPPERSIZE_OFFS = 0o17762
|
|
|
|
# System ID register
|
|
SYSTEMID_OFFS = 0o17764
|
|
|
|
# the CPU error register and some useful bit values
|
|
CPUERROR_OFFS = 0o17766
|
|
|
|
# not an Enum because ... need to do bitwise efficiently.
|
|
CPUERR_BITS = SimpleNamespace(
|
|
REDZONE=0o004, YELLOW=0o010, UNIBUS_TIMEOUT=0o020,
|
|
NXM=0o040, ODDADDR=0o100, ILLHALT=0o200)
|
|
|
|
# halt types. These are not architectural, but are helpful to see.
|
|
# They go into self.halted as "truthy" values
|
|
HALTED_INST = 1 # halt instruction
|
|
HALTED_VECTORS = 2 # vectors not mapped into kernel dataspace (!!)
|
|
HALTED_STACK = 3 # fatal kernel stack condition
|
|
|
|
# "straps" are synchronous traps. They are trap operations that occur
|
|
# AFTER an instruction has completely executed. Examples include the
|
|
# stack-limit violation traps, and the MMU's "management trap" which
|
|
# allows the OS to monitor page usage without a full-on page fault.
|
|
# [ see the distinction between "traps" and "aborts" in the MMU ]
|
|
#
|
|
# STRAPBITS are set by the emulation code when their condition obtains
|
|
# and a trap should be generated at the completion of the instruction.
|
|
# If multiple are requested in a single instruction, only the highest
|
|
# priority will fire. These STRAPBITS values are an implementation
|
|
# detail (presumably the PDP11 microarchitecture does something similar).
|
|
# They are never seen outside the processor implementation.
|
|
#
|
|
# NOTE: mid-instruction aborts essentially are the same as a strap, but
|
|
# abort an instruction midway rather than letting it continue. They
|
|
# are implemented by raising a PDPTrap exception, which is caught at
|
|
# the instruction loop top-level, and then turned into the same thing
|
|
# as a strap -- just one that gets there mid-instruction (and highest
|
|
# priority). See HIGHEST_ABORTTRAP in STRAPBITS and the try/except
|
|
# in the main instruction processing loop.
|
|
|
|
# there is no significance to the specific values, other than
|
|
# they must be single bits and they must sort in this order
|
|
STRAPBITS = SimpleNamespace(
|
|
|
|
HIGHEST_ABORTTRAP=0o100000, # absolutely MUST be the highest
|
|
MEMMGT=0o010000,
|
|
YELLOW=0o004000,
|
|
PIR=0o000040)
|
|
|
|
# this is just a handy thing to have, and this is as good a place
|
|
# for it as anywhere else
|
|
@staticmethod
|
|
def u16add(a, b):
|
|
return (a + b) & 0o177777
|
|
|
|
def __init__(self, *,
|
|
physmem=None, # default will be 512KB
|
|
unibus=None, # subclasses may want to supply variant
|
|
logger="pdp11", loglevel='WARNING'):
|
|
|
|
# logging is enabled by default and will go to
|
|
# a file logger + ".log" (i.e., "pdp11.log" by default).
|
|
# If logging is not a str instance it will just be used as is.
|
|
# (so logging can be configured by caller that way)
|
|
|
|
try:
|
|
logfname = logger + ".log"
|
|
except TypeError:
|
|
self.logger = logger
|
|
else:
|
|
loglevel = logging.getLevelNamesMapping().get(loglevel, loglevel)
|
|
logger = logging.getLogger(logger)
|
|
if not logger.hasHandlers(): # XXX is this the right/best way?
|
|
logger.propagate = False
|
|
|
|
logger.setLevel(loglevel)
|
|
logger_fh = logging.FileHandler(logfname)
|
|
formatter = logging.Formatter(
|
|
'%(name)s.%(levelname)s[%(asctime)s]: %(message)s',
|
|
datefmt='%H%M%S')
|
|
logger_fh.setFormatter(formatter)
|
|
logger.addHandler(logger_fh)
|
|
self.logger = logger
|
|
|
|
self.logger.info(f"{self.__class__.__name__} started;"
|
|
f" Logging level={logging.getLevelName(loglevel)}.")
|
|
|
|
# registers but usually get overridden by model-specific subclass
|
|
self.r = [0] * 8
|
|
self.ub = unibus(self) if unibus else UNIBUS(self)
|
|
self.mmu = MemoryMgmt(self)
|
|
|
|
# default physical memory is 256K WORDS (512KB)
|
|
self.physmem = physmem or ([0] * (256*1024))
|
|
|
|
# >>5 because len gives words not bytes, and -1 because manual says:
|
|
# defined to indicate the last addressable block of 32 words in
|
|
# memory (bit 0 is equivalent to bit 6 of the Physical Address).
|
|
self.lowersize = (len(self.physmem) >> 5) - 1
|
|
|
|
# The 16-bit view of the PSW is synthesized when read; the
|
|
# essential parts of it are split out internally like this:
|
|
self.psw_curmode = self.KERNEL
|
|
self.psw_prevmode = self.KERNEL
|
|
self.psw_regset = 0 # this is not in all processors
|
|
self.psw_pri = 7
|
|
self.psw_trap = 0
|
|
self.psw_n = 0
|
|
self.psw_z = 0
|
|
self.psw_v = 0
|
|
self.psw_c = 0
|
|
|
|
# some attributes ("registers") that appear in I/O page
|
|
for attrname, offs in (('psw', self.PS_OFFS),
|
|
('stack_limit_register', self.STACKLIM_OFFS),
|
|
('swleds', self.SWLEDS_OFFS),
|
|
('lowersize', self.LOWERSIZE_OFFS),
|
|
('uppersize', self.UPPERSIZE_OFFS),
|
|
('systemID', self.SYSTEMID_OFFS),
|
|
('error_register', self.CPUERROR_OFFS),
|
|
('logging_hack', self.LOGGING_OFFS)):
|
|
self.ub.mmio.register_simpleattr(self, attrname, offs)
|
|
|
|
# console switches (read) and blinken lights (write)
|
|
self.swleds = 0
|
|
self.error_register = 0 # CPU Error register per handbook
|
|
|
|
# NOTE: The cold machine starts out in stack limit violation.
|
|
# (stack pointer = 0). However, the limit semantics only apply
|
|
# during explicit stack push operations such as: mov foo,-(sp)
|
|
# or implicit pushes during traps/interrupts.
|
|
# Programs such as boot-loaders running from cold-start conditions
|
|
# should establish a valid stack early in their instruction sequence.
|
|
self.stack_limit_register = 0
|
|
|
|
# straps: keeps track of requests for synchronous traps
|
|
# during an instruction. Note that only one will really happen,
|
|
# whichever is the highest priority, though some might persist
|
|
# and recur
|
|
# - stack limit
|
|
# - mmu management traps (note: these are not aborts)
|
|
# ... others?
|
|
#
|
|
# _strapsclear contains straps to clear during/after processing
|
|
self.straps = 0
|
|
self._strapsclear = 0
|
|
|
|
# start off in halted state until .run() happens
|
|
self.halted = True
|
|
|
|
# device instances; see add_device
|
|
self.devices = {}
|
|
|
|
def physRW(self, physaddr, value=None):
|
|
"""like MMU.wordRW but takes physical addresses."""
|
|
|
|
if (physaddr & 1):
|
|
raise PDPTraps.AddressError(cpuerr=self.CPUERR_BITS.ODDADDR)
|
|
|
|
physaddr >>= 1 # physical mem is an array of WORDs
|
|
try:
|
|
if value is None: # i.e., reading
|
|
return self.physmem[physaddr]
|
|
else:
|
|
# sanity check should be taken out eventually
|
|
if (value & 0xFFFF) != value:
|
|
raise ValueError(f"{value} is out of range")
|
|
self.physmem[physaddr] = value
|
|
return value # generally ignored
|
|
except IndexError:
|
|
raise PDPTraps.AddressError(
|
|
cpuerr=self.CPUERR_BITS.NXM) from None
|
|
|
|
def physRW_N(self, physaddr, nwords, words=None):
|
|
"""Like physRW but for nwords at a time."""
|
|
|
|
if (physaddr & 1):
|
|
raise PDPTraps.AddressError(cpuerr=self.cpu.CPUERR_BITS.ODDADDR)
|
|
physaddr >>= 1 # physical mem is an array of WORDs
|
|
|
|
try:
|
|
if words is None:
|
|
return self.physmem[physaddr:physaddr+nwords]
|
|
else:
|
|
self.physmem[physaddr:physaddr+nwords] = words
|
|
except IndexError:
|
|
raise PDPTraps.AddressError(
|
|
cpuerr=self.CPUERR_BITS.NXM) from None
|
|
|
|
# "associate" an emulated device
|
|
#
|
|
# Typically a device is instantiated by passing the unibus
|
|
# attribute ('ub') of a PDP11 instance to its __init__ function:
|
|
#
|
|
# # p is a PDP11 instance
|
|
# # XYZ11 is a device class
|
|
#
|
|
# device_instance = XYZ11(p.ub)
|
|
#
|
|
# The device __init__ will typically use ub.mmio.register to connect up
|
|
# to its UNIBUS addresses. That's all that needs to happen.
|
|
#
|
|
# Nevertheless, on general principles, it seems like the pdp instance
|
|
# should "know" about its devices, so ... this method.
|
|
#
|
|
# The typical code sequence would be:
|
|
# p = PDP1170()
|
|
# p.associate_device(XY11(p.ub), 'XY')
|
|
# p.associate_device(AB11(p.ub), 'AB')
|
|
# etc., combining instantiation ("XY11(p.ub)") and name association.
|
|
#
|
|
# HOWEVER, note that this works just as well:
|
|
# p = PDP1170()
|
|
# _ = XY11(p.ub)
|
|
# _ = AB11(p.ub)
|
|
# the devices just won't be discoverable via the devices attribute.
|
|
# Device discovery via the devices attribute is not "architectural",
|
|
# but may be useful for some test programs or other introspection.
|
|
#
|
|
def associate_device(self, dev, device_name=None, /):
|
|
"""Associate the device instance 'dev' with 'device_name'
|
|
|
|
If device_name is None then __class__.__name__ is used.
|
|
"""
|
|
if device_name is None:
|
|
device_name = dev.__class__.__name__
|
|
|
|
try:
|
|
self.devices[device_name].append(dev)
|
|
except KeyError:
|
|
self.devices[device_name] = [dev]
|
|
|
|
# this the heart of all things related to 6-bit instruction operands.
|
|
# If value is not given this will be a read
|
|
# If value is given and not None, this will be a write
|
|
#
|
|
# If justEA is True, the address that would be used to access the
|
|
# operand is returned, vs the operand itself. This is not valid for
|
|
# register direct. See JMP/JSR for examples of how/when this happens.
|
|
#
|
|
# If rmw is True, this will return a tuple:
|
|
# value, extendedB6
|
|
# otherwise it returns just the value (read, or written)
|
|
|
|
def operandx(self, b6, value=None, /, *,
|
|
opsize=2, altmode=None, altspace=None,
|
|
rmw=False, justEA=False):
|
|
"""Parse a 6-bit operand and read it (value is None) or write it.
|
|
|
|
By default the value (read, or written) is returned.
|
|
Some instructions need the operand address, not the value
|
|
(JSR is the best example of this). Specify justEA=True for that.
|
|
Note that justEA=True will trap for register-direct mode.
|
|
|
|
Some opcodes use a single addressing mode twice:
|
|
val = read the operand
|
|
do something to val (i.e., INC)
|
|
write modified val to the operand
|
|
The problem is side-effects, which are executed here for
|
|
modes like (Rn)+ (pc-relative is also a problem)
|
|
|
|
For this case, specify rmw=True ("read/modify/write") on the read
|
|
call (value=None) in which case the return value will be a tuple:
|
|
(value, EXTENDED_B6)
|
|
|
|
and the EXTENDED_B6 should be passed back in as the "b6" for the
|
|
write call. Callers should treat it as opaque. It is encoded to
|
|
allow the same operand to be re-used but without side effects
|
|
the second time.
|
|
"""
|
|
|
|
# EXTENDED_B6 ENCODING -- it is a 32-bit value:
|
|
# Bits 31-24 = 0xFF or 0x00
|
|
# If 00: The entire value is just a native b6. The low 6 bits
|
|
# are a pdp11 b6 value and all other bits are zero.
|
|
# If FF:
|
|
# bits 23-8: 16-bit effective address
|
|
# bits 7-6: mmu.ISPACE or mmu.DSPACE value
|
|
# bits 5-0: 0o47 which is an illegal b6; just to avoid
|
|
# looking like an optimizable case and
|
|
# to catch bugs if somehow used
|
|
#
|
|
|
|
# NOTE: real PDP-11 implementations vary in corner cases.
|
|
# For example:
|
|
# MOV R5,-(R5)
|
|
# what value gets stored? This turns out to vary. In fact, DEC
|
|
# documented the variations across processors. FWIW, the MACRO-11
|
|
# assembler generates warnings for such cases. Given all that,
|
|
# the assumption here is that getting those tricky semantics
|
|
# "correct to the specific processor variations" is unnecessary.
|
|
|
|
# optimize addr mode 0 - register. 8 or 16 bits.
|
|
# Note that in all READ cases b6 will be the newb6 (reusable)
|
|
if (b6 & 0o70) == 0:
|
|
if justEA:
|
|
raise PDPTraps.AddressError
|
|
|
|
match b6 & 0o07, value, opsize:
|
|
case Rn, None, 2:
|
|
value = self.r[Rn]
|
|
case Rn, wv, 2:
|
|
self.r[Rn] = wv
|
|
case Rn, None, 1:
|
|
value = self.r[Rn] & 0o377
|
|
case Rn, bv, 1:
|
|
# NOTE: The MOVB instruction has different semantics
|
|
# than this; it is coded explicitly in op11_movb()
|
|
self.r[Rn] &= 0o177400
|
|
self.r[Rn] |= (bv & 0o377)
|
|
return (value, b6) if rmw else value
|
|
|
|
# harder cases
|
|
autocrement = 0 # increment/decrement
|
|
space = self.mmu.DSPACE # gets changed in various cases
|
|
extendedb6 = b6 # will be altered as necessary
|
|
|
|
match b6 & 0xFF_0000_00, (b6 & 0o70), (b6 & 0o07):
|
|
# (Rn) -- register deferred
|
|
case 0, 0o10, Rn:
|
|
addr = self.r[Rn]
|
|
if Rn == 7:
|
|
space = self.mmu.ISPACE
|
|
|
|
# both autoincrement addrmodes: (Rn)+ and @(Rn)+
|
|
case 0, 0o20 | 0o30 as addrmode, Rn:
|
|
addr = self.r[Rn]
|
|
if Rn == self.PC:
|
|
space = self.mmu.ISPACE
|
|
autocrement = 2 # regardless of opsize
|
|
elif Rn == self.SP:
|
|
autocrement = 2 # regardless of opsize
|
|
else:
|
|
autocrement = opsize
|
|
|
|
if addrmode == 0o30:
|
|
addr = self.mmu.wordRW(addr, space=space)
|
|
space = self.mmu.DSPACE
|
|
autocrement = 2 # regardless of opsize used above
|
|
extendedb6 = None # force update below
|
|
|
|
# both autodecrement addrmode, PC - NOPE.
|
|
case 0, 0o40 | 0o50, 7:
|
|
# ... did the pdp11 fault on this?
|
|
raise PDPTraps.ReservedInstruction
|
|
|
|
# both autodecrement addrmodes, not PC
|
|
# note that bytes and -(SP) still decrement by 2
|
|
case 0, 0o40 | 0o50 as addrmode, Rn:
|
|
if Rn == self.SP:
|
|
autocrement = -2
|
|
elif addrmode == 0o50:
|
|
autocrement = -2
|
|
else:
|
|
autocrement = -opsize
|
|
extendedb6 = None # force update below
|
|
addr = self.u16add(self.r[Rn], autocrement)
|
|
if addrmode == 0o50:
|
|
addr = self.mmu.wordRW(addr, space=self.mmu.DSPACE)
|
|
|
|
# X(Rn) and @X(Rn)
|
|
case 0, (0o60 | 0o70) as addrmode, Rn:
|
|
x = self.mmu.wordRW(self.r[self.PC], space=self.mmu.ISPACE)
|
|
self.r[self.PC] = self.u16add(self.r[self.PC], 2)
|
|
addr = self.u16add(self.r[Rn], x)
|
|
extendedb6 = None # force update below
|
|
if addrmode == 0o70:
|
|
addr = self.mmu.wordRW(addr, space=self.mmu.DSPACE)
|
|
|
|
case 0xFF_0000_00, _, _:
|
|
# the address was shifted up 8 bits (to get it away
|
|
# from the mode-0 optimization tests) and the space
|
|
# was encoded shifted up 6 bits (again, get away from mode 0)
|
|
addr = (b6 >> 8) & 0xFFFF
|
|
space = (b6 >> 6) & 3
|
|
case _: # should be unreachable
|
|
raise TypeError("internal error")
|
|
|
|
if autocrement != 0:
|
|
# the autoincrement/decrements have to be recorded into the MMU
|
|
# for instruction recovery if there is a page error.
|
|
self.mmu.MMR1mod(((autocrement & 0o37) << 3) | Rn)
|
|
self.r[Rn] = self.u16add(self.r[Rn], autocrement)
|
|
if Rn == self.SP and autocrement < 0:
|
|
self.redyellowcheck() # may raise a RED zone exception
|
|
|
|
if rmw and (value is None) and (extendedb6 is None):
|
|
extendedb6 = 0xFF_0000_27 | (addr << 8) | (space << 6)
|
|
|
|
# use alternate space (e.g. forced ISPACE) if requested.
|
|
if altspace is not None:
|
|
space = altspace
|
|
|
|
if justEA:
|
|
val = addr
|
|
elif opsize == 2:
|
|
val = self.mmu.wordRW(addr, value, mode=altmode, space=space)
|
|
else:
|
|
val = self.mmu.byteRW(addr, value, mode=altmode, space=space)
|
|
|
|
return (val, extendedb6) if rmw else val
|
|
|
|
# convenience, creates a breakpoint function to stop after N steps
|
|
def run_steps(self, steps, *args, **kwargs):
|
|
bpt = StepsBreakpoint(steps=steps)
|
|
return self.run(*args, breakpoint=bpt, **kwargs)
|
|
|
|
# convenience, creates a breakpoint to stop at the given pc,
|
|
# optionally limited to a specific mode (KERNEL, USER, SUPERVISOR)
|
|
def run_until(self, *args, stoppc, stopmode=None, **kwargs):
|
|
"""Run processor with breakpoint at 'stoppc'.
|
|
|
|
if stopmode is None (default), stop at the stoppc in any mode.
|
|
Otherwise, only stop if mode is stopmode.
|
|
"""
|
|
bpt = PCBreakpoint(stoppc=stoppc, stopmode=stopmode)
|
|
return self.run(*args, breakpoint=bpt, **kwargs)
|
|
|
|
def run(self, *, pc=None, breakpoint=None):
|
|
"""The CPU main loop - run the machine!
|
|
|
|
If pc is None (default) execution begins at the current pc; otherwise
|
|
the pc is set to the given value first.
|
|
|
|
If breakpoint is not None (default), it is expected to be a callable.
|
|
It will be invoked after every instruction and if it returns True
|
|
then a breakpoint will occur (the run() method returns).
|
|
"""
|
|
|
|
if pc is not None:
|
|
self.r[self.PC] = pc
|
|
|
|
# some shorthands for convenience
|
|
interrupt_mgr = self.ub.intmgr
|
|
mmu = self.mmu
|
|
|
|
abort_trap = None # a mid-instruction abort (vs strap)
|
|
self.halted = False
|
|
|
|
# NOTE WELL: everything in this loop is per-instruction overhead
|
|
while not self.halted:
|
|
|
|
# SUBTLETY: Trap handlers expect the PC to be 2 beyond the
|
|
# instruction causing the trap. Hence "+2 then execute"
|
|
thisPC = self.r[self.PC]
|
|
self.r[self.PC] = (thisPC + 2) & 0o177777 # "could" wrap
|
|
|
|
mmu.MMR1_staged = 0 # see discussion in go_trap
|
|
mmu.MMR2 = thisPC # per handbook
|
|
inst = None # so bkpt can know if wordRW faulted
|
|
|
|
try:
|
|
inst = mmu.wordRW(thisPC)
|
|
op4_dispatch_table[inst >> 12](self, inst)
|
|
except PDPTrap as trap:
|
|
abort_trap = trap
|
|
self.straps |= self.STRAPBITS.HIGHEST_ABORTTRAP
|
|
|
|
# pri order:abort traps (encoded as a strap), straps, interrupts
|
|
if self.straps:
|
|
self.go_trap(self.get_synchronous_trap(abort_trap))
|
|
elif interrupt_mgr.pri_pending > self.psw_pri:
|
|
self.go_trap(interrupt_mgr.get_pending(self.psw_pri))
|
|
|
|
if breakpoint and breakpoint(self, XInfo(thisPC, inst)):
|
|
break
|
|
|
|
# fall through to here if self.halted or breakpoont
|
|
if self.halted:
|
|
reason = ".run -- HALTED: {}"
|
|
else:
|
|
reason = ".run -- breakpoint: {}"
|
|
self.logger.info(reason.format(self.machinestate()))
|
|
|
|
def redyellowcheck(self):
|
|
"""stack limits: possibly sets YELLOW straps, or go RED."""
|
|
|
|
# only applies to kernel stack operations
|
|
if self.psw_curmode != self.KERNEL:
|
|
return
|
|
|
|
# Note special semantic of zero which means 0o400
|
|
# (as defined by hardware book)
|
|
lim = self.stack_limit_register or 0o400
|
|
if self.r[self.SP] < lim:
|
|
if not (self.straps & self.STRAPBITS.YELLOW):
|
|
self.logger.info(f"YELLOW ZONE, {list(map(oct, self.r))}")
|
|
|
|
# how about red?
|
|
if self.r[self.SP] + 32 < lim: # uh oh - below the yellow!
|
|
# this is a red zone trap which is immediate
|
|
# the stack pointer is set to location 4
|
|
# and this trap is executed
|
|
self.r[self.SP] = 4 # !! just enough room for...
|
|
raise PDPTraps.AddressError(cpuerr=self.CPUERR_BITS.REDZONE)
|
|
else:
|
|
self.straps |= self.STRAPBITS.YELLOW
|
|
|
|
def get_synchronous_trap(self, abort_trap):
|
|
"""Return a synchronous trap, or possibly None.
|
|
|
|
For notational convenience in the instruction loop, the
|
|
abort_trap argument, if not None, represents a mid-instruction
|
|
abort which is the highest priority trap and it is just returned.
|
|
The corresponding straps bit is cleared.
|
|
|
|
After that, finds the highest priority strap if any, and returns it.
|
|
"""
|
|
|
|
# as described above... this is how aborts work
|
|
if self.straps & self.STRAPBITS.HIGHEST_ABORTTRAP:
|
|
self.straps &= ~self.STRAPBITS.HIGHEST_ABORTTRAP
|
|
return abort_trap
|
|
|
|
# Synchronous traps are events that are caused by an instruction
|
|
# but happen AFTER the instruction completes. The handbook shows
|
|
# eight of them, in this priority order (high to low)
|
|
#
|
|
# HIGHEST -- Parity error
|
|
# Memory Management violation
|
|
# Stack Limit Yellow
|
|
# Power Failure
|
|
# Floating Point
|
|
# Program Interrupt Request
|
|
# Bus Request
|
|
# LOWEST Trace Trap
|
|
#
|
|
# If there are multiple, only the highest priority will fire,
|
|
# though some types of them are persistent (in their root cause)
|
|
# and would therefore come back with the next instruction and
|
|
# (potentially) fire there instead.
|
|
|
|
# The stack limit yellow bit is a little different ... have
|
|
# to also check for the red zone here.
|
|
if self.straps & self.STRAPBITS.YELLOW:
|
|
# at a minimum, it's a yellow zone fault
|
|
self.error_register |= self.CPUERR_BITS.YELLOW
|
|
|
|
# note that these are tested in priority order. With only two
|
|
# cases here, if/elif seemed better than iterating a table
|
|
if self.straps & self.STRAPBITS.MEMMGT:
|
|
self.straps &= ~self.STRAPBITS.MEMMGT
|
|
return PDPTraps.MMU()
|
|
elif self.straps & self.STRAPBITS.YELLOW: # red handled as an abort
|
|
self._strapsclear = self.STRAPBITS.YELLOW
|
|
return PDPTraps.AddressError(cpuerr=self.CPUERR_BITS.YELLOW)
|
|
return None
|
|
|
|
def go_trap(self, trap):
|
|
"""Control transfer for all types of traps, INCLUDING interrupts."""
|
|
|
|
# it's convenient to allow trap to be None meaning "never mind"
|
|
if trap is None:
|
|
return
|
|
|
|
self.logger.debug(f"TRAP: {trap}:\n{self.machinestate()}")
|
|
self.error_register |= trap.cpuerr
|
|
|
|
# get the vector information -- always from KERNEL/DSPACE
|
|
try:
|
|
newpc = self.mmu.wordRW_KD(trap.vector)
|
|
newps = self.mmu.wordRW_KD(trap.vector+2)
|
|
except PDPTrap:
|
|
# this is an egregious kernel programming error -- the vectors
|
|
# are not mapped into KERNEL/DSPACE. It is a fatal halt.
|
|
self.logger.info(f"Trap accessing trap vectors")
|
|
self.halted = self.HALTED_VECTORS
|
|
return
|
|
|
|
# From the PDP11 processor book:
|
|
# The old PS and PC are then pushed onto the current stack
|
|
# as indicated by bits 15,14 of the new PS and the previous
|
|
# mode in effect is stored in bits 13,12 of the new PS.
|
|
# Thus:
|
|
|
|
# easiest to get the "previous" (currently current) mode this way:
|
|
saved_curmode = self.psw_curmode
|
|
saved_psw = self.psw
|
|
|
|
# note: this (likely) switches SP and of course various psw_xxx fields
|
|
self.psw = newps
|
|
self.psw_prevmode = saved_curmode # i.e., override newps<13:12>
|
|
|
|
self._trappush(self.r[self.PC], saved_psw)
|
|
|
|
# The error register records (accumulates) reasons (if given)
|
|
self.error_register |= trap.cpuerr
|
|
|
|
# if a strap is being processed, this is where it is reset
|
|
# IMPORTANT/SUBTLE NOTE: This is after the stack pushes, so this
|
|
# is how they avoid (re-)causing YELLOW while processing YELLOW
|
|
self.straps &= ~self._strapsclear
|
|
self._strapsclear = 0
|
|
|
|
# alrighty then, can finally jump to the PC from the vector
|
|
self.r[self.PC] = newpc
|
|
|
|
@property
|
|
def swleds(self):
|
|
return 0 # no switches implementation, yet
|
|
|
|
@swleds.setter
|
|
def swleds(self, v): # writing to the lights is a no-op for now
|
|
pass
|
|
|
|
# technically not all -11's have stack limit support.
|
|
# Meh, do it in base class anyway
|
|
@property
|
|
def stack_limit_register(self):
|
|
return self._stklim
|
|
|
|
@stack_limit_register.setter
|
|
def stack_limit_register(self, v):
|
|
|
|
# at __init__ time it's important to NOT indicate the need
|
|
# for a stack check or else the first instruction executed
|
|
# will fail the stack limit.
|
|
#
|
|
# Any other time, set the bit so the main instruction loop
|
|
# will know it needs to examine the stack limit status.
|
|
#
|
|
# This could also have been fixed by initializing _stklim in
|
|
# __init__ and not "stack_limit_register = 0" , or it could
|
|
# have been fixed by slamming strapcheck back to false after that.
|
|
# But this way ensures The Right Thing happens no matter what.
|
|
# Performance is no issue in setting the stack limit obviously.
|
|
self.logger.debug(
|
|
f"setting stack limit to {oct(v)}, sp={oct(self.r[6])}")
|
|
checkit = hasattr(self, '_stklim')
|
|
self._stklim = v & 0o177400
|
|
if checkit:
|
|
self.redyellowcheck()
|
|
|
|
def stackpush(self, w):
|
|
self.r[6] = self.u16add(self.r[6], -2)
|
|
# stacklimit checks only apply to the kernel and do not
|
|
# apply when pushing the frame for a stacklimit fault (!)
|
|
if self.psw_curmode == self.KERNEL:
|
|
self.redyellowcheck() # may raise a RED exception
|
|
self.mmu.wordRW(self.r[6], w, space=self.mmu.DSPACE)
|
|
|
|
def stackpop(self):
|
|
w = self.mmu.wordRW(self.r[6], space=self.mmu.DSPACE)
|
|
self.r[6] = self.u16add(self.r[6], 2)
|
|
return w
|
|
|
|
# this is special because stack limit checking is disabled
|
|
# during pushes of trap/interrupt frames. Any other types of
|
|
# traps during a stack trap push are a fatal CPU halt and represent
|
|
# a serious kernel programming error (invalid kernel stack)
|
|
def _trappush(self, pc, psw):
|
|
try:
|
|
# NOTE: The stack pointer is only modified if
|
|
# both of these succeed with no mmu/addressing traps
|
|
self.mmu.wordRW_KD(self.u16add(self.r[self.SP], -2), psw)
|
|
self.mmu.wordRW_KD(self.u16add(self.r[self.SP], -4), pc)
|
|
except PDPTrap as e:
|
|
# again this is a pretty egregious error it means the kernel
|
|
# stack is not mapped, or the stack pointer is odd, or similar
|
|
# very bad mistakes by the kernel code. It is a fatal halt
|
|
self.logger.info(f"Trap ({e}) pushing trap frame onto stack")
|
|
self.logger.info(f"Machine state: {self.machinestate()}")
|
|
self.logger.info("HALTING")
|
|
self.halted = self.HALTED_STACK
|
|
else:
|
|
self.r[self.SP] = self.u16add(self.r[self.SP], -4)
|
|
|
|
|
|
class PDP1170(PDP11):
|
|
|
|
# some 1170-specific values
|
|
IOPAGE_REGSET_SIZE = 0o20 # 11/70 has two sets of registers
|
|
|
|
def __init__(self, *, physmem=None, **kwargs):
|
|
super().__init__(physmem=physmem, unibus=UNIBUS_1170, **kwargs)
|
|
|
|
# there are two register files, though r6 and r7 are special
|
|
self.registerfiles = [[0] * 8, [0] * 8]
|
|
|
|
# There are four stack pointers, but only 3 are legal.
|
|
# This can be indexed by self.KERNEL, self.SUPERVISOR, etc
|
|
self.stackpointers = [0, 0, 0, 0]
|
|
|
|
# The 16-bit view of the PSW is synthesized when read; the
|
|
# essential parts of it are split out internally like this:
|
|
self.psw_curmode = self.KERNEL
|
|
self.psw_prevmode = self.KERNEL
|
|
self.psw_regset = 0
|
|
self.psw_pri = 7
|
|
self.psw_trap = 0
|
|
self.psw_n = 0
|
|
self.psw_z = 0
|
|
self.psw_v = 0
|
|
self.psw_c = 0
|
|
|
|
# self.r points to the current register set
|
|
self.r = self.registerfiles[self.psw_regset]
|
|
|
|
# how the registers appear in IOPAGE space
|
|
self.ub.mmio.register(self._ioregsets,
|
|
self.IOPAGE_REGSETS_OFFS,
|
|
self.IOPAGE_REGSET_SIZE)
|
|
|
|
@property
|
|
def r_alt(self):
|
|
"""The other set of registers (the one that is not self.r)."""
|
|
return self.registerfiles[1 - self.psw_regset]
|
|
|
|
def _ioregsets(self, addr, value=None, /):
|
|
# NOTE that the encoding of the register addresses is quite funky
|
|
# and includes ODD addresses (!!!)
|
|
# [ addresses given relative to I/O page base ]
|
|
# REGISTER SET ZERO
|
|
# 17700 : R0
|
|
# 17701 : R1 -- this being at ODD address is not a typo!
|
|
# 17702 : R2
|
|
# 17703 : R3 -- not a typo
|
|
# 17704 : R4
|
|
# 17705 : R5 -- not a typo
|
|
# 17706 : KERNEL SP
|
|
# 17707 : PC
|
|
#
|
|
# REGISTER SET ONE
|
|
# 17710 : R0
|
|
# 17711 : R1
|
|
# 17712 : R2
|
|
# 17713 : R3
|
|
# 17714 : R4
|
|
# 17715 : R5
|
|
# 17716 : SUPERVISOR SP
|
|
# 17717 : USER SP
|
|
regset = addr & 0o10
|
|
regnum = addr & 0o07
|
|
|
|
# copy the stack pointer out of its r6 "cache" and dup the pc
|
|
self._syncregs()
|
|
|
|
# regset regnum r/w (value None or not)
|
|
match ((addr & 0o10) >> 3, addr & 0o07, value):
|
|
case (0, 6, None):
|
|
return self.stackpointers[self.KERNEL]
|
|
case (0, 6, newksp):
|
|
self.stackpointers[self.KERNEL] = newksp
|
|
case (1, 6, None):
|
|
return self.stackpointers[self.SUPERVISOR]
|
|
case (1, 6, newssp):
|
|
self.stackpointers[self.SUPERVISOR] = newssp
|
|
case (1, 7, None):
|
|
return self.stackpointers[self.USER]
|
|
case (1, 7, newusp):
|
|
self.stackpointers[self.USER] = newusp
|
|
case (regset, regnum, None):
|
|
return self.registerfiles[regset][regnum]
|
|
case (regset, regnum, _):
|
|
self.registerfiles[regset][regnum] = value
|
|
|
|
# if the stack pointer for the current mode was updated
|
|
# then reestablish it as r[6]. Can just do this unconditionally
|
|
# because syncregs copied out the active r[6] above
|
|
self.r[6] = self.stackpointers[self.psw_curmode]
|
|
|
|
def _syncregs(self):
|
|
# When there is a register set change, a mode change, or when
|
|
# the registers are being examined via their I/O addresses then
|
|
# the "cached" stack pointer in R6 has to be synced up to its
|
|
# real home, and the PC (R7) has to be duplicated into the other set.
|
|
|
|
self.stackpointers[self.psw_curmode] = self.r[6]
|
|
|
|
# sync the PC into the other register set
|
|
self.r_alt[self.PC] = self.r[self.PC]
|
|
|
|
@property
|
|
def psw(self):
|
|
# NOTE: to simplify/accelerate condition code handling during
|
|
# instructions, the NZVC bits are broken out into individual
|
|
# attributes, and are stored as truthy/falsey not necessarily
|
|
# 1/0 or True/False.
|
|
|
|
# so, to reconstitute NZVC bits ...
|
|
NZVC = 0
|
|
if self.psw_n:
|
|
NZVC |= 0o10
|
|
if self.psw_z:
|
|
NZVC |= 0o04
|
|
if self.psw_v:
|
|
NZVC |= 0o02
|
|
if self.psw_c:
|
|
NZVC |= 0o01
|
|
|
|
return (((self.psw_curmode & 3) << 14) |
|
|
((self.psw_prevmode & 3) << 12) |
|
|
((self.psw_regset & 1) << 11) |
|
|
((self.psw_pri & 7) << 5) |
|
|
((self.psw_trap & 1) << 4) |
|
|
NZVC)
|
|
|
|
# Write the ENTIRE processor word, without any privilege enforcement.
|
|
# The lack of privilege enforcement is necessary because, e.g., that's
|
|
# how traps get from user to kernel mode. Generally speaking, the
|
|
# only way for user mode programs to modify the PSW is via its I/O
|
|
# address, which (obviously) an OS should not put into user space.
|
|
@psw.setter
|
|
def psw(self, value):
|
|
"""Set entire PSW. NOTE: no privilege enforcement."""
|
|
|
|
# could test if necessary but it's just easier to do this every time
|
|
self._syncregs() # in case any mode/regset changes
|
|
|
|
# prevent UNDEFINED_MODE from entering the PSW (current mode)
|
|
m = (value >> 14) & 3
|
|
if m == self.UNDEFINED_MODE:
|
|
raise PDPTraps.ReservedInstruction
|
|
self.psw_curmode = m
|
|
|
|
# prevent UNDEFINED_MODE from entering the PSW (previous mode)
|
|
m = (value >> 12) & 3
|
|
if m == self.UNDEFINED_MODE:
|
|
raise PDPTraps.ReservedInstruction
|
|
self.psw_prevmode = m
|
|
|
|
prevregset = self.psw_regset
|
|
self.psw_regset = (value >> 11) & 1
|
|
|
|
newpri = (value >> 5) & 7
|
|
self.psw_pri = newpri
|
|
|
|
self.psw_trap = (value >> 4) & 1
|
|
self.psw_n = (value >> 3) & 1
|
|
self.psw_z = (value >> 2) & 1
|
|
self.psw_v = (value >> 1) & 1
|
|
self.psw_c = value & 1
|
|
|
|
# set up the correct register file and install correct SP
|
|
self.r = self.registerfiles[self.psw_regset]
|
|
self.r[6] = self.stackpointers[self.psw_curmode]
|
|
|
|
# the PC was already sync'd in syncregs()
|
|
|
|
@property
|
|
def logging_hack(self):
|
|
return self.logger.level
|
|
|
|
@logging_hack.setter
|
|
def logging_hack(self, value):
|
|
self.logger.setLevel(value)
|
|
|
|
# this is convenient to have for debugging and logging
|
|
def spsw(self, v=None):
|
|
"""Return string rep of a psw value."""
|
|
if v is None:
|
|
v = self.psw
|
|
|
|
cm = (v >> 14) & 3
|
|
pm = (v >> 12) & 3
|
|
|
|
m2s = "KS!U"
|
|
|
|
s = f"CM={m2s[cm]} PM={(m2s[pm])}"
|
|
if v & 0o04000:
|
|
s += " Rx=1"
|
|
s += f" PRI={(v >> 5) & 0o07}"
|
|
if v & 0o020:
|
|
s += " T"
|
|
if v & 0o017:
|
|
s += " "
|
|
if v & 0o010:
|
|
s += "N"
|
|
if v & 0o004:
|
|
s += "Z"
|
|
if v & 0o002:
|
|
s += "V"
|
|
if v & 0o001:
|
|
s += "C"
|
|
return s
|
|
|
|
# logging/debugging convenience
|
|
def machinestate(self):
|
|
# machine state is arbitrarily collected into a dictionary:
|
|
d = {}
|
|
regnames = (* (f"R{i}" for i in range(6)), "SP", "PC")
|
|
for i in range(8):
|
|
d[regnames[i]] = self.r[i]
|
|
|
|
d['PSW'] = self.psw
|
|
|
|
# these are redundant but convenient to have broken out
|
|
d['CURMODE'] = self.psw_curmode
|
|
d['PREVMODE'] = self.psw_prevmode
|
|
d['PRI'] = self.psw_pri
|
|
|
|
for m in (0, 1, 3):
|
|
d[("KSP", "SSP", "!X!", "USP")[m]] = self.stackpointers[m]
|
|
|
|
for mmr in ('MMR0', 'MMR1', 'MMR2', 'MMR3'):
|
|
d[mmr] = getattr(self.mmu, mmr)
|
|
|
|
try:
|
|
# _invisread so as not to disturb state (MMR/cpuerror regs etc)
|
|
d['MMR2inst'] = self.mmu._invisread(self.mmu.MMR2)
|
|
except PDPTrap as e:
|
|
d['MMR2inst'] = e
|
|
|
|
return d
|