From f251672d5d15e4bfeebc1b781c0fe7d010c499eb Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Fri, 30 Oct 2026 16:21:02 +0200 Subject: [PATCH] udpstream: payload sends and receives raw Ethernet with its own LANCE driver lance_bcast.c drives the chip entirely on its own -- init block, PADR/LADRF, eight TX and eight RX descriptors, INIT/IDON/STRT, TDMD, completion polling -- and test-bcast.sh runs it in MAME and reports: TXD=6 RXF=5 ET=0x88B5 source MAC 0xC6FC ok=1 PASS The tap capture separates the directions: six 60-byte 0x88b5 broadcasts from 08:00:2b:16:4b:5a (our payload) and 49 from the host. RXF counts host frames (early stop at five) and the recorded source MAC is the host's. Fixes behind it: per-frame TX descriptor (the chip advances its own ring position), cfg-net needed for a tap to exist at all, pkill -x instead of -f, and two bit-vs-byte addressing bugs. --- harness/mame/NETWORK_REPORT.md | 56 +++++++++++ harness/udpstream/build/lance_bcast.coff | Bin 6197 -> 6301 bytes harness/udpstream/lance_bcast.c | 44 ++++++--- harness/udpstream/sendraw.py | 65 ++++++++++++ harness/udpstream/test-bcast.sh | 121 +++++++++++++++++++++++ 5 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 harness/udpstream/sendraw.py create mode 100755 harness/udpstream/test-bcast.sh 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 c6ba0d9730f47446d0ef68e96aa7471018286fe1..6fc934ace9c441df1200a9611c7e95896f9112d7 100644 GIT binary patch delta 935 zcmZvbL1+^}6o%hy8`E?t*^s(9G>4d`tzrljp%+C1Mn$k7LaETh&YG^FnzoSaK~YGc zha#-eIA_UftWm3lCY2mr@X%-vO)vH&<{;EV<594P>&(_{Q*~f==Y9XYf94G{Z-yTo z%RGQiSbF#6`biPs5CHh019hQQz>74XO}`6JFK7rrLwHd`5mmXVT1tyi-HP`<0XmFK z%BoENEQfWbvNhUU6d<2*b+xAC^FDG~>{(v9(@jSB%cr#Q9NBh7vj}oT>H(yo_2MqO z^dE-*VwI14xC2d-4)@V;sfuAWG{gty5PW;s^4fO698m9;Dp)GH==R6@jP>GvEcG<# z2ArewyqPi018b$K0YgHwY(TeBsz$AG6xirI|F+SnY2zf~6@8zVzt@-%n~JH7sr_%j610nF@)Z>B2HL84 zy=M?-8kH-(W#p(A0A3Ndc#y-3WKcZE)eB@s4DLojvLcRTa7jb#8gU?{6nRpjck8)L zCY7XoO9H%LlW94boQ+HPsZG*KQi-b)eqocVQ!|#&W1FOB<2MvKM{f~!Upr~X7oN0` zbS{%6yc&xl2d6_nUU}_o1|4arLxCF%D&(ryY#!&w7(S2s!y@s4%zXY F_zmEhDKr28 delta 957 zcmZ{jPiPZC6vp4AP13l=Y-nwxjW9-{NTsw0f(RP$9|{#wOYpQwlQmRh14#~whZJ&% z!dg-X#6zm9G2M8G)C9@V1rdV^Q4r5T6fcQ4g{x%N9&fvU5hJ|MgJag-y6`GZF6H;#8a-vn7s!`Up9Poc%tyRqhcfu7gjjJ|KuC(=s6Z>OWRj>D z58o63KQYPma6F!f2>7u{Qes?;qy@ZclDYW>-RGf6Qi;elk!n^9!cI3x8$R>MK*E)z zD6;U1Nh0w?iXtqVBtD-?iwgoSnk1DDC+T~liS6|G_Ohq2kxegg30d^~MDs> 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