#!/usr/bin/env python3 # testStrap.py - the $DF00 strap option, end to end. # # python3 testStrap.py # # A real SwiftLink has a jumper that moves the 6551 from I/O1 ($DE00) to I/O2 ($DF00), and some # Ultimate configurations default to the second setting. The driver probes $DE00 first and falls # back to $DF00, but every run before this one answered at $DE00, so the fallback had never executed. # # This script starts VICE with the cartridge at $DF00 only and watches BOTH pages at once: # # * $DE00-$DE03 - where the probe looks first. With nothing mapped there VICE returns open bus, # so the write of the probe pattern and the read that fails to match should both show up here, # and nothing else ever should. # * $DF00-$DF03 - the real registers. Every ACIA access the driver makes has to land here. # * $DE04-$DEFF and $DF04-$DFFF - the register mirrors. A real SwiftLink decodes only A0 and A1 # inside its page, so all 64 four-byte windows of the page are the same four registers; VICE maps # only the first four bytes, which makes these ranges a clean tripwire: a stop anywhere in them is # an instruction reaching into the cartridge page that has no business being there, and on real # hardware would be a register access. The stock driver's 'sta $DF59,y' is exactly that case. # # It then reads back the six self-modified accessors at $E551-$E56E, which are the driver's whole # record of where the hardware is. import os import re import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from viceHarness import ViceSession, aciaArgs, readByte, ACIA_BASE, SCRATCH from testModemSelect import SerialSink, collectHits SHOTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots") LOGS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testLogs") # The six one-instruction ACIA accessors. Each is a three-byte absolute instruction whose operand # high byte aciaSetPage patches; the low byte says which register it is. ACCESSORS = [(0xE554, "aciaCmdWrite sta $xx02 command"), (0xE558, "aciaStatusRead lda $xx01 status"), (0xE55F, "aciaDataRead lda $xx00 receive"), (0xE563, "aciaDataWrite sta $xx00 transmit"), (0xE567, "aciaCtrlWrite sta $xx03 control"), (0xE56B, "aciaCtrlRead lda $xx03 control read-back / the probe")] STOP = re.compile(r"#(\d+)\s+\(Stop on\s+(\w+)\s+([0-9a-f]{4})\)") DISASM = re.compile(r"\.C:([0-9a-f]{4})\s+((?:[0-9A-F]{2} )+)\s*(\S+)\s+(\S*)\s*-\s*A:([0-9A-Fa-f]{2})") def describe(blob): # Turn the monitor's watchpoint stops into an ordered list of "who touched what, with what". out = [] lines = blob.splitlines() for index, line in enumerate(lines): stop = STOP.search(line) if not stop: continue detail = "" for follow in lines[index:index + 4]: hit = DISASM.search(follow) if hit: detail = (f"{hit.group(3)} {hit.group(4)} at ${hit.group(1)} " f"(A=${hit.group(5).upper()})") break out.append((int(stop.group(1)), stop.group(2), int(stop.group(3), 16), detail)) return out def readAccessors(session): # Each accessor is one three-byte absolute instruction, so the operand high byte - the only place # the driver records which page the cartridge is on - is at the instruction address plus two. values = {} for addr, name in ACCESSORS: values[addr] = (name, readByte(session, addr + 2)) return values def showAccessors(values, note): print(f"--- the six ACIA accessors' operand high bytes ({note}) ---", flush=True) for addr, name in ACCESSORS: _, value = values[addr] page = "??" if value is None else f"${value:02X}00" print(f" ${addr:04X} {name:42s} high byte at ${addr + 2:04X} = " f"${'??' if value is None else '%02X' % value} -> page {page}", flush=True) def main(): disk = os.path.abspath(sys.argv[1]) prefix = sys.argv[2] if len(sys.argv) > 2 else "strap" os.makedirs(SHOTS, exist_ok=True) os.makedirs(LOGS, exist_ok=True) sink = SerialSink() print(f"serial sink listening on 127.0.0.1:{sink.port}", flush=True) args = aciaArgs(rsDevAddress=f"127.0.0.1:{sink.port}", baud=2400) print(f"acia base for this run: ${ACIA_BASE:04X}", flush=True) print("acia args:", args, flush=True) session = ViceSession(disk, f"{SCRATCH}/{prefix}.vice.log", args, label=prefix) try: session.connect() session.bootPastLoader() session.findWindow() session.focus() time.sleep(8) session.shot(f"{SHOTS}/{prefix}01menu.png") session.enterMonitor() row = readByte(session, 0x91D5) print(f"menu row before input: {row}", flush=True) session.sock.sendall(b"x\n") session.recv(3) for _ in range(3): session.enterMonitor() row = readByte(session, 0x91D5) session.sock.sendall(b"x\n") session.recv(3) if row == 0: break session.hold("KP_8", 250) time.sleep(0.8) print(f"menu row after input: {row}", flush=True) # Both pages watched before the module can touch anything. session.enterMonitor() for kind in ("store", "load"): session.mon(f"watch {kind} $de00 $de03") # where the probe looks first session.mon(f"watch {kind} $df00 $df03") # the real registers session.mon(f"watch {kind} $de04 $deff") # mirrors: nothing should ever land here session.mon(f"watch {kind} $df04 $dfff") session.sock.sendall(b"x\n") session.recv(2) session.hold("KP_0", 300) hits = [] for attempt in range(24): session.hold("a", 250) session.hold("space", 250) hits += collectHits(session, 4, maxHits=400) if hits: print(f"first cartridge-page access after {attempt + 1} attempt(s)", flush=True) break hits += collectHits(session, 25, maxHits=400) blob = "".join(hits) open(f"{LOGS}/{prefix}.hits.txt", "w").write(blob) events = describe(blob) print(f"=== {len(events)} watchpoint stops, in order ===", flush=True) for number, kind, addr, detail in events[:80]: page = "$DE00 probe" if 0xDE00 <= addr <= 0xDE03 else \ "$DF00 registers" if 0xDF00 <= addr <= 0xDF03 else "MIRROR - unexpected" print(f" {number:4d} {kind:5s} ${addr:04X} [{page}] {detail}", flush=True) mirrors = [event for event in events if not (0xDE00 <= event[2] <= 0xDE03 or 0xDF00 <= event[2] <= 0xDF03)] print(f"accesses to the mirror ranges $DE04-$DEFF / $DF04-$DFFF: {len(mirrors)}", flush=True) for event in mirrors[:40]: print(f" MIRROR {event}", flush=True) session.enterMonitor() showAccessors(readAccessors(session), "after the link was opened") session.mon("m e540 e56f") session.mon("m e030 e05f") session.mon("m e5b8 e5bf") session.mon("m df00 df03") session.mon("m de00 de03") session.mon("d fffa fffb") session.sock.sendall(b"x\n") session.recv(3) time.sleep(8) session.shot(f"{SHOTS}/{prefix}02afterOpen.png") # Leave only the mirror tripwires armed and let the game run on, so anything the rest of the # program does to the cartridge page while the link is live is caught. session.enterMonitor() session.mon("del") for kind in ("store", "load"): session.mon(f"watch {kind} $de04 $deff") session.mon(f"watch {kind} $df04 $dfff") session.sock.sendall(b"x\n") session.recv(2) late = collectHits(session, 45, maxHits=200) lateEvents = describe("".join(late)) print(f"=== mirror-only watch, 45 s of running: {len(lateEvents)} stops ===", flush=True) for event in lateEvents[:40]: print(f" MIRROR {event}", flush=True) open(f"{LOGS}/{prefix}.mirror.txt", "w").write("".join(late)) session.enterMonitor() session.mon("del") session.sock.sendall(b"x\n") session.recv(3) time.sleep(6) session.shot(f"{SHOTS}/{prefix}03later.png") finally: session.close() time.sleep(1) sink.stop() print(f"serial sink connected={sink.connected} bytes={len(sink.data)}", flush=True) print("serial bytes:", sink.data[:400].hex(" "), flush=True) print("as text:", repr(bytes(sink.data[:400])), flush=True) if __name__ == "__main__": main()