python-pdp1170/op00.py

338 lines
11 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.
from pdptraps import PDPTraps
from op000 import op000_dispatcher
from branches import branches
def op00_4_jsr(cpu, inst):
Rn = (inst & 0o700) >> 6
if Rn == cpu.SP:
raise PDPTraps.ReservedInstruction
# according to the PDP11 handbook...
# R7 is the only register that can be used for
# both the link and destination, the other GPRs cannot.
# Thus, if the link is R5, any register except R5 can be used
# for one destination field.
#
# Does the PDP11 Trap if this is violated, or is it just "undefined"??
if Rn != cpu.PC and Rn == (inst & 0o07):
raise PDPTraps.ReservedInstruction
# Note that the computed b6 operand address IS the new PC value.
# In other words, JSR PC,(R0) means that the contents of R0 are
# the subroutine address. This is one level of indirection less
# than ordinary instructions. Hence the justEA for operandx().
# Corollary: JSR PC, R0 is illegal (instructions cannot reside
# in the registers themselves)
tmp = cpu.operandx(inst & 0o77, justEA=True)
# NOTE: no condition code modifications
# cpu.logger.debug(f"JSR to {oct(tmp)} from {oct(cpu.r[cpu.PC])}")
cpu.mmu.MMR1mod(0o366) # the encoding for -2 on sp
cpu.stackpush(cpu.r[Rn])
cpu.r[Rn] = cpu.r[cpu.PC] # this could be a no-op if Rn == 7
cpu.r[cpu.PC] = tmp
def op00_50_clr(cpu, inst, opsize=2):
"""CLR(B) (determined by opsize). Clear destination."""
cpu.psw_n = cpu.psw_v = cpu.psw_c = 0
cpu.psw_z = 1
dstb6 = (inst & 0o77)
# optimize the common register case
if opsize == 2 and dstb6 < 8:
cpu.r[dstb6] = 0
else:
cpu.operandx(dstb6, 0, opsize=opsize)
def op00_51_com(cpu, inst, opsize=2):
"""COM(B) (determined by opsize). 1's complement destination."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
# Have to be careful about python infinite length integers
# For example, ~0xFFFF == -65536 whereas the desired result is zero.
# Hence the explicit masking
val = (~val) & cpu.MASK816[opsize]
cpu.psw_n = val & cpu.SIGN816[opsize]
cpu.psw_z = (val == 0)
cpu.psw_v = 0
cpu.psw_c = 1
cpu.operandx(xb6, val, opsize=opsize)
def op00_52_inc(cpu, inst, opsize=2):
"""INC(B) (determined by opsize). Increment destination."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
newval = (val + 1) & cpu.MASK816[opsize]
cpu.psw_n = newval & cpu.SIGN816[opsize]
cpu.psw_z = (newval == 0)
cpu.psw_v = (newval == cpu.SIGN816)
# C bit not affected
cpu.operandx(xb6, newval, opsize=opsize)
def op00_53_dec(cpu, inst, opsize=2):
"""DEC(B) (determined by opsize). Decrement destination."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
newval = (val - 1) & cpu.MASK816[opsize]
cpu.psw_n = newval & cpu.SIGN816[opsize]
cpu.psw_z = (newval == 0)
cpu.psw_v = (val == cpu.SIGN816[opsize])
# C bit not affected
cpu.operandx(xb6, newval, opsize=opsize)
def op00_54_neg(cpu, inst, opsize=2):
"""NEG(B) (determined by opsize). Negate the destination."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
newval = (-val) & cpu.MASK816[opsize]
cpu.psw_n = newval & cpu.SIGN816[opsize]
cpu.psw_z = (newval == 0)
cpu.psw_v = (val == newval) # happens at the maximum negative value
cpu.psw_c = (newval != 0)
cpu.operandx(xb6, newval, opsize=opsize)
def op00_55_adc(cpu, inst, opsize=2):
"""ADC(B) (determined by opsize). Add carry."""
# NOTE: "add carry" (not "add with carry")
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
if cpu.psw_c:
oldsign = val & cpu.SIGN816[opsize]
val = (val + 1) & cpu.MASK816[opsize]
cpu.psw_v = (val & cpu.SIGN816[opsize]) and not oldsign
cpu.psw_c = (val == 0) # because this is the NEW (+1'd) val
else:
cpu.psw_v = 0
cpu.psw_c = 0
cpu.psw_n = (val & cpu.SIGN816[opsize])
cpu.psw_z = (val == 0)
cpu.operandx(xb6, val, opsize=opsize)
def op00_56_sbc(cpu, inst, opsize=2):
"""SBC(B) (determined by opsize). Subtract carry."""
# NOTE: "subtract carry" (not "subtract with carry/borrow")
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
if cpu.psw_c:
oldsign = val & cpu.SIGN816[opsize]
val = (val - 1) & cpu.MASK816[opsize]
cpu.psw_v = oldsign and not (val & cpu.SIGN816[opsize])
cpu.psw_c = (val == cpu.MASK816[opsize]) # bcs this is the (-1'd) val
else:
cpu.psw_v = 0
cpu.psw_c = 0
cpu.psw_n = (val & cpu.SIGN816[opsize])
cpu.psw_z = (val == 0)
cpu.operandx(xb6, val, opsize=opsize)
def op00_57_tst(cpu, inst, opsize=2):
"""TST(B) (determined by opsize). Test destination."""
dst = inst & 0o77
val = cpu.operandx(dst, opsize=opsize)
cpu.psw_n = (val & cpu.SIGN816[opsize])
cpu.psw_z = (val == 0)
cpu.psw_v = 0
cpu.psw_c = 0
def op00_60_ror(cpu, inst, opsize=2):
"""ROR(B) - rotate one bit right."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
signmask = cpu.SIGN816[opsize]
vc = signmask if cpu.psw_c else 0
cpu.psw_c, val = (val & 1), ((val >> 1) | vc) & cpu.MASK816[opsize]
cpu.psw_n = val & signmask
cpu.psw_z = (val == 0)
cpu.psw_v = bool(cpu.psw_n) != bool(cpu.psw_c)
cpu.operandx(xb6, val, opsize=opsize)
def op00_61_rol(cpu, inst, opsize=2):
"""ROL(B) - rotate one bit left."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
signmask = cpu.SIGN816[opsize]
vc = 1 if cpu.psw_c else 0
cpu.psw_c, val = (val & signmask), ((val << 1) | vc) & cpu.MASK816[opsize]
cpu.psw_n = val & signmask
cpu.psw_z = (val == 0)
cpu.psw_v = bool(cpu.psw_n) != bool(cpu.psw_c)
cpu.operandx(xb6, val, opsize=opsize)
def op00_62_asr(cpu, inst, opsize=2):
"""ASR(B) - arithmetic shift right one bit."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
signbit = (val & cpu.SIGN816[opsize])
cpu.psw_c = (val & 1)
val >>= 1
val |= signbit
cpu.psw_n = (val & cpu.SIGN816[opsize])
cpu.psw_z = (val == 0)
cpu.psw_v = bool(cpu.psw_n) != bool(cpu.psw_c)
cpu.operandx(xb6, val, opsize=opsize)
def op00_63_asl(cpu, inst, opsize=2):
"""ASL(B) - arithmetic shift left one bit."""
val, xb6 = cpu.operandx(inst & 0o77, opsize=opsize, rmw=True)
cpu.psw_c = (val & cpu.SIGN816[opsize])
val = (val << 1) & cpu.MASK816[opsize]
cpu.psw_n = (val & cpu.SIGN816[opsize])
cpu.psw_z = (val == 0)
cpu.psw_v = bool(cpu.psw_n) != bool(cpu.psw_c)
cpu.operandx(xb6, val, opsize=opsize)
def op00_64_mark(cpu, inst):
# this instruction is... what it is. Note: if I/D separation
# is enabled, the stack must be in BOTH D and I space, as control
# will be transfered to the stack (typically) for this instruction.
nn = inst & 0o77
cpu.r[cpu.SP] = (cpu.r[cpu.PC] + (2 * nn)) & cpu.MASK16
cpu.r[cpu.PC] = cpu.r[5]
cpu.r[5] = cpu.stackpop()
def op00_65_mfpi(cpu, inst, opsize=2):
"""MFPI - move from previous instruction space.
The "opsize" -- which really is just the top bit of the instruction,
encodes whether this is mfpi or mfpd:
opsize = 2 mfpi (top bit was 0)
opsize = 1 mfpd (top bit was 1)
"""
# There are some wonky special semantics. In user mode if prevmode
# is USER (which it always is in Unix) then this refers to DSPACE
# (despite the MFPI name) to protect the notion of "execute only" I space
prvm = cpu.psw_prevmode
curm = cpu.psw_curmode
if prvm == cpu.USER and (curm == prvm):
space = cpu.mmu.DSPACE
else:
space = (cpu.mmu.DSPACE, cpu.mmu.ISPACE)[opsize - 1]
# MFPx SP is a special case, it means get the other SP register.
if (inst & 0o77) == 6 and (prvm != curm):
pival = cpu.stackpointers[prvm]
else:
pival = cpu.operandx(inst & 0o77, altmode=prvm, altspace=space)
cpu.psw_n = pival & cpu.MASK16
cpu.psw_z = (pival == 0)
cpu.psw_v = 0
cpu.stackpush(pival)
def op00_66_mtpi(cpu, inst, opsize=2):
"""MTPI - move to previous instruction space.
The "opsize" encodes whether this is mtpi or mtpd:
opsize = 2 mtpi
opsize = 1 mtpd
"""
# there are some wonky semantics ... this instruction is NOT restricted
# to privileged modes and is potentially a path to writing into
# a privileged space (!!). Unix (and probably all others) deals with
# this by ensuring psw_prevmode is also USER when in USER mode.
targetspace = (cpu.mmu.DSPACE, cpu.mmu.ISPACE)[opsize - 1]
w = cpu.stackpop()
cpu.psw_n = w & cpu.MASK16
cpu.psw_z = (w == 0)
cpu.psw_v = 0
prvm = cpu.psw_prevmode
curm = cpu.psw_curmode
# note the special case that MTPx SP writes the other mode's SP register
if (inst & 0o77) == 6 and (prvm != curm):
cpu.stackpointers[prvm] = w
else:
cpu.operandx(inst & 0o077, w, altmode=prvm, altspace=targetspace)
def op00_67_sxt(cpu, inst):
if cpu.psw_n:
val = cpu.MASK16
else:
val = 0
cpu.psw_z = not cpu.psw_n
cpu.psw_v = 0
cpu.operandx(inst & 0o0077, val)
ops56tab = (
op00_50_clr,
op00_51_com,
op00_52_inc,
op00_53_dec,
op00_54_neg,
op00_55_adc,
op00_56_sbc,
op00_57_tst,
op00_60_ror,
op00_61_rol,
op00_62_asr,
op00_63_asl,
op00_64_mark,
op00_65_mfpi, # note: "byte" variant is really MFPD
op00_66_mtpi, # note: "byte" variant is really MTPD
op00_67_sxt)
op00_dispatch_table = (
op000_dispatcher,
branches,
branches,
branches,
op00_4_jsr,
lambda cpu, inst: ops56tab[((inst & 0o7700) >> 6) - 0o50](cpu, inst),
lambda cpu, inst: ops56tab[((inst & 0o7700) >> 6) - 0o50](cpu, inst),
None)