udpstream: the payload announces itself; test-osboot.sh has no sleep

Loading the COFF over rdebug pokes thousands of words one at a time, so how
long the payload takes to appear varies by tens of seconds -- a fixed sleep is
either wasteful or too short.  The payload now broadcasts three frames the
moment its ring is armed: ethertype 0x88B6, 'K' 'S', opcode 0xFF, magic
"LANC".  `vtcmd.py wait` listens for it; measured at 8.7 s in MAME.

Two details found by measurement:

  * The announce must use TX slot 0, because the chip transmits only from its
    ring position -- arming slot 7 while the position is 0 never sends
    anything (the first attempt did exactly that and was silently lost).
  * After the announce the reply cursor advances to slot 1 to match the chip's
    new position, so replies do not collide with the announce descriptor.
    txfull then stays 0 across consecutive replies; before this it read 1.

test-osboot.sh ties the hand-back together with no sleep: listen, load, wait
for the announce, stats, osboot, wait for :771.  Report sections 73.3/73.4.
This commit is contained in:
arti committed 2026-10-30 17:19:28 +02:00
1 parent f74bd7ae05
commit ed185279ca
5 files changed
+181 -6

No files matched your search

+43 -5
View File
@@ -4854,8 +4854,46 @@ Also observed: the display comes back **before** the network. At 32 s the
operator could see the screen while `:771` was still closed; the interface comes
up later in the OS boot.
**This is 1 of 1.** It is the vendor's own entry, it does exactly what a payload
breaks (STOP the LANCE, re-program the board window), and it worked first time —
but this section has already recorded three conclusions drawn from single
observations, so the status is "one confirmed success", not "reliable". A second
run is needed before it is treated as the standard hand-back.
### 73.3 Second run: 2 of 2, also 40 s
Repeated at **17:14:10**, and this time the machine started from a state
`osboot` itself had produced (no power cycle in between), rather than fresh from
one:
```
=== osboot #2 at 17:14:10 ===
osboot: control -> bit 0x3f5788c0, status 0
rdebug back after 40s at 17:14:50
pings
```
A second screenshot shows the Terminal Manager live again — `08-00-2B-16-4B-5A`,
`100.103.100.103`, `LAT_08002B164B5A`, 4 MB free, and the boot-time
`Attempting to set font path. / Using local fonts.` messages.
**2 of 2, both 40 s**, from two different starting states. That is enough to
treat `osboot` as the hand-back, and it is the only command in this report whose
success rate is above zero on the real machine.
### 73.4 The payload announces itself, so the host does not sleep
Loading the COFF over rdebug pokes thousands of words one at a time, so how long
the payload takes to appear varies by tens of seconds; a fixed sleep is either
wasteful or too short. The payload therefore transmits an **announce** the
moment its RX ring is armed:
* three broadcast frames, ethertype `0x88B6`, `'K' 'S'`, opcode `0xFF`, magic
`"LANC"` at `[17..20]`;
* back-to-back, with no clock involved (three because this is a half-duplex
segment and one lost broadcast should not cost a cycle);
* sent from TX **slot 0**, because the chip transmits only from its ring
position and arming any other slot does nothing until the ring reaches it (a
first attempt on slot 7 was silently never sent — measured);
* after the announce the reply cursor advances to slot 1, matching where the
chip's ring position now is, so replies never collide with the announce
descriptor; announce skips are not counted in `txfull`, which stays the reply
path's diagnostic.
`vtcmd.py wait` listens for it, and `test-osboot.sh` ties the whole hand-back
together with **no sleep**: listen, load, wait for the announce, `stats`,
`osboot`, wait for `:771`.
Binary file not shown.
+49
View File
@@ -338,6 +338,55 @@ int main()
rxf = 0; bytes = 0; cmds = 0; laststat = 0xEEEE; txslot = 0; ncmd = 0;
chaindrop = 0; txfull = 0; errbits = 0; jaddr = 0; dojump = 0;
/* ---- announce: the ring is armed, so say hello on the wire ----------
* Three broadcast frames, ethertype 0x88B6, cmd 0xFF, magic "LANC".
* A host waiting for this knows the payload is live and does not have to
* guess with a sleep. The frame is sent three times because this is a
* half-duplex segment and one lost broadcast should not cost a cycle.
* No clock is involved: back-to-back is enough at 10 Mbit. */
{
unsigned int a;
for (a = 0; a < 6; a++) {
wb(TXBUF + a * 8, 0xFF); /* broadcast dst */
}
wb(TXBUF + 6 * 8, 0x08); wb(TXBUF + 7 * 8, 0x00);
wb(TXBUF + 8 * 8, 0x2B); wb(TXBUF + 9 * 8, 0x16);
wb(TXBUF + 10 * 8, 0x4B); wb(TXBUF + 11 * 8, 0x5A);
wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB6);
wb(TXBUF + 14 * 8, 'K'); wb(TXBUF + 15 * 8, 'S');
wb(TXBUF + 16 * 8, 0xFF); /* announce opcode */
wb(TXBUF + 17 * 8, 0x4C); wb(TXBUF + 18 * 8, 0x41);
wb(TXBUF + 19 * 8, 0x4E); wb(TXBUF + 20 * 8, 0x43); /* "LANC" */
for (a = 21; a < 60; a++) {
wb(TXBUF + a * 8, 0);
}
for (a = 0; a < 3; a++) {
/* Slot 0 is where the chip's ring position starts, and it only
* transmits from that position -- arming any other slot does
* nothing until the ring gets there. Announce skips are not
* counted in txfull, which is the reply path's diagnostic. */
for (j = 0; j < 8000; j++) {
if (!(r16(TXR + 16) & TMD1_OWN)) {
break;
}
}
if (r16(TXR + 16) & TMD1_OWN) {
break; /* still busy: stop trying */
}
w16(TXR + 48, 0);
w16(TXR + 32, (0x10000 - 60) & 0xFFFF);
w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP);
csr_w(0, 0x0040 | CSR0_TDMD);
for (j = 0; j < 60000; j++) {
if (!(r16(TXR + 16) & TMD1_OWN)) {
break;
}
}
}
/* the chip's TX ring position is now 1, so replies must start there */
txslot = 1;
}
/* ---- main loop: receive, count, and serve commands ---- */
for (idle = 0; ; idle++) {
for (i = 0; i < NRX; i++) {
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
# test-osboot.sh -- take the LANCE over, then hand it back with `osboot`.
#
# sudo ./test-osboot.sh [iface] [host]
#
# The point of this script is that it contains no sleep. The payload
# broadcasts an announce frame (ethertype 0x88B6, opcode 0xFF, magic "LANC")
# as soon as its RX ring is armed, so the host starts listening first and acts
# the moment the payload is really up. Loading the COFF over rdebug is slow
# (thousands of word pokes), and a fixed sleep either wastes seconds or is too
# short -- waiting for the announce is exact.
#
# Steps:
# 1. listen for the announce (background)
# 2. load dwt/loader/hostload.py, which pokes and jumps into the payload
# 3. when the announce arrives, read stats and send `osboot`
# 4. wait for the terminal's rdebug port to come back
#
# `osboot` jumps to the OS cold start at 0x3F5788C0, which STOPs the LANCE and
# re-programs the board window before restarting the OS. Measured: 40 s twice.
IFACE=${1:-vt1200}
HOST=${2:-100.103.100.103}
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/../.." && pwd)
LOG=${LOG:-/tmp/vt1200-osboot.log}
if [ "$(id -u)" != "0" ]; then
echo "run as root: raw sockets and rdebug need it" >&2
exit 2
fi
echo "[*] listening for the announce on $IFACE"
python3 "$HERE/vtcmd.py" wait --iface "$IFACE" --wait-timeout 240 &
LISTENER=$!
echo "[*] loading $(basename "$HERE")/build/lance_cmd.coff onto $HOST"
python3 "$ROOT/dwt/loader/hostload.py" --host "$HOST" \
--coff "$HERE/build/lance_cmd.coff" --real --no-screenshot \
> "$LOG" 2>&1 &
if ! wait "$LISTENER"; then
echo "[!] no announce: the payload did not start (see $LOG)" >&2
exit 1
fi
echo "[*] payload is up, stats:"
python3 "$HERE/vtcmd.py" stats --iface "$IFACE" || true
echo "[*] handing back with osboot"
python3 "$HERE/vtcmd.py" osboot --iface "$IFACE" || exit 1
echo "[*] waiting for rdebug :771 on $HOST"
T0=$(date +%s)
i=0
while [ $i -lt 36 ]; do
sleep 5
i=$((i + 1))
if timeout 2 sh -c "</dev/tcp/$HOST/771" 2>/dev/null; then
echo "[+] back after $(( $(date +%s) - T0 ))s"
exit 0
fi
done
echo "[!] did not come back within 180 s" >&2
exit 1
+23 -1
View File
@@ -13,6 +13,8 @@ Protocol, ethertype 0x88B6, both directions:
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 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.
@@ -132,7 +134,7 @@ def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("cmd", choices=["stats", "read", "poke", "jump",
"reboot", "osboot", "bench"])
"reboot", "osboot", "wait", "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)
@@ -157,6 +159,8 @@ def main():
"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("--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)
@@ -177,6 +181,24 @@ def main():
print(" %04x %s" % (i, data[i:i + 16].hex(" ")))
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: