305 lines
13 KiB
Python
305 lines
13 KiB
Python
# MIT License
|
|
#
|
|
# Copyright (c) 2023 Neil Webber
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
from enum import Enum
|
|
from interrupts import InterruptManager
|
|
from pdptraps import PDPTraps, PDPTrap
|
|
|
|
|
|
BusCycle = Enum('BusCycle', ('READ16', 'WRITE16', 'WRITE8', 'RESET'))
|
|
BusWrites = {BusCycle.WRITE16, BusCycle.WRITE8} # convenience
|
|
|
|
|
|
class UNIBUS:
|
|
|
|
def __init__(self, cpu):
|
|
self.cpu = cpu
|
|
self.intmgr = InterruptManager()
|
|
self.logger = cpu.logger
|
|
self.mmiomap = [self.__nodev] * (self.cpu.IOPAGE_SIZE >> 1)
|
|
|
|
def resetbus(self):
|
|
# this isn't especially efficient but it really doesn't matter.
|
|
# Construct a sequence of tuples: (a, f)
|
|
# where a is the address (really offset) in the I/O page
|
|
# and f is the callback
|
|
# but only for callbacks that are not __nodev
|
|
# and then invoke those callbacks w/BusCycle.RESET
|
|
resets = ((x << 1, f)
|
|
for x, f in enumerate(self.mmiomap) if f != self.__nodev)
|
|
for ioaddr, f in resets:
|
|
f(ioaddr, BusCycle.RESET)
|
|
|
|
# register() -- connect a callback function to an I/O page address
|
|
#
|
|
# A convenient reference for addresses:
|
|
# https://gunkies.org/wiki/UNIBUS_Device_Addresses
|
|
#
|
|
# 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.
|
|
#
|
|
# Callbacks must be declared like this:
|
|
# def f(ioaddr, cycle, /, *, value=None):
|
|
#
|
|
# and should contain code dispatching on cycle and (potentially) ioaddr.
|
|
# If cycle is WRITE8 (byte) or WRITE16 (word), the 'value' argument will
|
|
# be supplied. Otherwise 'value' is not supplied.
|
|
#
|
|
# If cycle is READ16 (word), the read value should be returned. Note that
|
|
# no READ8 exists as the UNIBUS is not capable of expressing a byte read.
|
|
#
|
|
# The processor RESET instruction generates calls with a RESET cycle.
|
|
#
|
|
# The ioaddr will always be relative to the base of the I/O page:
|
|
# (ioaddr & 8191) == ioaddr will always be True
|
|
#
|
|
# If a device's semantics are simple enough to allow synthesizing a
|
|
# WRITE8 as a READ16 / byte-update / WRITE16 sequence it can use
|
|
# the autobyte wrapper and omit a WRITE8 cycle handler. If the underlying
|
|
# address is write-only the autobyte wrapper substitutes a zero value
|
|
# for the READ16 part of the synthesis. If this is not correct, the
|
|
# callback must handle WRITE8 itself directly.
|
|
#
|
|
# 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 -- though
|
|
# as it turns out only very few models allow **CPU** access to cpu
|
|
# registers via the Unibus (vs allowing other devices, e.g., the console
|
|
# switches/display, to access them that way).
|
|
#
|
|
# There are a lot of subtleties to take into account accessing I/O
|
|
# addresses via bytes (and possibly-odd addresses) but so be it.
|
|
# Such byte operations in I/O space abound in real world code.
|
|
#
|
|
# 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.
|
|
#
|
|
|
|
def register(self, iofunc, offsetaddr, nwords=1, /):
|
|
"""register a callback for an I/O address.
|
|
|
|
Arguments:
|
|
iofunc -- callback function. See documentation for signature.
|
|
offsetaddr -- Offset within the 8K I/O page.
|
|
nwords -- Optional. Default=1. Number of words to span.
|
|
|
|
iofunc may be None in which case a dummy is supplied that ignores
|
|
all write operations and always reads as zero.
|
|
"""
|
|
|
|
if offsetaddr >= self.cpu.IOPAGE_SIZE:
|
|
raise ValueError(f"UNIBUS: offset too large {oct(offsetaddr)}")
|
|
|
|
# None is a shorthand for "this is a dummy always-zero addr"
|
|
if iofunc is None:
|
|
iofunc = self.autobyte(self.__ignoredev)
|
|
|
|
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")
|
|
|
|
for i in range(nwords):
|
|
self.mmiomap[idx+i] = iofunc
|
|
|
|
# this is a convenience routine for devices that want to signal things
|
|
# such as a write to a read-only register (if they don't simply just
|
|
# ignore them). Devices can, of course, just raise the traps themselves.
|
|
def illegal_cycle(self, addr, /, *, cycle=BusCycle.WRITE16, msg=None):
|
|
if msg is None:
|
|
msg =f"Illegal cycle ({cycle}) at {oct(addr)}"
|
|
self.cpu.logger.info(msg)
|
|
raise PDPTraps.AddressError(cpuerr=self.cpu.CPUERR_BITS.UNIBUS_TIMEOUT)
|
|
|
|
# the default entry for unoccupied I/O: cause an AddressError trap
|
|
def __nodev(self, addr, cycle, /, *, value=None):
|
|
self.illegal_cycle(addr, cycle=cycle,
|
|
msg=f"Non-existent I/O @ offset {oct(addr)}")
|
|
|
|
# Devices may have simple "dummy" I/O addresses that always read zero
|
|
# and ignore writes; See "if iofunc is None" in register() method.
|
|
# NOTE: register() byteme-wraps this so opsize not needed.
|
|
def __ignoredev(self, addr, cycle, /, *, value=None):
|
|
self.cpu.logger.debug(f"dummy zero device @ {oct(addr)}, {value=}")
|
|
return 0
|
|
|
|
# wrap an I/O function with code that automatically handles byte writes.
|
|
# CAUTION: Some devices may have semantics that make this ill-advised.
|
|
# Such devices should handle WRITE8 explicitly themselves.
|
|
# This is essentially a decorator but typically is invoked explicitly
|
|
# rather than by '@' syntax.
|
|
def autobyte(self, iofunc):
|
|
def byteme(ioaddr, cycle, /, **kwargs):
|
|
if cycle != BusCycle.WRITE8:
|
|
return iofunc(ioaddr, cycle, **kwargs)
|
|
|
|
# A byte write to I/O space. Synthesize it.
|
|
# 'value' is present (by definition) in kwargs
|
|
value = kwargs['value']
|
|
self.cpu.logger.debug(f"Byte write to {oct(ioaddr)} {value=}")
|
|
try:
|
|
wv = iofunc(ioaddr & 0o177776, BusCycle.READ16)
|
|
except PDPTrap: # this can happen on write-only registers
|
|
wv = 0
|
|
if ioaddr & 1:
|
|
wv = (wv & 0o377) | (value << 8)
|
|
else:
|
|
wv = (wv & 0o177400) | value
|
|
iofunc(ioaddr & 0o177776, BusCycle.WRITE16, value=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
|
|
#
|
|
# BusCycle.RESET handling: If a device wants RESET to zero the attribute,
|
|
# it should specify reset=True. Otherwise RESET will be ignored.
|
|
#
|
|
# BusCycle.WRITE8 handling: autobyte() is used. If those semantics are not
|
|
# suitable, the device must create its own i/o func instead of this.
|
|
#
|
|
# readonly: If True (default is False) then writes to this address will
|
|
# cause an AddressError trap. The device implementation can
|
|
# safely (if it wants to) implement the attribute directly
|
|
# (i.e., without using @property) and no write to the attribute
|
|
# will ever originate from here.
|
|
#
|
|
def register_simpleattr(self, obj, attrname, addr, /, *,
|
|
reset=False, readonly=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.
|
|
|
|
reset - if True, attrname will be set to zero on BusCycle.RESET
|
|
readonly - if True, attrname is readonly and will never be written.
|
|
Write cycles to the addr will cause AddressError.
|
|
"""
|
|
|
|
# could do this with partial, but can also do it with this nested
|
|
# func def. One way or another need this func logic anyway.
|
|
|
|
if attrname is None:
|
|
def _rwattr(_, cycle, /, *, value=None):
|
|
if cycle == BusCycle.READ16:
|
|
return 0
|
|
if cycle in BusWrites and readonly:
|
|
self.illegal_cycle(addr, cycle)
|
|
# all other operations just silently ignored
|
|
else:
|
|
def _rwattr(_, cycle, /, *, value=None):
|
|
if cycle == BusCycle.READ16:
|
|
return getattr(obj, attrname)
|
|
elif cycle in BusWrites:
|
|
if readonly:
|
|
self.illegal_cycle(addr, cycle)
|
|
else: # autobyte assumed ... see register() below
|
|
setattr(obj, attrname, value)
|
|
elif cycle == BusCycle.RESET:
|
|
if reset:
|
|
setattr(obj, attrname, 0)
|
|
else:
|
|
assert False, f"Unknown {cycle=} in simpleattr"
|
|
|
|
# NOTES:
|
|
# * it's a new defn/closure of _rwattr each time through, so the
|
|
# individual (per registration) addr/etc values are closure'd
|
|
# * Do not autobyte if readonly, simply so that the correct
|
|
# (unmolested) BusCycle.WRITE8 will be seen in the trap/errors
|
|
# * _rwattr ASSUMES autobyte() wrapper if not readonly
|
|
if readonly:
|
|
self.register(_rwattr, addr)
|
|
else:
|
|
self.register(self.autobyte(_rwattr), addr)
|
|
|
|
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, BusCycle.READ16)
|
|
else:
|
|
self.mmiomap[ioaddr >> 1](ioaddr, BusCycle.WRITE16, value=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.mmiomap[ioaddr >> 1](ioaddr, BusCycle.WRITE8, value=value)
|
|
return None
|
|
|
|
|
|
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.register(
|
|
self.autobyte(self.uba_mmio), self.UBMAP_OFFS, self.UBMAP_N)
|
|
|
|
def uba_mmio(self, addr, cycle, /, *, value=None):
|
|
ubanum, hi22 = divmod(addr - self.UBMAP_OFFS, 4)
|
|
uba22 = self.ubas[ubanum]
|
|
|
|
self.logger.debug(f"UBA addr={oct(addr)}, {value=}")
|
|
self.logger.debug(f"{ubanum=}, {hi22=}")
|
|
|
|
if cycle == BusCycle.READ16:
|
|
if hi22 == 0:
|
|
return (uba22 >> 16) & 0o077
|
|
else:
|
|
return uba22 & 0o177777
|
|
elif cycle == BusCycle.WRITE16:
|
|
# 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
|
|
elif cycle == BusCycle.RESET:
|
|
pass
|
|
|
|
def busRW(self, ubaddr, value=None):
|
|
pass
|