290 lines
12 KiB
Python
290 lines
12 KiB
Python
# MIT License
|
|
#
|
|
# Copyright (c) 2023 Neil Webber
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
from pdptraps import PDPTraps
|
|
|
|
|
|
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
|
|
#
|
|
# Zero, of course, is a possible and common value to write, so do not make
|
|
# the rookie mistake of testing value for truthiness; test for *is* None.
|
|
#
|
|
# 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_register(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 to register(), which will cause it 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
|
|
|
|
# wrap an I/O function with code that automatically handles byte writes.
|
|
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.mmiomap[ioaddr >> 1](ioaddr, value, opsize=1)
|
|
return None
|