# 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 import itertools from types import SimpleNamespace from pdptraps import PDPTrap, PDPTraps from mmu import MemoryMgmt from unibus import UNIBUS, UNIBUS_1170 from kl11 import KL11 from rp import RPRM # from rpa import RPRM_AIO as RPRM from kw11 import KW11 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. 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 # 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 significant 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 console=True, # automated tests need to turn this off logger="pdp11", loglevel='INFO', instlog=False, pswlog=False): # 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)}.") # instruction logging and/or PSW logging - HUGE LOGS but # sometimes helpful. Often the best way to use this is to insert # custom code into the run loop to trigger these as desired. self.instlog = instlog self.pswlog = pswlog 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)) # 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), ('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. # However, the semantics are that no check happens until something # stack-related occurs. Boot programs need to 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? # self.straps = 0 # start off in halted state until .run() happens self.halted = True # The console, the disk drive, and the clock are never really # accessed directly (everything is triggered through mmio I/O) # but of course must be instantiated if console: # it's helpful to disable for tests self._KL = KL11(self.ub) try: rp = RPRM(self.ub) except FileNotFoundError: self.logger.info("NO DISK DRIVE FILE FOUND; DISK DISABLED") else: self._RP = rp # line clock self._KW = KW11(self.ub) 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 # 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: self.r[Rn] = bv if bv > 127: self.r[Rn] |= 0xFF00 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 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: autocrement = -2 if Rn == self.SP else -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) if Rn == self.SP: self.strapcheck = True # XXX THIS IS A NO-OP LOOK # 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 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 def run(self, *, steps=None, pc=None, stopat=None, loglevel=None): """Run the machine for a number of steps (instructions). If steps is None (default), the machine runs until a HALT instruction is encountered. It may run forever and the method might never return. Otherwise, it runs for that many instructions (or until a HALT). If pc is None (default) execution begins at the current pc; otherwise the pc is set to the given value first. """ if loglevel is not None: loglevel = logging.getLevelNamesMapping().get(loglevel, loglevel) self.logger.setLevel(loglevel) if pc is not None: self.r[self.PC] = pc # Breakpoints (and step limits) are in the critical path. # To keep overhead to a minimum, breakpointfunc creates a # custom function to evaluate breakpoint criteria. When there # are no breakpoints or step limits at all, stop_here will be None. # Hence the test construction: # # if stop_here and stop_here() # # which is as fast as it can be when there are no execution limits. # When there ARE breakpoints etc, stop_here is a callable that # evaluates all stop criteria and returns True if the inner loop # should break. stop_here = self.breakpointfunc(stopat, steps) # 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: # stop_here function will also break # 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 try: inst = mmu.wordRW(thisPC) if self.instlog: self.instlogging(inst, 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 stop_here and stop_here(): break # fall through to here if self.halted or a stop_here condition # log halts (stop_here was already logged) if self.halted: self.logger.debug(f".run HALTED: {self.machinestate()}") def breakpointfunc(self, stopat, steps): # create a custom function that returns True if execution # meets the stop criteria. The returned function MUST be # called EXACTLY ONCE per instruction execution. # # If steps is not None, then at most that many invocations can # occur before execution will be halted (i.e., True returned). # # stopat can be a tuple: (pc, mode) or just a naked pc value. # Execution will halt when the processor reaches that pc # (in the given mode, or in any mode if not given). # # If both stopat and steps are None, then this returns None, # which allows the run() loop to optimize out the check. if stopat is None and steps is None: return None if steps is None: stepsgen = itertools.count() else: stepsgen = range(steps) try: stoppc, stopmode = stopat except TypeError: stoppc = stopat stopmode = None def _evalstop(): for icount in stepsgen: # this is sneaky ... it's can be handy in debugging to # know the instruction count; stuff it into the cpu object self.xxx_instcount = icount if self.r[self.PC] == stoppc: if stopmode is None or self.psw_curmode == stopmode: self.logger.info(f".run: breakpt at {oct(stoppc)}") break yield False else: self.logger.info(f".run: ran {icount+1} steps") yield True g = _evalstop() return lambda: next(g) 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. # no synchronous traps honored in certain error states ignores = self.CPUERR_BITS.REDZONE | self.CPUERR_BITS.YELLOW if self.error_register & ignores: return None # The stack limit yellow bit is a little different ... it gets # set when there is the *possibility* of a stack limit violation. # (because the stack pointer changed, or because the limits changed). # This is where the actual limit test gets checked. if self.straps & self.STRAPBITS.YELLOW: # 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: self.straps &= ~self.STRAPBITS.YELLOW # never mind, all good! else: self.logger.info(f"YELLOW ZONE, {list(map(oct, self.r))}") # yup definitely in at least a yellow condition self.error_register |= self.CPUERR_BITS.YELLOW # 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[6] = 4 # !! just enough room for... return PDPTraps.AddressError( cpuerr=self.CPUERR_BITS.REDZONE) # note that only the first (should be highest) will fire for bit, trapcl in ((self.STRAPBITS.MEMMGT, PDPTraps.MMU), (self.STRAPBITS.YELLOW, PDPTraps.AddressError)): if self.straps & bit: self.straps &= ~bit return trapcl() 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> prepushSP = self.r[6] try: self.stackpush(saved_psw) self.stackpush(self.r[self.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 # NOTE: The stack register is restored self.logger.info(f"Trap pushing trap onto stack") self.r[6] = prepushSP self.halted = self.HALTED_STACK # The error register records (accumulates) reasons (if given) self.error_register |= trap.cpuerr # alrighty then, can finally jump to the PC from the vector self.r[self.PC] = newpc # This is called when the run loop wants to log an instruction. # Pulled out so can be overridden for specific debugging sceanrios. def instlogging(self, inst, pc): try: logit = self.instlog(self, inst, thisPC) except TypeError: logit = True if logit: m = "KS!U"[self.psw_curmode] self.logger.debug(f"{oct(thisPC)}/{m} :: {oct(inst)}") @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 this, but ... meh do it here 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. if hasattr(self, '_stklim'): self.straps |= self.STRAPBITS.YELLOW self._stklim = v & 0o177400 def stackpush(self, w): # XXX YELLOW CHECK ??? self.r[6] = self.u16add(self.r[6], -2) 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 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 m = (value >> 14) & 3 if m == self.UNDEFINED_MODE: raise PDPTraps.ReservedInstruction self.psw_curmode = m # prevent UNDEFINED_MODE from entering the PSW 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 if self.pswlog and newpri != self.psw_pri: self.logger.debug(f"PSW pri change: {self.spsw()} -> " f"{self.spsw(value)}") 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, brief=False): s = self.spsw() + '; ' stacknames = ("KSP", "SSP", "!X!", "USP") regnames = (* (f"R{i}" for i in range(6)), stacknames[self.psw_curmode], "PC") for i in range(8): s += f"{regnames[i]}: {oct(self.r[i])} " for m in (0, 1, 3): name = stacknames[m] if m == self.psw_curmode: name = name[0] + "xx" s += f"{name}: {oct(self.stackpointers[m])} " return s