fix(stream): ring ownership window, FCS-error accounting, unit loader procedure

This commit is contained in:
opencode.energia.arti.ee committed 2026-10-31 01:25:14 +00:00
1 parent 9917c5659a
commit 22c539bf74
9 files changed
+353 -51

No files matched your search

+5 -4
View File
@@ -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
+67 -3
View File
@@ -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)
+5 -1
View File
@@ -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)
+13
View File
@@ -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)
+107 -9
View File
@@ -12,7 +12,55 @@
# <mac> 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 <terminal-ip> \
# --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 <host-ip> --bootp \
# --file harness/udpstream/build/vidstream.coff
# python3 dwt/loader/hostload.py --host <terminal-ip> --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 <terminal-ip> --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)
+18 -1
View File
@@ -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)
+5 -4
View File
@@ -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:
+27 -5
View File
@@ -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
+106 -24
View File
@@ -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;