192 lines
6.6 KiB
Python
192 lines
6.6 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.
|
|
|
|
# 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
|