mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-31 12:05:05 +02:00
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.
This commit is contained in:
1 parent
dfc968e906
commit
f251672d5d
5 files changed
+271
-15
No files matched your search
@@ -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.
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -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())
|
||||
Executable
+121
@@ -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("<I", d[o: o + 4])[0]
|
||||
txd, rxf, ok, et, m0 = cell(0x160), cell(0x180), cell(0x1A0), cell(0x200), cell(0x220)
|
||||
print(" TXD (frames transmitted) = %d" % txd)
|
||||
print(" RXF (frames received) = %d" % rxf)
|
||||
print(" last ethertype = 0x%04X" % et)
|
||||
print(" last source MAC hi word = 0x%04X" % m0)
|
||||
print(" ok = %d" % ok)
|
||||
unwritten = (txd == 0x55555555 or rxf == 0x55555555)
|
||||
if unwritten:
|
||||
print(" markers not written (dump landed outside the run)")
|
||||
sys.exit(0 if (not unwritten and txd > 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
|
||||
Reference in new issue
Block a user