diff --git a/harness/mame/NETWORK_REPORT.md b/harness/mame/NETWORK_REPORT.md index 0adfa83..bd80a6c 100644 --- a/harness/mame/NETWORK_REPORT.md +++ b/harness/mame/NETWORK_REPORT.md @@ -4380,3 +4380,59 @@ unrelated to the takeover payload. ```sh ping -c3 -W2 100.103.100.103 ``` + +## 66. The payload sends and receives on the wire — with its own driver code + +A standalone payload (`harness/udpstream/lance_bcast.c`) now drives the LANCE +with **no ROM calls at all**: it writes its own init block (PADR, LADRF, +rings), programs `CSR1/CSR2/CSR3`, runs `INIT`/`IDON`/`STRT`, owns eight TX and +eight RX descriptors, kicks `TDMD`, and reads completions itself. + +### 66.1 The test + +`harness/udpstream/test-bcast.sh` runs it in MAME and reports the result: + +``` +== results == + TXD (frames transmitted) = 6 + RXF (frames received) = 5 + last ethertype = 0x88B5 + last source MAC hi word = 0xC6FC + ok = 1 + payload broadcasts seen on tap-mess-1000-0: 55 +== PASS == +``` + +and the tap capture separates the two directions: + +``` +6 08:00:2b:16:4b:5a > ff:ff:ff:ff:ff:ff, ethertype 0x88b5, length 60 <- our payload +49 c6:fc:cc:16:f5:66 > ff:ff:ff:ff:ff:ff, ethertype 0x88b5, length 60 <- the host +``` + +`TXD = 6` is the payload's own counter for descriptors it armed and saw +complete; `RXF = 5` counts descriptors the chip filled with host frames (the +listen loop stops early at five), and the recorded source MAC `0xC6FC…` is the +host's, not ours — so it really consumed frames from the wire rather than its own +transmissions. + +### 66.2 What it took (all of these were my bugs, not the chip's) + +| symptom | cause | +|---|---| +| `TMD3 = LCAR`, no carrier | MAME's `-networkprovider taptun` only *permits* the module; the interface choice lives in `harness/mame/cfg-net/vt1200.cfg`. An empty cfg dir means no tap is ever opened | +| `TXD = 1` then timeouts | the payload always armed **descriptor 0**; the chip advances its own ring position after each completion, so frames 2..n were kicked at descriptors it had already passed | +| test killed instantly | `pkill -f mame0289/vt1200` matched the wrapper shell's own command line; `pkill -x vt1200` is the safe form | +| markers read as `0x55555555` | two bit-vs-byte addressing mistakes: the payload called `rb(buf + 12)` with a *byte* address, and the script read `cell()`'s *bit* offsets as byte offsets | +| payload finished before the host frames | its listen window was too short and it returned, letting the OS reuse the marker memory; it now listens for 400M iterations, stops early at five frames, and holds after drawing the panel | + +### 66.3 What this establishes + +* The register, ring, descriptor and DMA behaviour documented in this report is + sufficient to build a working Ethernet driver: our code transmits frames that + appear on the wire with the terminal's MAC, and receives frames sent to it. +* No firmware routine is involved — the payload is loaded by the COFF loader and + runs from RAM. +* Practical consequence: a payload can now speak any Ethernet protocol it wants + (ARP, IP, a custom one) without the OS's stack, which is the foundation for a + wire-based loader or a faster frame path than the OS's ~20 datagrams/s. diff --git a/harness/udpstream/build/lance_bcast.coff b/harness/udpstream/build/lance_bcast.coff index c6ba0d9..6fc934a 100644 Binary files a/harness/udpstream/build/lance_bcast.coff and b/harness/udpstream/build/lance_bcast.coff differ diff --git a/harness/udpstream/lance_bcast.c b/harness/udpstream/lance_bcast.c index 37ad5c7..129fd8e 100644 --- a/harness/udpstream/lance_bcast.c +++ b/harness/udpstream/lance_bcast.c @@ -255,14 +255,14 @@ int main() /* --- chip init: normal mode, our MAC, accept everything --- */ csr_w(0, CSR0_STOP); - w16(INITB + 0 * 16, 0x8000); /* MODE: PROM, TX/RX enabled */ + w16(INITB + 0 * 16, 0x0000); /* MODE: normal, TX/RX enabled */ w16(INITB + 1 * 16, 0x0008); /* PADR 08:00:2b:16:4b:5a */ w16(INITB + 2 * 16, 0x162b); w16(INITB + 3 * 16, 0x5a4b); - w16(INITB + 4 * 16, 0xFFFF); /* LADRF: all multicast */ - w16(INITB + 5 * 16, 0xFFFF); - w16(INITB + 6 * 16, 0xFFFF); - w16(INITB + 7 * 16, 0xFFFF); + w16(INITB + 4 * 16, 0); /* LADRF: no multicast */ + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); w16(INITB + 8 * 16, (RXR >> 3) & 0xFFFF); w16(INITB + 9 * 16, 3 << 13); /* 8 RX descriptors */ w16(INITB + 10 * 16, (TXR >> 3) & 0xFFFF); @@ -315,23 +315,30 @@ int main() txd = 0; for (n = 0; n < 6; n++) { wb(TXBUF + 14 * 8, n); /* sequence byte */ - w16(TXR + 32, (0x10000 - txlen) & 0xFFFF); /* -len */ - w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + n * 64 + 32, (0x10000 - txlen) & 0xFFFF); /* -len */ + w16(TXR + n * 64 + 16, + TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); csr_w(0, 0x0042 | CSR0_TDMD); /* STRT | INEA | TDMD */ - for (idle = 0; idle < 40000; idle++) { - if (!(r16(TXR + 16) & TMD1_OWN)) { + for (idle = 0; idle < 8000; idle++) { + if (!(r16(TXR + n * 64 + 16) & TMD1_OWN)) { txd = txd + 1; break; } } - for (idle = 0; idle < 120000; idle++) { + for (idle = 0; idle < 20000; idle++) { } } dot(3); /* --- listen: count what arrives, remember the last ethertype and MAC --- */ rxf = 0; lastet = 0; m0 = 0; m1 = 0; m2 = 0; - for (idle = 0; idle < 400000; idle++) { + for (idle = 0; idle < 400000000; idle++) { + if (idle & 1023) { + continue; /* cheap spin; poll every 1024 */ + } + if (rxf >= 5) { + break; /* enough frames: stop early */ + } for (i = 0; i < 8; i++) { unsigned int buf, len; if (r16(RXR + i * 64 + 16) & RMD1_OWN) { @@ -341,14 +348,17 @@ int main() len = r16(RXR + i * 64 + 48) & 0x0FFF; if (len > 0) { rxf = rxf + 1; - lastet = (rb(buf + 12) << 8) | rb(buf + 13); - m0 = (rb(buf + 6) << 8) | rb(buf + 7); - m1 = (rb(buf + 8) << 8) | rb(buf + 9); - m2 = (rb(buf + 10) << 8) | rb(buf + 11); + /* buf is a BYTE address, rb() takes a BIT address */ + lastet = (rb((buf + 12) * 8) << 8) | rb((buf + 13) * 8); + m0 = (rb((buf + 6) * 8) << 8) | rb((buf + 7) * 8); + m1 = (rb((buf + 8) * 8) << 8) | rb((buf + 9) * 8); + m2 = (rb((buf + 10) * 8) << 8) | rb((buf + 11) * 8); } /* re-arm this descriptor */ w16(RXR + i * 64 + 16, RMD1_OWN | ((buf >> 16) & 0xFF)); } + /* publish live counters so a dump *during* the listen window is useful */ + cell(0x160, txd); cell(0x180, rxf); cell(0x200, lastet); cell(0x220, m0); } dot(4); @@ -392,5 +402,9 @@ int main() } draw_text(104, 814, lab_rest); hex4(312, 814, csr0res); } + /* stay here: the panel and the marker cells stay valid for inspection + * (a returned payload lets the OS reuse this memory) */ + for (;;) { + } return 0; } diff --git a/harness/udpstream/sendraw.py b/harness/udpstream/sendraw.py new file mode 100644 index 0000000..88b45b5 --- /dev/null +++ b/harness/udpstream/sendraw.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""sendraw.py -- send raw Ethernet frames from the host. + +Used to exercise the VT1200 payload's receive path without a protocol stack: +the payload counts frames and records the last ethertype and source MAC on its +panel, so a single broadcast frame is enough to prove RX works. + + # MAME: the bridge floods to the enslaved tap + sudo python3 harness/udpstream/sendraw.py --iface vt1200 --count 3 + + # real unit: the host's own NIC + sudo python3 harness/udpstream/sendraw.py --iface eth0 --dst ff:ff:ff:ff:ff:ff + +Options default to a 60-byte broadcast frame with ethertype 0x88B5, which is the +one the payload's panel shows in its ET row. +""" +import argparse +import socket +import struct +import sys +import time + + +def parse_mac(s): + parts = s.split(":") + if len(parts) != 6: + raise argparse.ArgumentTypeError("expected aa:bb:cc:dd:ee:ff") + return bytes(int(p, 16) for p in parts) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--iface", required=True, help="interface to send on") + ap.add_argument("--dst", type=parse_mac, default=parse_mac("ff:ff:ff:ff:ff:ff"), + help="destination MAC (default broadcast)") + ap.add_argument("--src", type=parse_mac, default=None, + help="source MAC (default: the interface's own)") + ap.add_argument("--ethertype", type=lambda v: int(v, 0), default=0x88B5) + ap.add_argument("--size", type=int, default=60, help="frame size from dst onwards") + ap.add_argument("--count", type=int, default=1) + ap.add_argument("--gap", type=float, default=0.2, help="seconds between frames") + args = ap.parse_args() + + src = args.src + if src is None: + with open("/sys/class/net/%s/address" % args.iface) as f: + src = parse_mac(f.read().strip()) + + size = max(args.size, 14) + body = bytes((i & 0xFF) for i in range(size - 14)) + frame = args.dst + src + struct.pack(">H", args.ethertype) + body + + s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) + s.bind((args.iface, 0)) + for i in range(args.count): + s.send(frame) + print("sent %d B on %s: %s -> %s ethertype 0x%04x" + % (len(frame), args.iface, src.hex(":"), args.dst.hex(":"), args.ethertype)) + time.sleep(args.gap) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/harness/udpstream/test-bcast.sh b/harness/udpstream/test-bcast.sh new file mode 100755 index 0000000..f8f0f3a --- /dev/null +++ b/harness/udpstream/test-bcast.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# test-bcast.sh -- run the raw-Ethernet broadcast payload in MAME and report +# what it sent and received. +# +# harness/udpstream/test-bcast.sh [payload.coff] +# +# Everything the earlier manual runs got wrong is handled here: +# * stale MAME is killed with `pkill -x` (a `pkill -f` pattern also matches +# this script's own command line and kills the test itself) +# * the network interface choice lives in harness/mame/cfg-net -- an empty +# cfg dir means MAME never opens a tap, so the LANCE sees no carrier and +# reports TMD3_LCAR +# * the host's tap only has carrier once MAME has attached it, so the script +# waits for LOWER_UP before sending anything +# * host frames are sent while the payload is inside its listen window +# +# Exit status 0 only if the payload reported transmitting and receiving. +set -uo pipefail + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +COFF=${1:-$ROOT/harness/udpstream/build/lance_bcast.coff} +RUN=${RUN:-/tmp/vt1200-bcast} +MARK=0x018D0000 # must match #define MARK in the payload +LOADER_AT=${LOADER_AT:-400} +LOADER_END=${LOADER_END:-2500} +DUMP_AT=${DUMP_AT:-1200} # payload now holds after its panel, so any later frame works +HOST_FRAMES=${HOST_FRAMES:-60} + +[ -x "$M" ] || { echo "no MAME binary at $M (set MAME=...)" >&2; exit 1; } +[ -f "$COFF" ] || { echo "no payload at $COFF" >&2; exit 1; } + +echo "== setting up ==" +pkill -9 -x vt1200 2>/dev/null +sleep 2 +sudo -n ip link set vt1200 up 2>/dev/null + +rm -rf "$RUN" +mkdir -p "$RUN/cfg" "$RUN/nvram" +cp "$ROOT/harness/mame/cfg-net/vt1200.cfg" "$RUN/cfg/" 2>/dev/null \ + || { echo "missing harness/mame/cfg-net/vt1200.cfg (the taptun selection)" >&2; exit 1; } + +TAP=$(ip -o link show 2>/dev/null | sed -n 's/^[0-9]*: \(tap-mess-[^:]*\):.*/\1/p' | head -1) +[ -n "$TAP" ] || { echo "no tap-mess-* interface -- start MAME once so it exists" >&2; exit 1; } +echo " tap: $TAP" + +CAP="$RUN/frames.cap" +sudo -n timeout 70 tcpdump -i "$TAP" -e -n ether proto 0x88b5 > "$CAP" 2>&1 & + +VT1200_SKIP_LEVEL1=1 \ +LOADER_AT=$LOADER_AT LOADER_END=$LOADER_END \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$COFF" LOADER_OUT="$RUN" \ +VT1200_DUMP_RAM=$DUMP_AT VT1200_DUMP_DIR="$RUN" \ +timeout 110 "$M" vt1200 \ + -rompath "$ROOT/harness/mame/roms" -video none -sound none -skip_gameinfo \ + -networkprovider taptun -nvram_directory "$RUN/nvram" -cfg_directory "$RUN/cfg" \ + -debug -debugger none -autoboot_script /tmp/opencode/loader_probe_l3.lua \ + -autoboot_delay 0 > "$RUN/mame.log" 2>&1 & + +echo "== waiting for link ==" +for i in $(seq 1 60); do + if ip -o link show "$TAP" 2>/dev/null | grep -q LOWER_UP; then + echo " carrier up after ${i}s" + break + fi + sleep 1 +done +ip -o link show "$TAP" 2>/dev/null | grep -q LOWER_UP \ + || echo " WARNING: no carrier on $TAP -- the chip will report LCAR" + +echo "== waiting for the payload to start (frame $LOADER_AT ~ $((LOADER_AT / 60))s) ==" +sleep $((LOADER_AT / 60 + 2)) + +echo "== sending $HOST_FRAMES host broadcast frames ==" +for i in $(seq 1 "$HOST_FRAMES"); do + sudo -n python3 "$ROOT/harness/udpstream/sendraw.py" --iface vt1200 --count 1 --gap 0 >/dev/null 2>&1 + sleep 0.25 +done + +echo "== waiting for the payload to finish ==" +sleep 8 +pkill -9 -x vt1200 2>/dev/null + +echo "== results ==" +DUMP=$(ls -1 "$RUN"/vt1200-ram-*.bin 2>/dev/null | head -1) +if [ -n "$DUMP" ]; then + python3 - "$DUMP" "$MARK" <<'PY' +import struct, sys +d = open(sys.argv[1], 'rb').read() +base = int(sys.argv[2], 16) >> 3 +def cell(bitoff): # cell() offsets are BIT offsets in the payload + o = base + (bitoff >> 3) + return struct.unpack(" 0 and rxf > 0) else 1) +PY + RC=$? +else + echo " no RAM dump in $RUN" + RC=1 +fi + +CAPN=$(grep -c 88b5 "$CAP" 2>/dev/null) +[ -n "$CAPN" ] || CAPN=0 +echo " payload broadcasts seen on $TAP: $CAPN" +[ "$CAPN" -gt 0 ] || RC=1 + +echo +if [ "$RC" = 0 ]; then echo "== PASS =="; else echo "== FAIL =="; fi +echo " log: $RUN/mame.log" +echo " pcap: $CAP" +exit $RC