Files
ILDEC/python/ildec.py
T
2026-06-29 20:09:40 +03:00

212 lines
8.0 KiB
Python
Executable File

#!/usr/bin/env python3
# ILDEC: a minimalistic DEC PDP-8/L emulation core in (Micro)Python
# See README.md for the usage guide
# Created by Luxferre in 2026, released into the public domain
import sys
try:
import select
except ImportError:
select = None
from array import array
try: # T-DeckARD workaround
from deck.input import input
from repl.tdeck_repl import Pydos_ui
def default_keyboard_check(pdp8):
if pdp8.kbd_delay <= 0:
pdp8.kbd_delay = 1000
if Pydos_ui.serial_bytes_available():
c = Pydos_ui.read_keyboard(1)
b = ord(c) if c else None
if b == 4 or c == "":
pdp8.halted = True
return
if b is not None:
pdp8.kbb, pdp8.kbf, pdp8.kbd_delay = (13 if b == 10 else b) | 0x80, 1, 500
except:
def default_keyboard_check(pdp8):
try:
if pdp8.kbd_delay <= 0:
pdp8.kbd_delay = 1000
try:
import supervisor
has_bytes = supervisor.runtime.serial_bytes_available
except (ImportError, AttributeError):
has_bytes = pdp8.poller.poll(0) if pdp8.poller else (select.select([sys.stdin], [], [], 0)[0] if select else False)
if has_bytes:
try:
import os; b, c = os.read(0, 1)[0], None
except Exception:
c = sys.stdin.read(1)
b = ord(c) if c else None
if b == 4 or c == "":
pdp8.halted = True
return
if b is not None:
pdp8.kbb, pdp8.kbf, pdp8.kbd_delay = (13 if b == 10 else b) | 0x80, 1, 500
except Exception: pass
def set_cbreak(enable):
try:
import termios
fd = sys.stdin.fileno()
if enable:
import tty; tty.setcbreak(fd)
else:
attr = termios.tcgetattr(fd)
attr[3] |= (termios.ICANON | termios.ECHO)
termios.tcsetattr(fd, termios.TCSADRAIN, attr)
except Exception: pass
def char_output(c):
sys.stdout.write(chr(c & 0x7F))
if hasattr(sys.stdout, 'flush'): sys.stdout.flush()
class ILDEC_PDP8:
def __init__(self, init_pc=0, init_sr=0, keyboard_check = None, outchar_cb = None):
self.memory = array('H', [0] * 4096)
self.kbdcheck, self.outc = keyboard_check or default_keyboard_check, outchar_cb or char_output
self.reset(init_pc, init_sr)
self.poller = None
try:
import supervisor
except ImportError:
try:
if select:
self.poller = select.poll()
self.poller.register(sys.stdin, select.POLLIN)
except Exception: pass
def reset(self, pcval=0, srval=0):
self.link = self.ac = self.kbb = self.kbf = self.chb = self.chf = self.kbd_delay = 0
self.ie = self.ion_delay = 0
self.pc, self.sr, self.halted = pcval, srval, False
def start(self, start_addr=0o200):
self.pc, self.halted = start_addr, False
set_cbreak(True)
try:
while not self.halted: self.step()
finally: set_cbreak(False)
def rotate_ac_link(self, dir, times):
for _ in range(times):
if dir > 0: self.ac, self.link = (self.ac >> 1) | (self.link << 11), self.ac & 1
else: self.ac, self.link = ((self.ac << 1) | self.link) & 0xFFF, self.ac >> 11
def step(self):
if self.halted: return
if not self.kbf: self.kbdcheck(self)
if self.halted: return
if self.kbd_delay > 0: self.kbd_delay -= 1
if self.ion_delay > 0:
self.ion_delay -= 1
if self.ion_delay == 0: self.ie = 1
if self.ie and (self.kbf or self.chf):
self.ie, self.memory[0], self.pc = 0, self.pc, 1
instruction, current_pc = self.memory[self.pc], self.pc
self.pc = (current_pc + 1) & 0xFFF
opcode, rest = (instruction >> 9) & 7, instruction & 511
if opcode < 6:
target = (current_pc & 0xF80 if rest & 0x80 else 0) | (rest & 0x7F)
if rest & 0x100:
if 8 <= target <= 15: self.memory[target] = (self.memory[target] + 1) & 0xFFF
target = self.memory[target]
if opcode == 0: self.ac &= self.memory[target]
elif opcode == 1:
self.ac, self.link = (self.ac + self.memory[target]) & 0xFFF, self.link ^ ((self.ac + self.memory[target]) >> 12)
elif opcode == 2:
self.memory[target] = (self.memory[target] + 1) & 0xFFF
if not self.memory[target]: self.pc = (self.pc + 1) & 0xFFF
elif opcode == 3: self.memory[target], self.ac = self.ac, 0
elif opcode == 4: self.memory[target], self.pc = self.pc, (target + 1) & 0xFFF
elif opcode == 5: self.pc = target & 0xFFF
elif opcode == 6: self.execute_iot(rest)
elif opcode == 7: self.execute_opr(rest)
def execute_opr(self, opr):
if opr in (0, 0o401): return
group = 1 if not (opr & 0x100) else (3 if (opr & 1) else 2)
b10 = (opr >> 1) & 1
if group == 1:
if opr & 0x80: self.ac = 0
if opr & 0x40: self.link = 0
if opr & 0x20: self.ac ^= 0xFFF
if opr & 0x10: self.link ^= 1
if opr & 1: self.ac, self.link = (self.ac + 1) & 0xFFF, self.link ^ ((self.ac + 1) >> 12)
if opr & 0x08: self.rotate_ac_link(1, b10 + 1)
if opr & 0x04: self.rotate_ac_link(-1, b10 + 1)
if b10 and not (opr & 0x0C): self.ac = ((self.ac << 6) | (self.ac >> 6)) & 0xFFF
elif group == 2:
if bool(((opr & 0x40) and self.ac > 0x7FF) or ((opr & 0x20) and not self.ac) or ((opr & 0x10) and self.link)) != bool(opr & 0x08): self.pc = (self.pc + 1) & 0xFFF
if opr & 0x80: self.ac = 0
if opr & 0x04: self.ac |= self.sr
if b10: self.halted = True
def execute_iot(self, cmd):
dev, op = (cmd >> 3) & 63, cmd & 7
if dev == 0:
if op == 1: self.ion_delay = 2
elif op == 2: self.ie = self.ion_delay = 0
elif op == 3 and (self.kbf or self.chf): self.pc = (self.pc + 1) & 0xFFF
elif op == 4: self.ac = (self.link << 11) | ((1 if (self.kbf or self.chf) else 0) << 9) | (self.ie << 7)
elif op == 5: self.link, self.ion_delay = (self.ac >> 11) & 1, 2
elif op == 7:
self.ac = self.link = self.kbf = self.kbb = self.chf = self.chb = self.ie = self.ion_delay = 0
elif dev == 3 and op != 5:
if (op & 1) and self.kbf: self.pc = (self.pc + 1) & 0xFFF
if op & 2: self.ac = self.kbf = 0
if op & 4: self.ac |= self.kbb
elif dev == 4 and op != 5:
if op == 0: self.chf = 1
else:
if (op & 1) and self.chf: self.pc = (self.pc + 1) & 0xFFF
if op & 2: self.chf = 0
if op & 4: self.chb, self.chf = self.ac, 1; self.outc(self.ac)
def load_oct(self, filepath):
with open(filepath, 'r') as f:
for line in f:
try:
p = line.split(':')
self.memory[int(p[0], 8) & 0xFFF] = int(p[1].strip().split()[0], 8)
except Exception: pass
# entry point (for ildec.py to be usable as a module)
def ildec(fname='', init_pc=0o200, init_sr=0):
print('\n ===== ILDEC PDP-8/L ONLINE =====')
emu = ILDEC_PDP8(init_pc, init_sr)
if fname: emu.load_oct(fname)
while True:
print(f"\nPC: {emu.pc:04o} | L AC: {emu.link} {emu.ac:04o} | SR: {emu.sr:04o}\n")
cmd = input('> ').strip().lower()
if cmd == 'q': break
elif cmd == 'r': emu.reset(init_pc, init_sr)
elif cmd == 'n': emu.__init__(init_pc, init_sr), print('All memory cleared!')
elif cmd == 'c': emu.start(emu.pc)
elif cmd == 'l': emu.pc = emu.sr
elif cmd == 's': emu.step()
elif cmd in ('e', 'd'):
if cmd == 'd': emu.memory[emu.pc] = emu.sr
print(f"{emu.pc:04o}: {emu.memory[emu.pc]:04o}")
emu.pc = (emu.pc + 1) & 0xFFF
elif cmd == 'i':
fname = input('File path: ').strip()
if fname: emu.load_oct(fname); print(f'Memory imported from {fname}')
elif cmd == 'x':
fname = input('File path: ').strip()
if fname:
with open(fname, 'w') as f:
for i, val in enumerate(emu.memory):
if val: f.write(f"{i:04o}: {val:04o}\n")
print(f'Memory exported to {fname}')
else:
try: emu.sr = int(cmd, 8)
except Exception: print('Invalid action!')
# Run it standalone
if __name__ == '__main__':
a = sys.argv if hasattr(sys, 'argv') else []
ildec(a[1] if len(a) > 1 else '', int(a[2], 8) if len(a) > 2 else 0o200, int(a[3], 8) if len(a) > 3 else 0)