249 lines
11 KiB
Python
249 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# testAdopt.py - does the driver really use the line settings the user's hardware is already
|
|
# configured for, and does the 38400 hot key work again?
|
|
#
|
|
# python3 testAdopt.py <disk.d64> [tag]
|
|
#
|
|
# Two questions, one boot, one machine (the far end is a TCP sink that never answers, which is enough
|
|
# for everything measured here - none of it needs a peer).
|
|
#
|
|
# 1. ADOPT MODE. baudEntryTable's last entry, index 21, has no control byte of its own: aciaProbeAdopted
|
|
# reads the 6551's control register before the page probe overwrites it and parks the result there,
|
|
# corrected only where it cannot work. So the test writes a control byte into $DE03 with the
|
|
# monitor, exactly as a C64 Ultimate or a previously-configured cartridge would leave it, and then
|
|
# makes the driver open the link and reads back what it chose.
|
|
# * the first case is the real cold-start path: the byte goes in before the A/O prompt is
|
|
# answered, i.e. before anything in the module has touched the chip;
|
|
# * the rest re-run the same code by clearing aciaPageLatch $E5BF - which is the only thing that
|
|
# stops aciaDetect probing twice - and pressing C=+U, so one boot covers the whole table of
|
|
# cases. Clearing that latch is a test manipulation and is called out as one.
|
|
#
|
|
# 2. THE 38400 HOT KEY. C=+8 must put baudIndex $E055 = 18 and the control register = $1F again, and
|
|
# C=+U must take the driver back out of the fixed rate and into adopt mode.
|
|
import os
|
|
import re
|
|
import subprocess
|
|
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
|
|
from testTwoMachines import pickModemOpponent, answerModemPrompts
|
|
from testHighRates import moduleResident
|
|
|
|
LOGS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testLogs")
|
|
SHOTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots")
|
|
|
|
CONTROL = ACIA_BASE + 3
|
|
BAUD_INDEX = 0xE055 # the three-byte index into baudEntryTable
|
|
LIVE_CONTROL = 0xE056 # bitPeriodLo: the control byte loadBaudParameters last wrote
|
|
ADOPT_SLOT = 0xE726 # baudEntryTable ($E710) + 22, the adopt entry's control byte
|
|
PAGE_LATCH = 0xE5BF
|
|
|
|
# what the port is set to what the driver must end up using, and why
|
|
CASES = [
|
|
(0x18, 0x18, "2400 8N1 - usable as it stands, so it is copied through untouched"),
|
|
(0x9F, 0x9F, "38400 with two stop bits - stop bits are free, so they survive"),
|
|
(0x5A, 0x1A, "4800 but six data bits - the word length is corrected, the rate is not"),
|
|
(0x3C, 0x1C, "9600 but seven data bits - same correction"),
|
|
(0x00, 0x15, "a cold, unconfigured chip - $00 selects external clocks, so the 300-baud fallback"),
|
|
(0x0C, 0x15, "9600 but bit 4 clear = external receive clock - unusable, so the fallback again"),
|
|
(0x10, 0x15, "internal clock but no rate at all - unusable, so the fallback again"),
|
|
]
|
|
|
|
|
|
def pause(session):
|
|
session.enterMonitor()
|
|
|
|
|
|
def resume(session):
|
|
session.sock.sendall(b"x\n")
|
|
session.recv(2)
|
|
|
|
|
|
def keyDown(session, keysym):
|
|
subprocess.run(["xdotool", "keydown", keysym], env=session.env, check=False)
|
|
|
|
|
|
def keyUp(session, keysym):
|
|
subprocess.run(["xdotool", "keyup", keysym], env=session.env, check=False)
|
|
|
|
|
|
def holdCombo(session, key, ms=900):
|
|
keyDown(session, "Tab") # the Commodore key under VICE's default symbolic keymap
|
|
time.sleep(0.2)
|
|
keyDown(session, key)
|
|
time.sleep(ms / 1000.0)
|
|
keyUp(session, key)
|
|
time.sleep(0.1)
|
|
keyUp(session, "Tab")
|
|
time.sleep(0.4)
|
|
|
|
|
|
def pressUntil(session, key, addr, want, tries=6):
|
|
# The hot-key layer ignores a repeat of the code it saw last ($E312 cmp lastHotkeyCode), so the
|
|
# same combination pressed twice running does nothing the second time. Every press here is
|
|
# therefore checked against the byte it is supposed to move, and repeated until it lands.
|
|
for attempt in range(tries):
|
|
# Re-assert the input focus first: there is no window manager on the private display, and a
|
|
# monitor stop between presses is enough to leave the keys going nowhere.
|
|
session.focus()
|
|
holdCombo(session, key)
|
|
time.sleep(0.4)
|
|
pause(session)
|
|
got = readByte(session, addr)
|
|
last = readByte(session, 0xE046)
|
|
resume(session)
|
|
if got == want:
|
|
return attempt + 1
|
|
print(f"[press] C=+{key} did not land (want ${want:02X}, got "
|
|
f"{'None' if got is None else '$%02X' % got}, lastHotkeyCode=${last:02X}) - again",
|
|
flush=True)
|
|
time.sleep(0.6)
|
|
return 0
|
|
|
|
|
|
def state(session):
|
|
pause(session)
|
|
out = {
|
|
"control": readByte(session, CONTROL),
|
|
"baudIndex": readByte(session, BAUD_INDEX),
|
|
"liveControl": readByte(session, LIVE_CONTROL),
|
|
"adoptSlot": readByte(session, ADOPT_SLOT),
|
|
"pageLatch": readByte(session, PAGE_LATCH),
|
|
"commandShadow": readByte(session, 0xE5BE),
|
|
"nmiState": readByte(session, 0xE5BD),
|
|
}
|
|
resume(session)
|
|
return out
|
|
|
|
|
|
def show(tag, s):
|
|
print(f"[{tag}] $DE03={s['control']:02X} baudIndex={s['baudIndex']:02X} "
|
|
f"live={s['liveControl']:02X} adoptSlot={s['adoptSlot']:02X} "
|
|
f"latch={s['pageLatch']:02X} cmd={s['commandShadow']:02X} nmiState={s['nmiState']:02X}",
|
|
flush=True)
|
|
|
|
|
|
def main():
|
|
disk = os.path.abspath(sys.argv[1])
|
|
tag = sys.argv[2] if len(sys.argv) > 2 else "adopt"
|
|
os.makedirs(LOGS, exist_ok=True)
|
|
os.makedirs(SHOTS, exist_ok=True)
|
|
sink = SerialSink()
|
|
session = ViceSession(disk, os.path.join(LOGS, f"vice.{tag}.log"),
|
|
aciaArgs(rsDevAddress=f"127.0.0.1:{sink.port}"), label=tag, warp=True)
|
|
results = []
|
|
try:
|
|
session.connect()
|
|
session.mon("warp on") # -warp on the command line does not survive the autostart
|
|
session.bootPastLoader()
|
|
session.findWindow()
|
|
session.focus()
|
|
# Out of warp before any key is pressed: a joystick press held for 250 ms of wall clock is a
|
|
# different number of emulated frames at warp speed than it is at C64 speed, and the options
|
|
# menu scrolls one row per frame, so menu keys under warp land wherever they like.
|
|
pause(session)
|
|
session.mon("warp off")
|
|
resume(session)
|
|
pickModemOpponent(session, tag, "a")
|
|
# Nothing below means anything until the opponent module is actually resident: before that,
|
|
# $E055 and $E5BD are whatever the trainer or the loader left in those bytes.
|
|
for attempt in range(30):
|
|
time.sleep(2)
|
|
if moduleResident(session, tag):
|
|
break
|
|
else:
|
|
raise SystemExit(f"{tag}: the opponent module never loaded")
|
|
|
|
# --- the module is resident and has not touched the chip yet ---
|
|
pause(session)
|
|
session.mon("warp off")
|
|
before = {
|
|
"baudIndex": readByte(session, BAUD_INDEX),
|
|
"adoptSlot": readByte(session, ADOPT_SLOT),
|
|
"latch": readByte(session, PAGE_LATCH),
|
|
"control": readByte(session, CONTROL),
|
|
}
|
|
print(f"[{tag}] at the A/O prompt: baudIndex=${before['baudIndex']:02X} "
|
|
f"adoptSlot=${before['adoptSlot']:02X} latch=${before['latch']:02X} "
|
|
f"$DE03=${before['control']:02X}", flush=True)
|
|
# set the port up the way a user's hardware would already be set up
|
|
session.mon(f"> {CONTROL:04x} 18")
|
|
resume(session)
|
|
results.append(("disk default baudIndex", before["baudIndex"], 0x15))
|
|
results.append(("adopt slot before the probe", before["adoptSlot"], 0x00))
|
|
results.append(("page latch before the probe", before["latch"], 0x00))
|
|
|
|
# --- the real cold-start path: open the link with $DE03 already set to 2400 ---
|
|
opened = answerModemPrompts(session, tag, "a")
|
|
cold = state(session)
|
|
show(tag + "/cold", cold)
|
|
results.append(("link opened", 1 if opened else 0, 1))
|
|
results.append(("cold start adopted $18 into the slot", cold["adoptSlot"], 0x18))
|
|
results.append(("cold start left the chip at $18", cold["control"], 0x18))
|
|
results.append(("cold start used index 21", cold["baudIndex"], 0x15))
|
|
session.shot(os.path.join(SHOTS, f"{tag}Cold.png"))
|
|
|
|
# --- the table of cases, each one a fresh probe ---
|
|
# Each case is two hot keys, and the pair is the point: C=+3 takes the driver out of adopt
|
|
# mode and on to a fixed 300 baud (and, with the latch cleared, re-runs the probe, which is
|
|
# what re-reads the preset), then C=+U puts it back on the port's own settings. Pressing two
|
|
# different combinations also keeps the hot-key layer's repeat filter out of the way.
|
|
for preset, expect, why in CASES:
|
|
pause(session)
|
|
session.mon(f"> {CONTROL:04x} {preset:02x}")
|
|
session.mon(f"> {PAGE_LATCH:04x} 00") # test manipulation: let aciaDetect probe again
|
|
resume(session)
|
|
landed3 = pressUntil(session, "3", BAUD_INDEX, 0x00)
|
|
afterFixed = state(session)
|
|
landedU = pressUntil(session, "u", BAUD_INDEX, 0x15)
|
|
s = state(session)
|
|
print(f"[{tag}] preset ${preset:02X} -> slot=${s['adoptSlot']:02X} "
|
|
f"$DE03=${s['control']:02X} ({why})", flush=True)
|
|
results.append((f"preset ${preset:02X}: C=+3 left adopt mode", afterFixed["baudIndex"], 0x00))
|
|
results.append((f"preset ${preset:02X}: C=+3 imposed 300 baud", afterFixed["control"], 0x15))
|
|
results.append((f"preset ${preset:02X}: the probe adopted ${expect:02X}",
|
|
afterFixed["adoptSlot"], expect))
|
|
results.append((f"preset ${preset:02X}: C=+U selected adopt", s["baudIndex"], 0x15))
|
|
results.append((f"preset ${preset:02X}: C=+U programmed ${expect:02X}", s["control"], expect))
|
|
|
|
# --- the 38400 hot key, and coming back out of it ---
|
|
pressUntil(session, "8", BAUD_INDEX, 0x12)
|
|
s = state(session)
|
|
show(tag + "/C=+8", s)
|
|
results.append(("C=+8 selects index 18", s["baudIndex"], 0x12))
|
|
results.append(("C=+8 writes control $1F", s["control"], 0x1F))
|
|
session.shot(os.path.join(SHOTS, f"{tag}Rate38400.png"))
|
|
|
|
pause(session)
|
|
session.mon(f"> {CONTROL:04x} 17") # pretend the port is set to 1200
|
|
session.mon(f"> {PAGE_LATCH:04x} 00")
|
|
resume(session)
|
|
pressUntil(session, "u", BAUD_INDEX, 0x15)
|
|
s = state(session)
|
|
show(tag + "/C=+U", s)
|
|
results.append(("C=+U leaves the fixed rate", s["baudIndex"], 0x15))
|
|
results.append(("C=+U adopts $17", s["control"], 0x17))
|
|
session.shot(os.path.join(SHOTS, f"{tag}Adopt.png"))
|
|
finally:
|
|
try:
|
|
session.shot(os.path.join(SHOTS, f"{tag}End.png"))
|
|
except Exception:
|
|
pass
|
|
session.close()
|
|
sink.stop()
|
|
|
|
print("\n=== results ===", flush=True)
|
|
bad = 0
|
|
for name, got, want in results:
|
|
ok = got == want
|
|
bad += 0 if ok else 1
|
|
gotText = "None" if got is None else f"${got:02X}"
|
|
print(f" {'PASS' if ok else 'FAIL'} {name}: got {gotText}, want ${want:02X}", flush=True)
|
|
print(f"\n{len(results) - bad} of {len(results)} checks passed", flush=True)
|
|
return 1 if bad else 0
|
|
|
|
|
|
sys.exit(main())
|