mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-31 12:05:05 +02:00
lance_cmd.c is a payload that drives the LANCE with its own code (no ROM
calls) and answers a command protocol on ethertype 0x88B6, so a unit whose
chip we have taken over can still be inspected:
read (peek), stats, poke, jump, reboot
The reply path was dead for a while and the cause was not the chip: the RX
ring had been grown to 16 entries (0x3020..0x309F) while TXR stayed at
0x3060, so all eight TX descriptors lay inside the RX ring. The chip's RX
write-back set OWN in what we believed were TX descriptors, and every reply
was discarded by transmit_poll() as "owned but not start of packet"
(r 00309A 8000 / w 00309A 0000 in the DMA log). TX ring moved to 0x30C0,
with a compile-time guard so the rings cannot overlap again.
Receive path aligned with the three drivers available locally (4.4BSD
pmax if_le.c, NetBSD am7990.c, Linux 7990.c):
* STP *and* ENP required on one descriptor, else the frame is counted as
chained and dropped rather than parsed from the middle of a buffer;
* the 4-byte FCS the chip writes into the buffer is subtracted, as all
three do (rmd3 - 4);
* re-arm restores RMD2 and clears RMD3, ownership written last;
* TX kick is TDMD|INEA (no reference driver re-writes STRT);
* a TX slot still owned is skipped instead of clobbered.
RX buffers are 1526 B so a full 1514-byte MTU frame fits one descriptor;
1500 B would have chained it.
vtcmd.py drives all of it; bench-sweep.sh sweeps size x rate. Measured on
the real VT1200 (10 Mbit half-duplex hub): 856-858 fps of 1514-byte frames
at 99.9-100% delivery, i.e. wire limited, and 1500 fps of 60-byte frames at
99.8-99.9%. NETWORK_REPORT.md sections 68-72 record the evidence, including
the 64-descriptor build that died with an illegal opcode on the unit and was
reverted.
294 lines
11 KiB
Python
294 lines
11 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
|
|
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", "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("--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 == "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"):
|
|
cmd = 4 if a.cmd == "jump" else 5
|
|
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())
|