First release
This commit is contained in:
parent
0a9c8f617d
commit
7760907dff
17 changed files with 4595 additions and 0 deletions
45
boot.py
Normal file
45
boot.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
def boot_hp(p, addr=0o10000):
|
||||
# this is the sort of thing that would be keyed in from
|
||||
# the console switches (if the machine was not equipped
|
||||
# with a boot rom option to hold it instead)
|
||||
#
|
||||
# It is a minimalist program, with lots of assumptions, to read 1K
|
||||
# from block zero of drive 0 into location 0. The execution start
|
||||
# at zero is done elsewhere.
|
||||
#
|
||||
# NOTE WELL: THIS ASSUMES THE MACHINE IS IN RESET CONDITION WHICH
|
||||
# MEANS MANY OF THE DEVICE REGISTERS ARE KNOWN TO BE ZERO
|
||||
#
|
||||
# MOV #176704,R0 -- note how used
|
||||
# MOV #177000,-(R0) -- word count - read 1K though boot really 512
|
||||
# MOV #071,-(R0) -- go!
|
||||
program_insts = (
|
||||
0o012700, # MOV #0176704,R0
|
||||
0o176704,
|
||||
0o012740, # MOV #177000,-(R0)
|
||||
0o177000,
|
||||
0o012740, # MOV #071, -(R0)
|
||||
0o000071,
|
||||
0o0, # HALT
|
||||
)
|
||||
|
||||
for o, w in enumerate(program_insts):
|
||||
p.physRW(addr + o + o, w)
|
||||
|
||||
return addr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
from machine import PDP1170
|
||||
p = PDP1170(loglevel='INFO')
|
||||
pc = boot_hp(p)
|
||||
print("starting PDP11; telnet/nc to localhost:1170 to connect to console")
|
||||
print("There will be no prompt; type 'boot' to start boot program")
|
||||
p.run(pc=pc)
|
||||
# technically need to confirm the drive is RDY, i.e., the read
|
||||
# completed, but using a delay is a lot simpler and works fine.
|
||||
# In real life, humans would have manipulated console switches to
|
||||
# start execution at location 0, which is also a source of delay. :)
|
||||
time.sleep(0.05)
|
||||
p.run(pc=0)
|
61
branches.py
Normal file
61
branches.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
# 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.
|
||||
|
||||
# keyed by masked "base code" (upper byte), not shifted
|
||||
brconds = {
|
||||
# NOTE: 000400 case is handled in op000 dispatch separately
|
||||
# 0o000400: lambda n, z, v, c: True, # BR
|
||||
0o001000: lambda n, z, v, c: not z, # BNE
|
||||
0o001400: lambda n, z, v, c: z, # BEQ
|
||||
0o100000: lambda n, z, v, c: not n, # BPL
|
||||
0o100400: lambda n, z, v, c: n, # BMI
|
||||
0o102000: lambda n, z, v, c: not v, # BVC
|
||||
0o102400: lambda n, z, v, c: v, # BVS
|
||||
0o103000: lambda n, z, v, c: not c, # BCC
|
||||
0o103400: lambda n, z, v, c: c, # BCS
|
||||
|
||||
# CAUTION: Python XOR ("^") is bitwise; hence bool() != for ^
|
||||
0o002000: lambda n, z, v, c: bool(n) == bool(v), # BGE
|
||||
0o002400: lambda n, z, v, c: bool(n) != bool(v), # BLT
|
||||
0o003000: lambda n, z, v, c: (bool(n) == bool(v)) and not z, # BGT
|
||||
0o003400: lambda n, z, v, c: (bool(n) != bool(v)) or z, # BLE
|
||||
|
||||
|
||||
0o101000: lambda n, z, v, c: not (c or z), # BHI
|
||||
0o101400: lambda n, z, v, c: c or z, # BLOS
|
||||
|
||||
# NOTE: These two are the same as BCC/BCS respectively
|
||||
# 0o103000: lambda n, z, v, c: not c, # BHIS
|
||||
# 0o103400: lambda n, z, v, c: c, # BLO
|
||||
}
|
||||
|
||||
|
||||
def branches(cpu, inst):
|
||||
branch(cpu, inst, brconds[inst & 0o177400])
|
||||
|
||||
|
||||
def branch(cpu, inst, condition):
|
||||
if condition(cpu.psw_n, cpu.psw_z, cpu.psw_v, cpu.psw_c):
|
||||
offset = (inst & 0o377) * 2
|
||||
if offset >= 256: # i.e., was a negative 8-bit value
|
||||
offset -= 512
|
||||
cpu.r[7] = cpu.u16add(cpu.r[7], offset)
|
246
interrupts.py
Normal file
246
interrupts.py
Normal file
|
@ -0,0 +1,246 @@
|
|||
# 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 collections import namedtuple
|
||||
from functools import partial
|
||||
import threading
|
||||
|
||||
from pdptraps import PDPTrap
|
||||
|
||||
|
||||
# an interrupt is, at the cpu implementation level, just a flavor of trap.
|
||||
class InterruptTrap(PDPTrap):
|
||||
def __init__(self, pri, vector):
|
||||
super().__init__()
|
||||
self.pri = pri
|
||||
self.vector = vector
|
||||
|
||||
|
||||
# contains the details for a pending interrupt (see discussion)
|
||||
PendingInterrupt = namedtuple(
|
||||
'PendingInterrupt', ('pri', 'vector', 'callback'))
|
||||
|
||||
|
||||
# To cause an interrupt, a device creates a PendingInterrupt containing:
|
||||
# pri -- priority
|
||||
# vector -- the interrupt vector
|
||||
# callback -- see discussion
|
||||
#
|
||||
# and then calls pend_interrupt() to get things going.
|
||||
#
|
||||
# The interrupt does not, of course, occur right away; it pends until
|
||||
# the processor is willing to accept it. The processor only accepts
|
||||
# interrupts at instruction boundaries, and even then only if the current
|
||||
# processor priority level is below the interrupt pri.
|
||||
#
|
||||
# When those conditions occur, the interrupt is accepted by the processor
|
||||
# (via a get_pending() method called from the processor). At that time the
|
||||
# interrupt request is "granted" and the callback function, if any is
|
||||
# provided, is invoked (from the cpu thread).
|
||||
#
|
||||
# The callback is invoked with no arguments and the return value is ignored.
|
||||
# Use partial() or other python techniques if the callback function requires
|
||||
# arguments for more context information (most will not).
|
||||
#
|
||||
# The purpose of this callback protocol is that some devices have internal
|
||||
# operations they want to perform when the interrupt is acknowledged, not
|
||||
# just when it is first made pending. Callbacks allow for that to happen.
|
||||
# CAUTION: The callback obviously executes in a separate thread and
|
||||
# will be asynchronous to any device-internal threads.
|
||||
#
|
||||
# In the simplest/common cases where none of this is needed, the
|
||||
# method simple_irq() bundles all this minutia up for the caller.
|
||||
|
||||
class InterruptManager:
|
||||
def __init__(self):
|
||||
self.pri_pending = 0
|
||||
self.requests = []
|
||||
self.condition = threading.Condition()
|
||||
|
||||
def simple_irq(self, pri, vector):
|
||||
"""Pend an interrupt at the given pri/vector."""
|
||||
self.pend_interrupt(PendingInterrupt(pri, vector, callback=None))
|
||||
|
||||
def pend_interrupt(self, irq):
|
||||
"""Pend a request for interrupt 'irq'."""
|
||||
with self.condition:
|
||||
# special case to accelerate zero-to-one common transition
|
||||
if not self.requests:
|
||||
self.requests = [irq]
|
||||
self.pri_pending = irq.pri
|
||||
else:
|
||||
# multiple identical requests are not pended
|
||||
# (it works this way in the hardware too of course --
|
||||
# if a device has asserted the interrupt request line
|
||||
# but that request hasn't been acknowledged/cleared by
|
||||
# by the bus signal protocol yet, you can't assert the
|
||||
# same interrupt line again ... it's already asserted)
|
||||
if irq not in self.requests:
|
||||
self.requests = sorted(
|
||||
self.requests + [irq], key=lambda q: q.pri)
|
||||
self.pri_pending = self.requests[-1].pri
|
||||
self.condition.notify_all()
|
||||
|
||||
# called by the processor, to get one pending interrupt (if any).
|
||||
# An InterruptTrap with the highest priority is returned, IF it is
|
||||
# above the given processor priority. Else None.
|
||||
def get_pending(self, processor_pri):
|
||||
"""Returns an InterruptTrap, or None."""
|
||||
with self.condition:
|
||||
try:
|
||||
if self.pri_pending > processor_pri:
|
||||
irq = self.requests.pop()
|
||||
else:
|
||||
return None
|
||||
except IndexError:
|
||||
return None
|
||||
else:
|
||||
if self.requests:
|
||||
self.pri_pending = self.requests[-1].pri
|
||||
else:
|
||||
self.pri_pending = 0
|
||||
|
||||
if irq.callback:
|
||||
irq.callback()
|
||||
return InterruptTrap(irq.pri, irq.vector)
|
||||
|
||||
def waitstate(self, processor_pri):
|
||||
"""Sit idle until any interrupt happens."""
|
||||
with self.condition:
|
||||
if self.pri_pending > processor_pri:
|
||||
return
|
||||
self.condition.wait_for(lambda: self.pri_pending)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
|
||||
class TestMethods(unittest.TestCase):
|
||||
def test__init__(self):
|
||||
IM = InterruptManager()
|
||||
|
||||
# initial state starts with no pending interrupts
|
||||
self.assertEqual(IM.pri_pending, 0)
|
||||
|
||||
# verify get_pending still "works" (returns None)
|
||||
self.assertEqual(IM.get_pending(0), None)
|
||||
|
||||
def test_queue1(self):
|
||||
IM = InterruptManager()
|
||||
test_pri = 4 # arbitrary
|
||||
test_vec = 17 # arbitrary
|
||||
IM.simple_irq(test_pri, test_vec)
|
||||
self.assertEqual(IM.pri_pending, test_pri)
|
||||
iinfo = IM.get_pending(0)
|
||||
self.assertEqual(IM.pri_pending, 0)
|
||||
self.assertEqual(iinfo.pri, test_pri)
|
||||
self.assertEqual(iinfo.vector, test_vec)
|
||||
|
||||
# support function for test cases, do a bunch of actions on an IM
|
||||
def _actions(self, IM, prog):
|
||||
cpupri = 0
|
||||
for action in prog:
|
||||
match action[0], action[1]:
|
||||
case 'RQ', t:
|
||||
IM.simple_irq(*t)
|
||||
case 'PRI', cpupri:
|
||||
pass
|
||||
case 'GET', xt:
|
||||
t = IM.get_pending(cpupri)
|
||||
if t is None:
|
||||
self.assertEqual(t, xt)
|
||||
else:
|
||||
xpri, xvec = xt
|
||||
# If the vector position is a tuple then that
|
||||
# means to accept anything in that tuple
|
||||
try:
|
||||
_ = (t.vector in xvec)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
xvec = t.vector # i.e., it's ok
|
||||
self.assertEqual(t.pri, xpri)
|
||||
self.assertEqual(t.vector, xvec)
|
||||
case 'CHK', pri:
|
||||
self.assertEqual(IM.pri_pending, pri)
|
||||
|
||||
case _:
|
||||
raise ValueError("bad action", action)
|
||||
|
||||
def test_mixedops(self):
|
||||
testprogs = (
|
||||
# (ACTION, ACTION-INFO)
|
||||
(('RQ', (4, 44)), # request IRQ 4
|
||||
('RQ', (5, 55)), # request IRQ 5
|
||||
('GET', (5, 55)), # get one, check that it is 5
|
||||
('CHK', 4), # check that pri_pending is 4
|
||||
('RQ', (3, 33)), # request IRQ 3
|
||||
('CHK', 4), # check that pri_pending is 4
|
||||
('RQ', (6, 66)), # request IRQ 6
|
||||
('CHK', 6), # check that pri_pending is 6
|
||||
('GET', (6, 66)), # get one, check that it is 6
|
||||
('CHK', 4), # check that pri_pending is 6
|
||||
('GET', (4, 44)), # get one, check that it is 4
|
||||
('CHK', 3), # check that pri_pending is 3
|
||||
('GET', (3, 33)), # get one, check that it is 3
|
||||
('CHK', 0), # check that pri_pending is 0
|
||||
('GET', None), # check that getting from empty works
|
||||
),
|
||||
|
||||
# check priority filtering
|
||||
(('RQ', (4, 44)), # request IRQ 4
|
||||
('RQ', (5, 55)), # request IRQ 5
|
||||
('PRI', 7), # spl7
|
||||
('GET', None), # shouldn't see anything
|
||||
('PRI', 5), # spl5
|
||||
('GET', None), # still shouldn't see anything
|
||||
('RQ', (6, 66)), # request IRQ 6
|
||||
('RQ', (7, 77)), # request IRQ 7
|
||||
('RQ', (6, 666)), # request IRQ 6
|
||||
('RQ', (7, 777)), # request IRQ 7
|
||||
('PRI', 6), # spl6
|
||||
('GET', (7, (77, 777))), # should get one of these
|
||||
('GET', (7, (77, 777))), # should get the other
|
||||
('GET', None), # no more
|
||||
('PRI', 0), # spl0
|
||||
('GET', (6, (66, 666))), # should get one of these
|
||||
('GET', (6, (66, 666))), # should get one of these
|
||||
('GET', (5, 55)),
|
||||
('GET', (4, 44)),
|
||||
('GET', None)),
|
||||
)
|
||||
for tp in testprogs:
|
||||
IM = InterruptManager()
|
||||
self._actions(IM, tp)
|
||||
|
||||
def test_vectorcallback(self):
|
||||
def foo(d):
|
||||
d['foo'] = 1234
|
||||
|
||||
foodict = {}
|
||||
pfoo = partial(foo, foodict)
|
||||
IM = InterruptManager()
|
||||
IM.pend_interrupt(PendingInterrupt(4, 888, pfoo))
|
||||
iinfo = IM.get_pending(0)
|
||||
self.assertEqual(foodict['foo'], 1234)
|
||||
|
||||
unittest.main()
|
192
kl11.py
Normal file
192
kl11.py
Normal file
|
@ -0,0 +1,192 @@
|
|||
# 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.
|
||||
|
||||
# simulation of a KL-11 console interface
|
||||
#
|
||||
# Trivial TCP server that accepts connections on port 1170 (cute)
|
||||
# and simply proxies character traffic back and forth.
|
||||
#
|
||||
import sys
|
||||
import socket
|
||||
import time
|
||||
import threading
|
||||
import queue
|
||||
|
||||
from pdptraps import PDPTraps
|
||||
|
||||
|
||||
class KL11:
|
||||
|
||||
KL11_DEFAULT = 0o17560 # offset within I/O page
|
||||
BUSY = 0o400 # probably no reason to ever set this
|
||||
RCDONE = 0o200 # means character available in buf
|
||||
TXRDY = 0o200 # same bit on tx side is called "RDY"
|
||||
IENABLE = 0o100
|
||||
RDRENA = 0o001
|
||||
|
||||
_SHUTDOWN_SENTINEL = object()
|
||||
SERVERHOST = ''
|
||||
SERVERPORT = 1170
|
||||
|
||||
def __init__(self, ub, baseaddr=KL11_DEFAULT):
|
||||
self.ub = ub
|
||||
self.addr = ub.mmio.register(self.klregs, baseaddr, 4)
|
||||
ub.mmio.devicereset_register(self.reset)
|
||||
|
||||
# output characters are just queued (via tq) to the output thread
|
||||
# input characters have to undergo a more careful 1-by-1
|
||||
# dance to properly match interrupts to their arrival
|
||||
self.tq = queue.Queue()
|
||||
self.rxc = threading.Condition()
|
||||
|
||||
# bits broken out of virtualized KL11 Reader Status Register
|
||||
self.rcdone = False
|
||||
self.r_ienable = False
|
||||
|
||||
# reader buffer register (address: baseaddr + 2)
|
||||
self.rdrbuf = 0
|
||||
|
||||
# transmit buffer status (address: baseaddr + 4)
|
||||
self.t_ienable = False
|
||||
|
||||
# The socket server connection/listener
|
||||
self._t = threading.Thread(target=self._connectionserver, daemon=True)
|
||||
self._t.start()
|
||||
|
||||
def reset(self, ub):
|
||||
"""Called for UNIBUS resets (RESET instruction)."""
|
||||
self.rcdone = False
|
||||
self.r_ienable = False
|
||||
self.r_tenable = False
|
||||
|
||||
def klregs(self, addr, value=None, /):
|
||||
match addr - self.addr:
|
||||
case 0: # rcsr
|
||||
if value is None:
|
||||
# *** READING ***
|
||||
|
||||
value = 0
|
||||
|
||||
if self.r_ienable:
|
||||
value |= self.IENABLE
|
||||
|
||||
if self.rcdone:
|
||||
value |= self.RCDONE
|
||||
|
||||
else:
|
||||
# *** WRITING ***
|
||||
if value & self.RDRENA:
|
||||
with self.rxc:
|
||||
# a request to get one character, which only
|
||||
# has to clear the rcdone bit here.
|
||||
self.rcdone = False
|
||||
self.rdrbuf = 0
|
||||
self.r_ienable = (value & self.IENABLE)
|
||||
self.rxc.notify()
|
||||
|
||||
case 2 if value is None: # rbuf
|
||||
# *** READING ***
|
||||
with self.rxc:
|
||||
value = self.rdrbuf
|
||||
self.rcdone = False
|
||||
self.rxc.notify()
|
||||
|
||||
# transmit buffer status (sometimes called tcsr)
|
||||
case 4:
|
||||
if value is None:
|
||||
# *** READING ***
|
||||
value = self.TXRDY # always ready to send chars
|
||||
if self.t_ienable:
|
||||
value |= self.IENABLE
|
||||
else:
|
||||
# *** WRITING ***
|
||||
prev = self.t_ienable
|
||||
self.t_ienable = (value & self.IENABLE)
|
||||
if self.t_ienable and not prev:
|
||||
self.ub.intmgr.simple_irq(pri=4, vector=0o64)
|
||||
|
||||
# transmit buffer
|
||||
case 6 if value is not None: # tbuf
|
||||
# *** WRITING ***
|
||||
value &= 0o177
|
||||
if (value != 0o177):
|
||||
s = chr(value)
|
||||
self.tq.put(s)
|
||||
if self.t_ienable:
|
||||
self.ub.intmgr.simple_irq(pri=4, vector=0o64)
|
||||
case _:
|
||||
raise PDPTraps.AddressError
|
||||
|
||||
return value
|
||||
|
||||
def _connectionserver(self):
|
||||
"""Server loop daemon thread for console I/O."""
|
||||
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
serversocket.bind((self.SERVERHOST, self.SERVERPORT))
|
||||
serversocket.listen(1)
|
||||
|
||||
def _outloop(q, s):
|
||||
while True:
|
||||
try:
|
||||
c = q.get(True, timeout=2)
|
||||
if c is self._SHUTDOWN_SENTINEL:
|
||||
break
|
||||
except queue.Empty:
|
||||
pass
|
||||
else:
|
||||
s.sendall(c.encode())
|
||||
|
||||
def _inloop(s):
|
||||
while len(b := s.recv(1)) != 0:
|
||||
with self.rxc:
|
||||
self.rxc.wait_for(lambda: not self.rcdone)
|
||||
self.rdrbuf = ord(b)
|
||||
self.rcdone = True
|
||||
if self.r_ienable:
|
||||
self.ub.intmgr.simple_irq(pri=4, vector=0o60)
|
||||
|
||||
while True:
|
||||
s, addr = serversocket.accept()
|
||||
|
||||
outthread = threading.Thread(target=_outloop, args=(self.tq, s))
|
||||
inthread = threading.Thread(target=_inloop, args=(s,))
|
||||
|
||||
outthread.start()
|
||||
inthread.start()
|
||||
|
||||
inthread.join()
|
||||
self.tq.put(self._SHUTDOWN_SENTINEL)
|
||||
outthread.join()
|
||||
|
||||
# debugging tool
|
||||
def statestring(self):
|
||||
s = ""
|
||||
if self.r_ienable:
|
||||
s += " RINT"
|
||||
if self.t_ienable:
|
||||
s += " TINT"
|
||||
if self.rdrbuf:
|
||||
s += f" LC={self.rdrbuf}"
|
||||
if self.rcdone:
|
||||
s += f" RCDONE"
|
||||
return s
|
66
kw11.py
Normal file
66
kw11.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# 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.
|
||||
|
||||
# line frequency clock
|
||||
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class KW11:
|
||||
|
||||
KW11_OFFS = 0o17546
|
||||
|
||||
def __init__(self, ub):
|
||||
interrupt_manager = ub.intmgr
|
||||
self._t = threading.Thread(
|
||||
target=self._cloop, args=(0.05, interrupt_manager), daemon=True)
|
||||
self.running = False
|
||||
self.monbit = 0
|
||||
ub.mmio.register_simpleattr(self, 'LKS', self.KW11_OFFS, reset=True)
|
||||
|
||||
# clock loop
|
||||
def _cloop(self, interval, imgr):
|
||||
while self.running:
|
||||
time.sleep(interval)
|
||||
# there are inherent races here (in the hardware too) but
|
||||
# seek to make the hazard smaller than the full interval
|
||||
# by testing self.running again here.
|
||||
if self.running:
|
||||
imgr.simple_irq(pri=6, vector=0o100)
|
||||
|
||||
@property
|
||||
def LKS(self):
|
||||
return (int(self.monbit) << 7) | (int(self.running) << 6)
|
||||
|
||||
@LKS.setter
|
||||
def LKS(self, value):
|
||||
if not self.running:
|
||||
if value & 0o100:
|
||||
self.running = True
|
||||
self._t.start()
|
||||
|
||||
self.monbit = (value & 0o200)
|
||||
if self.running and not (value & 0o100):
|
||||
# this never happens in unix but ...
|
||||
self.running = False
|
||||
self._t.join()
|
969
machine.py
Normal file
969
machine.py
Normal file
|
@ -0,0 +1,969 @@
|
|||
# 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
|
||||
|
||||
# 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)):
|
||||
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)
|
||||
self._RP = RPRM(self.ub)
|
||||
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
|
||||
|
||||
# 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()
|
||||
|
||||
# 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
|
289
mmio.py
Normal file
289
mmio.py
Normal file
|
@ -0,0 +1,289 @@
|
|||
# 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
|
||||
|
||||
|
||||
class MMIO:
|
||||
"""Memory Mapped I/O handling for the 8K byte I/O page."""
|
||||
|
||||
#
|
||||
# memory-mapped I/O is just a 4K array of function callbacks, one
|
||||
# entry per each I/O WORD (even) I/O offset. Most entries, of course,
|
||||
# remain set to an initial default "non-existent address" callback.
|
||||
#
|
||||
# See register() for setting a callback on a specific offset or range.
|
||||
#
|
||||
# Reads and writes to any offset within the I/O page invoke the
|
||||
# callback function. Assuming f is the callback function:
|
||||
#
|
||||
# READS: v = f(ioaddr)
|
||||
# The function f must return a value 0 .. 65535
|
||||
#
|
||||
# WRITES: f(ioaddr, v)
|
||||
# v will be an integer 0 .. 65535
|
||||
# The return value is ignored.
|
||||
#
|
||||
# Callbacks must be declared like this:
|
||||
# def f(ioaddr, value=None, /):
|
||||
# if value is None:
|
||||
# ... this is the read case
|
||||
# else:
|
||||
# ... this is the write case
|
||||
#
|
||||
# The ioaddr will always be relative to the base of the I/O page:
|
||||
# (ioaddr & 8191) == ioaddr will always be True
|
||||
#
|
||||
# Callbacks may also optionally receive a byte operation indicator,
|
||||
# but only if they register for that. See BYTE OPERATIONS, below.
|
||||
# Most callbacks will instead rely on the framework to synthesize
|
||||
# byte operations from words; see _byte_wrapper and byteme.
|
||||
#
|
||||
# Callback functions that need context or other data can, of course,
|
||||
# be bound methods (automatically receiving self) or also can use
|
||||
# functools.partial() to get additional arguments passed in.
|
||||
#
|
||||
#
|
||||
# ODD I/O ADDRESSES, BYTE OPERATIONS:
|
||||
#
|
||||
# The physical UNIBUS always *reads* full 16-bit words; there is no
|
||||
# capability for a byte read at the electrical bus level.
|
||||
#
|
||||
# Oddly (haha) the bus *can* specify byte access for writes.
|
||||
# Even odder (hahaha), devices that provide full-word access at
|
||||
# odd addresses exist, the best example being the CPU itself. In some
|
||||
# models the registers are available at a block of UNIBUS addresses,
|
||||
# and some of the 16-bit registers have ODD addresses.
|
||||
# For example, UNIBUS address 777707 is the PDP-11 cpu PC, as a
|
||||
# full 16-bit word, while 777706 is the PDP-11 cpu KSP.
|
||||
#
|
||||
# This creates potential for havoc if programmers use byte operations
|
||||
# on I/O addresses. Consider a typical sequence to read consecutive
|
||||
# bytes from an address in R0:
|
||||
#
|
||||
# MOVB (R0)+,R1 # get the low byte of the word R0 points to
|
||||
# MOVB (R0)+,R2 # get the high byte of that word
|
||||
#
|
||||
# If executed with R0 = 177706 (the KSP register virtual address
|
||||
# with a typical 16-bit mapping of the upper page to I/O space)
|
||||
# then R1 will be the low byte of KSP but the next access, which will
|
||||
# be seen by the UNIBUS as a word read on an odd address, will pull
|
||||
# the PC value (at 777707) as a word and extract the upper byte
|
||||
# from THAT (PC upper byte in R2; KSP lower byte in R1). This is
|
||||
# probably an unexpected result, which is a good argument against
|
||||
# using byte operations in I/O space. Nevertheless, byte operations
|
||||
# in I/O space abound in real world code.
|
||||
#
|
||||
# Thus:
|
||||
# * DEVICES THAT DIRECTLY SUPPORT BYTE-WRITE OPERATIONS:
|
||||
# Specify "byte_writes=True" in the registration call:
|
||||
#
|
||||
# mmio.register(somefunc, someoffset, somesize, byte_writes=True)
|
||||
#
|
||||
# and declare the somefunc callback like this:
|
||||
# def somefunc(ioaddr, value=None, /, *, opsize=2):
|
||||
# ...
|
||||
#
|
||||
# The opsize argument will be 2 for word operations and 1 for bytes.
|
||||
# NOTE: Byte READS will never be seen; only byte WRITES.
|
||||
#
|
||||
# * DEVICES THAT DON'T CARE ABOUT BYTE-WRITES:
|
||||
# The common/standard case. Let byte_writes default to False
|
||||
# (i.e., leave it out of the registration call). The callback
|
||||
# will be invoked with the simpler signature: f(ioaddr, v)
|
||||
#
|
||||
# * DEVICES THAT SUPPLY WORDS AT ODD ADDRESSES
|
||||
# The cpu being the canonical example of this... register the
|
||||
# I/O callback at the corresponding even address and use the ioaddr
|
||||
# determine which address (the even or the odd) was requested.
|
||||
#
|
||||
# Devices that respond to a block of related addresses can register
|
||||
# one callback to cover more than one word of Unibus address space.
|
||||
# The callback gets the ioaddr which it can further decode. Again,
|
||||
# the CPU register block is a good example of this.
|
||||
#
|
||||
#
|
||||
# The PDP-11 RESET INSTRUCTION
|
||||
#
|
||||
# The PDP-11 RESET instruction causes the UNIBUS to be reset.
|
||||
# Devices that want to know about this should:
|
||||
#
|
||||
# mmio.devicereset_reigster(resetfunc)
|
||||
#
|
||||
# And then resetfunc will be invoked as:
|
||||
# resetfunc(mmio.ub)
|
||||
# (i.e., passed a single argument, the UNIBUS object).
|
||||
#
|
||||
# For dead-simple cases, the optional reset=True argument can be
|
||||
# supplied, which casues register() to also arrange for the iofunc
|
||||
# to be called at RESET time, like this:
|
||||
#
|
||||
# iofunc(baseaddr, 0)
|
||||
#
|
||||
# which, it should be noted, is indistinguishable from a program
|
||||
# merely setting that I/O address to zero. Note too that if iofunc
|
||||
# was registered once for an N-word block a RESET will still only call
|
||||
# it ONCE, on the baseaddr of that block.
|
||||
#
|
||||
# If this convenience, with its limitations, is insufficient for a
|
||||
# device then it must use the devicereset registration instead.
|
||||
#
|
||||
|
||||
def __init__(self, cpu):
|
||||
self.cpu = cpu
|
||||
self.mmiomap = [self.__nodev] * (self.cpu.IOPAGE_SIZE >> 1)
|
||||
self.device_resets = set()
|
||||
|
||||
# the default entry for unoccupied I/O: cause an AddressError trap
|
||||
def __nodev(self, addr, value=None, /):
|
||||
self.cpu.logger.info(f"Access to non-existent I/O {oct(addr)}")
|
||||
raise PDPTraps.AddressError(
|
||||
cpuerr=self.cpu.CPUERR_BITS.UNIBUS_TIMEOUT)
|
||||
|
||||
# Devices may have simple "dummy" I/O addresses that always read zero
|
||||
# and ignore writes; See "if iofunc is None" in register() method.
|
||||
def __ignoredev(self, addr, value=None, /):
|
||||
self.cpu.logger.debug(f"dummy zero device @ {oct(addr)}, {value=}")
|
||||
return 0
|
||||
|
||||
# register a call-back for an I/O address, which MUST be an offset
|
||||
# within the 8K I/O page (which itself may reside at three different
|
||||
# physical locations depending on configurations, thus explaining
|
||||
# why this routine deals only in the offsets).
|
||||
#
|
||||
# Variations:
|
||||
# iofunc=None -- implement a dummy: reads as zero, ignores writes
|
||||
# reset=True -- also registers iofunc to be called at RESET time
|
||||
# byte_writes=True -- Request byte writes be sent to the iofunc
|
||||
# vs hidden/converted into word ops.
|
||||
#
|
||||
def register(self, iofunc, offsetaddr, nwords, *,
|
||||
byte_writes=False, reset=False):
|
||||
|
||||
if offsetaddr >= self.cpu.IOPAGE_SIZE:
|
||||
raise ValueError(f"MMIO: I/O offset too large {oct(offsetaddr)}")
|
||||
|
||||
# None is a shorthand for "this is a dummy always-zero addr"
|
||||
if iofunc is None:
|
||||
iofunc = self.__ignoredev
|
||||
|
||||
# register this (raw/unwrapped) iofunc for reset if so requested
|
||||
if reset:
|
||||
self.devicereset_register(lambda ub: iofunc(offsetaddr, 0))
|
||||
|
||||
idx, odd = divmod(offsetaddr, 2)
|
||||
if odd != 0:
|
||||
# See discussion of odd I/O addrs in block comment elsewhere
|
||||
raise ValueError("cannot register odd (byte) address in IO space")
|
||||
|
||||
if not byte_writes:
|
||||
# wrap the supplied I/O function with this code to implement
|
||||
# byte write operations automatically in terms of words.
|
||||
iofunc = self._byte_wrapper(iofunc)
|
||||
|
||||
for i in range(nwords):
|
||||
self.mmiomap[idx+i] = iofunc
|
||||
return offsetaddr
|
||||
|
||||
def _byte_wrapper(self, iofunc):
|
||||
def byteme(ioaddr, value=None, /, *, opsize=2):
|
||||
if (value is None) or (opsize == 2):
|
||||
return iofunc(ioaddr, value)
|
||||
else:
|
||||
# value is not None, and opsize is not 2
|
||||
# In other words: a byte write to I/O space. Synthesize it.
|
||||
self.cpu.logger.debug(f"Byte write to {oct(ioaddr)} {value=}")
|
||||
wv = self.wordRW(ioaddr)
|
||||
if ioaddr & 1:
|
||||
wv = (wv & 0o377) | (value << 8)
|
||||
else:
|
||||
wv = (wv & 0o177400) | value
|
||||
self.wordRW(ioaddr, wv)
|
||||
return byteme
|
||||
|
||||
# Convenience method -- registers simple attributes (or properties) into
|
||||
# I/O space in the obvious way: Make this attr (of obj) show at this addr
|
||||
#
|
||||
# If a device just needs some attributes set to zero on a RESET,
|
||||
# it can specify them here with reset=True and they will be automatically
|
||||
# set to zero by reset() (no need to devicereset_register).
|
||||
def register_simpleattr(self, obj, attrname, addr, reset=False):
|
||||
"""Create and register a handler to read/write the named attr.
|
||||
|
||||
obj - the object (often "self" for the caller of this method)
|
||||
attrname - the attribute name to read/write
|
||||
addr - the I/O address to register it to
|
||||
|
||||
If attrname is None, the address is registered as a dummy location
|
||||
that ignores writes and will always read as zero. This is sometimes
|
||||
useful for features that have to exist but are emulated as no-op.
|
||||
"""
|
||||
|
||||
# could do this with partial, but can also do it with this nested
|
||||
# func def. One way or another need this func logic anyway.
|
||||
|
||||
def _rwattr(_, value=None, /):
|
||||
"""Read/write the named attr via the I/O callback protocol."""
|
||||
if attrname is None:
|
||||
value = 0
|
||||
else:
|
||||
if value is None:
|
||||
value = getattr(obj, attrname)
|
||||
else:
|
||||
setattr(obj, attrname, value)
|
||||
return value
|
||||
|
||||
# NOTE: Registers a different ("closure of") rwattr each time.
|
||||
self.register(_rwattr, addr, 1, reset=reset)
|
||||
|
||||
# In the real hardware, the PDP-11 RESET instruction pulls a reset line
|
||||
# that all devices can see. In the emulation, devices that need to know
|
||||
# about the RESET instruction must register themselves here:
|
||||
def devicereset_register(self, func):
|
||||
"""Register func to be called whenever a RESET happens."""
|
||||
self.device_resets.add(func)
|
||||
|
||||
# The PDP-11 RESET instruction eventually ends up here, causing
|
||||
# a bus reset to be sent to all known registered devices.
|
||||
def resetdevices(self):
|
||||
for f in self.device_resets:
|
||||
self.cpu.logger.debug(f"RESET callback: {f}")
|
||||
f(self.cpu.ub)
|
||||
|
||||
def wordRW(self, ioaddr, value=None, /):
|
||||
"""Read (value is None) or write the given I/O address."""
|
||||
if value is None:
|
||||
value = self.mmiomap[ioaddr >> 1](ioaddr)
|
||||
else:
|
||||
self.mmiomap[ioaddr >> 1](ioaddr, value)
|
||||
return value
|
||||
|
||||
def byteRW(self, ioaddr, value=None, /):
|
||||
"""UNIBUS byte R/W - only write is legal."""
|
||||
if value is None:
|
||||
raise ValueError("Unibus cannot read bytes")
|
||||
else:
|
||||
self.cpu.logger.debug(
|
||||
f"UB: byte write {oct(ioaddr)}={oct(value)}"
|
||||
f" {self.cpu.machinestate()}")
|
||||
self.mmiomap[ioaddr >> 1](ioaddr, value, opsize=1)
|
||||
return None
|
561
mmu.py
Normal file
561
mmu.py
Normal file
|
@ -0,0 +1,561 @@
|
|||
# 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 functools import partial
|
||||
from pdptraps import PDPTraps
|
||||
from types import SimpleNamespace
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class MemoryMgmt:
|
||||
ISPACE = 0
|
||||
DSPACE = 1
|
||||
|
||||
# I/O addreses for various registers relative to I/O page base
|
||||
# From the pdp11/70 (and others) 1981 processor handbook, Appendix A
|
||||
#
|
||||
# Each block is:
|
||||
# I PDR (8 16-bit registers)
|
||||
# D PDR (8 16-bit registers)
|
||||
# I PAR (8 16-bit registers)
|
||||
# D PAR (8 16-bit registers)
|
||||
#
|
||||
# so each block is 64 bytes total, octal 100 in size
|
||||
#
|
||||
APR_SUPER_OFFS = 0o12200 # offset within I/O page
|
||||
APR_KERNEL_OFFS = 0o12300
|
||||
APR_USER_OFFS = 0o17600 # 0o17 vs 0o12 is not a typo
|
||||
|
||||
# expressed as offsets within the I/O page
|
||||
MMR0_OFFS = 0o17572
|
||||
MMR1_OFFS = 0o17574
|
||||
MMR2_OFFS = 0o17576
|
||||
MMR3_OFFS = 0o12516
|
||||
|
||||
# not an Enum because ... need to do bitwise efficiently.
|
||||
MMR0_BITS = SimpleNamespace(
|
||||
ABORT_NR=0o100000, ABORT_PLENGTH=0o040000, ABORT_RDONLY=0o020000,
|
||||
TRAP_MGMT=0o010000, TRAP_ENABLE=0o001000, INST_COMPLETED=0o000200,
|
||||
RELO_ENABLE=0o000001, FREEZER_TRAPS=0o160000,
|
||||
)
|
||||
|
||||
# memory control (parity, etc) is not implemented but needs to respond
|
||||
MCR_OFFS = 0o17746
|
||||
|
||||
# encodes read vs write cycles
|
||||
CYCLE = SimpleNamespace(READ='r', WRITE='w')
|
||||
|
||||
TransKey = namedtuple('TransKey', ('segno', 'mode', 'space', 'cycle'))
|
||||
|
||||
def __init__(self, cpu, /, *, nocache=False):
|
||||
|
||||
self.cpu = cpu
|
||||
self.ub = cpu.ub
|
||||
|
||||
mmio = self.ub.mmio
|
||||
|
||||
# The "segment cache" dramatically speeds up address translation
|
||||
# for the most common MMU usage scenarios.
|
||||
#
|
||||
# To preserve correct semantics, modifications to the mapping
|
||||
# parameters must (of course) dump some, or all, of this cache.
|
||||
self.segcache = {}
|
||||
|
||||
self.MMR0 = 0
|
||||
self.MMR1 = 0
|
||||
self.MMR2 = 0
|
||||
self.MMR3 = 0
|
||||
|
||||
self.MMR1_staged = 0
|
||||
self.nocache = nocache
|
||||
|
||||
# per the architecture manual, there are six (!) sets of
|
||||
# eight 32-bit Active Page Registers (APR0 ... APR7) where
|
||||
# each APR can be thought of as a 16-bit page address register (PAR)
|
||||
# and a 16 bit page descriptor register (PDR).
|
||||
#
|
||||
# A set (of 8) APRs is selected by a combination of two PSW bits
|
||||
# for kernel/supervisor/illegal/user modes and, if I/D separation
|
||||
# is enabled, I vs D space.
|
||||
#
|
||||
# To simplify the mapping, 8 sets (instead of 6) are provided here
|
||||
# but one of them is never used because one of the four combinations
|
||||
# of the psw mode bits is illegal (enforced elsewhere).
|
||||
|
||||
self.APR = [[[0, 0] for _ in range(8)] for setno in range(8)]
|
||||
|
||||
# register I/O space PDR/PARs: SUPER/KERNEL/USER blocks of 32 regs.
|
||||
#
|
||||
# It turns out all of this context is encoded (cleverly and
|
||||
# not entirely obviously) in the ioaddr bits, but it just seems
|
||||
# better to make it explicit here via extra args and partial():
|
||||
for mode, base in (
|
||||
(cpu.SUPERVISOR, self.APR_SUPER_OFFS),
|
||||
(cpu.KERNEL, self.APR_KERNEL_OFFS),
|
||||
(cpu.USER, self.APR_USER_OFFS)):
|
||||
# Each block is:
|
||||
# I PDR (8 16-bit registers)
|
||||
# D PDR (8 16-bit registers)
|
||||
# I PAR (8 16-bit registers)
|
||||
# D PAR (8 16-bit registers)
|
||||
for parpdr, space, offset in (
|
||||
(1, self.ISPACE, 0),
|
||||
(1, self.DSPACE, 16),
|
||||
(0, self.ISPACE, 32),
|
||||
(0, self.DSPACE, 48)):
|
||||
ioaddr = base+offset
|
||||
iofunc = partial(self.io_parpdr, parpdr, mode, space, ioaddr)
|
||||
mmio.register(iofunc, ioaddr, 8) # 8 words / 16 bytes
|
||||
|
||||
# register the simple attrs MMR0 etc into I/O space:
|
||||
mmio.register_simpleattr(self, 'MMR0', self.MMR0_OFFS, reset=True)
|
||||
mmio.register_simpleattr(self, 'MMR1', self.MMR1_OFFS)
|
||||
mmio.register_simpleattr(self, 'MMR2', self.MMR2_OFFS)
|
||||
mmio.register_simpleattr(self, 'MMR3', self.MMR3_OFFS, reset=True)
|
||||
mmio.register_simpleattr(self, None, self.MCR_OFFS)
|
||||
|
||||
def io_parpdr(self, parpdr, mode, space, base, addr, value=None, /):
|
||||
"""mmio I/O function for MMU PARs and PDRs.
|
||||
|
||||
NOTE: parpdr/mode/space/base args provided via partial() as
|
||||
supplied at registration time; see __init__.
|
||||
The mmio module calls this simply as f(addr, value)
|
||||
"""
|
||||
aprnum = (addr - base) >> 1
|
||||
aprfile = self.APR[(mode * 2) + space]
|
||||
if value is None:
|
||||
return aprfile[aprnum][parpdr]
|
||||
else:
|
||||
# dump any matching cache entries in both reading/writing form.
|
||||
for w in (self.CYCLE.READ, self.CYCLE.WRITE):
|
||||
if (aprnum, mode, space, w) in self.segcache:
|
||||
del self.segcache[(aprnum, mode, space, w)]
|
||||
|
||||
# --- XXX THIS IS JUST INFORMATIONAL / REASSURING FOR DEBUGGING
|
||||
# --- take this entire block out when satisfied
|
||||
if parpdr == 1:
|
||||
pdr = aprfile[aprnum][1]
|
||||
# various PDR mods are of interest for logging for debug
|
||||
if ((value & 4) == 0) and (pdr & 4):
|
||||
self.cpu.logger.debug(
|
||||
f"MMU: Write perm being removed "
|
||||
f"{aprnum=} {mode=} {space=}")
|
||||
if ((pdr >> 8) & 0xFF) > ((value >> 8) & 0xFF):
|
||||
self.cpu.logger.debug(
|
||||
f"MMU: segment being shortened "
|
||||
f"pdr={oct(pdr)} value={oct(value)}"
|
||||
f" {aprnum=} {mode=} {space=}")
|
||||
# --- XXX END XXX
|
||||
|
||||
aprfile[aprnum][parpdr] = value
|
||||
|
||||
# Per the handbook - the A and W bits in a PDR are reset to
|
||||
# zero when either the PAR or PDR is written.
|
||||
aprfile[aprnum][1] &= ~0o0300
|
||||
|
||||
@property
|
||||
def MMR0(self):
|
||||
return self._mmr0
|
||||
|
||||
@MMR0.setter
|
||||
def MMR0(self, value):
|
||||
self.cpu.logger.debug(f"MMR0 being set to {oct(value)}")
|
||||
self._mmr0 = value
|
||||
self._mmu_relo_enabled = (value & self.MMR0_BITS.RELO_ENABLE)
|
||||
self._mmu_trap_enabled = (value & self.MMR0_BITS.TRAP_ENABLE)
|
||||
self._mmr12_frozen = (value & self.MMR0_BITS.FREEZER_TRAPS)
|
||||
# XXX
|
||||
if self._mmr12_frozen:
|
||||
self.cpu.logger.debug(f"MMR12 FROZEN {self.MMR1=} {self.MMR2=}")
|
||||
|
||||
self.segcache = {}
|
||||
self.__rebaseIO()
|
||||
|
||||
# MMR1 records any autoincrement/decrement of the general purpose
|
||||
# registers, including explicit references through the PC. MMR1 is
|
||||
# cleared at the beginning of each instruction fetch. It is really
|
||||
# two subregisters each 8 bits, that record:
|
||||
# Bits <7:3> two's complement amount changed
|
||||
# Bits <2:0> register number (0 .. 7)
|
||||
#
|
||||
# Register set must be determined from appropriate PSW field(s)
|
||||
#
|
||||
# This is in the critical path for instruction performance, so
|
||||
# there is an optimization. At the beginning of every instruction
|
||||
# self.MMR1_staged is set to zero. Then "MMR1mod()" is used to
|
||||
# record any modifications (they still go into MMR1_staged) and
|
||||
# only when an MMU trap is generated are the staged values potentially
|
||||
# transferred into MMR1.
|
||||
#
|
||||
# This keeps the overhead down to a single self.MMR1_staged = 0
|
||||
# assignment for instructions that do not auto inc/dec and do not
|
||||
# cause MMU faults.
|
||||
|
||||
def MMR1mod(self, value):
|
||||
# record the given 8-bit register modification
|
||||
if value == 0 or value > 255: # this should never happen
|
||||
raise ValueError(f"bogus MMR1mod {value=}")
|
||||
|
||||
if self.MMR1_staged == 0:
|
||||
self.MMR1_staged = value
|
||||
else:
|
||||
self.MMR1_staged |= (value << 8)
|
||||
|
||||
def _MMR1commit(self):
|
||||
if not self._mmr12_frozen:
|
||||
self.MMR1 = self.MMR1_staged
|
||||
|
||||
@property
|
||||
def MMR2(self):
|
||||
return self._mmr2
|
||||
|
||||
@MMR2.setter
|
||||
def MMR2(self, value):
|
||||
if not self._mmr12_frozen:
|
||||
self._mmr2 = value
|
||||
|
||||
@property
|
||||
def MMR3(self):
|
||||
cpu = self.cpu
|
||||
return (
|
||||
((self._unibusmap & 1) << 5) |
|
||||
((self._22bit & 1) << 4) |
|
||||
(int(self._Dspaces[cpu.KERNEL] == self.DSPACE) << 2) |
|
||||
(int(self._Dspaces[cpu.SUPERVISOR] == self.DSPACE) << 1) |
|
||||
(int(self._Dspaces[cpu.USER] == self.DSPACE)))
|
||||
|
||||
@MMR3.setter
|
||||
def MMR3(self, value):
|
||||
self._unibusmap = (value >> 5) & 1
|
||||
self._22bit = (value >> 4) & 1
|
||||
self.segcache = {}
|
||||
self.__rebaseIO() # because 22bit affects where it ends up
|
||||
|
||||
# rather than store the kernel/super/user D-space enables,
|
||||
# store which space to use for D-space lookups
|
||||
self._Dspaces = {mode: [self.ISPACE, self.DSPACE][bool(value & mask)]
|
||||
for mode, mask in ((self.cpu.KERNEL, 4),
|
||||
(self.cpu.SUPERVISOR, 2),
|
||||
(self.cpu.USER, 1))}
|
||||
|
||||
def __rebaseIO(self):
|
||||
"""Where oh where has my little I/O page gone?"""
|
||||
|
||||
# whenver relo_enabled or _22bit change, which inconveniently
|
||||
# are in separate MMR registers, the I/O potentially moves.
|
||||
# Figure out where to put it.
|
||||
self.iopage_base = 0o160000 # end of the 16 bit space
|
||||
if self._mmu_relo_enabled:
|
||||
self.iopage_base |= (3 << 16) # 2 more bits (18 total)
|
||||
if self._22bit:
|
||||
self.iopage_base |= (15 << 18) # ... and 4 more
|
||||
|
||||
def v2p(self, vaddr, mode, space, cycle):
|
||||
"""Convert a 16-bit virtual address to physical.
|
||||
NOTE: Raises traps, updates A/W bits, & sets straps as needed.
|
||||
"""
|
||||
|
||||
if not self._mmu_relo_enabled:
|
||||
return vaddr
|
||||
|
||||
if mode is None: # the normal (not mtpi etc) case
|
||||
mode = self.cpu.psw_curmode
|
||||
|
||||
# fold I/D together (into I) if separation not on
|
||||
space = self._foldspaces(mode, space)
|
||||
|
||||
# the virtual address is broken down into three fields:
|
||||
# <15:13> APF active page field. Selects the APR = par,pdr pair
|
||||
# This is sometimes called the "segment number"
|
||||
# <12:6> The "block number"
|
||||
# <5:0> The displacement in block
|
||||
#
|
||||
# The block number will be added to the page address field in the par.
|
||||
# That whole thing is shifted left 6 bits and or'd with the
|
||||
# displacement within block. All this is per the PDP11 manuals.
|
||||
|
||||
segno = vaddr >> 13
|
||||
|
||||
# the segment number and these other parameters form
|
||||
# a "translation key" used in several places
|
||||
xkey = self.TransKey(segno, mode, space, cycle)
|
||||
|
||||
# All this translation code takes quite some time; caching
|
||||
# dramatically improves performance.
|
||||
#
|
||||
# I/O space mappings are not cached (not performance-critical).
|
||||
#
|
||||
|
||||
if self.nocache and self.segcache:
|
||||
self.segcache = {}
|
||||
try:
|
||||
xoff, validation_func = self.segcache[xkey]
|
||||
if validation_func(vaddr):
|
||||
return vaddr + xoff
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# not cached; do the translation...
|
||||
|
||||
par, pdr = self._getapr(xkey)
|
||||
|
||||
# In 22bit mode, the full 16 bits of the PAR are used.
|
||||
# In 18bit mode, the top four have to be masked off here.
|
||||
if not self._22bit:
|
||||
par &= 0o7777
|
||||
|
||||
# access checks:
|
||||
# "Aborts" (per the processor handbook) raise PDPTraps.MMU and
|
||||
# do not return from accesschecks()
|
||||
#
|
||||
# If there are "memory management traps" (which are to occur
|
||||
# at the *end* of instruction execution) they are returned as
|
||||
# bits suitable for OR'ing into cpu.straps; note that this
|
||||
# condition also prevents caching the APR. If the mgmt trap
|
||||
# handler modifies the APR to disable the management trap then
|
||||
# of course future lookups will be eligible for the cache then.
|
||||
|
||||
straps = self._v2p_accesschecks(pdr, vaddr, xkey)
|
||||
|
||||
# the actual translation...
|
||||
bn = (vaddr >> 6) & 0o177
|
||||
plf = (pdr >> 8) & 0o177
|
||||
if (pdr & 0o10) and bn < plf:
|
||||
self._raisetrap(self.MMR0_BITS.ABORT_PLENGTH, vaddr, xkey)
|
||||
elif (pdr & 0o10) == 0 and bn > plf:
|
||||
self._raisetrap(self.MMR0_BITS.ABORT_PLENGTH, vaddr, xkey)
|
||||
|
||||
# "Access" and "Written" bits updates. Subtle note: if this entry
|
||||
# gets cached, then by definition the corresponding AW updates
|
||||
# already happened (here). So the "found it in cache" logic up top
|
||||
# of this function needn't worry about AW bit updates.
|
||||
|
||||
AW_update = 0o300 if cycle == self.CYCLE.WRITE else 0o200
|
||||
# XXX ^^^^^ not sure if a write should be 0o300 or naked 0o100
|
||||
|
||||
if (pdr & AW_update) != AW_update:
|
||||
self._putapr(xkey, (par, pdr | AW_update))
|
||||
|
||||
dib = vaddr & 0o77
|
||||
pa = ((par + bn) << 6) | dib
|
||||
|
||||
self.cpu.straps |= straps
|
||||
|
||||
# only non-trapping non-io results can be cached:
|
||||
if straps == 0 and pa < self.iopage_base:
|
||||
self._encache(xkey, pdr, pa - vaddr)
|
||||
return pa
|
||||
|
||||
def _encache(self, k, pdr, offs):
|
||||
# the validation function (lambdas) is constructed for cases
|
||||
# where the segment is not full-length and therefore one more
|
||||
# check has to happen even on cache hits.
|
||||
plf = (pdr >> 8) & 0o177
|
||||
if pdr & 0o10 and plf > 0:
|
||||
self.segcache[k] = (offs, lambda a: ((a >> 6) & 0o177) >= plf)
|
||||
elif (pdr & 0o10) == 0 and plf < 0o177:
|
||||
self.segcache[k] = (offs, lambda a: ((a >> 6) & 0o177) <= plf)
|
||||
else:
|
||||
self.segcache[k] = (offs, lambda a: True) # full segment
|
||||
|
||||
def _foldspaces(self, mode, space):
|
||||
"""Folds DSPACE back into ISPACE if DSPACE not enabled for mode"""
|
||||
return space if space == self.ISPACE else self._Dspaces[mode]
|
||||
|
||||
def _getapr(self, xkey):
|
||||
"""CAUTION: xkey must already be space-folded."""
|
||||
nth = (xkey.mode * 2) + xkey.space
|
||||
return self.APR[nth][xkey.segno]
|
||||
|
||||
def _putapr(self, xkey, apr):
|
||||
"""CAUTION: xkey must already be space-folded."""
|
||||
nth = (xkey.mode * 2) + xkey.space
|
||||
self.APR[nth][xkey.segno] = list(apr)
|
||||
|
||||
def _v2p_accesschecks(self, pdr, vaddr, xkey):
|
||||
"""Raise traps or set post-instruction traps as appropriate.
|
||||
|
||||
Returns straps flags (if any required).
|
||||
"""
|
||||
|
||||
straps = 0
|
||||
|
||||
# There are aborts and "memory management traps".
|
||||
# As the handbook says:
|
||||
# """Aborts are used to catch "missing page faults," prevent
|
||||
# illegal access, etc.; traps are used as an aid in
|
||||
# gathering memory management information
|
||||
# """
|
||||
#
|
||||
# Thus, an "abort" raises a vector 4 (AddressError) exception and
|
||||
# a "management trap" sets a cpu bit to cause a vector 250 (MMU)
|
||||
# trap at the *completion* of the instruction.
|
||||
#
|
||||
# The 7 possible access control modes (pdr & 7) are:
|
||||
#
|
||||
# 000 -- abort all accesses
|
||||
# 001 -- read-only + mgmt trap (read)
|
||||
# 010 -- read-only no mgmt traps
|
||||
# 011 -- RESERVED/ILLEGAL, abort all accesses
|
||||
# 100 -- writable + mgmt trap (any)
|
||||
# 101 -- writable + mgmt trap if write
|
||||
# 110 -- writable no mgmt traps
|
||||
# 111 -- RESERVED/ILLEGAL abort all accesses
|
||||
|
||||
# Things that are not decoded in the match are accesses that
|
||||
# cause no traps or aborts. So, for example, control mode 6
|
||||
# is not in the cases; nor is control mode 5 if reading.
|
||||
|
||||
cycle = xkey.cycle
|
||||
match pdr & 7:
|
||||
# control modes 0, 3, and 7 are always aborts
|
||||
case 0 | 3 | 7:
|
||||
self.cpu.logger.debug(f"ABORT_NR trap, regs: "
|
||||
f"{list(map(oct, self.cpu.r))}"
|
||||
f", {oct(self.cpu.psw)}"
|
||||
f", PDR={oct(pdr)} {cycle=}")
|
||||
self._raisetrap(self.MMR0_BITS.ABORT_NR, vaddr, xkey)
|
||||
|
||||
# control mode 1 is an abort if writing, mgmt trap if read
|
||||
case 1 if cycle == self.CYCLE.READ:
|
||||
straps = self.cpu.STRAPBITS.MEMMGT
|
||||
|
||||
case 1 | 2 if cycle == self.CYCLE.WRITE:
|
||||
self._raisetrap(self.MMR0_BITS.ABORT_RDONLY, vaddr, xkey)
|
||||
|
||||
# control mode 4 is mgmt trap on any access (read or write)
|
||||
case 4:
|
||||
straps = self.cpu.STRAPBITS.MEMMGT
|
||||
|
||||
# control mode 5 is mgmt trap if WRITING
|
||||
case 5 if cycle == self.CYCLE.WRITE:
|
||||
straps = self.cpu.STRAPBITS.MEMMGT
|
||||
|
||||
return straps
|
||||
|
||||
def wordRW(self, vaddr, value=None, /, *, mode=None, space=ISPACE):
|
||||
"""Read/write a word at virtual address vaddr.
|
||||
|
||||
If value is None, perform a read and return a 16-bit value
|
||||
If value is not None, perform a write; return None.
|
||||
"""
|
||||
|
||||
cycle = self.CYCLE.READ if value is None else self.CYCLE.WRITE
|
||||
pa = self.v2p(vaddr, mode, space, cycle)
|
||||
if pa >= self.iopage_base:
|
||||
return self.ub.mmio.wordRW(pa & self.cpu.IOPAGE_MASK, value)
|
||||
else:
|
||||
return self.cpu.physRW(pa, value)
|
||||
|
||||
def byteRW(self, vaddr, value=None, /, mode=None, space=ISPACE):
|
||||
"""Read/write a byte at virtual address vaddr.
|
||||
|
||||
If value is None, perform a read and return an 8-bit value
|
||||
If value is not None, perform a write; return None.
|
||||
"""
|
||||
|
||||
cycle = self.CYCLE.READ if value is None else self.CYCLE.WRITE
|
||||
pa = self.v2p(vaddr, mode, space, cycle)
|
||||
|
||||
# Physical memory is represented as an array of 16-bit word
|
||||
# values, and byte operations are synthesized from that in
|
||||
# the obvious manner.
|
||||
#
|
||||
# However, the UNIBUS is different. At the physical electrical
|
||||
# signal level, the UNIBUS cannot perform byte reads, but CAN
|
||||
# perform byte writes.
|
||||
#
|
||||
# Given that - any byte read is synthesized from corresponding
|
||||
# word read operations, I/O or physical as appropriate.
|
||||
#
|
||||
# But byte write operations are dispatched as byte operations
|
||||
# to the unibus, while still being synthesized here for memory.
|
||||
|
||||
odd = (pa & 1)
|
||||
|
||||
if value is None:
|
||||
# *** READ ***
|
||||
#
|
||||
# Synthesized from containing word in the obvious way.
|
||||
# Note little-endianness.
|
||||
|
||||
pa &= ~1
|
||||
if pa >= self.iopage_base:
|
||||
wv = self.ub.mmio.wordRW(pa & self.cpu.IOPAGE_MASK)
|
||||
else:
|
||||
wv = self.cpu.physRW(pa)
|
||||
return ((wv >> 8) if odd else wv) & 0o377
|
||||
else:
|
||||
# *** WRITE ***
|
||||
|
||||
# This sanity check should be taken out eventually
|
||||
if (value & 0xFF) != value:
|
||||
raise ValueError(f"{value} is out of range")
|
||||
|
||||
# I/O byte writes are handled by Unibus;
|
||||
# Memory byte writes are synthesized.
|
||||
|
||||
if pa >= self.iopage_base:
|
||||
return self.ub.mmio.byteRW(pa & self.cpu.IOPAGE_MASK, value)
|
||||
else:
|
||||
wv = self.cpu.physRW(pa & ~1)
|
||||
if odd:
|
||||
wv = (wv & 0o377) | (value << 8)
|
||||
else:
|
||||
wv = (wv & 0o177400) | value
|
||||
self.cpu.physRW(pa & ~1, wv)
|
||||
return None
|
||||
|
||||
def wordRW_KD(self, a, v=None, /):
|
||||
"""Convenienence; version of wordRW for kernel/dspace."""
|
||||
return self.wordRW(a, v, mode=self.cpu.KERNEL, space=self.DSPACE)
|
||||
|
||||
def _raisetrap(self, trapflag, vaddr, xkey):
|
||||
"""Raise an MMU trap. Commits regmods and updates reason in MMR0."""
|
||||
if trapflag == self.MMR0_BITS.ABORT_PLENGTH:
|
||||
self.cpu.logger.debug(f"PLF trap @ {oct(vaddr)}, {xkey=}")
|
||||
self._MMR1commit()
|
||||
self.MMR0 |= (trapflag |
|
||||
xkey.segno << 1 | # bits <3:1>
|
||||
xkey.space << 4 | # bit 4
|
||||
xkey.mode << 5) # bits <6:5>
|
||||
|
||||
# XXX gotta figure out how to set this for Odd Addresses and
|
||||
# T bit conditions, but otherwise Bit 7 is not set. From handbook:
|
||||
# Bit 7 indicates that the current instruction has·been completed.
|
||||
# It will be set to a during T bit, Parity, Odd Address, and
|
||||
# Time Out traps and interrupts. Bit 7 is Read-Only (it cannot
|
||||
# be written). It is initialized to a 1. Note that EMT, TRAP,
|
||||
# BPT, and lOT do not set bit 7.
|
||||
raise PDPTraps.MMU()
|
||||
|
||||
# handy for logging / debugging
|
||||
def scstr(self):
|
||||
"""Return a string representation of the segment cache."""
|
||||
|
||||
s = ""
|
||||
for xkey, v in self.segcache.items():
|
||||
ms = "KS!U"[xkey.mode]
|
||||
ds = "ID"[xkey.space]
|
||||
s += f"{oct(xkey.segno << 13)}:{ms}{ds}{xkey.cycle} :"
|
||||
s += f" {oct(v[0])}\n"
|
||||
return s
|
325
op00.py
Normal file
325
op00.py
Normal file
|
@ -0,0 +1,325 @@
|
|||
# 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.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
|
||||
|
||||
cpu.operandx(inst & 0o77, 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 = cpu.psw_n ^ 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 = cpu.psw_n ^ 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 = cpu.psw_n ^ 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 = cpu.psw_n ^ cpu.psw_c
|
||||
cpu.operandx(xb6, val, opsize=opsize)
|
||||
|
||||
|
||||
def op00_64_mark(cpu, inst):
|
||||
raise ValueError
|
||||
|
||||
|
||||
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) 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.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)
|
133
op000.py
Normal file
133
op000.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
# 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
|
||||
|
||||
# ... and even further down the op decode rabbit hole we go!
|
||||
# These are the decodes for opcodes starting 0o000
|
||||
from branches import branch
|
||||
|
||||
|
||||
def op_halt(cpu, inst):
|
||||
if cpu.psw_curmode != cpu.KERNEL:
|
||||
# strange trap, but that's what it says
|
||||
raise PDPTraps.AddressError(cpu.CPUERR_BITS.ILLHALT)
|
||||
cpu.halted = True
|
||||
|
||||
|
||||
def op_reset(cpu, inst):
|
||||
cpu.logger.debug("RESET INSTRUCTION")
|
||||
cpu.ub.resetbus()
|
||||
|
||||
|
||||
def op_wait(cpu, inst):
|
||||
cpu.logger.debug("WAIT")
|
||||
cpu.ub.intmgr.waitstate(cpu.psw_pri) # will pend until an interrupt
|
||||
|
||||
|
||||
def op_rtt(cpu, inst):
|
||||
cpu.r[cpu.PC] = cpu.stackpop()
|
||||
cpu.psw = cpu.stackpop()
|
||||
|
||||
|
||||
def op_02xx(cpu, inst):
|
||||
x5 = (inst & 0o70)
|
||||
if x5 == 0o00:
|
||||
op_rts(cpu, inst)
|
||||
elif x5 == 0o30:
|
||||
op_spl(cpu, inst)
|
||||
elif x5 >= 0o40:
|
||||
op_xcc(cpu, inst)
|
||||
else:
|
||||
raise PDPTraps.ReservedInstruction
|
||||
|
||||
|
||||
def op_spl(cpu, inst):
|
||||
"""SPL; note that this is a no-op (!) not a trap in non-kernel mode."""
|
||||
if cpu.psw_curmode == cpu.KERNEL:
|
||||
cpu.psw = (cpu.psw & ~ (0o07 << 5)) | ((inst & 0o07) << 5)
|
||||
|
||||
|
||||
def op_rts(cpu, inst):
|
||||
Rn = (inst & 0o07)
|
||||
cpu.r[cpu.PC] = cpu.r[Rn] # will be a no-op for RTS PC
|
||||
cpu.r[Rn] = cpu.stackpop()
|
||||
|
||||
|
||||
def op_jmp(cpu, inst):
|
||||
# same justEA/operand non-indirection discussion as in JSR (see)
|
||||
cpu.r[cpu.PC] = cpu.operandx(inst & 0o77, justEA=True)
|
||||
|
||||
|
||||
def op_swab(cpu, inst):
|
||||
"""SWAB swap bytes."""
|
||||
val, xb6 = cpu.operandx(inst & 0o77, rmw=True)
|
||||
|
||||
val = ((val >> 8) & cpu.MASK8) | ((val & cpu.MASK8) << 8)
|
||||
cpu.psw_n = val & cpu.SIGN16
|
||||
|
||||
# note this screwy definition, per the handbook
|
||||
cpu.psw_z = ((val & cpu.MASK8) == 0)
|
||||
cpu.psw_v = 0
|
||||
cpu.psw_c = 0
|
||||
|
||||
cpu.operandx(xb6, val)
|
||||
|
||||
|
||||
def op_xcc(cpu, inst):
|
||||
"""XCC - all variations of set/clear condition codes."""
|
||||
|
||||
setclr = inst & 0o020 # set it or clear it
|
||||
if inst & 0o10:
|
||||
cpu.psw_n = setclr
|
||||
if inst & 0o04:
|
||||
cpu.psw_z = setclr
|
||||
if inst & 0o02:
|
||||
cpu.psw_v = setclr
|
||||
if inst & 0o01:
|
||||
cpu.psw_c = setclr
|
||||
|
||||
|
||||
def op000_dispatcher(cpu, inst):
|
||||
match (inst & 0o0700):
|
||||
case 0o0000:
|
||||
if inst == 0:
|
||||
op_halt(cpu, inst)
|
||||
elif inst == 6 or inst == 2: # RTI and RTT are identical!!
|
||||
op_rtt(cpu, inst)
|
||||
elif inst == 1:
|
||||
op_wait(cpu, inst)
|
||||
elif inst == 5:
|
||||
op_reset(cpu, inst)
|
||||
|
||||
case 0o0100:
|
||||
op_jmp(cpu, inst)
|
||||
|
||||
case 0o0200:
|
||||
op_02xx(cpu, inst)
|
||||
|
||||
case 0o0300:
|
||||
op_swab(cpu, inst)
|
||||
|
||||
# note that 2 bits of the branch offset sneak into this match
|
||||
case 0o0400 | 0o0500 | 0o0600 | 0o0700:
|
||||
branch(cpu, inst, lambda n, z, v, c: True)
|
224
op07.py
Normal file
224
op07.py
Normal file
|
@ -0,0 +1,224 @@
|
|||
# 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.
|
||||
|
||||
# op07 instructions
|
||||
|
||||
|
||||
# 6 simple instructions appear in the op07 space: 070 .. 074 and 077.
|
||||
# In these the first operand is restricted to being only a register
|
||||
# (because the three bits usually used for 'mode' are part of
|
||||
# these opcodes). The destination is still a full 6-bit specification.
|
||||
#
|
||||
# op075 is used for floating point instruction encoding (FP instructions
|
||||
# are also found in other nooks and crannies)
|
||||
#
|
||||
# op076 is the commercial instruction set
|
||||
#
|
||||
|
||||
|
||||
def op070_mul(cpu, inst):
|
||||
dstreg = (inst & 0o000700) >> 6
|
||||
r = cpu.r[dstreg]
|
||||
src = cpu.operandx(inst & 0o077)
|
||||
|
||||
# unlike add/subtract, need to explicitly treat as signed.
|
||||
# The right results require sign extending both 16 bit operands to
|
||||
# 32 bits, multiplying them, then taking the bottom 32 bits of the result.
|
||||
# It may not be obvious why this works; see google.
|
||||
if r >= 32768:
|
||||
r |= 0xFFFF0000
|
||||
if src >= 32768:
|
||||
src |= 0xFFFF0000
|
||||
m = (src * r) & 0xFFFFFFFF
|
||||
|
||||
# the result is stored:
|
||||
# high 16 bits in dstreg
|
||||
# low 16 bits in dstreg|1
|
||||
# and if dstreg is odd ONLY the low 16 bits are stored
|
||||
# This just stores both but in careful order
|
||||
cpu.r[dstreg] = (m >> 16) & 0xFFFF
|
||||
cpu.r[dstreg | 1] = m & 0xFFFF
|
||||
|
||||
cpu.psw_n = m & 0x80000000
|
||||
if cpu.psw_n:
|
||||
cpu.psw_c = (m < 0xFFFF8000)
|
||||
else:
|
||||
cpu.psw_c = (m >= 32768)
|
||||
cpu.psw_z = (m == 0)
|
||||
cpu.psw_v = 0
|
||||
|
||||
|
||||
def op071_div(cpu, inst):
|
||||
dstreg = (inst & 0o000700) >> 6
|
||||
if (dstreg & 1):
|
||||
raise PDPTraps.ReservedInstruction # dstreg must be even
|
||||
dividend = (cpu.r[dstreg] << 16) | cpu.r[dstreg | 1]
|
||||
divisor = cpu.operandx(inst & 0o077)
|
||||
if divisor == 0:
|
||||
cpu.psw_n = 0
|
||||
cpu.psw_z = 1
|
||||
cpu.psw_v = 1
|
||||
cpu.psw_c = 1
|
||||
elif divisor == 0o177777 and dividend == 0x80000000:
|
||||
# maxneg / -1 == too big
|
||||
cpu.psw_n = 0
|
||||
cpu.psw_z = 0
|
||||
cpu.psw_v = 1
|
||||
cpu.psw_c = 0
|
||||
else:
|
||||
# convert both numbers to positive equivalents
|
||||
# and track sign info manually
|
||||
if dividend & cpu.SIGN32:
|
||||
dividend = 4*1024*1024*1024 - dividend
|
||||
ddendposneg = -1
|
||||
else:
|
||||
ddendposneg = 1
|
||||
posneg = ddendposneg
|
||||
if divisor & cpu.SIGN16:
|
||||
divisor = 65536 - divisor
|
||||
posneg = -posneg
|
||||
q, rem = divmod(dividend, divisor)
|
||||
q *= posneg
|
||||
if q > 32767 or q < -32768:
|
||||
cpu.psw_n = 0
|
||||
cpu.psw_z = 0
|
||||
cpu.psw_v = 1
|
||||
cpu.psw_c = 0
|
||||
else:
|
||||
if ddendposneg < 0:
|
||||
rem = -rem
|
||||
cpu.psw_n = (q < 0)
|
||||
cpu.psw_z = (q == 0)
|
||||
cpu.psw_v = 0
|
||||
cpu.psw_c = 0
|
||||
|
||||
cpu.r[dstreg] = q & cpu.MASK16
|
||||
cpu.r[dstreg | 1] = rem & cpu.MASK16
|
||||
|
||||
|
||||
def op072_ash(cpu, inst):
|
||||
dstreg = (inst & 0o000700) >> 6
|
||||
r = cpu.r[dstreg]
|
||||
shift = cpu.operandx(inst & 0o077) & 0o077
|
||||
|
||||
r = _shifter(cpu, r, shift, opsize=2)
|
||||
|
||||
cpu.r[dstreg] = r
|
||||
|
||||
|
||||
def op073_ashc(cpu, inst):
|
||||
dstreg = (inst & 0o000700) >> 6
|
||||
r = (cpu.r[dstreg] << 16) | cpu.r[dstreg | 1]
|
||||
shift = cpu.operandx(inst & 0o077) & 0o077
|
||||
|
||||
r = _shifter(cpu, r, shift, opsize=4)
|
||||
|
||||
cpu.r[dstreg] = (r >> 16) & cpu.MASK16
|
||||
cpu.r[dstreg | 1] = r & cpu.MASK16
|
||||
|
||||
|
||||
# this is the heart of ash and ashc
|
||||
def _shifter(cpu, value, shift, *, opsize):
|
||||
"""Returns shifted value and sets condition codes."""
|
||||
|
||||
signmask = cpu.SIGN16
|
||||
signextend = 0xFFFFFFFF0000
|
||||
if opsize == 4:
|
||||
signmask <<= 16
|
||||
signextend <<= 16
|
||||
|
||||
vsign = value & signmask
|
||||
|
||||
if shift == 0:
|
||||
cpu.psw_n = vsign
|
||||
cpu.psw_z = (value == 0)
|
||||
cpu.psw_v = 0
|
||||
# C is not altered
|
||||
return value
|
||||
elif shift > 31: # right shift
|
||||
# sign extend if appropriate, so the sign propagates
|
||||
if vsign:
|
||||
value |= signextend
|
||||
|
||||
# right shift by 1 less, to capture bottom bit for C
|
||||
value >>= (63 - shift) # yes 63, see ^^^^^^^^^^^^^^^
|
||||
cbit = (value & 1)
|
||||
value >>= 1
|
||||
else:
|
||||
# shift by 1 less, again to capture cbit
|
||||
value <<= (shift - 1)
|
||||
cbit = value & signmask
|
||||
value <<= 1
|
||||
|
||||
value &= (signmask | (signmask - 1))
|
||||
cpu.psw_n = (value & signmask)
|
||||
cpu.psw_z = (value == 0)
|
||||
cpu.psw_v = (cpu.psw_n != vsign)
|
||||
cpu.psw_c = cbit
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def op074_xor(cpu, inst):
|
||||
srcreg = (inst & 0o000700) >> 6
|
||||
r = cpu.r[srcreg]
|
||||
|
||||
s, xb6 = cpu.operandx(inst & 0o077, rmw=True)
|
||||
|
||||
r ^= s
|
||||
|
||||
cpu.psw_n = (r & cpu.SIGN16)
|
||||
cpu.psw_z = (r == 0)
|
||||
cpu.psw_v = 0
|
||||
|
||||
cpu.operandx(xb6, r)
|
||||
|
||||
|
||||
def op077_sob(cpu, inst):
|
||||
srcreg = (inst & 0o000700) >> 6
|
||||
r = cpu.r[srcreg]
|
||||
|
||||
if r == 1:
|
||||
r = 0
|
||||
else:
|
||||
if r > 0:
|
||||
r -= 1
|
||||
else:
|
||||
r = 0o177777 # 0 means max, that's how SOB is defined
|
||||
|
||||
# technically if this instruction occurs low enough in memory
|
||||
# this PC subtraction could wrap, so be technically correct & mask
|
||||
cpu.r[cpu.PC] = (cpu.r[cpu.PC] - 2 * (inst & 0o077)) & cpu.MASK16
|
||||
|
||||
cpu.r[srcreg] = r
|
||||
|
||||
|
||||
# dispatch on the next digit after the 07 part...
|
||||
op07_dispatch_table = (
|
||||
op070_mul,
|
||||
op071_div,
|
||||
op072_ash,
|
||||
op073_ashc,
|
||||
op074_xor,
|
||||
None, # various float instructions, not implemented
|
||||
None, # CIS instructions, not implmented
|
||||
op077_sob)
|
60
op10.py
Normal file
60
op10.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# 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 branches import branches
|
||||
from op00 import ops56tab # these have byte variants in op10
|
||||
from pdptraps import PDPTraps
|
||||
|
||||
|
||||
# dispatch MOST (but not all) of the 105x and 106x instructions
|
||||
# to the op00 routines but with byte variation (opsize=1). But
|
||||
# there are exceptions: 1064 unused, 1065 MFPD, 1066 MTPD, 1067 unused
|
||||
def op156(cpu, inst):
|
||||
i56x = (inst & 0o7700) >> 6
|
||||
# 64 (mark) and 67 (sxt) do NOT have byte variants
|
||||
if i56x in (0o64, 0o67):
|
||||
raise PDPTraps.ReservedInstruction
|
||||
try:
|
||||
opf = ops56tab[i56x - 0o50]
|
||||
except IndexError:
|
||||
raise PDPTraps.ReservedInstruction
|
||||
opf(cpu, inst, opsize=1)
|
||||
|
||||
|
||||
def op10_4_emttrap(cpu, inst):
|
||||
# bit 8 determines EMT (0) or TRAP(1)
|
||||
if (inst & 0o000400):
|
||||
raise PDPTraps.TRAP
|
||||
else:
|
||||
raise PDPTraps.EMT
|
||||
|
||||
|
||||
op10_dispatch_table = (
|
||||
branches,
|
||||
branches,
|
||||
branches,
|
||||
branches,
|
||||
op10_4_emttrap,
|
||||
op156,
|
||||
op156,
|
||||
None)
|
226
op4.py
Normal file
226
op4.py
Normal file
|
@ -0,0 +1,226 @@
|
|||
# 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.
|
||||
|
||||
#
|
||||
# TOP LEVEL OP CODE DISPATCH AND INSTRUCTION IMPLEMENTATION
|
||||
#
|
||||
# NOTES:
|
||||
#
|
||||
# DISPATCH
|
||||
# 2-operand instructions are implemented here and are dispatched
|
||||
# off the top-4 bits of the instruction (hence "op4" name).
|
||||
#
|
||||
# The other instructions are encoded into the 0o00, 0o07, and 0o10
|
||||
# portions of this top-4 bit address space. They are dispatched
|
||||
# via d3dispatch and respective tables from other modules.
|
||||
#
|
||||
# BYTE vs WORD operations:
|
||||
# Most of them come in two flavors - word and byte, with the
|
||||
# top-bit distinguishing. This is communicated to the implementation
|
||||
# functions via "opsize=1" or "opsize=2" when a single function can
|
||||
# implement both variations. Note that MOV/MOVB are specifically
|
||||
# optimized, separately, for performance.
|
||||
#
|
||||
# PSW updates:
|
||||
# All instructions must be careful to do their final result writes
|
||||
# AFTER setting the PSW, because the PSW is addressible via memory
|
||||
# (a write to unibus 777776) and such a write is supposed to override
|
||||
# the otherwise-native instruction CC results.
|
||||
#
|
||||
|
||||
# dispatchers to next level for 00, 07, and 10 instructions:
|
||||
from op00 import op00_dispatch_table
|
||||
from op07 import op07_dispatch_table
|
||||
from op10 import op10_dispatch_table
|
||||
from pdptraps import PDPTraps
|
||||
|
||||
|
||||
def d3dispatcher(d3table, cpu, inst):
|
||||
opf = d3table[(inst & 0o7000) >> 9]
|
||||
if opf is None:
|
||||
raise PDPTraps.ReservedInstruction
|
||||
opf(cpu, inst)
|
||||
|
||||
|
||||
# This is ALWAYS a 16-bit MOV
|
||||
def op01_mov(cpu, inst):
|
||||
"""MOV src,dst -- always 16 bits"""
|
||||
|
||||
# avoid call to the more-general operandx for mode 0, direct register.
|
||||
# This optimization is a substantial speed up for register MOVs.
|
||||
if (inst & 0o7000) == 0:
|
||||
val = cpu.r[(inst & 0o700) >> 6]
|
||||
else:
|
||||
val = cpu.operandx((inst & 0o7700) >> 6)
|
||||
|
||||
cpu.psw_v = 0 # per manual; V is cleared
|
||||
cpu.psw_z = (val == 0)
|
||||
cpu.psw_n = (val > 32767)
|
||||
|
||||
# same optimization on the write side.
|
||||
if (inst & 0o70) == 0:
|
||||
cpu.r[(inst & 0o07)] = val
|
||||
else:
|
||||
cpu.operandx(inst & 0o0077, val)
|
||||
|
||||
|
||||
# This is ALWAYS an 8-bit MOVB
|
||||
def op11_movb(cpu, inst):
|
||||
"""MOVB src,dst -- always 8 bits"""
|
||||
|
||||
# avoid call to the more-general operandx for mode 0, direct register.
|
||||
# This optimization is a substantial speed up for register MOVs.
|
||||
if (inst & 0o7000) == 0:
|
||||
val = cpu.r[(inst & 0o700) >> 6] & 0o377
|
||||
else:
|
||||
val = cpu.operandx((inst & 0o7700) >> 6, opsize=1)
|
||||
|
||||
cpu.psw_v = 0
|
||||
cpu.psw_z = (val == 0)
|
||||
cpu.psw_n = (val & 0o200)
|
||||
|
||||
# No optimization on the write side, because doing so would require
|
||||
# duplicating the sign-extend logic here. Yuck.
|
||||
cpu.operandx(inst & 0o0077, val, opsize=1)
|
||||
|
||||
|
||||
def op02_cmp(cpu, inst, opsize=2):
|
||||
"""CMP(B) src,dst"""
|
||||
src = cpu.operandx((inst & 0o7700) >> 6, opsize=opsize)
|
||||
dst = cpu.operandx(inst & 0o0077, opsize=opsize)
|
||||
|
||||
# note: this is other order than SUB
|
||||
t = (src - dst) & cpu.MASK816[opsize]
|
||||
cpu.psw_c = (src < dst)
|
||||
signbit = cpu.SIGN816[opsize]
|
||||
cpu.psw_n = (t & signbit)
|
||||
cpu.psw_z = (t == 0)
|
||||
|
||||
# definition of V is: operands were of opposite signs and the sign
|
||||
# of the destination was the same as the sign of the result
|
||||
src_sign = src & signbit
|
||||
dst_sign = dst & signbit
|
||||
t_sign = t & signbit
|
||||
cpu.psw_v = (dst_sign == t_sign) and (src_sign != dst_sign)
|
||||
|
||||
|
||||
def op03_bit(cpu, inst, opsize=2):
|
||||
"""BIT(B) src,dst"""
|
||||
src = cpu.operandx((inst & 0o7700) >> 6, opsize=opsize)
|
||||
dst = cpu.operandx(inst & 0o0077, opsize=opsize)
|
||||
t = dst & src
|
||||
|
||||
cpu.psw_n = t & cpu.SIGN816[opsize]
|
||||
cpu.psw_z = (t == 0)
|
||||
cpu.psw_v = 0
|
||||
# cpu.logger.debug(f"BIT: {src=}, {dst=}, PSW={oct(cpu.psw)}")
|
||||
|
||||
|
||||
def op04_bic(cpu, inst, opsize=2):
|
||||
"""BIC(B) src,dst"""
|
||||
src = cpu.operandx((inst & 0o7700) >> 6, opsize=opsize)
|
||||
dst, xb6 = cpu.operandx(inst & 0o0077, opsize=opsize, rmw=True)
|
||||
dst &= ~src
|
||||
|
||||
cpu.psw_n = dst & cpu.SIGN816[opsize]
|
||||
cpu.psw_z = (dst == 0)
|
||||
|
||||
cpu.operandx(xb6, dst, opsize=opsize)
|
||||
|
||||
|
||||
def op05_bis(cpu, inst, opsize=2):
|
||||
"""BIS(B) src,dst"""
|
||||
src = cpu.operandx((inst & 0o7700) >> 6, opsize=opsize)
|
||||
dst, xb6 = cpu.operandx(inst & 0o0077, opsize=opsize, rmw=True)
|
||||
dst |= src
|
||||
|
||||
cpu.psw_n = dst & cpu.SIGN816[opsize]
|
||||
cpu.psw_z = (dst == 0)
|
||||
cpu.psw_v = 0
|
||||
|
||||
cpu.operandx(xb6, dst, opsize=opsize)
|
||||
|
||||
|
||||
def op06_add(cpu, inst):
|
||||
"""ADD src,dst"""
|
||||
src = cpu.operandx((inst & 0o7700) >> 6)
|
||||
dst, xb6 = cpu.operandx(inst & 0o0077, rmw=True)
|
||||
t = src + dst
|
||||
|
||||
cpu.psw_c = (t > cpu.MASK16)
|
||||
if cpu.psw_c:
|
||||
t &= cpu.MASK16
|
||||
|
||||
cpu.psw_n = (t & cpu.SIGN16)
|
||||
cpu.psw_z = (t == 0)
|
||||
|
||||
# definition of V is: operands were of the same signs and the
|
||||
# sign of the result is different.
|
||||
src_sign = src & cpu.SIGN16
|
||||
dst_sign = dst & cpu.SIGN16
|
||||
t_sign = t & cpu.SIGN16
|
||||
cpu.psw_v = (dst_sign != t_sign) and (src_sign == dst_sign)
|
||||
|
||||
cpu.operandx(xb6, t)
|
||||
|
||||
|
||||
def op16_sub(cpu, inst):
|
||||
"""SUB src,dst"""
|
||||
src = cpu.operandx((inst & 0o7700) >> 6)
|
||||
dst, xb6 = cpu.operandx(inst & 0o0077, rmw=True)
|
||||
|
||||
t = (dst - src) & cpu.MASK16 # note: this is opposite of CMP
|
||||
cpu.psw_n = (t & cpu.SIGN16)
|
||||
cpu.psw_z = (t == 0)
|
||||
|
||||
# definition of V is: operands were of opposite signs and the sign
|
||||
# of the source was the same as the sign of the result
|
||||
src_sign = src & cpu.SIGN16
|
||||
dst_sign = dst & cpu.SIGN16
|
||||
t_sign = t & cpu.SIGN16
|
||||
cpu.psw_v = (src_sign == t_sign) and (src_sign != dst_sign)
|
||||
cpu.psw_c = (src > dst)
|
||||
|
||||
cpu.operandx(xb6, t)
|
||||
|
||||
|
||||
def op17_reserved(cpu, inst):
|
||||
raise PDPTraps.ReservedInstruction
|
||||
|
||||
|
||||
op4_dispatch_table = (
|
||||
lambda c, i: d3dispatcher(op00_dispatch_table, c, i),
|
||||
op01_mov,
|
||||
op02_cmp,
|
||||
op03_bit,
|
||||
op04_bic,
|
||||
op05_bis,
|
||||
op06_add,
|
||||
lambda c, i: d3dispatcher(op07_dispatch_table, c, i),
|
||||
lambda c, i: d3dispatcher(op10_dispatch_table, c, i),
|
||||
op11_movb, # NOTE: optimized; not mov+lambda
|
||||
lambda c, i: op02_cmp(c, i, opsize=1), # 12
|
||||
lambda c, i: op03_bit(c, i, opsize=1), # 13
|
||||
lambda c, i: op04_bic(c, i, opsize=1), # 14
|
||||
lambda c, i: op05_bis(c, i, opsize=1), # 15
|
||||
op16_sub,
|
||||
op17_reserved) # 17 reserved
|
737
pdptests.py
Normal file
737
pdptests.py
Normal file
|
@ -0,0 +1,737 @@
|
|||
# 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 itertools import chain
|
||||
from types import SimpleNamespace
|
||||
|
||||
from machine import PDP1170
|
||||
from pdptraps import PDPTraps
|
||||
import unittest
|
||||
import random
|
||||
|
||||
|
||||
class TestMethods(unittest.TestCase):
|
||||
|
||||
PDPLOGLEVEL = 'INFO'
|
||||
|
||||
# DISCLAIMER ABOUT TEST CODING PHILOSOPHY:
|
||||
# For the most part, actual PDP-11 machine code is created and
|
||||
# used to establish the test conditions, as this provides additional
|
||||
# (albeit haphazard) testing of the functionality. Occasionally it's
|
||||
# just too much hassle to do that and the pdp object is manipulated
|
||||
# directly via methods/attributes to establish conditions.
|
||||
# There's no rhyme or reason in picking the approach for a given test.
|
||||
|
||||
# used to create various instances, collects all the options
|
||||
# detail into this one place...
|
||||
@classmethod
|
||||
def make_pdp(cls):
|
||||
return PDP1170(console=False, loglevel=cls.PDPLOGLEVEL)
|
||||
|
||||
@staticmethod
|
||||
def ioaddr(p, offs):
|
||||
"""Given a within-IO-page IO offset, return an IO addr."""
|
||||
return (offs + p.mmu.iopage_base) & 0o177777
|
||||
|
||||
# convenience routine to load word values into physical memory
|
||||
@staticmethod
|
||||
def loadphysmem(p, words, addr):
|
||||
for a, w in enumerate(words, start=(addr >> 1)):
|
||||
p.physmem[a] = w
|
||||
|
||||
# some of these can't be computed at class definition time, so...
|
||||
@classmethod
|
||||
def usefulconstants(cls):
|
||||
|
||||
p = cls.make_pdp() # meh, need this for some constants
|
||||
|
||||
ns = SimpleNamespace()
|
||||
|
||||
# Kernel instruction space PDR registers
|
||||
ns.KISD0 = cls.ioaddr(p, p.mmu.APR_KERNEL_OFFS)
|
||||
|
||||
# Kernel data space PDR registers
|
||||
ns.KDSD0 = ns.KISD0 + 0o20
|
||||
|
||||
# Kernel instruction space PAR registers
|
||||
ns.KISA0 = ns.KDSD0 + 0o20
|
||||
|
||||
# Kernel data space PAR registers
|
||||
ns.KDSA0 = ns.KISA0 + 0o20
|
||||
|
||||
# User mode similar
|
||||
ns.UISD0 = cls.ioaddr(p, p.mmu.APR_USER_OFFS)
|
||||
ns.UDSD0 = ns.UISD0 + 0o20
|
||||
ns.UISA0 = ns.UDSD0 + 0o20
|
||||
ns.UDSA0 = ns.UISA0 + 0o20
|
||||
|
||||
return ns
|
||||
|
||||
#
|
||||
# Create and return a test machine with a simple memory mapping:
|
||||
# Kernel Instruction space seg 0 points to physical 0
|
||||
# Kernel Data space segment 0 also points to physical 0
|
||||
# User instruction space seg 0 points to physical 0o20000
|
||||
# User Data space seg 0 points to physical 0o40000
|
||||
# and turns on the MMU
|
||||
#
|
||||
|
||||
def simplemapped_pdp(self, p=None, addons=[]):
|
||||
if p is None:
|
||||
p = self.make_pdp()
|
||||
|
||||
cn = self.usefulconstants()
|
||||
|
||||
# this is a table of instructions that ...
|
||||
# Puts the system stack at 0o20000 (8K)
|
||||
# Puts 0o22222 into physical location 0o20000
|
||||
# Puts 0o33333 into physical location 0o20002
|
||||
# Puts 0o44444 into physical location 0o40000
|
||||
# Sets Kernel Instruction space A0 to point to physical 0
|
||||
# Sets Kernel Data space A0 to point to the first 8K physical memory
|
||||
# Sets Kernel Data space A7 to point to the IO page
|
||||
# Sets User Instruction space A0 to point to physical 0o20000
|
||||
# sets User Data space D0 to point to physical 0o40000
|
||||
# and turns on the MMU with I/D sep
|
||||
#
|
||||
# These instructions will be placed at 2K in memory
|
||||
#
|
||||
setup_instructions = (
|
||||
0o012706, 0o20000, # put system stack at 8k and works down
|
||||
|
||||
0o012737, 0o22222, 0o20000,
|
||||
0o012737, 0o33333, 0o20002,
|
||||
0o012737, 0o44444, 0o40000,
|
||||
|
||||
# point both kernel seg 0 PARs to physical zero
|
||||
0o005037, cn.KISA0, # CLR $KISA0
|
||||
0o005037, cn.KDSA0, # CLR $KDSA0
|
||||
|
||||
# kernel seg 7 D space PAR to I/O page (at 22-bit location)
|
||||
0o012737, 0o017760000 >> 6, cn.KDSA0 + (7 * 2),
|
||||
|
||||
# user I seg 0 to 0o20000, user D seg 0 to 0o40000
|
||||
0o012737, 0o20000 >> 6, cn.UISA0,
|
||||
0o012737, 0o40000 >> 6, cn.UDSA0,
|
||||
|
||||
# set the PDRs for segment zero
|
||||
|
||||
0o012703, 0o077406, # MOV #77406,R3
|
||||
# 77406 = PDR<2:0> = ACF = 0o110 = read/write
|
||||
# PLF<14:8> =0o0774 = full length (128*64 bytes = 8K)
|
||||
0o010337, cn.KISD0, # MOV R3,KISD0 ...
|
||||
0o010337, cn.KDSD0,
|
||||
0o010337, cn.UISD0,
|
||||
0o010337, cn.UDSD0,
|
||||
# PDR for segment 7
|
||||
0o010337, cn.KDSD0 + (7 * 2),
|
||||
|
||||
|
||||
# set previous mode to USER, keeping current mode KERNEL, pri 7
|
||||
0o012737, (p.KERNEL << 14) | (p.USER << 12) | (7 << 5),
|
||||
self.ioaddr(p, p.PS_OFFS),
|
||||
|
||||
# turn on 22-bit mode, unibus mapping, and I/D sep for k & u
|
||||
0o012737, 0o000065, self.ioaddr(p, p.mmu.MMR3_OFFS),
|
||||
|
||||
# turn on relocation mode ... yeehah! (MMR0 known zero here)
|
||||
0o005237, self.ioaddr(p, p.mmu.MMR0_OFFS), # INC MMR0
|
||||
)
|
||||
|
||||
instloc = 0o4000 # 2K
|
||||
self.loadphysmem(p, chain(setup_instructions, addons, (0o0,)), instloc)
|
||||
return p, instloc
|
||||
|
||||
# these tests end up testing a other stuff too of course, including MMU
|
||||
def test_mfpi(self):
|
||||
# ((r0, ..., rN) results, (instructions)), ...
|
||||
tvecs = (
|
||||
|
||||
# r1=2, mfpi (r1) -> r0; expect r0 = 33333
|
||||
((0o33333,), (0o012701, 0o02, 0o006511, 0o012600)),
|
||||
|
||||
# r1=0, mfpi (r1) -> r0; expect r0 = 22222
|
||||
((0o22222,), (0o012701, 0o00, 0o006511, 0o012600)),
|
||||
)
|
||||
|
||||
for rslts, insts in tvecs:
|
||||
with self.subTest(rslts=rslts, insts=insts):
|
||||
p, pc = self.simplemapped_pdp(addons=insts)
|
||||
p.run(pc=pc)
|
||||
for rN, v in enumerate(rslts):
|
||||
self.assertEqual(p.r[rN], v)
|
||||
|
||||
def test_mtpi(self):
|
||||
# need an instance just for the constants, meh
|
||||
px = self.make_pdp()
|
||||
tvecs = (
|
||||
((0o1717,), (0o012746, 0o1717, 0o006637, 0o02,
|
||||
# turn MMU back off (!)
|
||||
0o005037, self.ioaddr(px, px.mmu.MMR0_OFFS),
|
||||
0o013700, 0o20002)),
|
||||
)
|
||||
for rslts, insts in tvecs:
|
||||
with self.subTest(rslts=rslts, insts=insts):
|
||||
p, pc = self.simplemapped_pdp(addons=insts)
|
||||
p.run(pc=pc)
|
||||
for rN, v in enumerate(rslts):
|
||||
self.assertEqual(p.r[rN], v)
|
||||
|
||||
def test_add_sub(self):
|
||||
p = self.make_pdp()
|
||||
|
||||
testvecs = (
|
||||
# (op0, op1, expected op0 + op1, nzvc, expected op0 - op1, nzvc)
|
||||
# None for nzvc means dont test that (yet/for-now/need to verify)
|
||||
(1, 1, 2, 0, 0, 4), # 1 + 1 = 2(_); 1 - 1 = 0(Z)
|
||||
(1, 32767, 32768, 0o12, 32766, 0),
|
||||
(0, 0, 0, 0o04, 0, 0o04),
|
||||
(32768, 1, 32769, 0o10, 32769, 0o13),
|
||||
(65535, 1, 0, 0o05, 2, 1),
|
||||
)
|
||||
|
||||
testloc = 0o10000
|
||||
add_loc = testloc
|
||||
sub_loc = testloc + 4
|
||||
|
||||
p.physmem[add_loc >> 1] = 0o060001 # ADD R0,R1
|
||||
p.physmem[(add_loc >> 1) + 1] = 0
|
||||
p.physmem[sub_loc >> 1] = 0o160001 # SUB R0,R1
|
||||
p.physmem[(sub_loc >> 1) + 1] = 0
|
||||
|
||||
for r0, r1, added, a_nzvc, subbed, s_nzvc in testvecs:
|
||||
with self.subTest(r0=r0, r1=r1, op="add"):
|
||||
p.r[0] = r0
|
||||
p.r[1] = r1
|
||||
p.run(pc=add_loc)
|
||||
self.assertEqual(p.r[1], added)
|
||||
if a_nzvc is not None:
|
||||
self.assertEqual(p.psw & 0o17, a_nzvc)
|
||||
|
||||
with self.subTest(r0=r0, r1=r1, op="sub"):
|
||||
p.r[0] = r0
|
||||
p.r[1] = r1
|
||||
p.run(pc=sub_loc)
|
||||
self.assertEqual(p.r[1], subbed)
|
||||
if s_nzvc is not None:
|
||||
self.assertEqual(p.psw & 0o17, s_nzvc)
|
||||
|
||||
def test_bne(self):
|
||||
p = self.make_pdp()
|
||||
loopcount = 0o1000
|
||||
insts = (
|
||||
# Program is:
|
||||
# MOV loopcount,R1
|
||||
# CLR R0
|
||||
# LOOP: INC R0
|
||||
# DEC R1
|
||||
# BNE LOOP
|
||||
# HALT
|
||||
0o012701, loopcount, 0o005000, 0o005200, 0o005301, 0o001375, 0)
|
||||
|
||||
instloc = 0o4000
|
||||
self.loadphysmem(p, insts, instloc)
|
||||
|
||||
p.run(pc=instloc)
|
||||
self.assertEqual(p.r[0], loopcount)
|
||||
self.assertEqual(p.r[1], 0)
|
||||
|
||||
def test_cc(self):
|
||||
# various condition code tests
|
||||
p = self.make_pdp()
|
||||
insts = (
|
||||
# program is:
|
||||
# CLR R0
|
||||
# BEQ 1f
|
||||
# HALT
|
||||
# 1: CCC
|
||||
# BNE 1f
|
||||
# HALT
|
||||
# 1: DEC R0
|
||||
|
||||
# MOV @#05000,R1 ; see discussion below
|
||||
# MOV @#05002,R2 ; see discussion below
|
||||
# CMP R1,R2
|
||||
# BLE 1f
|
||||
# HALT
|
||||
# 1: DEC R0
|
||||
# CMP R2,R1
|
||||
# BGT 1f
|
||||
# HALT
|
||||
# 1: DEC R0
|
||||
# HALT
|
||||
#
|
||||
# and the program will poke various test cases into locations
|
||||
# 5000 and 5002, with the proviso that 5000 is always the lesser.
|
||||
#
|
||||
# Given that, after running the program R0 should be 65553
|
||||
|
||||
0o005000, 0o101401, 0o0, 0o000257, 0o001001, 0, 0o005300,
|
||||
|
||||
# MOV @#5000 etc
|
||||
0o013701, 0o5000, 0o013702, 0o5002,
|
||||
|
||||
# CMP R1,R2 BLE
|
||||
0o020102, 0o003401, 0, 0o005300,
|
||||
|
||||
# CMP R2,R1 BGT
|
||||
0o020201, 0o003001, 0, 0o005300,
|
||||
|
||||
0)
|
||||
|
||||
instloc = 0o4000
|
||||
self.loadphysmem(p, insts, instloc)
|
||||
|
||||
# just a convenience so the test data can use neg numbers
|
||||
def s2c(x):
|
||||
return x & 0o177777
|
||||
|
||||
for lower, higher in ((0, 1), (s2c(-1), 0), (s2c(-1), 1),
|
||||
(s2c(-32768), 32767),
|
||||
(s2c(-32768), 0), (s2c(-32768), 32767),
|
||||
(17, 42), (s2c(-42), s2c(-17))):
|
||||
p.physmem[0o5000 >> 1] = lower
|
||||
p.physmem[0o5002 >> 1] = higher
|
||||
with self.subTest(lower=lower, higher=higher):
|
||||
p.run(pc=instloc)
|
||||
self.assertEqual(p.r[0], 65533)
|
||||
|
||||
# probably never a good idea, but ... do some random values
|
||||
for randoms in range(1000):
|
||||
a = random.randint(-32768, 32767)
|
||||
b = random.randint(-32768, 32767)
|
||||
while a == b:
|
||||
b = random.randint(-32768, 32767)
|
||||
if a > b:
|
||||
a, b = b, a
|
||||
p.physmem[0o5000 >> 1] = s2c(a)
|
||||
p.physmem[0o5002 >> 1] = s2c(b)
|
||||
with self.subTest(lower=a, higher=b):
|
||||
p.run(pc=instloc)
|
||||
self.assertEqual(p.r[0], 65533)
|
||||
|
||||
def test_unscc(self):
|
||||
# more stuff like test_cc but specifically testing unsigned Bxx codes
|
||||
p = self.make_pdp()
|
||||
insts = (
|
||||
# program is:
|
||||
# CLR R0
|
||||
# MOV @#05000,R1 ; see discussion below
|
||||
# MOV @#05002,R2 ; see discussion below
|
||||
# CMP R1,R2
|
||||
# BCS 1f ; BCS same as BLO
|
||||
# HALT
|
||||
# 1: DEC R0
|
||||
# CMP R2,R1
|
||||
# BHI 1f
|
||||
# HALT
|
||||
# 1: DEC R0
|
||||
# HALT
|
||||
#
|
||||
# test values in 5000,5002 .. unsigned and 5002 always higher
|
||||
#
|
||||
# Given that, after running the program R0 should be 65534
|
||||
|
||||
0o005000,
|
||||
|
||||
# MOV @#5000 etc
|
||||
0o013701, 0o5000, 0o013702, 0o5002,
|
||||
|
||||
# CMP R1,R2 BCS
|
||||
0o020102, 0o103401, 0, 0o005300,
|
||||
|
||||
# CMP R2,R1 BHI
|
||||
0o020201, 0o101001, 0, 0o005300,
|
||||
|
||||
0)
|
||||
|
||||
instloc = 0o4000
|
||||
self.loadphysmem(p, insts, instloc)
|
||||
|
||||
for lower, higher in ((0, 1), (0, 65535), (32768, 65535),
|
||||
(65534, 65535),
|
||||
(32767, 32768),
|
||||
(17, 42)):
|
||||
p.physmem[0o5000 >> 1] = lower
|
||||
p.physmem[0o5002 >> 1] = higher
|
||||
with self.subTest(lower=lower, higher=higher):
|
||||
p.run(pc=instloc)
|
||||
self.assertEqual(p.r[0], 65534)
|
||||
|
||||
# probably never a good idea, but ... do some random values
|
||||
for randoms in range(1000):
|
||||
a = random.randint(0, 65535)
|
||||
b = random.randint(0, 65535)
|
||||
while a == b:
|
||||
b = random.randint(0, 65535)
|
||||
if a > b:
|
||||
a, b = b, a
|
||||
p.physmem[0o5000 >> 1] = a
|
||||
p.physmem[0o5002 >> 1] = b
|
||||
with self.subTest(lower=a, higher=b):
|
||||
p.run(pc=instloc)
|
||||
self.assertEqual(p.r[0], 65534)
|
||||
|
||||
def test_ash1(self):
|
||||
# this code sequence taken from Unix startup, it's not really
|
||||
# much of a test.
|
||||
insts = (0o012702, 0o0122451, # mov #122451,R2
|
||||
0o072227, 0o0177772, # ash -6,R2
|
||||
0o042702, 0o0176000, # bic #0176000,R2
|
||||
0) # R2 should be 1224
|
||||
p = self.make_pdp()
|
||||
instloc = 0o4000
|
||||
self.loadphysmem(p, insts, instloc)
|
||||
p.run(pc=instloc)
|
||||
self.assertEqual(p.r[2], 0o1224)
|
||||
|
||||
def test_br(self):
|
||||
# though the bug has been fixed, this is a test of whether
|
||||
# all branch offset values work correctly. Barn door shut...
|
||||
p = self.make_pdp()
|
||||
|
||||
# the idea is a block of INC R0 instructions
|
||||
# followed by a halt, then a spot for a branch
|
||||
# then a block of INC R1 instructions followed by a halt
|
||||
#
|
||||
# By tweaking the BR instruction (different forward/back offsets)
|
||||
# and starting execution at the BR, the result on R0 and R1
|
||||
# will show if the correct branch offset was effected.
|
||||
#
|
||||
# NOTE: 0o477 (branch offset -1) is a tight-loop branch to self
|
||||
# and that case is tested separately.
|
||||
#
|
||||
insts = [0o5200] * 300 # 300 INC R0 instructions
|
||||
insts += [0] # 1 HALT instruction
|
||||
insts += [0o477] # BR instruction .. see below
|
||||
|
||||
# want to know where in memory this br will is
|
||||
brspot = len(insts) - 1
|
||||
|
||||
insts += [0o5201] * 300 # 300 INC R1 instructions
|
||||
insts += [0] # 1 HALT instruction
|
||||
|
||||
# put that mess into memory at an arbitrary spot
|
||||
baseloc = 0o10000
|
||||
for a, w in enumerate(insts, start=(baseloc >> 1)):
|
||||
p.physmem[a] = w
|
||||
|
||||
# test the negative offsets:
|
||||
# Set R0 to 65535 (-1)
|
||||
# Set R1 to 17
|
||||
# -1 is a special case, that's the tight loop and not tested here
|
||||
# -2 reaches the HALT instruction only, R0 will remain 65535
|
||||
# -3 reaches back to one INC R0, R0 will be 0
|
||||
# -4 reaches back two INC R0's, R0 will be 1
|
||||
# and so on
|
||||
|
||||
# 0o400 | offset starting at 0o376 will be the BR -2 case
|
||||
expected_R0 = 65535
|
||||
for offset in range(0o376, 0o200, -1):
|
||||
p.physmem[(baseloc >> 1) + brspot] = (0o400 | offset)
|
||||
p.r[0] = 65535
|
||||
p.r[1] = 17
|
||||
|
||||
# note the 2* because PC is an addr vs physmem word index
|
||||
p.run(pc=baseloc + (2*brspot))
|
||||
|
||||
with self.subTest(offset=offset):
|
||||
self.assertEqual(p.r[0], expected_R0)
|
||||
self.assertEqual(p.r[1], 17)
|
||||
expected_R0 = (expected_R0 + 1) & 0o177777
|
||||
|
||||
# and the same sort of test but with forward branching
|
||||
|
||||
expected_R1 = 42 + 300
|
||||
for offset in range(0, 0o200):
|
||||
p.physmem[(baseloc >> 1) + brspot] = (0o400 | offset)
|
||||
p.r[0] = 17
|
||||
p.r[1] = 42
|
||||
|
||||
# note the 2* because PC is an addr vs physmem word index
|
||||
p.run(pc=baseloc + (2*brspot))
|
||||
|
||||
with self.subTest(offset=offset):
|
||||
self.assertEqual(p.r[0], 17)
|
||||
self.assertEqual(p.r[1], expected_R1)
|
||||
expected_R1 = (expected_R1 - 1) & 0o177777
|
||||
|
||||
def test_trap(self):
|
||||
# test some traps
|
||||
|
||||
p = self.make_pdp()
|
||||
|
||||
# put a handlers for different traps into memory
|
||||
# starting at location 0o10000 (4K). This just knows
|
||||
# that each handler is 3 words long, the code being:
|
||||
# MOV something,R4
|
||||
# RTT
|
||||
#
|
||||
# where the "something" changes with each handler.
|
||||
handlers_addr = 0o10000
|
||||
handlers = (
|
||||
0o012704, 0o4444, 0o000006, # for vector 0o004
|
||||
0o012704, 0o1010, 0o000006, # for vector 0o010
|
||||
0o012704, 0o3030, 0o000006, # for vector 0o030
|
||||
0o012704, 0o3434, 0o000006 # for vector 0o034
|
||||
)
|
||||
self.loadphysmem(p, handlers, handlers_addr)
|
||||
|
||||
# and just jam the vectors in place
|
||||
p.physmem[2] = handlers_addr # vector 0o004
|
||||
p.physmem[3] = 0 # new PSW, stay in kernel mode
|
||||
p.physmem[4] = handlers_addr + 6 # each handler above was 6 bytes
|
||||
p.physmem[5] = 0
|
||||
p.physmem[12] = handlers_addr + 12 # vector 0o30 (EMT)
|
||||
p.physmem[13] = 0
|
||||
p.physmem[14] = handlers_addr + 18 # vector 0o34 (TRAP)
|
||||
p.physmem[15] = 0
|
||||
|
||||
# (tnum, insts)
|
||||
testvectors = (
|
||||
# this will reference an odd address, trap 4
|
||||
(0o4444, (
|
||||
# establish reasonable stack pointer (at 8K)
|
||||
0o012706, 0o20000,
|
||||
# CLR R3 and R4 so will know if they get set to something
|
||||
0o005003, 0o005004,
|
||||
# put 0o1001 into R0
|
||||
0o012700, 0o1001,
|
||||
# and reference it ... boom!
|
||||
0o011001,
|
||||
# show that the RTT got to here by putting magic into R3
|
||||
0o012703, 0o123456)),
|
||||
|
||||
# this will execute a reserved instruction trap 10
|
||||
(0o1010, (
|
||||
# establish reasonable stack pointer (at 8K)
|
||||
0o012706, 0o20000,
|
||||
# CLR R3 and R4 so will know if they get set to something
|
||||
0o005003, 0o005004,
|
||||
# 0o007777 is a reserved instruction ... boom!
|
||||
0o007777,
|
||||
# show that the RTT got to here by putting magic into R3
|
||||
0o012703, 0o123456)),
|
||||
|
||||
# this will execute an EMT instruction
|
||||
(0o3030, (
|
||||
# establish reasonable stack pointer (at 8K)
|
||||
0o012706, 0o20000,
|
||||
# CLR R3 and R4 so will know if they get set to something
|
||||
0o005003, 0o005004,
|
||||
# EMT #42
|
||||
0o104042,
|
||||
# show that the RTT got to here by putting magic into R3
|
||||
0o012703, 0o123456)),
|
||||
|
||||
# this will execute an actual TRAP instruction
|
||||
(0o3434, (
|
||||
# establish reasonable stack pointer (at 8K)
|
||||
0o012706, 0o20000,
|
||||
# CLR R3 and R4 so will know if they get set to something
|
||||
0o005003, 0o005004,
|
||||
# TRAP #17
|
||||
0o104417,
|
||||
# show that the RTT got to here by putting magic into R3
|
||||
0o012703, 0o123456)),
|
||||
)
|
||||
|
||||
for R4, insts in testvectors:
|
||||
self.loadphysmem(p, insts, 0o3000)
|
||||
p.run(pc=0o3000)
|
||||
self.assertEqual(p.r[3], 0o123456)
|
||||
self.assertEqual(p.r[4], R4)
|
||||
|
||||
def test_trapcodes(self):
|
||||
# a more ambitious testing of TRAP which verifies all
|
||||
# available TRAP instruction codes work
|
||||
|
||||
p = self.make_pdp()
|
||||
# poke the TRAP vector info directly in
|
||||
p.physmem[14] = 0o10000 # vector 0o34 (TRAP) --> 0o10000
|
||||
p.physmem[15] = 0
|
||||
|
||||
# this trap handler puts the trap # into R3
|
||||
handler = (
|
||||
# the saved PC is at the top of the stack ... get it
|
||||
0o011600, # MOV (SP),R0
|
||||
# get the low byte of the instruction which is the trap code
|
||||
# note that the PC points after the TRAP instruction so
|
||||
# MOVB -2(R0),R3
|
||||
0o116003, 0o177776,
|
||||
# RTT
|
||||
6)
|
||||
self.loadphysmem(p, handler, 0o10000)
|
||||
|
||||
# just bash a stack pointer directly in
|
||||
p.r[6] = 0o20000 # 8K and working down
|
||||
|
||||
for i in range(256):
|
||||
insts = (
|
||||
0o104400 | i, # TRAP #i
|
||||
0o010301, # MOV R3,R1 just to show RTT worked
|
||||
0)
|
||||
self.loadphysmem(p, insts, 0o30000)
|
||||
p.run(pc=0o30000)
|
||||
self.assertEqual(p.r[3], p.r[1])
|
||||
|
||||
# because the machine code did MOVB, values over 127 get
|
||||
# sign extended, so take that into consideration
|
||||
if i > 127:
|
||||
trapexpected = 0xFF00 | i
|
||||
else:
|
||||
trapexpected = i
|
||||
self.assertEqual(p.r[1], trapexpected)
|
||||
|
||||
# test_mmu_1 .. test_mmu_N .. a variety of MMU tests.
|
||||
#
|
||||
# Any of the other tests that use simplemapped_pdp() implicitly
|
||||
# test some aspects of the MMU but these are more targeted tests.
|
||||
# NOTE: it's a lot easier to test via the methods than via writing
|
||||
# elaborate PDP-11 machine code so that's what these do.
|
||||
|
||||
def test_mmu_1(self):
|
||||
# test the page length field support
|
||||
p = self.make_pdp()
|
||||
|
||||
# using ED=0 (segments grow upwards), create a (bizarre!)
|
||||
# user DSPACE mapping where the the first segment has length 0,
|
||||
# the second has 16, the third has 32 ... etc and then check
|
||||
# that that valid addresses map correctly and invalid ones fault
|
||||
# correctly. NOTE that there are subtle semantics to the so-called
|
||||
# "page length field" ... in a page that grows upwards, a plf of
|
||||
# zero means that to be INVALID the block number has to be greater
|
||||
# than zero (therefore "zero" length really means 64 bytes of
|
||||
# validity) and there is a similar off-by-one semantic to ED=1
|
||||
# downward pages. The test understands this.
|
||||
|
||||
cn = self.usefulconstants()
|
||||
for segno in range(8):
|
||||
p.mmu.wordRW(cn.UDSA0 + (segno*2), (8192 * segno) >> 6)
|
||||
pln = segno * 16
|
||||
p.mmu.wordRW(cn.UDSD0 + (segno*2), (pln << 8) | 0o06)
|
||||
|
||||
# enable user I/D separation
|
||||
p.mmu.MMR3 |= 0o01
|
||||
|
||||
# turn on the MMU!
|
||||
p.mmu.MMR0 = 1
|
||||
|
||||
for segno in range(8):
|
||||
basea = segno * 8192
|
||||
maxvalidoffset = 63 + ((segno * 64) * 16)
|
||||
for o in range(8192):
|
||||
if o <= maxvalidoffset:
|
||||
_ = p.mmu.v2p(basea + o, p.USER, p.mmu.DSPACE,
|
||||
p.mmu.CYCLE.READ)
|
||||
else:
|
||||
with self.assertRaises(PDPTraps.MMU):
|
||||
_ = p.mmu.v2p(basea + o, p.USER, p.mmu.DSPACE,
|
||||
p.mmu.CYCLE.READ)
|
||||
|
||||
def test_mmu_2(self):
|
||||
# same test as _1 but with ED=1 so segments grow downwards
|
||||
# test the page length field support
|
||||
p = self.make_pdp()
|
||||
|
||||
cn = self.usefulconstants()
|
||||
for segno in range(8):
|
||||
p.mmu.wordRW(cn.UDSA0 + (segno*2), (8192 * segno) >> 6)
|
||||
pln = 0o177 - (segno * 16)
|
||||
p.mmu.wordRW(cn.UDSD0 + (segno*2), (pln << 8) | 0o16)
|
||||
|
||||
# enable user I/D separation
|
||||
p.mmu.MMR3 |= 0o01
|
||||
|
||||
# turn on the MMU!
|
||||
p.mmu.MMR0 = 1
|
||||
|
||||
for segno in range(8):
|
||||
basea = segno * 8192
|
||||
minvalidoffset = 8192 - (64 + ((segno * 64) * 16))
|
||||
for o in range(8192):
|
||||
if o >= minvalidoffset:
|
||||
_ = p.mmu.v2p(basea + o, p.USER, p.mmu.DSPACE,
|
||||
p.mmu.CYCLE.READ)
|
||||
else:
|
||||
with self.assertRaises(PDPTraps.MMU):
|
||||
_ = p.mmu.v2p(basea + o, p.USER, p.mmu.DSPACE,
|
||||
p.mmu.CYCLE.READ)
|
||||
|
||||
def test_ubmap(self):
|
||||
p = self.make_pdp()
|
||||
|
||||
ubmaps = self.ioaddr(p, p.ub.UBMAP_OFFS)
|
||||
|
||||
# code paraphrased from UNIX startup, creates a mapping pattern
|
||||
# that the rest of the code expects (and fiddles upper bits)
|
||||
# So ... test that.
|
||||
for i in range(0, 62, 2):
|
||||
p.mmu.wordRW(ubmaps + (2 * i), i << 12 & 0o1777777)
|
||||
p.mmu.wordRW(ubmaps + (2 * (i + 1)), 0)
|
||||
|
||||
# XXX there is no real test yet because the UBMAPs
|
||||
# are all just dummied up right now
|
||||
|
||||
# this is not a unit test, invoke it using timeit etc
|
||||
def speed_test_setup(self, *, loopcount=10000, mmu=True, inst=None):
|
||||
|
||||
p, pc = self.simplemapped_pdp()
|
||||
|
||||
# the returned pdp is loaded with instructions for setting up
|
||||
# the mmu; only do them if that's what is wanted
|
||||
if mmu:
|
||||
p.run(pc=pc)
|
||||
|
||||
# by default the instruction being timed will be MOV R1,R0
|
||||
# but other instructions could be used. MUST ONLY BE ONE WORD
|
||||
if inst is None:
|
||||
inst = 0o010100
|
||||
|
||||
# now load the test timing loop... 9 MOV R1,R0 instructions
|
||||
# and an SOB for looping (so 10 instructions per loop)
|
||||
|
||||
insts = (0o012704, loopcount, # loopcount into R4
|
||||
inst,
|
||||
inst,
|
||||
inst,
|
||||
inst,
|
||||
inst,
|
||||
inst,
|
||||
inst,
|
||||
inst,
|
||||
inst,
|
||||
|
||||
0o077412, # SOB R4 back to first inst
|
||||
0) # HALT
|
||||
|
||||
instloc = 0o4000
|
||||
for a2, w in enumerate(insts):
|
||||
p.mmu.wordRW(instloc + (2 * a2), w)
|
||||
return p, instloc
|
||||
|
||||
def speed_test_run(self, p, instloc):
|
||||
p.run(pc=instloc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
70
pdptraps.py
Normal file
70
pdptraps.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
# 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.
|
||||
|
||||
# exceptions representing processor traps
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
class PDPTrap(Exception):
|
||||
vector = -1
|
||||
|
||||
def __init__(self, cpuerr=0, **kwargs):
|
||||
|
||||
# if specified, the cpuerr bit(s) will be OR'd into
|
||||
# the CPU Error Register when the trap is processed by go_trap
|
||||
self.cpuerr = cpuerr
|
||||
|
||||
# any additional arguments that are specific per-trap info
|
||||
# simply get stored as-is
|
||||
self.trapinfo = kwargs
|
||||
|
||||
def __str__(self):
|
||||
s = self.__class__.__name__ + "("
|
||||
s += f"vector={oct(self.vector)}"
|
||||
if self.cpuerr:
|
||||
s += f", cpuerr={oct(self.cpuerr)}"
|
||||
if self.trapinfo:
|
||||
s += f", {self.trapinfo=}"
|
||||
s += ")"
|
||||
return s
|
||||
|
||||
|
||||
# rather than copy/pasta the above class, they are made this way
|
||||
# It's not clear this is much better
|
||||
# XXX the for/setattr loop instead of a dict() in SimpleNamespace
|
||||
# only because it seems to be more readable this way
|
||||
|
||||
PDPTraps = SimpleNamespace()
|
||||
for __nm, __v in (
|
||||
('AddressError', 0o004),
|
||||
('ReservedInstruction', 0o010),
|
||||
('BPT', 0o014),
|
||||
('IOT', 0o20),
|
||||
('PowerFail', 0o24),
|
||||
('EMT', 0o30),
|
||||
('TRAP', 0o34),
|
||||
('Parity', 0o114),
|
||||
('PIRQ', 0o240),
|
||||
('FloatingPoint', 0o244),
|
||||
('MMU', 0o250)):
|
||||
setattr(PDPTraps, __nm, type(__nm, (PDPTrap,), dict(vector=__v)))
|
317
rp.py
Normal file
317
rp.py
Normal file
|
@ -0,0 +1,317 @@
|
|||
# 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.
|
||||
|
||||
# Emulate (a bare subset of) RP04..07 RM02-80 disks
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
class RPRM:
|
||||
RPADDR_OFFS = 0o16700
|
||||
|
||||
NSECT = 22 # sectors per track
|
||||
NTRAC = 19 # tracks per cylinder
|
||||
SECTOR_SIZE = 512
|
||||
|
||||
# NOTE: The key names become the attribute names. See __init__
|
||||
HPREG_OFFS = {
|
||||
'CS1': 0o00, # control and status register
|
||||
'WC': 0o02, # word count
|
||||
'UBA': 0o04, # UNIBUS address
|
||||
'DA': 0o06, # desired address
|
||||
'CS2': 0o10, # control/status register 2
|
||||
'DS': 0o12, # drive status
|
||||
'AS': 0o16, # unified attention status
|
||||
'RMLA': 0o20, # lookahead (sector under head!!)
|
||||
'OFR': 0o32, # heads offset -- seriously, boot program??
|
||||
'DC': 0o34, # desired cylinder
|
||||
'CC': 0o36, # "current cylinder" and/or holding register
|
||||
'BAE': 0o50, # address extension (pdp11/70 extra phys bits)
|
||||
}
|
||||
|
||||
HPDS_BITS = SimpleNamespace(
|
||||
OFM=0o000001, # offset mode
|
||||
VV=0o000100, # volume valid
|
||||
DRY=0o000200, # drive ready
|
||||
DPR=0o000400, # drive present
|
||||
MOL=0o010000, # medium online
|
||||
)
|
||||
|
||||
HPCS1_BITS = SimpleNamespace(
|
||||
GO=0o000001, # GO bit
|
||||
FN=0o000076, # 5 bit function code - this is the mask
|
||||
IE=0o000100, # Interrupt enable
|
||||
RDY=0o000200, # Drive ready
|
||||
A16=0o000400,
|
||||
A17=0o001000,
|
||||
TRE=0o040000,
|
||||
)
|
||||
|
||||
def __init__(self, ub, baseoffs=RPADDR_OFFS):
|
||||
self.addr = baseoffs
|
||||
self.ub = ub
|
||||
self.logger = ub.cpu.logger
|
||||
|
||||
self.command_history = [(0, tuple())] * 100
|
||||
# XXX needs to be configurable somehow
|
||||
self._diskimage = open('rp.disk', 'r+b')
|
||||
|
||||
for attr, offs in self.HPREG_OFFS.items():
|
||||
setattr(self, attr, 0)
|
||||
|
||||
# CS1 is a special case in several ways
|
||||
if attr == 'CS1':
|
||||
ub.mmio.register(self.rw_cs1, baseoffs+offs, 1,
|
||||
byte_writes=True, reset=True)
|
||||
else:
|
||||
# the rest are simple attributes; some as properties
|
||||
ub.mmio.register_simpleattr(self, attr, baseoffs+offs)
|
||||
|
||||
# XXX obviously this is just fake for now
|
||||
self.DS = (self.HPDS_BITS.DPR | self.HPDS_BITS.MOL |
|
||||
self.HPDS_BITS.VV | self.HPDS_BITS.DRY)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self._diskimage.close()
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
self._diskimage = None
|
||||
|
||||
# Pass __del__ up the inheritance tree, carefully.
|
||||
# Note that __del__ is not always defined, Because Reasons.
|
||||
getattr(super(), '__del__', lambda self: None)(self)
|
||||
|
||||
@property
|
||||
def UBA(self):
|
||||
return self._uba
|
||||
|
||||
@UBA.setter
|
||||
def UBA(self, value):
|
||||
self.logger.debug(f"UBA address being set to {oct(value)}")
|
||||
self._uba = value
|
||||
|
||||
@property
|
||||
def CS2(self):
|
||||
return self._cs2
|
||||
|
||||
@CS2.setter
|
||||
def CS2(self, value):
|
||||
self.logger.debug(f"CS2: value={oct(value)}")
|
||||
self._cs2 = value
|
||||
|
||||
@property
|
||||
def DS(self):
|
||||
return (self._ds | self.HPDS_BITS.DPR | self.HPDS_BITS.MOL |
|
||||
self.HPDS_BITS.VV | self.HPDS_BITS.DRY)
|
||||
|
||||
@DS.setter
|
||||
def DS(self, value):
|
||||
self._ds = value
|
||||
|
||||
@property
|
||||
def CS1(self):
|
||||
# XXX what if CS1 is just always RDY??
|
||||
self._cs1 |= self.HPCS1_BITS.RDY
|
||||
|
||||
# --- XXX DEBUGGING XXX ---
|
||||
if (self._cs1 & 0x4000):
|
||||
self.logger.debug(f"RP: XXX! CS1={oct(self._cs1)}")
|
||||
self.logger.debug(f"RP: reading CS1: {oct(self._cs1)}")
|
||||
return self._cs1
|
||||
|
||||
@CS1.setter
|
||||
def CS1(self, value):
|
||||
self.command_history.pop(-1)
|
||||
self.command_history.insert(0, (value, self.statestring()))
|
||||
|
||||
self.logger.debug(f"RP: writing CS1 to {oct(value)}; "
|
||||
f"state: {self.statestring()}")
|
||||
self._cs1 = value
|
||||
self.logger.debug(f"RP: CS1 set to {oct(self._cs1)}")
|
||||
if self._cs1 & 0x4000:
|
||||
self.logger.debug(f"LOOK!!!! XXX")
|
||||
if self._cs1 & self.HPCS1_BITS.RDY:
|
||||
self.AS = 1 # this is very bogus but maybe works for now
|
||||
|
||||
# TRE/ERROR always cleared on next op
|
||||
if value & self.HPCS1_BITS.GO:
|
||||
self._cs1 &= ~self.HPCS1_BITS.TRE
|
||||
|
||||
match value & self.HPCS1_BITS.FN, value & self.HPCS1_BITS.GO:
|
||||
case 0, go:
|
||||
self._cs1 &= ~go
|
||||
|
||||
case 0o06 | 0o12 | 0o16 | 0o20 | 0o22 as fcode, go:
|
||||
self.logger.debug(f"RP: operation {oct(fcode)} ignored.")
|
||||
self.logger.debug(self.statestring())
|
||||
self._cs1 &= ~(go | fcode)
|
||||
self._cs1 |= self.HPCS1_BITS.RDY
|
||||
if self._cs1 & self.HPCS1_BITS.IE:
|
||||
self.ub.intmgr.simple_irq(5, 0o254)
|
||||
|
||||
case 0o30, 1: # SEARCH
|
||||
self._cs1 &= ~0o31
|
||||
self._cs1 |= self.HPCS1_BITS.RDY
|
||||
self.CC = self.DC
|
||||
if self._cs1 & self.HPCS1_BITS.IE:
|
||||
self.ub.intmgr.simple_irq(5, 0o254)
|
||||
|
||||
case 0o60, 1:
|
||||
self._cs1 &= ~0o61
|
||||
self.writecmd()
|
||||
if self._cs1 & self.HPCS1_BITS.IE:
|
||||
self.ub.intmgr.simple_irq(5, 0o254)
|
||||
|
||||
case 0o70, 1:
|
||||
self._cs1 &= ~0o71
|
||||
self.readcmd()
|
||||
if self._cs1 & self.HPCS1_BITS.IE:
|
||||
self.ub.intmgr.simple_irq(5, 0o254)
|
||||
|
||||
case _, 0: # anything else without the go bit is a nop
|
||||
pass
|
||||
|
||||
case _: # but with the go bit, bail out for now
|
||||
raise ValueError(value)
|
||||
|
||||
# special function for handling writes to the CS1 attribute
|
||||
# Because byte writes to the upper byte need to be treated carefully
|
||||
def rw_cs1(self, addr, value=None, /, *, opsize=2):
|
||||
|
||||
if opsize == 1:
|
||||
# by definition byte reads are impossible; this will obviously
|
||||
# bomb out if they happen somehow (it is physically impossible
|
||||
# to have a byte write on the real UNIBUS)
|
||||
value &= 0o377 # paranoia but making sure
|
||||
self.logger.debug(f"RP: BYTE addr={oct(addr)}, "
|
||||
f"{value=}, _cs1={oct(self._cs1)}")
|
||||
self.logger.debug(self.statestring())
|
||||
if addr & 1:
|
||||
self._cs1 = (value << 8) | (self._cs1 & 0o377)
|
||||
else:
|
||||
self.CS1 = (self._cs1 & 0o177400) | value
|
||||
elif value is None:
|
||||
return self.CS1 # let property getter do its thing
|
||||
else:
|
||||
self.CS1 = value # let property setter do its thing
|
||||
return None
|
||||
|
||||
def _compute_offset(self):
|
||||
# cyl num, track num, sector num, which were written like this:
|
||||
# HPADDR->hpdc = cn;
|
||||
# HPADDR->hpda = (tn << 8) + sn;
|
||||
cn = self.DC
|
||||
tn = (self.DA >> 8) & 0o377
|
||||
sn = self.DA & 0o377
|
||||
|
||||
# each cylinder is NSECT*NTRAC sectors
|
||||
# each track is NSECT sectors
|
||||
offs = cn * (self.NSECT * self.NTRAC)
|
||||
offs += (tn * self.NSECT)
|
||||
offs += sn
|
||||
offs *= self.SECTOR_SIZE
|
||||
return offs
|
||||
|
||||
def readcmd(self):
|
||||
offs = self._compute_offset()
|
||||
self.logger.debug(f"RP READ: offs=0x{hex(offs)}, {self.WC=}")
|
||||
|
||||
addr = self._getphysaddr()
|
||||
self._diskimage.seek(offs)
|
||||
nw = (65536 - self.WC)
|
||||
sector = self._diskimage.read(nw*2)
|
||||
|
||||
# Note conversion: from little-endian on disk to native 0 .. 65535
|
||||
self.ub.cpu.physRW_N(addr, nw, self.__b2wgen(sector))
|
||||
self.WC = 0
|
||||
self.CS1 |= self.HPCS1_BITS.RDY
|
||||
|
||||
def writecmd(self):
|
||||
offs = self._compute_offset()
|
||||
self.logger.debug(f"RP WRITE: offs=0x{hex(offs)}, {self.WC=}")
|
||||
|
||||
addr = self._getphysaddr()
|
||||
self._diskimage.seek(offs)
|
||||
nw = (65536 - self.WC)
|
||||
|
||||
# Words in physmem are just python integers; they have to be
|
||||
# converted into a little-endian byte stream for disk...
|
||||
sector = bytes(self.__w2bgen(self.ub.cpu.physRW_N(addr, nw)))
|
||||
self._diskimage.write(sector)
|
||||
self.WC = 0
|
||||
self.CS1 |= self.HPCS1_BITS.RDY
|
||||
|
||||
def __b2wgen(self, b):
|
||||
"""Generate native python ints from sequence of little endian bytes"""
|
||||
g = iter(b)
|
||||
for lo in g:
|
||||
hi = next(g)
|
||||
yield lo + (hi << 8)
|
||||
|
||||
def __w2bgen(self, words):
|
||||
"""Generate little-endian bytes from sequence of python ints"""
|
||||
for w in words:
|
||||
yield w & 0o377
|
||||
yield (w >> 8) & 0o377
|
||||
|
||||
def _getphysaddr(self):
|
||||
# low 16 bits in UBA, and tack on A16/A17
|
||||
A16 = bool(self.CS1 & self.HPCS1_BITS.A16)
|
||||
A17 = bool(self.CS1 & self.HPCS1_BITS.A17)
|
||||
|
||||
# but also bits may be found in bae... the assumption here is
|
||||
# if these bits are non-zero they override A16/A17 but they
|
||||
# really need to be consistent...
|
||||
if self.BAE == 0:
|
||||
A1621 = 0
|
||||
else:
|
||||
A16 = 0 # subsumed in A1621
|
||||
A17 = 0 # subsumed
|
||||
A1621 = self.BAE & 0o77
|
||||
|
||||
phys = self.UBA | (A16 << 16) | (A17 << 17) | (A1621 << 16)
|
||||
self.logger.debug(f"RP: physaddr={oct(phys)}")
|
||||
return phys
|
||||
# return self.UBA | (A16 << 16) | (A17 << 17) | (A1621 << 16)
|
||||
|
||||
def statestring(self):
|
||||
s = "RP XXX:"
|
||||
for attr in self.HPREG_OFFS:
|
||||
s += f"{attr}={oct(getattr(self, attr, 0))} "
|
||||
return s
|
||||
|
||||
# produce a pretty-print version of a single RP history
|
||||
@staticmethod
|
||||
def rph_pps(rph):
|
||||
written = rph[0]
|
||||
s = f"CS1 <-- {oct(written)} : "
|
||||
cmd = written & 0o70
|
||||
s += {0o70: 'READ', 0o60: 'WRITE', 0o30: 'SEARCH'}.get(cmd, oct(cmd))
|
||||
if rph[0] & 1:
|
||||
s += "|GO"
|
||||
if written & 0o100:
|
||||
s += "|IE"
|
||||
if written & 0o040000:
|
||||
s += "|TRE"
|
||||
s += f"\n {rph[1]}"
|
||||
return s
|
74
unibus.py
Normal file
74
unibus.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
# 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 interrupts import InterruptManager
|
||||
from mmio import MMIO
|
||||
|
||||
# A convenient reference for addresses:
|
||||
# https://gunkies.org/wiki/UNIBUS_Device_Addresses
|
||||
|
||||
|
||||
class UNIBUS:
|
||||
def __init__(self, cpu):
|
||||
self.cpu = cpu
|
||||
self.mmio = MMIO(cpu)
|
||||
self.intmgr = InterruptManager()
|
||||
|
||||
def resetbus(self):
|
||||
self.mmio.resetdevices()
|
||||
|
||||
|
||||
class UNIBUS_1170(UNIBUS):
|
||||
UBMAP_OFFS = 0o10200
|
||||
UBMAP_N = 62
|
||||
|
||||
def __init__(self, cpu):
|
||||
super().__init__(cpu)
|
||||
|
||||
# UBAs being 32-bit (well, really 22 bit) values, they
|
||||
# are just stored natively that way and broken down
|
||||
# into 16-bit components by the mmio function as needed.
|
||||
self.ubas = [0] * (self.UBMAP_N // 2)
|
||||
self.mmio.register(self.uba_mmio, self.UBMAP_OFFS, self.UBMAP_N)
|
||||
|
||||
def uba_mmio(self, addr, value=None, /):
|
||||
ubanum, hi22 = divmod(addr - self.UBMAP_OFFS, 4)
|
||||
uba22 = self.ubas[ubanum]
|
||||
|
||||
self.cpu.logger.debug(f"UBA addr={oct(addr)}, {value=}")
|
||||
self.cpu.logger.debug(f"{ubanum=}, {hi22=}")
|
||||
|
||||
if value is None:
|
||||
if hi22 == 0:
|
||||
return (uba22 >> 16) & 0o077
|
||||
else:
|
||||
return uba22 & 0o177777
|
||||
else:
|
||||
# the low bit is enforced to be zero
|
||||
if hi22:
|
||||
uba22 = ((value & 0o077) << 16) | (uba22 & 0o177776)
|
||||
else:
|
||||
uba22 = (uba22 & 0o17600000) | (value & 0o177776)
|
||||
self.ubas[ubanum] = uba22
|
||||
|
||||
def busRW(self, ubaddr, value=None):
|
||||
pass
|
Loading…
Add table
Reference in a new issue