python-pdp1170/kl11.py
2023-09-04 12:49:58 -05:00

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