Files
arti 636b3d6985 udpstream: the CPU store path costs ~14 us/store regardless of destination
Measured on the unit: byte stores to DRAM 13.92 us, to VRAM 13.93 us -- the
same, so the display arbiter is not involved.  In MAME 16-bit stores halve the
time exactly (863 ms vs 1588 ms per frame), i.e. the cost is per store
instruction, not per byte.

Against the graphics engine (FILL L 0.40 us/word, PIXBLT L,XY 0.62 us/word)
the CPU is 25-35x slower per word.  So frames must never be built with CPU
stores: the LANCE can chain descriptors so a 160 KB frame lands contiguously
in a pre-armed buffer with no CPU copy, and one PIXBLT L,XY puts it on screen.

Budget: full frame ~5 fps (129 ms wire + 60 ms blit), 640x512 ~21 fps,
320x240 ~80 fps.  Report section 75; cmd 10 (PIXBLT from RAM) and cmd 11
(16-bit store control) are the benchmarks.
2026-10-30 17:42:22 +02:00

389 lines
16 KiB
Python

#!/usr/bin/env python3
"""vtcmd.py -- talk to the LANCE command payload (harness/udpstream/lance_cmd.c).
The payload runs entirely on its own (no OS stack), so this is the only way to
inspect the machine while it owns the chip.
Protocol, ethertype 0x88B6, both directions:
[14] 'K' [15] 'S' [16] cmd
cmd 1 read : [17..20] address (32-bit big-endian BIT address), [21..22] len
cmd 2 stats: no arguments
cmd 3 poke : [17..20] address (BIT), [21..22] len, [23..] the bytes
cmd 4 jump : [17..20] address (BIT) -- reply is sent, then control moves
cmd 5 reboot: no arguments -- jumps to the ROM reset vector (wedges the
real machine; kept only because MAME tolerates it)
cmd 0xFF announce: sent BY the payload, three broadcasts, magic "LANC"
right after its ring is armed -- `vtcmd.py wait` listens for it
cmd 7 vram : [21..22] frame count -- fill that many full 1280x1024
frames, reply when done (the host times it)
cmd 6 osboot: no arguments -- jumps to the OS cold-start entry 0x3F5788C0,
the path POST takes when it skips level 1. That path STOPs
the LANCE and re-programs the board window first.
reply : [16] cmd echoed, [17..18] status, [19..] data
Every address in this protocol is a BIT address, the way the TMS34010 sees
memory. For a byte address, send byte_addr * 8.
Usage:
# one-off statistics (frames/bytes the payload has seen)
sudo python3 vtcmd.py stats --iface vt1200
# write 4 bytes at bit address 0x018D0300 (byte 0x31A60)
sudo python3 vtcmd.py poke --iface vt1200 --addr 0x018D0300 --data "de ad be ef"
# run code at an address, or soft-reset the machine back into the firmware
sudo python3 vtcmd.py jump --iface vt1200 --addr 0x018C0000
sudo python3 vtcmd.py reboot --iface vt1200
# read 32 bytes of memory at bit address 0x018D0000
sudo python3 vtcmd.py read --iface vt1200 --addr 0x018D0000 --len 32
# throughput: blast N frames, then ask the payload how many it counted
sudo python3 vtcmd.py bench --iface vt1200 --count 4000 --size 1514
"""
import argparse
import socket
import struct
import sys
import time
ETHCMD = 0x88B6
VT_MAC = bytes.fromhex("08002b164b5a")
def parse_mac(s):
return bytes(int(p, 16) for p in s.split(":"))
def iface_mac(iface):
with open("/sys/class/net/%s/address" % iface) as f:
return parse_mac(f.read().strip())
def open_sock(iface):
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETHCMD))
s.bind((iface, 0))
return s
def frame(iface, cmd, addr=0, length=0, size=60, payload=b""):
src = iface_mac(iface)
body = bytes([ord('K'), ord('S'), cmd]) + struct.pack(">I", addr) + \
struct.pack(">H", length) + payload
body = body + bytes(max(0, size - 14 - len(body)))
return b"\xff" * 6 + src + struct.pack(">H", ETHCMD) + body
def request(sock, iface, cmd, addr=0, length=0, timeout=2.0, tries=3,
payload=b""):
"""Send a command and wait for its reply.
Retried because the first frame after the payload takes the chip over is
routinely lost: the ring is armed slightly before the receive loop runs.
"""
for attempt in range(tries):
sock.send(frame(iface, cmd, addr, length,
size=max(60, 23 + len(payload)), payload=payload))
end = time.time() + timeout
sock.settimeout(0.2)
while time.time() < end:
try:
data, addr_in = sock.recvfrom(2048)
except socket.timeout:
continue
if len(data) < 19:
continue
if data[12:14] != struct.pack(">H", ETHCMD):
continue
if data[14:16] != b"KS":
continue
if data[16] != cmd:
continue
status = (data[17] << 8) | data[18]
return status, data[19:]
return None, None
def cmd_stats(sock, iface, quiet=False):
status, data = request(sock, iface, 2)
if status is None:
if not quiet:
print("no reply -- is the payload running and the link up?")
return None
rxf, nbytes, cmds, chaindrop, txfull, errbits = struct.unpack(">IIHHHH", data[:16])
flags = []
if errbits & 0x1000:
flags.append("MISS(no descriptor)")
if errbits & 0x0800:
flags.append("MERR")
if errbits & 0x4000:
flags.append("BABL")
if errbits & 0x2000:
flags.append("CERR")
if errbits & 0x8000:
flags.append("ERR")
if not quiet:
print("payload: RXF=%d bytes=%d cmds=%d chained_dropped=%d txfull=%d "
"csr0_err=%#06x%s status=%#06x"
% (rxf, nbytes, cmds, chaindrop, txfull, errbits,
(" [" + ",".join(flags) + "]") if flags else " [none]", status))
return rxf, nbytes, cmds, chaindrop, txfull, errbits
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("cmd", choices=["stats", "read", "poke", "jump",
"reboot", "osboot", "wait", "vram",
"bench"])
ap.add_argument("--iface", default="vt1200")
ap.add_argument("--addr", type=lambda v: int(v, 0), default=0)
ap.add_argument("--len", type=int, default=32)
ap.add_argument("--data", default="", help="poke: bytes, e.g. 'de ad be ef'")
ap.add_argument("--file", default="", help="poke: read the bytes from a file")
ap.add_argument("--count", type=int, default=0,
help="send exactly N frames instead of a timed burst")
ap.add_argument("--seconds", type=float, default=3.0,
help="duration of the timed burst when --count is 0")
ap.add_argument("--size", type=int, default=1514)
ap.add_argument("--etype", type=lambda v: int(v, 0), default=0x88B7,
help="ethertype for benchmark frames (must NOT be 0x88B6, "
"or the payload answers every one of them)")
ap.add_argument("--dst", default="ff:ff:ff:ff:ff:ff",
help="destination MAC; unicast avoids the switch's "
"broadcast storm control")
ap.add_argument("--rate", type=float, default=0,
help="pace to at most N frames/s (0 = as fast as possible)")
ap.add_argument("--busy", action="store_true",
help="busy-wait instead of time.sleep while pacing; "
"sleep granularity is ~250us and bunches frames "
"into microbursts that the switch drops")
ap.add_argument("--force", action="store_true",
help="reboot: required, it succeeded 1 of 2 times and can hang")
ap.add_argument("--frames", type=int, default=5,
help="vram: how many frames to fill")
ap.add_argument("--rows", type=int, default=1024,
help="vram: how many of the 1024 rows to write")
ap.add_argument("--word", action="store_true",
help="vram: 16-bit store loop instead of byte stores")
ap.add_argument("--blit", action="store_true",
help="vram: use PIXBLT L,XY from a RAM frame buffer")
ap.add_argument("--dram", action="store_true",
help="vram: same byte-store loop but into DRAM (no VRAM "
"arbiter) -- sizes the frame-expansion stage")
ap.add_argument("--fill", action="store_true",
help="vram: use the hardware FILL L instruction instead of "
"byte stores")
ap.add_argument("--wait-timeout", type=float, default=180.0,
help="wait: how long to listen for the payload's announce")
ap.add_argument("--terse", action="store_true",
help="one CSV line per run, for sweeps and for the report")
ap.add_argument("--timeout", type=float, default=2.0)
a = ap.parse_args()
sock = open_sock(a.iface)
if a.cmd == "stats":
return 0 if cmd_stats(sock, a.iface) else 1
if a.cmd == "read":
status, data = request(sock, a.iface, 1, a.addr, a.len, a.timeout)
if status is None:
print("no reply")
return 1
print("read %#010x len %d status %#06x:" % (a.addr, a.len, status))
for i in range(0, min(len(data), a.len), 16):
print(" %04x %s" % (i, data[i:i + 16].hex(" ")))
return 0
if a.cmd == "vram":
n = a.frames
opcode = 10 if a.blit else (8 if a.fill else (11 if a.word else
(9 if a.dram else 7)))
t0 = time.time()
status, data = request(sock, a.iface, opcode, a.rows, n,
timeout=max(120.0, a.timeout))
t1 = time.time()
if status is None:
print("no reply")
return 1
dt = t1 - t0
if a.blit:
nf = struct.unpack(">H", data[:2])[0]
rows, stores = 1024, nf * 1024 * 80
elif a.fill:
nf, x = struct.unpack(">HH", data[:4])
rows = 1024
stores = nf * 1024 * 80 # destination words, not bytes
elif a.dram or a.word:
nf, rows, stores = struct.unpack(">HHI", data[:8])
else:
nf, rows, stores = struct.unpack(">HHI", data[:8])
per = dt / nf if nf else 0
rate = stores / dt if dt else 0
print("filled %d frame(s) x %d rows (%s): %d stores in %.3f s"
% (nf, rows, "PIXBLT L,XY" if a.blit else
("FILL L" if a.fill else
("DRAM stores" if a.dram else "byte stores")),
stores, dt))
print(" %.1f ms per frame -> %.2f fps ceiling"
% (per * 1000.0, 1.0 / per if per else 0))
if a.fill or a.blit:
print(" %.0f words/s (%.2f us per destination word)"
% (rate, 1e6 / rate if rate else 0))
else:
print(" %.0f bytes/s (%.2f us per byte store)"
% (rate, 1e6 / rate if rate else 0))
return 0
if a.cmd == "wait":
# the payload broadcasts this the moment its RX ring is armed, so the
# host does not have to sleep and hope
sock.settimeout(1.0)
t0 = time.time()
while time.time() - t0 < a.wait_timeout:
try:
data, _ = sock.recvfrom(2048)
except socket.timeout:
continue
if (len(data) >= 21 and data[12:14] == b"\x88\xb6"
and data[14:16] == b"KS" and data[16] == 0xFF):
mac = ":".join("%02x" % b for b in data[6:12])
print("announce from %s after %.1f s" % (mac, time.time() - t0))
return 0
print("no announce within %gs" % a.wait_timeout)
return 1
if a.cmd == "poke":
if a.file:
with open(a.file, "rb") as f:
blob = f.read()
else:
blob = bytes(int(b, 16) for b in a.data.replace(",", " ").split())
if not blob:
print("nothing to poke: use --data or --file")
return 1
status, data = request(sock, a.iface, 3, a.addr, len(blob), a.timeout,
payload=blob)
if status is None:
print("no reply")
return 1
print("poked %d bytes at bit %#010x (byte %#x) status %#06x"
% (len(blob), a.addr, a.addr >> 3, status))
return 0
if a.cmd in ("jump", "reboot", "osboot"):
if a.cmd == "reboot" and not a.force:
print("refusing reboot without --force.")
print(" It has NEVER worked on the real VT1200: 0 of 2 attempts.")
print(" Both ended with no ping, no rdebug and the monitor asleep;")
print(" the one time it appeared to recover, the operator had power")
print(" cycled the machine. Expect to need a power cycle.")
print(" See NETWORK_REPORT.md 72.3.")
return 2
if a.cmd == "reboot":
print("rebooting: it may take ~90 s and it may never come back.")
cmd = 4 if a.cmd == "jump" else (5 if a.cmd == "reboot" else 6)
status, data = request(sock, a.iface, cmd, a.addr, 0, a.timeout)
if status is None:
print("no reply")
return 1
target = struct.unpack(">I", data[:4])[0] if len(data) >= 4 else 0
print("%s: control -> bit %#010x (byte %#x), status %#06x"
% (a.cmd, target, target >> 3, status))
print("the payload has handed control over; it will not answer again")
return 0
# bench: offer frames continuously for --seconds, then let the payload's
# counter settle. A fixed-duration burst makes the window long enough that
# the settle time is negligible, and retrying on ENOBUFS paces the host to
# whatever its socket buffer allows instead of aborting.
before = cmd_stats(sock, a.iface, quiet=a.terse)
size = max(a.size, 60)
body = bytes(size - 14)
f = parse_mac(a.dst) + iface_mac(a.iface) + struct.pack(">H", a.etype) + body
sent = 0
t0 = time.time()
if a.count:
for i in range(a.count):
try:
sock.send(f)
sent += 1
except OSError:
time.sleep(0.001)
sock.send(f)
sent += 1
else:
while time.time() - t0 < a.seconds:
if a.rate:
want = sent / a.rate
behind = want - (time.time() - t0)
if behind > 0:
if a.busy:
end = time.time() + behind
while time.time() < end:
pass
else:
time.sleep(behind)
try:
sock.send(f)
sent += 1
except OSError:
time.sleep(0.001)
t1 = time.time()
# poll until the *data* counter is stable (each stats request is itself a
# received command frame, so comparing rxf alone would never settle)
after = before
quiet = 0
last = before[0] - before[2]
deadline = time.time() + 15.0
while time.time() < deadline:
time.sleep(0.2)
now = cmd_stats(sock, a.iface, quiet=a.terse)
if not now:
break
after = now
cur = now[0] - now[2]
if cur == last:
quiet += 1
if quiet >= 2:
break
else:
quiet = 0
last = cur
t2 = time.time()
if before and after and after[0] >= before[0]:
dt = t1 - t0
dcmds = after[2] - before[2] # our own stats frames
frames = (after[0] - before[0]) - dcmds
nbytes = (after[1] - before[1]) - 60 * dcmds
if a.terse:
print("%d,%g,%.2f,%d,%d,%d,%.0f,%.1f,%.2f,%.1f,%#06x,%d,%d"
% (size, a.rate, dt, sent, frames, nbytes,
frames / dt if dt else 0,
nbytes / 1024.0 / dt if dt else 0,
nbytes * 8 / dt / 1e6 if dt else 0,
100.0 * frames / sent if sent else 0,
after[5], after[4], after[3]))
return 0
print("sent %d frames of %d B over %.3f s (%.0f frames/s offered)"
% (sent, size, dt, sent / dt if dt else 0))
print("payload received %d frames (%d bytes) while they were offered"
% (frames, nbytes))
print(" receive rate: %.0f frames/s, %.1f KiB/s, %.1f Mbit/s"
% (frames / dt if dt else 0,
nbytes / 1024.0 / dt if dt else 0,
nbytes * 8 / dt / 1e6 if dt else 0))
print(" delivery ratio: %.1f%% (below 100%% = the ring or the wire "
"could not keep up)" % (100.0 * frames / sent if sent else 0))
print(" drain after the burst: %.2f s" % max(0.0, t2 - t1))
return 0
if __name__ == "__main__":
sys.exit(main())