From 22c539bf74b177e8bfe63a70002364a03c0d6baf Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Sat, 31 Oct 2026 01:25:14 +0000 Subject: [PATCH] fix(stream): ring ownership window, FCS-error accounting, unit loader procedure --- harness/mame/run-vidstream.sh | 9 +- harness/mame/vidstream.lua | 70 ++++++++++++- harness/udpstream/frame.py | 6 +- harness/udpstream/frame_parse.h | 13 +++ harness/udpstream/test-stream.sh | 116 ++++++++++++++++++++-- harness/udpstream/tests/test_vidsrc.py | 19 +++- harness/udpstream/vidsend.py | 9 +- harness/udpstream/vidsrc.py | 32 +++++- harness/udpstream/vidstream.c | 130 ++++++++++++++++++++----- 9 files changed, 353 insertions(+), 51 deletions(-) diff --git a/harness/mame/run-vidstream.sh b/harness/mame/run-vidstream.sh index 2034c18..80217bf 100755 --- a/harness/mame/run-vidstream.sh +++ b/harness/mame/run-vidstream.sh @@ -28,10 +28,11 @@ # plus the completed descriptor, and the wire path needs the unit. # # Mode ring asserts the transport's structure: 16 descriptors armed with OWN set -# for slot A, a completed frame moving them to slot B, and a header claiming -# more than SLOT_BYTES rejected without a blit (RX_BAD moves, RX_OK does not, -# the screen does not change). The verdict is in the Lua; the evidence is -# $LOGDIR/ring-status.txt. +# for slot A, a completed frame moving them to slot B, a header claiming more +# than SLOT_BYTES rejected without a blit (RX_BAD moves, RX_OK does not, the +# screen does not change), and a datagram the chip flags bad (RMD3 = 0, so it +# carries no bytes) counted in RX_FCS_DROP with its frame abandoned. The verdict +# is in the Lua; the evidence is $LOGDIR/ring-status.txt. # # The run uses VT1200_SKIP_LEVEL1=1: the payload's heap is only initialised in # the OS phase. In the diagnostics menu the free counter (0x1E8BA0) and the diff --git a/harness/mame/vidstream.lua b/harness/mame/vidstream.lua index dd784d6..fe63cf4 100644 --- a/harness/mame/vidstream.lua +++ b/harness/mame/vidstream.lua @@ -163,6 +163,7 @@ local RX_RING_ACC = MARK_PAGE + 0x18 * 8 local RX_RING_ARMED = MARK_PAGE + 0x1c * 8 local RX_RING_DROPS = MARK_PAGE + 0x40 * 8 local RX_DGRAM = MARK_PAGE + 0x48 * 8 +local RX_FCS_DROP = MARK_PAGE + 0x4c * 8 -- Task 5: the OS-repaint canary (frame_parse.h, ruling R12). local OS_TOUCH = MARK_PAGE + 0x44 * 8 @@ -188,6 +189,8 @@ local ETH_FCS = 4 local RMD1_OWN = 0x8000 local RMD1_ENP = 0x0100 local RMD1_STP = 0x0200 +local RMD1_CRC = 0x0800 +local RMD1_ERR = 0x4000 -- The frame every mode delivers first: 8 rows of 1280 pixels, 1 bpp MSB-first -- (160 bytes a row), each row repeated 8 times. 1280 B of payload -- the same @@ -493,6 +496,23 @@ local function complete_dgram(ringb, base, idx, len) return true end +-- Complete descriptor `idx` as the chip does for a packet whose FCS failed: +-- am79c90.cpp's recv_complete_cb() writes RMD3 = 0 when RMD1_ERR is set +-- (`(m_rx_md[1] & RMD1_ERR) ? 0 : (result & RMD3_MCNT)`), and RMD1 carries +-- ERR | CRC on top of the STP | ENP a complete datagram has. The buffer still +-- holds the bytes the DMA wrote -- that is the whole point: the payload must +-- notice the datagram even though the chip gave it no length. +local function complete_dgram_err(ringb, base, idx) + if (rmd1_of(ringb, idx) & RMD1_OWN) == 0 then + return false + end + local byte = base // 8 + idx * RXBUFLEN + prog:write_u16(rmd_bit(ringb, idx, 6), 0) + prog:write_u16(rmd_bit(ringb, idx, 2), + RMD1_STP | RMD1_ENP | RMD1_ERR | RMD1_CRC | ((byte >> 16) & 0xFF)) + return true +end + -- Deliver a whole VS frame (16-byte header + payload, `flen` bytes) as -- datagrams of at most `chunk` bytes, in ring order from RX_RING_POS. Returns -- the number of datagrams delivered, or nil when the ring is not ready or a @@ -856,9 +876,11 @@ end -- The structural check of the payload's own receive ring. Nothing here looks at -- pixels -- draw mode owns those. What this mode establishes is the transport's -- contract: 16 descriptors armed for one slot, a completed frame moving them to --- the other slot, and a frame larger than a slot rejected without a blit. That --- is as far as an emulator with no network interface (R11) can go; on the unit --- the same descriptors are filled by the chip's DMA instead of by this script. +-- the other slot, a frame larger than a slot rejected without a blit, and a +-- datagram the chip flags bad (RMD3 = 0, RMD1_ERR) counted as a drop and its +-- frame abandoned rather than lost silently. That is as far as an emulator with +-- no network interface (R11) can go; on the unit the same descriptors are filled +-- by the chip's DMA instead of by this script. local RING_WAIT = tonumber(os.getenv("VID_RING_WAIT") or "300") local inspect_pool -- defined below, after the dumpers @@ -968,6 +990,7 @@ local function dump_ring(timed_out) string.format("RX_SLOT=%d", prog:read_u32(RX_SLOT)), string.format("RX_DGRAM=%d", prog:read_u32(RX_DGRAM)), string.format("RX_RING_DROPS=%d", prog:read_u32(RX_RING_DROPS)), + string.format("RX_FCS_DROP=%d", prog:read_u32(RX_FCS_DROP)), string.format("RX_RING_ACC=%d", prog:read_u32(RX_RING_ACC)), string.format("VRAM_ROW0_BEFORE=%s", ring_vram0 and hex_bytes(ring_vram0) or "-"), string.format("VRAM_ROW0_AFTER=%s", hex_bytes(read_vram_head(32))), @@ -1082,6 +1105,47 @@ local function ring_step() ring_armed_for(a, "slot A after the rejection") ring_check(prog:read_u32(RX_RING_DROPS) == 0, "no frame was dropped for want of descriptors") + -- Phase 3: one datagram the chip flagged bad (FCS). The chip gives + -- it no length at all (RMD3 = 0), so the payload can only see it + -- through RMD1_ERR -- and it must count it as a drop, or a + -- body-corrupting packet would vanish from both counters the gate + -- is made of (drops and crc_err). + local abase, apos = ring_state() + local eidx = apos + local ebit = abase + eidx * RXBUFLEN * 8 + local ef = make_frame(1, 0x0091, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN, + draw_row_byte) + poke_packet(ebit, ef, 0, DRAW_CHUNK) + ring_check(complete_dgram_err(ring_ringb, abase, eidx), + string.format("the FCS-error datagram was completed at descriptor %d, RMD3 = 0", eidx)) + ring_phase = 3 + ring_phase_at = frames + return false + end + + if ring_phase == 3 then + if prog:read_u32(RX_FCS_DROP) < 1 then + if frames >= ring_phase_at + RING_WAIT then + print(string.format("VID: timed out waiting for the FCS-error datagram to be counted (RX_FCS_DROP=%d RX_RING_ACC=%d RX_OK=%d)", + prog:read_u32(RX_FCS_DROP), + prog:read_u32(RX_RING_ACC), prog:read_u32(RX_OK))) + dump_ring(true) + return true + end + return false + end + ring_check(prog:read_u32(RX_FCS_DROP) == 1, + "RX_FCS_DROP counts the datagram the chip flagged bad") + ring_check(prog:read_u32(RX_RING_ACC) == 0, + "the FCS error abandoned its frame: nothing is left in progress") + ring_check(prog:read_u32(RX_OK) == 1, + "RX_OK did not move: the errored datagram painted nothing") + ring_check(prog:read_u32(RX_RING_ARMED) == 1, + "the FCS error re-armed the ring on slot B") + ring_armed_for(b, "slot B after the FCS error") + local after = read_vram_head(32) + ring_check(ring_vram0 ~= nil and after == ring_vram0, + "the screen did not change: the errored datagram was not blitted") local pool_ok = inspect_pool() ring_check(pool_ok, "the startup allocation is still the one under test") dump_ring(false) diff --git a/harness/udpstream/frame.py b/harness/udpstream/frame.py index 7481895..cdf0dd7 100644 --- a/harness/udpstream/frame.py +++ b/harness/udpstream/frame.py @@ -45,7 +45,11 @@ ordinary frame: the same 16-byte header, the same header-only CRC, with 4 2 crc_err, u16 little-endian (frames it rejected: header CRC, magic, version or length -- since R14 a *body* error is the LANCE's FCS and can never appear here) - 6 2 drops, u16 little-endian (missed-packet events, CSR0 MISS) + 6 2 drops, u16 little-endian (frames that arrived and did not + reach the screen: CSR0 MISS report windows -- the bit is + latched, so two misses in one window count once -- plus + frames too large for the ring, plus datagrams the chip + flagged bad, which carry no bytes at all) 8 2 csr0, u16 little-endian (the chip's status word when the report was built) 10 2 heap_free_kb, u16 little-endian (the terminal's free heap) diff --git a/harness/udpstream/frame_parse.h b/harness/udpstream/frame_parse.h index a0d69ac..7a85d32 100644 --- a/harness/udpstream/frame_parse.h +++ b/harness/udpstream/frame_parse.h @@ -45,6 +45,9 @@ * RX_RING_ARMED +0x0E0 0x018100E0 slot the ring is armed on (0=A) * RX_RING_DROPS +0x200 0x01810200 frames too big for the ring * RX_DGRAM +0x240 0x01810240 datagrams consumed + * RX_FCS_DROP +0x260 0x01810260 datagrams the chip flagged bad + * (no bytes delivered; each + * abandons its frame) * POOL_PTR +0x00 0x01810100 bit pointer to the 244 KB pool * POOL_FREEB +0x20 0x01810120 heap free bytes before malloc * POOL_FREEA +0x40 0x01810140 heap free bytes after malloc @@ -55,6 +58,15 @@ * RX_SLOT +0xE0 0x018101E0 slot of the last accepted (0=A) * OS_TOUCH +0x220 0x01810220 times the VRAM canary was not ours * + * NOTE ON OWNERSHIP OF THE PAGE: nothing allocates this page. The firmware's + * large free block starts at byte 0x0012191A and grows upward, the page sits at + * byte 0x00302000 inside it and .text starts at byte 0x00318000 just above, so + * an allocator asked for enough memory -- or a long soak of small allocations -- + * can hand these cells to someone else and the markers will be overwritten + * under the payload. Read them WHILE the payload runs (a live debugger peek, + * or a dump taken during the run); a marker page read after a soak is a claim + * about whatever now owns the memory, not about the payload. + * * OS_TOUCH is Task 5's OS-repaint canary (design spec 8.9): the blit leaves one * 16-bit sentinel word in the unused right edge of the last screen row, and * OS_TOUCH counts how often that word was not what the payload left there. It @@ -158,6 +170,7 @@ #define RX_RING_ARMED MARK_CELL(0x1c) #define RX_RING_DROPS MARK_CELL(0x40) #define RX_DGRAM MARK_CELL(0x48) +#define RX_FCS_DROP MARK_CELL(0x4c) #define RX_SLOT_A MARK_CELL(0x60) #define RX_SLOT_B MARK_CELL(0x64) diff --git a/harness/udpstream/test-stream.sh b/harness/udpstream/test-stream.sh index 5ac5324..d0c1850 100755 --- a/harness/udpstream/test-stream.sh +++ b/harness/udpstream/test-stream.sh @@ -12,7 +12,55 @@ # the destination address frames are sent to: the terminal's own # 08:00:2b:16:4b:5a, or broadcast, which its LANCE accepts with an empty filter. # [rows] and [stretch] are the strip geometry (default 128 x 8 = the whole -# screen, ruling R3); rows*stretch must be 1024 or the encoder refuses. +# screen, ruling R3); rows*stretch must be 1024 or the encoder refuses, and rows +# may not exceed 144 (the terminal's 16-datagram ring). +# +# =========================================================================== +# BEFORE ANY OF THIS: THE PAYLOAD MUST ALREADY BE RUNNING ON THE TERMINAL. +# =========================================================================== +# This script drives the wire; it installs nothing. Getting vidstream onto the +# unit is the operator's step, and it is the loader route this tree actually +# runs (dwt/notes/LOADER.md, dwt/notes/LOADER_CROSS.md): +# +# 1. Build the linked image the loader consumes: +# harness/udpstream/build-stream.sh +# -> harness/udpstream/build/vidstream.coff (plus .bin, .map, .env) +# +# 2. Put it in the terminal's RAM and start it as a process, over the +# terminal's own debugger service (rdebug, TCP 771): +# python3 dwt/loader/hostload.py --host \ +# --coff harness/udpstream/build/vidstream.coff +# hostload.py mallocs a buffer, pokes the COFF into it, pokes the loader at +# 0x01880000 and jumps there; the loader places the sections from their COFF +# headers and calls create_process(entry, "LOADER", ...), so the payload +# becomes a scheduled process. The TFTP variant fetches the same image +# instead of poking it -- the terminal pulls the file, the host serves it: +# sudo python3 -u dwt/tcpip/serve_probe.py --bootp \ +# --file harness/udpstream/build/vidstream.coff +# python3 dwt/loader/hostload.py --host --tftp \ +# --coff harness/udpstream/build/vidstream.coff +# +# 3. THE LOAD IS ONE SHOT. The payload's lance_start() STOPs the LANCE, loads +# its own init block, INITs and STRTs it -- the chip belongs to the payload +# from then on, so the OS's network stack (and with it the rdebug session +# that loaded it) is dead. If the jump fails there is no second attempt +# over the wire, only a power cycle, and nothing about the payload can be +# inspected over rdebug afterwards. +# +# 4. What stays observable is the wire. The payload answers with a +# KIND_STATUS frame every 64 accepted frames, so this script's PREFLIGHT +# sends PROBE frames (default 70) and refuses to run the measurement unless +# one comes back. No status frame means the payload is not running (or the +# cable/interface is wrong), and the script exits 4 saying that instead of +# sending 300 frames into nothing and reporting "unmeasured" later. +# +# THE malloc CONTEXT IS UNPROVEN ON THE UNIT. Every run to date enters the +# payload by a debugger/loader jump, and hostload.py's own malloc call is made in +# that jumped context; whether malloc_() from inside the payload's +# create_process child sees the same free list has not been shown on the +# hardware. The markers that would answer it are POOL_PTR, POOL_FREEB and +# POOL_FREEA (bit 0x01810100, 0x01810120, 0x01810140), read WHILE the payload +# runs -- that page is unowned heap and a long soak can reuse it (frame_parse.h). # # THE REPORTING LAG. The terminal emits a status frame every 64 ACCEPTED frames # (STATUS_EVERY in vidstream.c), and its `frame_no` field is that report's own @@ -40,10 +88,12 @@ # The sender is run with --report-every 1 so a status frame can be paired with # the host's sent count to within one frame; only every 30th row is printed. # -# Exit status: 0 only if a status arrived, at least two reports could be paired, -# the end-of-run gap is inside the lag budget, and vidsend.py itself passed (it -# fails when the last status reports drops or crc_err). 2/3 mean a missing -# prerequisite (arguments, interface, root, sender); 1 means unmeasured or loss. +# Exit status: 0 only if the preflight saw the payload, a status arrived, at +# least two reports could be paired, the end-of-run gap is inside the lag +# budget, and vidsend.py itself passed (it fails when the last status reports +# drops or crc_err). 2/3 mean a missing prerequisite (arguments, interface, +# root, sender); 4 means the payload did not answer the preflight -- it is not +# running, and nothing was measured. # # WHAT THIS CANNOT SHOW FROM HERE: OS_TOUCH (spec 8.9's VRAM canary, marker bit # 0x01810220) and the terminal's free heap before the run are on the terminal, @@ -104,6 +154,43 @@ mkdir -p "$(dirname "$OUT")" echo "test-stream.sh: $IFACE -> $MAC, $ROWS rows x $STRETCH, $FRAMES frames at $FPS fps" >&2 echo "test-stream.sh: capture $OUT" >&2 +# --------------------------------------------------------------------------- +# Preflight: is the payload running at all? Its only wire-visible trace is the +# status frame it builds every 64 accepted frames, so the check is a short run +# of PROBE frames: STATUS_EVERY + 6 covers the cadence and the mid-run report. +# Without this a 300-frame run against a powered-off payload would spend ten +# seconds to report "no status frame arrived", which reads like a measurement. +# --------------------------------------------------------------------------- +PROBE=${PROBE:-70} +PROBE_LOG=$OUT.preflight +echo "test-stream.sh: preflight: $PROBE frames, looking for a status frame" >&2 +set +e +python3 "$SENDER" --iface "$IFACE" --dst "$MAC" --rows "$ROWS" \ + --stretch "$STRETCH" --fps "$FPS" --frames "$PROBE" \ + --report-every 0 --status-wait 5 > "$PROBE_LOG" 2>&1 +PROBE_STATUS=$? +set -e +if ! grep -q '^status ' "$PROBE_LOG"; then + echo "test-stream.sh: PAYLOAD NOT RUNNING: $PROBE frames went out and no" \ + "status frame came back, so nothing on the wire is running vidstream" \ + "(or the NIC/MAC is wrong -- check '$IFACE' and '$MAC')." >&2 + echo " Load it first: harness/udpstream/build-stream.sh, then" >&2 + echo " python3 dwt/loader/hostload.py --host --coff" \ + "harness/udpstream/build/vidstream.coff (see this script's header);" >&2 + echo " the load is one shot because the payload takes the LANCE over." >&2 + echo " preflight log: $PROBE_LOG" >&2 + sed -n '1,5p' "$PROBE_LOG" >&2 + exit 4 +fi +if [ "$PROBE_STATUS" -ne 0 ]; then + echo "test-stream.sh: the payload answered but the preflight already" \ + "failed (see $PROBE_LOG): a run that starts with drops or crc_err" \ + "cannot measure anything." >&2 + grep -E '^(FAIL|PASS|status )' "$PROBE_LOG" | tail -5 >&2 + exit 1 +fi +echo "test-stream.sh: preflight: the payload is running" >&2 + set +e python3 "$SENDER" --iface "$IFACE" --dst "$MAC" --rows "$ROWS" \ --stretch "$STRETCH" --fps "$FPS" --frames "$FRAMES" \ @@ -121,10 +208,17 @@ fi # runs with --report-every 1, so `sent` is exact to within the one frame that is # in flight when a status is read. set +e -python3 - "$OUT" "$REPORT" <<'PY' +python3 - "$OUT" "$REPORT" "$PROBE" <<'PY' import re, sys -path, every = sys.argv[1], int(sys.argv[2]) +# The third argument is the number of frames the PREFLIGHT sent. The terminal's +# frames_rx keeps counting across it, so the CSV's `sent` column has to start +# from the same place: without the offset the first rows would compare a +# main-run `sent` against a frames_rx that already includes 70 probe frames and +# print deliveries well over 100 %. Both columns are then cumulative over +# probe + run, so every gap and every interval delta is measured between the +# same two events, which is all the pairing needs. +path, every, sent0 = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]) row = re.compile(r"^(\d+),([\d.]+),([\d.]+),(\d+)$") status = re.compile( r"^status (\d+): frame_no=(\d+) frames_rx=(\d+) crc_err=(\d+) " @@ -145,8 +239,12 @@ with open(path, "r", encoding="utf-8", errors="replace") as f: line = line.rstrip("\n") m = row.match(line) if m: - sent = int(m.group(1)) - if every > 0 and sent % every == 0: + n = int(m.group(1)) + sent = n + sent0 + # The 30-frame cadence is the SENDER's frame number, not the + # cumulative one: with a probe offset the cumulative value is never + # a multiple of 30, and no row would ever print. + if every > 0 and n % every == 0: rows.append((sent, m.group(2), m.group(3), m.group(4), last)) continue m = status.match(line) diff --git a/harness/udpstream/tests/test_vidsrc.py b/harness/udpstream/tests/test_vidsrc.py index 94a5584..9c7932a 100644 --- a/harness/udpstream/tests/test_vidsrc.py +++ b/harness/udpstream/tests/test_vidsrc.py @@ -13,7 +13,8 @@ of the CRC. import pytest from harness.udpstream.frame import HEADER_SIZE, KIND_STRIP, unpack_header -from harness.udpstream.vidsrc import Rows, StripSource +from harness.udpstream.vidsrc import ( + MAX_ROWS, RING_DATAGRAMS, Rows, StripSource) #: Largest UDP payload the transport promises per packet. CHUNK = 1450 @@ -138,3 +139,19 @@ def test_strip_source_rejects_rows_that_do_not_fit_the_header(): StripSource(rows=0, stretch=128) with pytest.raises(ValueError): StripSource(rows=256, stretch=4) + + +def test_strip_source_caps_rows_at_the_ring_not_the_header_field(): + """The terminal's 16-datagram ring, not the u8 ``rows`` field, is the limit. + + 144 rows is 23,040 body bytes: 16 datagrams of 1450 with the 16-byte header + in the first. A 145th row would need a 17th datagram, which the receiver + drops (RX_DESC in vidstream.c), so the encoder must refuse it here -- with a + message that names the cap -- rather than put an unreceivable frame on the + wire. + """ + src = StripSource(rows=MAX_ROWS, stretch=8) + pkts = src.encode(frame_no=1, rows_1bpp=bytes(MAX_ROWS * 160)) + assert len(pkts) == RING_DATAGRAMS == 16 + with pytest.raises(ValueError, match=str(MAX_ROWS)): + StripSource(rows=MAX_ROWS + 1, stretch=8) diff --git a/harness/udpstream/vidsend.py b/harness/udpstream/vidsend.py index 83b4c6f..9c3d36c 100644 --- a/harness/udpstream/vidsend.py +++ b/harness/udpstream/vidsend.py @@ -49,7 +49,8 @@ if __package__ in (None, ""): # run as a script, not a module from harness.udpstream.frame import ( # noqa: E402 BROADCAST, ETHERTYPE, HEADER_SIZE, KIND_STATUS, MAGIC, STATUS_SIZE, ethernet_frame, unpack_header, unpack_status) -from harness.udpstream.vidsrc import CHUNK, ROW_BYTES, StripSource # noqa: E402 +from harness.udpstream.vidsrc import ( # noqa: E402 + CHUNK, MAX_ROWS, ROW_BYTES, StripSource) __all__ = [ "CSV_HEADER", "Sender", "bars", "split", "PATTERNS", @@ -432,9 +433,9 @@ def main(argv=None) -> int: StripSource(args.rows, args.stretch) except ValueError as e: print("vidsend.py: bad geometry: %s\n" - " rows*stretch must be 1024 for a full screen: 128x8 is the " - "ruled default (R3); 80 rows cannot fill it." % e, - file=sys.stderr) + " for a full screen rows*stretch must be 1024 (128x8 is the " + "ruled default, R3) and rows may not exceed %d, the terminal's " + "ring ceiling." % (e, MAX_ROWS), file=sys.stderr) return 2 if args.dry_run: diff --git a/harness/udpstream/vidsrc.py b/harness/udpstream/vidsrc.py index 5575d65..82a8019 100644 --- a/harness/udpstream/vidsrc.py +++ b/harness/udpstream/vidsrc.py @@ -24,10 +24,11 @@ on the terminal's CPU. ``pack_header`` is still called with Pure standard library; no dependency on any host-side capture stack. """ -from harness.udpstream.frame import KIND_STRIP, pack_header +from harness.udpstream.frame import HEADER_SIZE, KIND_STRIP, pack_header __all__ = [ - "CHUNK", "ROW_BYTES", "ROW_PIXELS", "SCREEN_ROWS", "Rows", "StripSource", + "CHUNK", "MAX_ROWS", "ROW_BYTES", "ROW_PIXELS", "SCREEN_ROWS", "Rows", + "StripSource", ] #: Width of the terminal's line buffer, in pixels, and the row it packs to. @@ -40,6 +41,20 @@ SCREEN_ROWS = 1024 #: Largest datagram this transport sends, header included. CHUNK = 1450 +#: Datagrams in the terminal's RX ring (``RX_DESC`` in vidstream.c). A frame +#: that needs more cannot be received at all: the ring runs out of descriptors +#: and the payload drops it. +RING_DATAGRAMS = 16 + +#: Most source rows one frame can carry, from the ring rather than the header +#: field: the first datagram holds the 16-byte header plus a body, the other 15 +#: carry body only, so the ring holds +#: ``(RING_DATAGRAMS-1)*CHUNK + (CHUNK-HEADER_SIZE)`` body bytes = 144 rows of +#: 160. The 24 KB slot alone would allow 153, and the u8 ``rows`` field 255 -- +#: neither is reachable, because the ring is what binds. +MAX_ROWS = (((RING_DATAGRAMS - 1) * CHUNK + (CHUNK - HEADER_SIZE)) + // ROW_BYTES) + class Rows: """Pack one 1-bpp source row into the bytes the blitter reads.""" @@ -69,7 +84,9 @@ class StripSource: ``rows`` is how many unique source rows a frame carries; ``stretch`` is how many destination scanlines each of them paints, so it must divide :data:`SCREEN_ROWS` or the last row would stop short of the screen bottom. - Both must fit their one-byte header fields. + ``rows`` is capped by the terminal's ring, not by its one-byte header field: + :data:`MAX_ROWS` (144) rows already fill all 16 datagrams, and a frame that + needs a 17th is dropped by the receiver. """ def __init__(self, rows: int, stretch: int): @@ -80,8 +97,13 @@ class StripSource: raise ValueError("stretch %d does not divide %d; a row would paint " "a fractional number of scanlines" % (stretch, SCREEN_ROWS)) - if not isinstance(rows, int) or not 1 <= rows <= 0xFF: - raise ValueError("rows out of range 1..255: %r" % (rows,)) + if not isinstance(rows, int) or not 1 <= rows <= MAX_ROWS: + raise ValueError("rows out of range 1..%d: %r (the terminal's ring " + "is %d datagrams of %d bytes, so a frame can carry " + "at most %d rows of %d bytes; the header's one-byte " + "field allows 255 but the ring does not)" + % (MAX_ROWS, rows, RING_DATAGRAMS, CHUNK, + MAX_ROWS, ROW_BYTES)) self.rows = rows self.stretch = stretch diff --git a/harness/udpstream/vidstream.c b/harness/udpstream/vidstream.c index edff933..08ff25a 100644 --- a/harness/udpstream/vidstream.c +++ b/harness/udpstream/vidstream.c @@ -133,12 +133,15 @@ * position the chip owns is not something this payload can do reliably. * * Task 7 arms the receiver (MODE = 0, so CSR0's STRT sets RXON) against those - * 16 descriptors. `drops` in the status body stays the chip's own counter -- - * CSR0's MISS bit, one per missed-packet event, cleared after each report -- - * plus the payload's own count of frames the ring was too small to finish - * (RX_RING_DROPS). Both mean "a frame that arrived and did not reach the - * screen"; the chip's field is not replaced, and the payload's own count is - * added to it rather than invented. + * 16 descriptors. `drops` in the status body is three counts, all meaning "a + * frame that arrived and did not reach the screen": the chip's CSR0 MISS bit, + * counted once per report window in which it was latched (the bit cannot + * distinguish two misses in one window, and send_status() says so), plus the + * payload's own count of frames the ring was too small to finish + * (RX_RING_DROPS), plus the datagrams the chip flagged bad (RX_FCS_DROP) -- + * those deliver no bytes at all, so without the third count a body-corrupting + * packet would be invisible. The chip's field is not replaced, and the + * payload's own counts are added to it rather than invented. */ #define LANCE_RAP 0x05900000 /* bit address of the register-select port */ #define LANCE_RDP 0x05800000 /* bit address of the data port */ @@ -168,6 +171,11 @@ #define RMD1_OWN 0x8000 /* the chip owns the descriptor */ #define RMD1_ENP 0x0100 /* end of packet */ #define RMD1_STP 0x0200 /* start of packet */ +#define RMD1_BUFF 0x0400 /* buffer (descriptor) error */ +#define RMD1_CRC 0x0800 /* CRC/FCS error */ +#define RMD1_OFLO 0x1000 /* overflow error */ +#define RMD1_FRAM 0x2000 /* framing error */ +#define RMD1_ERR 0x4000 /* any of the above: the datagram is bad */ #define ETH_FCS 4 /* the chip stores this after the frame */ @@ -226,7 +234,7 @@ static unsigned int status_buf; /* bit address of the status frame */ static unsigned int lance_txslot; /* next TX descriptor to arm */ static unsigned int status_seq; /* reports sent, the status frame_no*/ static unsigned int status_seen; /* frames_rx at the last report */ -static unsigned int drops_total; /* CSR0 MISS events, cumulative */ +static unsigned int drops_total; /* CSR0 MISS windows, lower bound on misses */ /* * Task 7 receive state. @@ -253,6 +261,10 @@ static unsigned int drops_total; /* CSR0 MISS events, cumulative */ * RX_RING_DROPS counts frames abandoned because the ring ran out of descriptors * before the frame completed: a frame larger than 16 datagrams cannot be * received at all, so it is dropped and reported rather than half-painted. + * RX_FCS_DROP counts datagrams the chip flagged bad (RMD1_ERR) whose ethertype + * says they are ours: they deliver no bytes at all -- the chip writes RMD3 = 0 + * for them -- so without this cell a body-corrupting packet would be invisible + * to both crc_err and drops. Each one abandons the frame it belongs to. */ static unsigned int rx_armed; static unsigned int rx_pos; @@ -437,10 +449,22 @@ static unsigned int slot_base[2]; * am79c90.h) but uses a descriptor's buffer address as written, in RMD1 bits * 0-7 (high byte) and RMD0 (low 16). NetBSD's am7990_rint() re-arms a used * descriptor with "length first, ownership last" and leaves the address alone; - * here the address changes on every frame (the slot alternates), so RMD0 is - * written too -- and before RMD1, because a descriptor with OWN set and a stale - * RMD0 would DMA the next packet into the other slot, which is the one being - * painted. + * here the address changes on every frame (the slot alternates), so the whole + * descriptor is rewritten, and RMD1 is written FIRST with OWN CLEAR. + * + * The order is not cosmetic: the chip fetches RMD1 BEFORE the address. In + * am79c90.cpp's receive path it reads `m_rx_md[1] = m_dma_in_cb(ring_address | + * 2)` and only then `m_rx_md[0] = m_dma_in_cb(ring_address | 0)` and + * `m_rx_md[2] = ... | 4`. With OWN set in the same write that carries the new + * high byte, a fetch inside the window pairs the NEW slot's high byte with the + * PREVIOUS slot's RMD0 -- a mixed buffer address. That is harmless only while + * both slots share a 64 KB page; on the unit a 48 KB slot pair can cross one, + * and the chip would then DMA into memory outside the pool. + * + * Publishing the address and length under OWN clear closes it: a fetch inside + * the window reads OWN clear and the packet is a missed packet + * (recv_complete_cb() case -2 latches CSR0 MISS), so it is counted and dropped + * instead of being written somewhere wrong. OWN is then set in the last write. * * RMD2 is the buffer size as a negative two's complement count (0x10000 - * RXBUFLEN), the form am79c90's get_buf_length() decodes and lance_cmd.c arms @@ -456,9 +480,12 @@ unsigned int slot; base = slot_base[slot]; for (i = 0; i < RX_DESC; i++) { cb = (base >> 3) + i * RXBUFLEN; + /* OWN clear, with the new high byte: a fetch here is a MISS. */ + w16(lance_rxring + i * 64 + 16, (cb >> 16) & 0xFF); w16(lance_rxring + i * 64 + 0, cb & 0xFFFF); - w16(lance_rxring + i * 64 + 48, 0); w16(lance_rxring + i * 64 + 32, (0x10000 - RXBUFLEN) & 0xFFFF); + w16(lance_rxring + i * 64 + 48, 0); + /* Ownership last, over a complete descriptor. */ w16(lance_rxring + i * 64 + 16, RMD1_OWN | ((cb >> 16) & 0xFF)); } @@ -501,6 +528,41 @@ unsigned int i; mlen = r16(lance_rxring + i * 64 + 48) & 0x0FFF; src = slot_base[rx_armed] + i * RXBUFLEN * 8 + ETH_HDR_LEN * 8; len = 0; + if (rmd1 & RMD1_ERR) { + /* + * A datagram the chip flagged -- CRC/FCS, buffer, overflow or + * framing -- delivers NO bytes: am79c90.cpp's + * recv_complete_cb() writes RMD3 as + * (m_rx_md[1] & RMD1_ERR) ? 0 : (result & RMD3_MCNT) + * so the length checks below would drop it silently and a + * body-corrupting packet would never appear in any counter. + * + * The bytes ARE in the buffer (dma_out() ran before the FCS + * was checked), so a datagram of ours is still identifiable by + * its ethertype -- the Ethernet header is written before the + * body, so a body-corrupting error leaves it intact. Count one + * drop for the frame it breaks, abandon that frame and re-arm + * the other slot: with a datagram missing the accumulation can + * never reach the frame's payload_len, and letting later + * datagrams keep appending would mix two frames into one + * picture. Traffic that is not ours is ignored, exactly as at + * a frame start. + * + * If the corruption landed on the ethertype the datagram cannot + * be attributed and is not counted. That is the limit of what + * the chip leaves readable; it is stated in the report rather + * than papered over. + */ + if (*(unsigned char *)(src - ETH_HDR_LEN * 8 + 12 * 8) == 0x88 + && *(unsigned char *)(src - ETH_HDR_LEN * 8 + 13 * 8) == 0xB7) { + *(unsigned int *)RX_FCS_DROP = + *(unsigned int *)RX_FCS_DROP + 1; + rx_acc = 0; + rx_nfrag = 0; + rx_switch(); + } + return; + } if ((rmd1 & (RMD1_STP | RMD1_ENP)) == (RMD1_STP | RMD1_ENP) && mlen > ETH_HDR_LEN + ETH_FCS && mlen - ETH_HDR_LEN - ETH_FCS <= RXBUFLEN - ETH_HDR_LEN) { @@ -749,10 +811,19 @@ unsigned int ptr; * crc_err is RX_BAD: the frames fp_parse() rejected (header CRC, magic, * version or length). Since ruling R14 a corrupted *body* cannot appear * here -- the LANCE validated each datagram's FCS before the bytes were - * stored -- so this is the only CRC error the terminal can count. drops is - * the chip's CSR0 MISS events plus RX_RING_DROPS (frames too large for the 16 - * descriptors, Task 7); both are frames that arrived and did not reach the - * screen, and neither number is inferred from the other. + * stored -- so this is the only CRC error the terminal can count. A datagram + * whose FCS the CHIP rejected is not a frame at all: it delivers no bytes + * (RMD3 = 0) and is counted in drops, via RX_FCS_DROP. + * + * drops is three counts of frames that arrived and did not reach the screen: + * + * drops_total CSR0 MISS, counted per REPORT WINDOW in which the latched bit + * was set -- the chip gives no per-packet count, so this is one + * per window at most (a lower bound, see send_status()). + * RX_RING_DROPS frames too large for the 16 descriptors (Task 7). + * RX_FCS_DROP datagrams the chip flagged bad; each abandons its frame. + * + * Neither number is inferred from another. * * Returns 1 when the frame was handed to the chip, 0 when the ring slot was * still owned and nothing was armed. Overwriting a descriptor the chip still @@ -763,7 +834,8 @@ unsigned int ptr; static int send_status(frames_rx) unsigned int frames_rx; { - unsigned int i, crc, csr0, tmd1, off, hb, miss, bad, rdrops, free_kb; + unsigned int i, crc, csr0, tmd1, off, hb, miss, bad, rdrops, fdrops, + free_kb; hb = status_buf; @@ -796,8 +868,15 @@ unsigned int frames_rx; wb(off + 14 * 8, 0); wb(off + 15 * 8, 0); - /* The chip's status word, and the missed-packet count it reports. Read - * first, report what was read, then clear MISS (write-1-to-clear). */ + /* The chip's status word, and the missed-packet bit it latches. CSR0 + * MISS is ONE BIT: am79c90.cpp's recv_complete_cb() only ORs it in + * (case -2), so two missed packets inside one report window look exactly + * like one. drops_total therefore counts report windows in which at + * least one packet was missed -- a lower bound on missed packets, not an + * event count -- and the bit is cleared here after the report is built + * (write-1-to-clear) so the next window starts clean. The status field + * is a "something was missed" indicator, and this comment says so + * instead of claiming a count the hardware cannot give. */ csr0 = lance_csr_r(0); miss = 0; if (csr0 & CSR0_MISS) { @@ -806,10 +885,12 @@ unsigned int frames_rx; } bad = *(unsigned int *)RX_BAD; - /* Both drop counts are frames that arrived and did not reach the screen: - * the chip's missed-packet events (no descriptor free) and the payload's - * own count of frames too large for the ring. */ + /* The three drop counts described above: the chip's MISS windows, the + * payload's count of frames too large for the ring, and the datagrams + * the chip flagged bad (they carry no bytes, so nothing else sees + * them). */ rdrops = *(unsigned int *)RX_RING_DROPS; + fdrops = *(unsigned int *)RX_FCS_DROP; free_kb = heap_bytes() >> 10; off = hb + STAT_BODY * 8; wb(off + 0 * 8, frames_rx & 0xFF); @@ -818,8 +899,8 @@ unsigned int frames_rx; wb(off + 3 * 8, (frames_rx >> 24) & 0xFF); wb(off + 4 * 8, bad & 0xFF); wb(off + 5 * 8, (bad >> 8) & 0xFF); - wb(off + 6 * 8, (drops_total + rdrops) & 0xFF); - wb(off + 7 * 8, ((drops_total + rdrops) >> 8) & 0xFF); + wb(off + 6 * 8, (drops_total + rdrops + fdrops) & 0xFF); + wb(off + 7 * 8, ((drops_total + rdrops + fdrops) >> 8) & 0xFF); wb(off + 8 * 8, csr0 & 0xFF); wb(off + 9 * 8, (csr0 >> 8) & 0xFF); wb(off + 10 * 8, free_kb & 0xFF); @@ -894,6 +975,7 @@ int main() *(unsigned int *)RX_RING_ACC = 0; *(unsigned int *)RX_RING_ARMED = 0; *(unsigned int *)RX_RING_DROPS = 0; + *(unsigned int *)RX_FCS_DROP = 0; *(unsigned int *)RX_DGRAM = 0; *(unsigned int *)OS_TOUCH = 0;