# 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