feat(stream): paced sender and terminal status frames

This commit is contained in:
opencode.energia.arti.ee committed 2026-10-31 00:26:26 +00:00
1 parent 5cd708460a
commit 53b1580abd
11 files changed
+2225 -15

No files matched your search

+30 -1
View File
@@ -5,6 +5,7 @@
# BENCH=0 harness/mame/run-vidstream.sh # use the existing .bin
# VT1200_MODE=rx harness/mame/run-vidstream.sh # alloc + the receive path
# VT1200_MODE=draw harness/mame/run-vidstream.sh # alloc + rx + the blit
# VT1200_MODE=stream harness/mame/run-vidstream.sh # the status back-channel
# STREAM_POOL_BYTES=0x7FFF000 harness/mame/run-vidstream.sh # force fallback
#
# Mode rx injects one valid frame and one corrupted frame with the debugger and
@@ -15,13 +16,22 @@
# then runs harness/mame/vidstream_check.py, which compares every screen row
# against its source row. The pixel verdict is the checker's, not the Lua's.
#
# Mode stream plays the sender: 200 strip frames, then RX_EOF so the terminal
# reports the final count. The payload takes the LANCE over for transmit and
# builds a KIND_STATUS frame every 64 accepted frames; the Lua reads those
# frames back from the payload's own transmit buffer and this script runs
# harness/mame/vidstream_status_check.py, which decodes them with the host
# sender's wire format. There is no network in this run: the emulated LANCE has
# no interface (ruling R11), so the evidence is the buffer's bytes plus the
# completed descriptor, and the wire path belongs to Task 7.
#
# 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
# free-list head (0x1E8AC0) both read 0, so the malloc success path cannot be
# tested there. The skip is a documented divergence (harness/mame/README.md).
#
# Exit status: 0 only if vidstream.lua printed "VID: RESULT PASS" and, in draw
# mode, vidstream_check.py passed too.
# or stream mode, the checker for that mode passed too.
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
@@ -65,6 +75,9 @@ if [ "$MODE" = "draw" ]; then
rm -f "$LOGDIR/vram-draw.bin" "$LOGDIR/frame-draw.bin" \
"$LOGDIR/draw-status.txt"
fi
if [ "$MODE" = "stream" ]; then
rm -f "$LOGDIR/stream-status.bin" "$LOGDIR/stream-status.txt"
fi
# Absolute: the Lua runs with MAME's cwd, so a relative default only resolves
# when the caller happens to be at the repo root.
VID_PAYLOAD="$ROOT/harness/udpstream/build/vidstream.bin"
@@ -111,4 +124,20 @@ if [ "$MODE" = "draw" ]; then
exit "$STATUS"
fi
fi
# The status frames are decoded by the host side's wire format, so the payload's
# encoder and frame.py cannot drift apart without failing here.
if [ "$MODE" = "stream" ]; then
set +e
python3 "$HERE/vidstream_status_check.py" \
--bin "$LOGDIR/stream-status.bin" \
--status "$LOGDIR/stream-status.txt" \
--frames "${VID_STREAM_FRAMES:-200}"
STATUS=$?
set -e
if [ "$STATUS" -ne 0 ]; then
echo "run-vidstream.sh: status check failed (log $LOG)" >&2
exit "$STATUS"
fi
fi
exit 0
+339 -9
View File
@@ -34,6 +34,20 @@
-- cannot be blessed here. No blit exists in the payload before Task 5, which is
-- why this mode fails on a Task 4 tree.
--
-- Mode "stream" (Task 6) is the status frame. It plays the *sender*: 200 strip
-- frames, one per emulated frame whenever a slot is free, alternating slots.
-- The payload accepts them and, every 64 accepted frames, builds a 60-byte
-- status frame in its own buffer and hands it to the LANCE. This mode watches
-- ST_SENT move, reads the frame back from ST_BUF, and when the 200th frame has
-- been accepted it sets RX_EOF so the terminal reports the final count (192 is
-- the last multiple of 64, so without RX_EOF the run could never see 200).
-- It writes stream-status.bin (the frames exactly as the payload built them)
-- and stream-status.txt (counters), and vidstream_status_check.py decodes both
-- with the same wire format the host sender uses. What this does NOT do is
-- put a byte on a wire: the emulated LANCE has no network interface (R11), so
-- the transmit is proven by the descriptor completing (TMD1's OWN cleared) and
-- by the bytes in the buffer, not by a socket.
--
-- Why injection and not a real Ethernet frame: harness/mame/vt1200.cpp never
-- creates a device_network_interface, so the emulated LANCE is not attached to
-- the host at all (ruling R11). Wiring it would be a MAME change, which
@@ -47,17 +61,21 @@
-- including the RX_* cells below, is in harness/udpstream/frame_parse.h.
--
-- Env:
-- VT1200_MODE alloc (default), rx or draw
-- VT1200_MODE alloc (default), rx, draw or stream
-- VID_AT load/entry frame (default 2400, the OS phase)
-- VID_END inspection/exit frame (default VID_AT + 60)
-- VID_DUMP_DIR where draw mode writes vram-draw.bin / frame-draw.bin /
-- draw-status.txt (default /tmp/opencode)
-- VID_DUMP_DIR where draw and stream modes write their dumps
-- (default /tmp/opencode)
-- VID_SENTINEL_CLOBBER=1 draw mode only: overwrite the VRAM canary word
-- before the frame is announced, so the payload must count
-- an OS touch. The negative control for OS_TOUCH.
-- VID_DRAW_WAIT draw mode only: emulated frames to wait for the payload to
-- accept the frame (default 300). Set it to 1 to exercise
-- the timeout path.
-- VID_STREAM_FRAMES stream mode only: how many frames the sender sends
-- (default 200)
-- VID_STREAM_WAIT stream mode only: emulated frames to wait for the final
-- status after RX_EOF (default 900)
-- VID_PAYLOAD raw .text image (default harness/udpstream/build/vidstream.bin)
-- VID_ORIGIN payload load (poke) bit address (default 0x018C0000)
-- VID_ENTRY_PTR _start bit address from vidstream.env (default VID_ORIGIN)
@@ -80,7 +98,14 @@ end
local MODE = os.getenv("VT1200_MODE") or "alloc"
local AT = tonumber(os.getenv("VID_AT") or "2400")
local STOP_AT = tonumber(os.getenv("VID_END") or tostring(AT + 60))
-- The stop frame depends on the mode: a stream run needs room for
-- VID_STREAM_FRAMES frames plus the final-status wait, and the payload needs
-- a few emulated frames per accepted frame (one full-screen blit each).
local DEFAULT_END = AT + 60
if MODE == "stream" and not os.getenv("VID_END") then
DEFAULT_END = AT + tonumber(os.getenv("VID_STREAM_FRAMES") or "200") * 8 + 2000
end
local STOP_AT = tonumber(os.getenv("VID_END") or tostring(DEFAULT_END))
local ORIGIN = tonumber(os.getenv("VID_ORIGIN") or "0x018C0000")
local ENTRY_PC = tonumber(os.getenv("VID_ENTRY_PC") or "0x3F579970")
local PATH = os.getenv("VID_PAYLOAD") or "harness/udpstream/build/vidstream.bin"
@@ -112,6 +137,15 @@ local RX_LEN_B = MARK_PAGE + 0x74 * 8
-- Task 5: the OS-repaint canary (frame_parse.h, ruling R12).
local OS_TOUCH = MARK_PAGE + 0x44 * 8
-- Task 6: the sender/keyframe cell and the status record.
local RX_EOF = MARK_PAGE + 0x78 * 8
local ST_SENT = MARK_PAGE + 0x7c * 8
local ST_FRAMES = MARK_PAGE + 0x80 * 8
local ST_BUF = MARK_PAGE + 0x84 * 8
local ST_TMD1 = MARK_PAGE + 0x88 * 8
local ST_INIT = MARK_PAGE + 0x8c * 8
local ST_TMLAST = MARK_PAGE + 0x90 * 8
-- Must match vidstream.c.
local SLOT_BYTES = 24576
@@ -131,6 +165,26 @@ local DRAW_STRETCH = 8
local DRAW_PLEN = DRAW_ROWS * RX_ROW_BYTES
local DRAW_FRAME = 16 + DRAW_PLEN
-- Stream mode (Task 6): how many frames the sender plays, how long to wait for
-- the final status once RX_EOF is set, and the status frame's geometry. The
-- terminal reports every 64 accepted frames (STATUS_EVERY in vidstream.c), so
-- 200 frames produce reports at 64, 128, 192 and -- after RX_EOF -- at 200.
local STREAM_FRAMES = tonumber(os.getenv("VID_STREAM_FRAMES") or "200")
local STREAM_WAIT = tonumber(os.getenv("VID_STREAM_WAIT") or "900")
-- Emulated frames to give the chip to hand the last TX descriptor back after
-- the final status. It is a bounded safety net, not a routine delay: MAME's
-- LANCE model completes a transmit in len/m_bandwidth, where dinetwork.cpp
-- converts the chip's 10 Mbit/s into m_bandwidth = bandwidth << 17 bytes/s,
-- so a 60-byte frame takes 60/1310720 = 46 us emulated. Measured in
-- VT1200_LANCE_DMA_LOG for this run: 49 us from the descriptor read to the
-- write-back (t=35.992888 -> 35.992937). The payload's own bounded wait
-- normally sees that completion (ST_TMD1's OWN is clear), and this wait only
-- matters if the host is too slow to service the model's timer.
local STREAM_DRAIN = tonumber(os.getenv("VID_STREAM_DRAIN") or "900")
local STATUS_EVERY = 64
local STATUS_BYTES = 60 -- STATUS_LEN in vidstream.c
local STATUS_BODY = 14 + 16 -- Ethernet header + VS header
-- Screen memory: bit 0x04000000, 256-byte row pitch (the real unit's, see
-- harness/c-hello/README.md; MAME's own renderer uses a different pitch and is
-- not what this check reads).
@@ -140,7 +194,7 @@ local VRAM_ROWS = 1024
local VRAM_ROW_BYTES = 256
local DUMP_DIR = os.getenv("VID_DUMP_DIR") or "/tmp/opencode"
if MODE ~= "alloc" and MODE ~= "rx" and MODE ~= "draw" then
if MODE ~= "alloc" and MODE ~= "rx" and MODE ~= "draw" and MODE ~= "stream" then
print("VID ERROR: mode '" .. MODE .. "' is not implemented")
machine:exit()
return
@@ -346,17 +400,21 @@ local DRAW_WAIT = tonumber(os.getenv("VID_DRAW_WAIT") or "300")
-- Evidence freshness (ruling R15): remove this mode's dump files before the
-- run, so a failed run cannot be checked against the previous run's screen.
local DRAW_FILES = { "vram-draw.bin", "frame-draw.bin", "draw-status.txt" }
local STREAM_FILES = { "stream-status.bin", "stream-status.txt" }
local function clear_draw_files()
if MODE ~= "draw" then
local function clear_dump_files()
if MODE ~= "draw" and MODE ~= "stream" then
return
end
for _, name in ipairs(DRAW_FILES) do
os.remove(DUMP_DIR .. "/" .. name)
end
for _, name in ipairs(STREAM_FILES) do
os.remove(DUMP_DIR .. "/" .. name)
end
end
clear_draw_files()
clear_dump_files()
local function draw_row_byte(i)
if (i // RX_ROW_BYTES) % 3 == 2 then
@@ -425,6 +483,219 @@ local function write_file(path, s)
return true
end
-- --- Task 6: stream mode ---------------------------------------------------
--
-- The harness plays the sender here, and the terminal's status frame is the
-- thing under test. Nothing in this section pretends to be a NIC: the frames
-- are poked into the slots the payload published, exactly as in modes rx and
-- draw, because the emulated LANCE has no network interface (R11).
-- The 16-byte VS header for one frame number, sealed over its first 12 bytes
-- (ruling R14). The body of each slot is written once; only this header
-- changes per frame, so the harness is not slower than the payload.
local function header_bytes(kind, frame_no, rows, stretch, plen)
local b = {}
b[0] = 0x56
b[1] = 0x53
b[2] = 0x01
b[3] = kind
b[4] = frame_no & 0xFF
b[5] = (frame_no >> 8) & 0xFF
b[6] = plen & 0xFF
b[7] = (plen >> 8) & 0xFF
b[8] = rows
b[9] = stretch
b[10] = 0
b[11] = 0
local crc = crc_add(0xFFFF, b, 0, 12)
b[12] = crc & 0xFF
b[13] = (crc >> 8) & 0xFF
b[14] = 0
b[15] = 0
return b
end
local function read_bytes(base, n)
local chars = {}
local t = {}
for i = 0, n - 1 do
local v = prog:read_u8(base + i * 8)
t[i] = v
chars[#chars + 1] = string.char(v)
end
return table.concat(chars), t
end
local function le16(t, off)
return t[off] | (t[off + 1] << 8)
end
local function le32(t, off)
return t[off] | (t[off + 1] << 8) | (t[off + 2] << 16) | (t[off + 3] << 24)
end
local stream_prefilled = false
local announced = 0
local eof_at = nil
local drain_from = nil
local st_sent_seen = 0
local bad_captures = 0
local captures = {}
-- Fill both slots' bodies once. Slot A holds the draw-mode pattern, slot B
-- its inverse, so a decoder that read the wrong slot (or cached one body) can
-- never pass a frame check. The headers are rewritten per frame.
local function stream_prefill()
local a = prog:read_u32(RX_SLOT_A)
local b = prog:read_u32(RX_SLOT_B)
if a == 0 or b == 0 then
return false
end
local fa = make_frame(1, 0, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN, draw_row_byte)
poke(a, fa, DRAW_FRAME)
local fb = make_frame(1, 0, DRAW_ROWS, DRAW_STRETCH, DRAW_PLEN,
function(i)
if draw_row_byte(i) == 0xF0 then
return 0x0F
end
return 0xFF
end)
poke(b, fb, DRAW_FRAME)
print(string.format("VID: frame %d: stream slots prefilled (A=%s, B=%s, %d B each); sender will announce %d frames",
frames, hex(a), hex(b), DRAW_FRAME, STREAM_FRAMES))
return true
end
-- Announce one frame in whichever slot is free. A busy slot is not
-- overwritten: the payload sets the ready flag and clears it when it is done
-- with the slot, so this is how the sender learns the terminal's pace.
local function inject_stream()
local slot = announced % 2
local ready = prog:read_u32(slot == 0 and RX_READY_A or RX_READY_B)
if ready ~= 0 then
return false
end
local base = prog:read_u32(slot == 0 and RX_SLOT_A or RX_SLOT_B)
if base == 0 then
return false
end
poke(base, header_bytes(1, announced & 0xFFFF, DRAW_ROWS, DRAW_STRETCH,
DRAW_PLEN), 16)
if slot == 0 then
prog:write_u32(RX_LEN_A, DRAW_FRAME)
prog:write_u32(RX_READY_A, 1)
else
prog:write_u32(RX_LEN_B, DRAW_FRAME)
prog:write_u32(RX_READY_B, 1)
end
announced = announced + 1
return true
end
-- Read the status frame the payload last produced, when ST_SENT moves. The
-- payload publishes ST_SENT last, so a reader that sees it move reads a frame
-- that was built, transmitted and recorded. This Lua only decodes enough to
-- decide when to stop; the wire format is checked by
-- harness/mame/vidstream_status_check.py, which decodes the dumped bytes.
local function capture_status()
local sent = prog:read_u32(ST_SENT)
if sent == st_sent_seen then
return
end
st_sent_seen = sent
local base = prog:read_u32(ST_BUF)
if base == 0 then
return
end
local raw, t = read_bytes(base, STATUS_BYTES)
local rec = {
bytes = raw,
magic_ok = (t[14] == 0x56 and t[15] == 0x53),
kind = t[17],
payload_len = le16(t, 20),
frames_rx = le32(t, STATUS_BODY),
crc_err = le16(t, STATUS_BODY + 4),
drops = le16(t, STATUS_BODY + 6),
csr0 = le16(t, STATUS_BODY + 8),
heap_kb = le16(t, STATUS_BODY + 10),
}
-- Only a well-formed VS status frame counts. A payload without the
-- status path leaves ST_SENT/ST_BUF as whatever RAM held, so the first
-- "capture" would otherwise be garbage presented as a report -- and
-- "no status frames" is exactly what a Task 5 tree must say.
if not rec.magic_ok or rec.kind ~= 5 or rec.payload_len ~= 12 then
bad_captures = bad_captures + 1
print(string.format("VID: status capture %d is not a VS status frame (magic=%s kind=%d payload_len=%d); ignored",
bad_captures, rec.magic_ok and "VS" or "WRONG", rec.kind,
rec.payload_len))
return
end
captures[#captures + 1] = rec
print(string.format("VID: status %d: frames_rx=%d crc_err=%d drops=%d csr0=%04X heap_free_kb=%d kind=%d payload_len=%d",
#captures, rec.frames_rx, rec.crc_err, rec.drops, rec.csr0,
rec.heap_kb, rec.kind, rec.payload_len))
end
-- Dump the captured status frames and the run's counters, then end the run.
-- The verdict here is only "the run produced what the harness asked for"; the
-- bytes themselves are decoded independently by the checker, so a wrong field
-- offset or a bad CRC cannot be blessed by this Lua.
local function dump_stream(timed_out)
local chunks = {}
local last = captures[#captures]
local lines = {
string.format("INJECTED=%d", announced),
string.format("STATUS_COUNT=%d", #captures),
string.format("BAD_STATUS_FRAMES=%d", bad_captures),
string.format("RX_OK=%d", prog:read_u32(RX_OK)),
string.format("RX_BAD=%d", prog:read_u32(RX_BAD)),
string.format("ST_INIT=%d", prog:read_u32(ST_INIT)),
string.format("ST_TMD1=%d", prog:read_u32(ST_TMD1)),
string.format("EOF_SET=%d", eof_at and 1 or 0),
string.format("TIMEOUT=%d", timed_out and 1 or 0),
}
for _, rec in ipairs(captures) do
chunks[#chunks + 1] = rec.bytes
end
if last then
lines[#lines + 1] = string.format("LAST_FRAMES_RX=%d", last.frames_rx)
lines[#lines + 1] = string.format("LAST_CRC_ERR=%d", last.crc_err)
lines[#lines + 1] = string.format("LAST_DROPS=%d", last.drops)
lines[#lines + 1] = string.format("LAST_CSR0=%d", last.csr0)
lines[#lines + 1] = string.format("LAST_HEAP_KB=%d", last.heap_kb)
lines[#lines + 1] = string.format("LAST_KIND=%d", last.kind)
lines[#lines + 1] = string.format("LAST_PAYLOAD_LEN=%d", last.payload_len)
end
-- The descriptor's state now, and how long the chip took to finish it.
local tp = prog:read_u32(ST_TMLAST)
local tmd1_now = tp ~= 0 and prog:read_u16(tp) or 0
lines[#lines + 1] = string.format("TMD1_DRAINED=%d", tmd1_now)
lines[#lines + 1] = string.format("DRAIN_FRAMES=%d",
drain_from and (frames - drain_from) or -1)
local okb = write_file(DUMP_DIR .. "/stream-status.bin", table.concat(chunks))
local okt = write_file(DUMP_DIR .. "/stream-status.txt",
table.concat(lines, "\n") .. "\n")
print(string.format("VID: %d status frames captured (%d B) in %s/stream-status.bin",
#captures, #table.concat(chunks), DUMP_DIR))
local ok = okb and okt and not timed_out and bad_captures == 0
and #captures >= 3 and last ~= nil
and last.frames_rx == STREAM_FRAMES and last.drops == 0
and last.crc_err == 0
print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL")
machine:exit()
end
-- Dump the screen and the payload's record for vidstream_check.py, then end
-- the run. The pass/fail for pixels belongs to the checker; here we report
-- what the payload itself says (one accepted frame, no rejects, no repaint by
@@ -700,11 +971,70 @@ emu.register_frame_done(function()
end
end
-- Stream mode: play the sender, one announced frame per emulated frame
-- whenever a slot is free. The cadence is NOT the sender's 30 fps: MAME's
-- frame is not the wire (R11), and what this run has to show is that every
-- announced frame is accepted and that the terminal reports on time, not
-- that a wall clock was kept.
if entered and MODE == "stream" and not stream_prefilled then
stream_prefilled = stream_prefill()
end
if MODE == "stream" and stream_prefilled then
if announced < STREAM_FRAMES then
inject_stream()
elseif eof_at == nil then
-- The sender has stopped. Without this the last report would be
-- the 192nd frame's; RX_EOF is how a finite run asks for the
-- final count (frame_parse.h; harness-only, like RX_READY_*).
prog:write_u32(RX_EOF, 1)
eof_at = frames
print(string.format("VID: frame %d: %d frames announced; RX_EOF set, waiting for the final status",
frames, announced))
end
capture_status()
local last = captures[#captures]
if eof_at ~= nil and last ~= nil and last.frames_rx >= STREAM_FRAMES then
-- The final report is in. Wait for the chip to hand the last
-- descriptor back before calling the transmit done: a descriptor
-- the chip never took would mean the payload armed it for nothing.
-- The model completes it in ~46 us emulated (see STREAM_DRAIN);
-- the wait is a safety net for a slow host, not a delay.
if drain_from == nil then
drain_from = frames
end
local tp = prog:read_u32(ST_TMLAST)
local tmd1 = tp ~= 0 and prog:read_u16(tp) or 0xFFFF
if (tmd1 & 0x8000) == 0 then
print(string.format("VID: frame %d: last TX descriptor completed (TMD1=%04X, OWN clear)",
frames, tmd1))
dump_stream(false)
return
end
if frames >= drain_from + STREAM_DRAIN then
print(string.format("VID: timed out %d emulated frames after the final status: last TX descriptor still has OWN set (TMD1=%04X at %s)",
STREAM_DRAIN, tmd1, hex(tp)))
dump_stream(true)
return
end
end
if (eof_at ~= nil and frames >= eof_at + STREAM_WAIT)
or frames >= STOP_AT then
print(string.format("VID: timed out waiting for the final status: %d frames announced, %d status frames, last frames_rx=%s, RX_EOF set at %s",
announced, #captures,
last and tostring(last.frames_rx) or "none",
eof_at and tostring(eof_at) or "never"))
dump_stream(true)
return
end
end
if entered and frames % 10 == 0 and frames < STOP_AT then
alive_last = prog:read_u32(VID_ALIVE)
end
if frames >= STOP_AT and not (MODE == "draw" and draw_at ~= nil) then
if frames >= STOP_AT and MODE ~= "stream"
and not (MODE == "draw" and draw_at ~= nil) then
if not entered then
print("VID: payload never entered -> FAIL")
print("VID: RESULT FAIL")
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""vidstream_status_check.py -- decode the terminal's status frames.
Task 6's host-side reading of the status path. In MAME the payload takes the
LANCE over for transmit and, every 64 accepted frames, builds a 60-byte
Ethernet frame in its own buffer and hands it to the chip
(harness/udpstream/vidstream.c, send_status()). The Lua side
(harness/mame/vidstream.lua, VT1200_MODE=stream) plays the sender: it announces
200 strip frames, then sets RX_EOF so the terminal reports the final count, and
dumps every status frame it saw in the payload's transmit buffer:
stream-status.bin each status frame exactly as the payload built it
(60 bytes each, back to back)
stream-status.txt key=value counters from the payload's marker page
This script is the independent decode: it uses harness/udpstream/frame.py, the
same module the host sender parses with, so the C encoder and the Python parser
cannot drift apart without a failure here. It checks:
* the Ethernet envelope (broadcast destination, the terminal's own address,
the stream's ethertype 0x88B7);
* the VS header (magic, version, KIND_STATUS, rows/stretch zero, payload_len
12, header CRC valid);
* the 12-byte body (frames_rx, crc_err, drops, csr0, heap_free_kb);
* the cadence and the verdict the brief asks for: at least three status
frames, the periodic ones at 64, 128, 192, and the last reporting
frames_rx == 200 with drops == 0 and crc_err == 0;
* that the transmit descriptor actually completed (TMD1's OWN bit clear).
TMD1's ERR bit is expected to be set here and is not a failure: the LANCE
has no network medium in MAME (ruling R11), so the chip reports loss of
carrier after completing the DMA. The bytes and the descriptor are the
evidence this run can produce; the wire is Task 7's.
usage:
vidstream_status_check.py [--bin PATH] [--status PATH] [--frames 200]
Exit status 0 only if every check passes.
"""
import argparse
import os
import struct
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from harness.udpstream.frame import ( # noqa: E402
BROADCAST, ETHERTYPE, HEADER_SIZE, KIND_STATUS, STATUS_SIZE, TERMINAL_MAC,
unpack_header, unpack_status)
STATUS_BYTES = 60 # STATUS_LEN in vidstream.c
STATUS_BODY = 14 + HEADER_SIZE # Ethernet header + VS header = 30
STATUS_EVERY = 64 # STATUS_EVERY in vidstream.c
CSR0_IDON = 0x0100
CSR0_TXON = 0x0010
TMD1_OWN = 0x8000
TMD1_ERR = 0x4000
failures = 0
def check(cond, what):
global failures
if not cond:
failures += 1
print("FAIL: " + what)
return cond
def read_file(path):
try:
with open(path, "rb") as f:
return f.read()
except OSError as e:
print("vidstream_status_check: cannot read %s: %s" % (path, e))
return None
def read_status(path):
st = {}
try:
with open(path, "r", encoding="ascii", errors="replace") as f:
for line in f:
if "=" in line:
k, v = line.strip().split("=", 1)
try:
st[k] = int(v, 0)
except ValueError:
st[k] = v
except OSError as e:
print("vidstream_status_check: cannot read %s: %s" % (path, e))
return None
return st
def decode_one(raw, n):
"""Decode one 60-byte captured frame; returns the body dict or None."""
tag = "status frame %d" % n
if not check(len(raw) == STATUS_BYTES,
"%s is %d bytes, expected %d"
% (tag, len(raw), STATUS_BYTES)):
return None
if not check(raw[0:6] == BROADCAST,
"%s destination is %s, expected broadcast"
% (tag, raw[0:6].hex(":"))):
return None
if not check(raw[6:12] == TERMINAL_MAC,
"%s source is %s, expected the terminal's %s"
% (tag, raw[6:12].hex(":"), TERMINAL_MAC.hex(":"))):
return None
if not check(raw[12:14] == struct.pack(">H", ETHERTYPE),
"%s ethertype is %s, expected 0x%04X"
% (tag, raw[12:14].hex(), ETHERTYPE)):
return None
try:
h = unpack_header(raw[14:14 + HEADER_SIZE])
except ValueError as e:
check(False, "%s header does not parse: %s" % (tag, e))
return None
if not check(h["magic"] == b"VS", "%s magic is %r" % (tag, h["magic"])):
return None
if not check(h["kind"] == KIND_STATUS,
"%s kind is %d, expected KIND_STATUS=%d"
% (tag, h["kind"], KIND_STATUS)):
return None
if not check(h["payload_len"] == STATUS_SIZE,
"%s payload_len is %d, expected %d"
% (tag, h["payload_len"], STATUS_SIZE)):
return None
if not check(h["rows"] == 0 and h["stretch"] == 0,
"%s reports geometry rows=%d stretch=%d; a status has none"
% (tag, h["rows"], h["stretch"])):
return None
if not check(h["crc_ok"], "%s header CRC is wrong" % tag):
return None
body = unpack_status(raw[STATUS_BODY:STATUS_BODY + STATUS_SIZE])
body["frame_no"] = h["frame_no"]
return body
def main():
ap = argparse.ArgumentParser()
d = os.environ.get("VID_DUMP_DIR", "/tmp/opencode")
ap.add_argument("--bin", default=os.path.join(d, "stream-status.bin"))
ap.add_argument("--status", default=os.path.join(d, "stream-status.txt"))
ap.add_argument("--frames", type=int, default=200,
help="frames the sender announced (VID_STREAM_FRAMES)")
args = ap.parse_args()
blob = read_file(args.bin)
st = read_status(args.status)
if blob is None or st is None:
return 2
check(len(blob) % STATUS_BYTES == 0,
"%s is %d bytes, not a whole number of %d-byte frames"
% (args.bin, len(blob), STATUS_BYTES))
records = [blob[i:i + STATUS_BYTES]
for i in range(0, len(blob) - STATUS_BYTES + 1, STATUS_BYTES)]
print("vidstream_status_check: %d status frame(s) in %s; counters from %s"
% (len(records), args.bin, args.status))
# --- the run's own record --------------------------------------------
if st.get("TIMEOUT", 0) != 0:
print("FAIL: the run timed out waiting for the final status "
"(injected=%r, status_count=%r, last frames_rx=%r)"
% (st.get("INJECTED"), st.get("STATUS_COUNT"),
st.get("LAST_FRAMES_RX")))
print("vidstream_status_check: FAIL (timeout)")
return 1
check(st.get("INJECTED") == args.frames,
"INJECTED=%r, expected %d" % (st.get("INJECTED"), args.frames))
check(st.get("RX_OK") == args.frames,
"the payload accepted RX_OK=%r frames, expected %d"
% (st.get("RX_OK"), args.frames))
check(st.get("RX_BAD") == 0,
"RX_BAD=%r: the payload rejected a frame it should have accepted"
% (st.get("RX_BAD"),))
check(st.get("BAD_STATUS_FRAMES", 0) == 0,
"BAD_STATUS_FRAMES=%r: the payload published something that is not "
"a VS status frame" % (st.get("BAD_STATUS_FRAMES"),))
check(st.get("STATUS_COUNT") == len(records),
"STATUS_COUNT=%r but the dump holds %d frames"
% (st.get("STATUS_COUNT"), len(records)))
check(st.get("EOF_SET") == 1,
"RX_EOF was never set, so the final report cannot exist")
# The LANCE is ours: IDON set. ST_INIT is CSR0 right after the INIT wait
# and BEFORE STRT, so TXON is not expected there; the transmitter is
# checked on the status body's own csr0 below.
st_init = st.get("ST_INIT", 0)
check(isinstance(st_init, int) and (st_init & CSR0_IDON) != 0,
"CSR0 after the INIT wait is %r; IDON (0x%04X) is not set, so the "
"init block was never read by the chip" % (st_init, CSR0_IDON))
# The last descriptor must have been given back: the chip consumed it.
# TMD1_DRAINED is read from the descriptor after the run waited for it;
# ST_TMD1 is the value the payload itself saw at send time (its wait is
# bounded, and the model completes a 60-byte frame in about 46 us: 10
# Mbit/s is m_bandwidth = 10 << 17 bytes/s in dinetwork.cpp).
tmd1 = st.get("TMD1_DRAINED", 0)
check(isinstance(tmd1, int) and (tmd1 & TMD1_OWN) == 0,
"the last TX descriptor still has OWN set after the drain wait "
"(TMD1=%r): the chip never took the status frame" % (tmd1,))
if isinstance(tmd1, int) and (tmd1 & TMD1_ERR):
print("note: the drained TMD1=0x%04X has ERR set -- expected: the "
"LANCE has no network medium in MAME (R11), so it reports loss "
"of carrier after completing the DMA" % tmd1)
at_send = st.get("ST_TMD1", 0)
if isinstance(at_send, int) and (at_send & TMD1_OWN):
print("note: the descriptor was still owned when the payload finished "
"its bounded wait (ST_TMD1=0x%04X); the drain check above is "
"what decides whether the chip ever took it" % at_send)
# --- the bytes -------------------------------------------------------
if not check(len(records) >= 3,
"%d status frames, expected at least 3 (every %d accepted "
"frames)" % (len(records), STATUS_EVERY)):
print("vidstream_status_check: FAIL")
return 1
bodies = []
for i, raw in enumerate(records, 1):
body = decode_one(raw, i)
if body is None:
continue
bodies.append(body)
print(" frame %d: frame_no=%d frames_rx=%d crc_err=%d drops=%d "
"csr0=0x%04X heap_free_kb=%d"
% (i, body["frame_no"], body["frames_rx"], body["crc_err"],
body["drops"], body["csr0"], body["heap_free_kb"]))
if len(bodies) != len(records):
print("vidstream_status_check: FAIL (%d of %d frames decoded)"
% (len(bodies), len(records)))
return 1
# --- the cadence -----------------------------------------------------
for i, want in enumerate((STATUS_EVERY, 2 * STATUS_EVERY, 3 * STATUS_EVERY)):
if i < len(bodies):
check(bodies[i]["frames_rx"] == want,
"status %d reports frames_rx=%d, expected %d (a report "
"every %d accepted frames)"
% (i + 1, bodies[i]["frames_rx"], want, STATUS_EVERY))
for i in range(1, len(bodies)):
check(bodies[i]["frames_rx"] > bodies[i - 1]["frames_rx"],
"status %d reports frames_rx=%d, which does not advance past "
"the previous %d" % (i + 1, bodies[i]["frames_rx"],
bodies[i - 1]["frames_rx"]))
# After RX_EOF the terminal reports each further accepted frame, so the
# reports after the third are consecutive. That is how the final count
# (200) is reached without waiting for 256.
after = bodies[3:]
for i in range(1, len(after)):
check(after[i]["frames_rx"] == after[i - 1]["frames_rx"] + 1,
"post-EOF report %d jumps from %d to %d; the terminal reports "
"each further accepted frame"
% (len(bodies) - len(after) + i,
after[i - 1]["frames_rx"], after[i]["frames_rx"]))
# --- the brief's verdict --------------------------------------------
last = bodies[-1]
check(last["frames_rx"] == args.frames,
"the last status reports frames_rx=%d, expected %d"
% (last["frames_rx"], args.frames))
check(last["drops"] == 0,
"the last status reports drops=%d, expected 0" % last["drops"])
check(last["crc_err"] == 0,
"the last status reports crc_err=%d, expected 0" % last["crc_err"])
# The transmitter must have been on when the reports were built: the
# payload ran STRT once, and this is the chip's own answer at report time.
check((last["csr0"] & CSR0_TXON) != 0,
"the last status reports csr0=0x%04X with TXON (0x%04X) clear, so "
"the transmitter was never started"
% (last["csr0"], CSR0_TXON))
# The free-heap field must be a reading, not the zero padding a wrong
# offset would pick up.
check(last["heap_free_kb"] > 0,
"heap_free_kb=%d: the heap field looks like padding"
% last["heap_free_kb"])
# The Lua's own decode of the same frame must agree with this one.
if "LAST_FRAMES_RX" in st:
check(st["LAST_FRAMES_RX"] == last["frames_rx"],
"stream-status.txt says LAST_FRAMES_RX=%r but the decoded last "
"frame says %d" % (st["LAST_FRAMES_RX"], last["frames_rx"]))
print("vidstream_status_check: %d/%d status frames decoded, last reports "
"frames_rx=%d drops=%d crc_err=%d heap_free_kb=%d"
% (len(bodies), len(records), last["frames_rx"], last["drops"],
last["crc_err"], last["heap_free_kb"]))
if failures:
print("vidstream_status_check: FAIL (%d checks)" % failures)
return 1
print("vidstream_status_check: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())
+102
View File
@@ -31,6 +31,30 @@ geometry a frame was *sent* with is the geometry the terminal acts on.
So a corrupted **payload** byte does not invalidate a frame here -- that is the
chip's job -- while a corrupted **header** byte does.
The status body
---------------
A ``KIND_STATUS`` frame is the terminal's report to the host, and it is an
ordinary frame: the same 16-byte header, the same header-only CRC, with
``rows`` and ``stretch`` zero (a status has no geometry). Its body is 12 bytes:
offset size field
------ ---- ------------------------------------------------------
0 4 frames_rx, u32 little-endian (frames fp_parse accepted)
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)
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)
Both ends put that frame in an Ethernet frame: 6 bytes destination, 6 bytes
source, the ethertype below, then the ``VS`` frame. The terminal does not
filter on ethertype (its LANCE accepts broadcast and its own physical address),
so the value is for the host's benefit: a status frame must not be mistaken for
a probe (0x88B5, sendraw.py) or a command (0x88B6, lance_cmd.c).
"""
import struct
@@ -38,7 +62,10 @@ import struct
__all__ = [
"HEADER_SIZE", "MAGIC", "VERSION", "CRC_OFFSET",
"KIND_STRIP", "KIND_STATUS",
"ETHERTYPE", "BROADCAST", "TERMINAL_MAC", "MIN_ETHERNET_PAYLOAD",
"STATUS_SIZE",
"crc16", "pack_header", "seal_header", "unpack_header",
"pack_status", "unpack_status", "ethernet_frame",
]
#: Total size of the fixed header every frame starts with.
@@ -59,10 +86,31 @@ KIND_STRIP = 1
#: A status report travelling the other way.
KIND_STATUS = 5
#: Ethernet type the stream's frames carry. 0x88B5 is the probe frame
#: sendraw.py sends and 0x88B6 the command frame lance_cmd.c uses; the video
#: stream gets its own so a captured frame cannot be mistaken for either.
ETHERTYPE = 0x88B7
#: Destination that always passes the LANCE's address filter: with LADRF zero
#: the chip accepts its own physical address and broadcast, nothing else.
BROADCAST = b"\xff" * 6
#: The terminal's own physical address, from its Ethernet address ROM
#: (bytes 0..5 of the eight at bit 0x6800000; the last two are the checksum).
TERMINAL_MAC = b"\x08\x00\x2b\x16\x4b\x5a"
#: Bytes of padding an Ethernet frame needs after its 14-byte header.
MIN_ETHERNET_PAYLOAD = 46
#: Size of the status body (see the module docstring).
STATUS_SIZE = 12
#: Everything before the CRC field, little-endian.
_PREFIX = struct.Struct("<2sBBHHBBH")
#: The CRC field and the two padding bytes behind it.
_TAIL = struct.Struct("<HH")
#: The status body: frames_rx u32, crc_err u16, drops u16, csr0 u16, heap u16.
_STATUS = struct.Struct("<IHHHH")
def _make_crc_table():
@@ -200,3 +248,57 @@ def unpack_header(buf: bytes, payload: bytes | None = None) -> dict:
"stretch": stretch,
"crc_ok": crc16(bytes(buf[:CRC_OFFSET])) == stored_crc,
}
# --- the status body and the Ethernet envelope ---------------------------
def pack_status(frames_rx: int, crc_err: int, drops: int, csr0: int,
heap_free_kb: int) -> bytes:
"""Build the 12-byte status body the terminal sends (see the docstring).
The field widths are the terminal's: ``frames_rx`` is a 32-bit counter that
must not wrap in any run, the rest are 16-bit quantities whose limits
(65,535 missed packets, a 64 MB heap) are far above anything a VT1200
reaches, so an out-of-range value is a bug here rather than a wrap on the
wire. ``send_status()`` in vidstream.c writes these same offsets.
"""
frames_rx = _checked("frames_rx", frames_rx, 0xFFFFFFFF)
crc_err = _checked("crc_err", crc_err, 0xFFFF)
drops = _checked("drops", drops, 0xFFFF)
csr0 = _checked("csr0", csr0, 0xFFFF)
heap_free_kb = _checked("heap_free_kb", heap_free_kb, 0xFFFF)
return _STATUS.pack(frames_rx, crc_err, drops, csr0, heap_free_kb)
def unpack_status(body: bytes) -> dict:
"""Decode a status body, or raise ValueError if it is not 12 bytes."""
if len(body) != STATUS_SIZE:
raise ValueError("a status body is %d bytes, got %d"
% (STATUS_SIZE, len(body)))
frames_rx, crc_err, drops, csr0, heap_free_kb = _STATUS.unpack_from(body, 0)
return {
"frames_rx": frames_rx,
"crc_err": crc_err,
"drops": drops,
"csr0": csr0,
"heap_free_kb": heap_free_kb,
}
def ethernet_frame(payload: bytes, src: bytes, dst: bytes = BROADCAST,
ethertype: int = ETHERTYPE, pad: bool = False) -> bytes:
"""Put ``payload`` in an Ethernet frame.
``pad`` is deliberately off by default and is the *caller's* decision:
Ethernet needs 46 bytes after the 14-byte header, but a frame split across
datagrams may only be padded at its very end. Padding a middle chunk would
insert bytes between two chunks of one frame, shifting the body the
receiver reassembles -- so the chunking layer pads its last datagram and
nothing else.
"""
if len(src) != 6 or len(dst) != 6:
raise ValueError("MAC addresses are 6 bytes")
data = bytes(dst) + bytes(src) + struct.pack(">H", ethertype) + bytes(payload)
if pad and len(data) < 14 + MIN_ETHERNET_PAYLOAD:
data += b"\x00" * (14 + MIN_ETHERNET_PAYLOAD - len(data))
return data
+17
View File
@@ -116,3 +116,20 @@ fp_header *out;
out->payload_off = FP_HEADER_SIZE;
return 1;
}
/*
* unsigned int fp_crc(unsigned char *buf, unsigned int len)
*
* The same CRC over an arbitrary span, for the transmit side. The terminal
* builds the status frame's header itself (vidstream.c, send_status()) and
* seals it with this, so there is ONE CRC implementation in the payload rather
* than two that could drift apart. It is the function fp_parse() already uses
* over the 12 header bytes -- the initial value, the polynomial and the shift
* direction are stated once, in fp_crc_byte() above.
*/
unsigned int fp_crc(buf, len)
unsigned char *buf;
unsigned int len;
{
return fp_crc_add(0xFFFF, buf, len);
}
+43
View File
@@ -68,6 +68,26 @@
* ready flag; MAME cannot hand this emulated LANCE an Ethernet frame (the
* driver attaches no network interface -- ruling R11), so the debugger writes
* the slot and raises the flag instead. The payload clears it.
*
* Task 6 adds one more cell of the same kind and the status record the harness
* reads back:
*
* RX_EOF +0x3C0 0x018103C0 1 = the sender stopped
* ST_SENT +0x3E0 0x018103E0 status frames transmitted
* ST_FRAMES +0x400 0x01810400 frames_rx in the last status
* ST_BUF +0x420 0x01810420 bit address of the 60-byte frame
* ST_TMD1 +0x440 0x01810440 TMD1 as the payload read it at
* the end of its own wait
* ST_INIT +0x460 0x01810460 CSR0 after the LANCE INIT wait
* ST_TMLAST +0x480 0x01810480 bit address of the last armed
* TX descriptor's TMD1 word
*
* RX_EOF is harness-only in exactly the same sense as RX_READY_x: it says "the
* sender has stopped announcing frames", which on a real wire is silence and
* carries no signal. It exists so a finite run can ask for a final status
* instead of waiting for the count to reach the next multiple of 64. The
* payload reports as soon as it sees it, and keeps reporting for each further
* accepted frame; nothing reads it in a continuous stream.
* ---------------------------------------------------------------------------
*
* K&R C only. The 1987 toolchain in build-stream.sh compiles this header, so
@@ -117,6 +137,18 @@
#define RX_READY_B MARK_CELL(0x70)
#define RX_LEN_B MARK_CELL(0x74)
/* Task 6: the sender/keyframe control cell and the status record. */
#define RX_EOF MARK_CELL(0x78)
#define ST_SENT MARK_CELL(0x7c)
#define ST_FRAMES MARK_CELL(0x80)
#define ST_BUF MARK_CELL(0x84)
#define ST_TMD1 MARK_CELL(0x88)
#define ST_INIT MARK_CELL(0x8c)
#define ST_TMLAST MARK_CELL(0x90)
/* The status body (Task 6): frames_rx u32, then four u16, little-endian. */
#define FP_STATUS_SIZE 12
/* What a valid frame told us. payload_off is FP_HEADER_SIZE on success. */
typedef struct {
unsigned int kind;
@@ -139,4 +171,15 @@ typedef struct {
*/
int fp_parse();
/*
* unsigned int fp_crc(unsigned char *buf, unsigned int len)
*
* The CCITT-FALSE CRC of len bytes (poly 0x1021, init 0xFFFF, not reflected,
* no final xor) -- the same routine fp_parse() runs over the 12 header bytes,
* exported so the terminal can seal the status header it builds (Task 6).
* One implementation, so the receive and transmit checks cannot drift apart.
* Returns the 16-bit CRC in the low half of an unsigned int.
*/
unsigned int fp_crc();
#endif /* FRAME_PARSE_H */
+98 -2
View File
@@ -10,14 +10,21 @@ purpose: a corrupted **payload** byte leaves a frame ACCEPTED (the Ethernet FCS
of each datagram is the check for those bytes), while a corrupted **header**
byte is REJECTED, and a truncated frame (``len < 16 + payload_len``) is rejected
by the length bound that ``fp_parse`` applies before the CRC.
The last section pins the *other* direction: the terminal's status body
(``KIND_STATUS``) and the Ethernet envelope both ends put the frame in. The
offsets there are the contract with ``send_status()`` in vidstream.c.
"""
import struct
import pytest
from harness.udpstream.frame import (CRC_OFFSET, HEADER_SIZE, KIND_STATUS,
KIND_STRIP, MAGIC, VERSION, crc16,
pack_header, seal_header, unpack_header)
KIND_STRIP, MAGIC, VERSION, STATUS_SIZE,
BROADCAST, ETHERTYPE, TERMINAL_MAC,
crc16, ethernet_frame, pack_header,
pack_status, seal_header, unpack_header,
unpack_status)
def test_header_is_16_bytes_and_round_trips():
@@ -192,3 +199,92 @@ def test_out_of_range_fields_raise(kwargs):
base.update(kwargs)
with pytest.raises(ValueError):
pack_header(**base)
# --- the status body that travels back (Task 6) --------------------------
def test_status_body_is_twelve_bytes_little_endian():
"""The terminal's status body, field by field, at the offsets the C writes.
The C side (vidstream.c, send_status()) writes the same five fields with
byte stores, so the offsets below are the contract between the two ends.
"""
b = pack_status(frames_rx=0x12345678, crc_err=0x0102, drops=0x0304,
csr0=0x20F2, heap_free_kb=0x0506)
assert len(b) == STATUS_SIZE == 12
assert b[0:4] == b"\x78\x56\x34\x12" # frames_rx, u32 LE
assert b[4:6] == b"\x02\x01" # crc_err, u16 LE
assert b[6:8] == b"\x04\x03" # drops, u16 LE
assert b[8:10] == b"\xF2\x20" # csr0, u16 LE
assert b[10:12] == b"\x06\x05" # heap_free_kb, u16 LE
assert unpack_status(b) == {
"frames_rx": 0x12345678, "crc_err": 0x0102, "drops": 0x0304,
"csr0": 0x20F2, "heap_free_kb": 0x0506,
}
@pytest.mark.parametrize("kwargs", [
dict(frames_rx=0x100000000), dict(frames_rx=-1),
dict(crc_err=0x10000), dict(drops=-1),
dict(csr0=0x10000), dict(heap_free_kb=-1),
])
def test_status_fields_out_of_range_raise(kwargs):
base = dict(frames_rx=1, crc_err=0, drops=0, csr0=0, heap_free_kb=0)
base.update(kwargs)
with pytest.raises(ValueError):
pack_status(**base)
def test_status_body_must_be_exactly_twelve_bytes():
b = pack_status(1, 0, 0, 0, 0)
with pytest.raises(ValueError):
unpack_status(b[:-1])
with pytest.raises(ValueError):
unpack_status(b + b"\x00")
def test_status_uses_the_same_header_with_zero_geometry():
"""A status frame is a normal frame: same header, same header-only CRC."""
body = pack_status(200, 0, 0, 0x20F2, 2048)
h = pack_header(KIND_STATUS, frame_no=3, payload_len=len(body), rows=0,
stretch=0, payload=body)
d = unpack_header(h, body)
assert d["kind"] == KIND_STATUS
assert d["rows"] == 0 and d["stretch"] == 0
assert d["crc_ok"] is True
def test_ethernet_envelope_is_the_streams_own_type():
"""The status frame travels in an ordinary Ethernet frame on its own type.
0x88B7 is the video stream's: 0x88B5 is the probe frame sendraw.py sends
and 0x88B6 the command frame lance_cmd.c uses, so a captured status frame
cannot be mistaken for either. The terminal's LANCE does not filter on
ethertype at all (it accepts broadcast and its own physical address), so
this value is for the host's benefit.
"""
body = pack_status(1, 0, 0, 0, 0)
frame = pack_header(KIND_STATUS, 1, len(body), 0, 0) + body
data = ethernet_frame(frame, src=TERMINAL_MAC, dst=BROADCAST)
assert len(data) == 14 + HEADER_SIZE + STATUS_SIZE
assert data[0:6] == b"\xff" * 6
assert data[6:12] == TERMINAL_MAC
assert data[12:14] == struct.pack(">H", ETHERTYPE) == b"\x88\xB7"
assert data[14:] == frame
def test_ethernet_frame_pads_to_the_minimum_only_at_the_end():
"""A 60-byte Ethernet minimum must not be inserted between chunks.
Padding mid-frame would put bytes the sender never meant between two
datagrams of one frame, and the receiver would see a frame whose body is
longer than payload_len and shifted -- exactly the bug this pins.
"""
short = ethernet_frame(b"VS" + bytes(10), src=TERMINAL_MAC, pad=True)
assert len(short) == 60
assert short[-18:] == bytes(18)
long = b"x" * 1450
assert ethernet_frame(long, src=TERMINAL_MAC, pad=True) == (
BROADCAST + TERMINAL_MAC + b"\x88\xB7" + long)
# Off by default: the chunking layer decides, and only for the last chunk.
assert len(ethernet_frame(b"VS" + bytes(10), src=TERMINAL_MAC)) == 26
@@ -219,6 +219,28 @@ int main(void)
"bare header, one byte short, rejected");
}
/* ---- fp_crc(): the exported CRC the transmit side seals with ----
*
* Task 6's terminal builds its own status header and must use the same
* CRC as the parser. These are the canonical check values, not values
* this code produced: CCITT-FALSE over "123456789" is 0x29B1, and the
* empty input is the initial value. The golden frame's own CRC bytes
* are the third reading, so the function and fp_parse() cannot drift. */
{
static const unsigned char nine[] = "123456789";
unsigned int crc;
crc = fp_crc((unsigned char *) nine, 9);
check(crc == 0x29B1, "fp_crc(\"123456789\") == 0x29B1 (CCITT-FALSE)");
check(fp_crc((unsigned char *) nine, 0) == 0xFFFF,
"fp_crc(empty) == 0xFFFF (the initial value)");
crc = fp_crc((unsigned char *) golden, FP_PREFIX_SIZE);
check(crc == (((unsigned int) golden[FP_CRC_OFF + 1] << 8)
| (unsigned int) golden[FP_CRC_OFF]),
"fp_crc over the golden prefix equals its stored crc16");
}
printf("frame_parse: %d/%d ok\n", checks - failures, checks);
return failures != 0;
}
+385
View File
@@ -0,0 +1,385 @@
"""Tests for the host sender: pacing, chunking, and the status back-channel.
The plan's acceptance run says "MAME with a tap interface: run vidsend.py for
200 frames at 30 fps". That run cannot happen in this repository: the MAME
driver creates no ``device_network_interface``, so the emulated LANCE is not
attached to the host at all (ruling R11). What can be tested here is every
layer below the socket, and that is what this file does:
* the *schedule* -- frame ``n`` leaves at ``t0 + n/fps``, measured on an
injected clock rather than by sleeping for real;
* the *wire form* -- a frame is chunked at 1450 bytes, only the last datagram is
padded to the 60-byte Ethernet minimum, and the pieces reassemble to exactly
the bytes ``vidsrc.StripSource`` produced;
* the *status* path -- a status frame is recognised in an Ethernet frame,
decoded field by field, and ``drops``/``crc_err`` decide the exit status;
* the *loop* -- a fake counterparty that reassembles datagrams, counts frames
and answers with status frames, so a 200-frame run's verdict is exercised
without a NIC or a terminal.
No test here touches a socket, and none of them claims anything about whether a
frame would arrive on a real wire.
"""
import io
import struct
import pytest
from harness.udpstream import vidsend
from harness.udpstream.frame import (BROADCAST, ETHERTYPE, HEADER_SIZE,
KIND_STATUS, TERMINAL_MAC, ethernet_frame,
pack_header, pack_status, unpack_header)
from harness.udpstream.vidsrc import CHUNK, ROW_BYTES, StripSource
#: The host's own address. Locally administered, so it cannot be mistaken for
#: the terminal's (08:00:2b:16:4b:5a, TERMINAL_MAC) in a datagram assertion.
HOST_MAC = b"\x02\x00\x00\x00\x00\x01"
class FakeClock:
"""A monotonic clock that only moves when something sleeps on it."""
def __init__(self):
self.t = 0.0
self.sleeps = []
def now(self):
return self.t
def sleep(self, seconds):
self.sleeps.append(seconds)
self.t += seconds
class RecordingTransport:
"""Stands in for the NIC: records each datagram and when it was sent."""
def __init__(self, clock):
self.clock = clock
self.datagrams = []
self.times = []
def __call__(self, data):
self.datagrams.append(data)
self.times.append(self.clock.now())
def make_sender(clock, transport, **kw):
kw.setdefault("rows", 8)
kw.setdefault("stretch", 128)
kw.setdefault("fps", 10.0)
kw.setdefault("status_wait", 0.0)
kw.setdefault("out", io.StringIO())
kw.setdefault("err", io.StringIO())
kw.setdefault("src", HOST_MAC)
return vidsend.Sender(transport=transport, clock=clock.now,
sleep=clock.sleep, **kw)
def status_frame(frames_rx=0, crc_err=0, drops=0, csr0=0x20F2, heap_free_kb=2048,
frame_no=0, body=None):
body = pack_status(frames_rx, crc_err, drops, csr0, heap_free_kb) \
if body is None else body
hdr = pack_header(KIND_STATUS, frame_no, len(body), 0, 0, payload=body)
return ethernet_frame(hdr + body, src=TERMINAL_MAC)
# --- pacing ---------------------------------------------------------------
def test_frame_n_leaves_at_t0_plus_n_over_fps():
"""The schedule, not "about 10 a second": exactly t0 + n/fps."""
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, fps=10.0, frames=5)
sender.run() # the exit status is the status verdict, tested below
assert transport.times == pytest.approx([0.0, 0.1, 0.2, 0.3, 0.4])
# And the sleeps are the differences: the first frame does not sleep.
assert clock.sleeps == pytest.approx([0.1, 0.1, 0.1, 0.1])
def test_pacing_does_not_accumulate_drift():
"""A slow frame is followed by a shorter sleep, not by a later schedule.
``t0 + n/fps`` is absolute, so the sender catches up instead of falling
behind by however long the previous frame took.
"""
clock = FakeClock()
sent_at = []
def transport(data):
sent_at.append(clock.now())
clock.t += 0.03 # this frame took 30 ms to send
sender = make_sender(clock, transport, fps=10.0, frames=4)
sender.run() # the exit status is the status verdict, tested below
assert sent_at == pytest.approx([0.0, 0.1, 0.2, 0.3])
def test_fps_zero_sends_unpaced():
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, fps=0.0, frames=3)
sender.run() # the exit status is the status verdict, tested below
assert transport.times == [0.0, 0.0, 0.0]
assert clock.sleeps == []
def test_a_csv_line_every_thirty_frames():
clock = FakeClock()
transport = RecordingTransport(clock)
out = io.StringIO()
sender = make_sender(clock, transport, fps=0.0, frames=65, out=out)
sender.run() # the exit status is the status verdict, tested below
lines = out.getvalue().strip().splitlines()
assert lines[0] == vidsend.CSV_HEADER == "frame,elapsed_s,fps,bytes"
# 30 and 60, not a line per frame and not 0 (nothing sent yet). stdout
# carries the table only; the prose (status, verdict) goes to stderr.
assert [ln.split(",")[0] for ln in lines[1:]] == ["30", "60"]
frame_col, elapsed_col, _fps, bytes_col = lines[1].split(",")
assert int(frame_col) == 30 and float(elapsed_col) == 0.0
assert int(bytes_col) > 0
# --- the wire form --------------------------------------------------------
def test_only_the_last_datagram_is_short_and_none_is_padded_mid_frame():
"""128 rows = 20,480 B: 15 datagrams at 1450, the last one alone short.
Each datagram carries the 14-byte Ethernet header in front of its chunk,
so a middle datagram is exactly CHUNK + 14 bytes; only the last one may be
padded to the minimum and here it is already well over it.
"""
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, rows=128, stretch=8, frames=1)
sender.run() # the exit status is the status verdict, tested below
dgrams = transport.datagrams
assert len(dgrams) == 15
assert [len(d) for d in dgrams[:-1]] == [CHUNK + 14] * 14
assert len(dgrams[-1]) == 14 + (HEADER_SIZE + 128 * ROW_BYTES - 14 * CHUNK)
def test_datagrams_reassemble_to_the_frame_the_encoder_built():
"""What the receiver gets back is byte for byte vidsrc's frame."""
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, rows=8, stretch=128, frames=1)
sender.run() # the exit status is the status verdict, tested below
body = b"".join(d[14:] for d in transport.datagrams)
want = StripSource(8, 128).encode(0, vidsend.bars(8, 0))
assert body == b"".join(want)
h = unpack_header(body)
assert h["crc_ok"] is True and h["kind"] == 1 and h["rows"] == 8
def test_a_datagram_carries_the_ethernet_header_and_padding():
"""The smallest valid strip (8 rows x 128) is 1296 bytes: one datagram."""
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, rows=8, stretch=128, frames=1)
sender.run() # the exit status is the status verdict, tested below
(d,) = transport.datagrams
assert len(d) == 14 + HEADER_SIZE + 8 * ROW_BYTES
assert d[:6] == BROADCAST and d[6:12] == HOST_MAC
assert d[12:14] == struct.pack(">H", ETHERTYPE)
def test_a_short_last_datagram_is_padded_and_a_mid_one_is_not():
"""The padding rule, stated where it can bite: Ethernet needs 60 bytes.
The smallest valid strip is 1296 bytes; chunked at 1255 the second (and
last) datagram carries 41 bytes of frame, so it must be padded to 60 while
the 1255-byte chunk before it must not be. Padding a middle chunk would
insert bytes into the frame body.
"""
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, rows=8, stretch=128, frames=1,
chunk=1255)
sender.run() # the exit status is the status verdict, tested below
sizes = [len(d) for d in transport.datagrams]
assert sizes == [1255 + 14, 60] # 1296 - 1255 = 41 bytes of frame
frame = b"".join(d[14:] for d in transport.datagrams)
assert frame[:1296] == b"".join(
StripSource(8, 128).encode(0, vidsend.bars(8, 0)))
assert frame[1296:] == bytes(5) # the pad, and only at the end
def test_frame_numbers_wrap_at_16_bits_and_bodies_differ_per_frame():
"""frame_no is a u16 field; the pattern must move or a stream is not one."""
assert vidsend.bars(8, 0) != vidsend.bars(8, 1)
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, fps=0.0, frames=2)
sender.frame_offset = 0xFFFE # next two frames straddle the wrap
sender.run() # the exit status is the status verdict, tested below
nos = [unpack_header(d[14:])["frame_no"] for d in transport.datagrams]
assert nos == [0xFFFE, 0xFFFF]
# --- the status path ------------------------------------------------------
def test_a_status_frame_is_decoded_from_an_ethernet_frame():
d = vidsend.decode_status_frame(status_frame(frames_rx=200, csr0=0x20F2,
heap_free_kb=2048))
assert d is not None
assert d["kind"] == KIND_STATUS and d["crc_ok"] is True
assert d["frames_rx"] == 200 and d["drops"] == 0 and d["crc_err"] == 0
assert d["csr0"] == 0x20F2 and d["heap_free_kb"] == 2048
def test_a_strip_frame_is_not_mistaken_for_a_status():
clock = FakeClock()
transport = RecordingTransport(clock)
sender = make_sender(clock, transport, fps=0.0, frames=1)
sender.run() # the exit status is the status verdict, tested below
assert vidsend.decode_status_frame(transport.datagrams[0]) is None
def test_a_status_frame_with_a_corrupt_header_is_flagged_not_decoded():
bad = bytearray(status_frame(frames_rx=1))
bad[14 + 8] ^= 0x01 # rows byte, inside the CRC prefix
d = vidsend.decode_status_frame(bytes(bad))
assert d is not None and d["crc_ok"] is False
def test_a_status_in_a_foreign_ethertype_is_ignored():
body = pack_status(1, 0, 0, 0, 0)
hdr = pack_header(KIND_STATUS, 0, len(body), 0, 0)
data = (BROADCAST + TERMINAL_MAC + struct.pack(">H", 0x88B6) + hdr + body)
assert vidsend.decode_status_frame(data) is None
# --- the verdict ----------------------------------------------------------
def run_with_statuses(*status_bytes, frames=3):
clock = FakeClock()
transport = RecordingTransport(clock)
pending = list(status_bytes)
def recv():
out, pending[:] = list(pending), []
return out
out, err = io.StringIO(), io.StringIO()
sender = vidsend.Sender(transport=transport, receive=recv, src=HOST_MAC,
clock=clock.now, sleep=clock.sleep, frames=frames,
fps=0.0, status_wait=0.0, rows=8, stretch=128,
out=out, err=err)
code = sender.run()
return sender, code, out.getvalue() + err.getvalue()
def test_exit_zero_only_when_drops_and_crc_err_are_clean():
_, code, text = run_with_statuses(status_frame(frames_rx=3))
assert code == 0
assert "drops=0" in text and "crc_err=0" in text
_, code, _ = run_with_statuses(status_frame(frames_rx=3, drops=1))
assert code == 1
_, code, _ = run_with_statuses(status_frame(frames_rx=3, crc_err=2))
assert code == 1
def test_exit_is_non_zero_when_no_status_arrives():
"""No status means the two conditions were never observed, so no PASS."""
_, code, text = run_with_statuses()
assert code == 1
assert "no status" in text.lower()
def test_a_status_with_a_bad_header_crc_does_not_count():
bad = bytearray(status_frame(frames_rx=3))
bad[14 + 3] ^= 0x01 # kind byte, inside the CRC prefix
_, code, text = run_with_statuses(bytes(bad))
assert code == 1
assert "crc" in text.lower()
# --- the whole loop, with a fake counterparty -----------------------------
class FakeTerminal:
"""Reassembles the sender's datagrams and answers with status frames.
This is the sender's counterparty, not a VT1200 model: it counts what
arrived and every 64 accepted frames hands back the status frame the
payload would send. It exists to exercise vidsend.py's loop, not to stand
in for the terminal -- the terminal's own status emission is checked in
MAME (VT1200_MODE=stream) and decoded by vidstream_status_check.py.
"""
def __init__(self, every=64, drops=0, crc_err=0):
self.every = every
self.drops = drops
self.crc_err = crc_err
self.buf = b""
self.frames_rx = 0
self.reported = []
self.pending = []
def send(self, data):
if data[12:14] != struct.pack(">H", ETHERTYPE):
return
self.buf += data[14:]
if len(self.buf) < HEADER_SIZE:
return
plen = struct.unpack_from("<H", self.buf, 6)[0]
if len(self.buf) < HEADER_SIZE + plen:
return
frame, self.buf = self.buf[:HEADER_SIZE + plen], b""
h = unpack_header(frame)
assert h["crc_ok"], "the sender built a frame the parser rejects"
self.frames_rx += 1
if self.frames_rx % self.every == 0:
self.reported.append(self.frames_rx)
self.pending.append(status_frame(
frames_rx=self.frames_rx, drops=self.drops,
crc_err=self.crc_err, frame_no=len(self.reported)))
def recv(self):
out, self.pending = self.pending, []
return out
def test_two_hundred_frames_get_three_periodic_statuses_and_a_clean_verdict():
"""The brief's run on the layer below the socket.
200 frames at 30 fps, a status every 64 accepted: 64, 128, 192, each with
drops=0 and crc_err=0, so the verdict is PASS. The brief's final reading,
``frames_rx == 200``, needs one more thing the sender cannot assume: the
terminal only reports on its 64th frame, so the last report is 192 unless
the terminal is told the stream ended -- in MAME that is the RX_EOF cell,
see vidstream.lua and the task-6 report.
"""
clock = FakeClock()
term = FakeTerminal()
out, err = io.StringIO(), io.StringIO()
sender = vidsend.Sender(transport=term.send, receive=term.recv,
src=HOST_MAC, clock=clock.now,
sleep=clock.sleep, rows=8, stretch=128,
frames=200, fps=30.0, status_wait=0.0, out=out,
err=err)
code = sender.run()
assert code == 0
assert term.frames_rx == 200
assert term.reported == [64, 128, 192]
assert [s["frames_rx"] for s in sender.statuses] == [64, 128, 192]
# The schedule: 200 frames at 30 fps take 199/30 s of virtual time.
assert clock.now() == pytest.approx(199 / 30.0)
assert "PASS" in err.getvalue()
def test_a_terminal_reporting_drops_makes_the_verdict_fail():
"""drops is the terminal's number, not something the sender invents."""
clock = FakeClock()
term = FakeTerminal(drops=1)
out, err = io.StringIO(), io.StringIO()
sender = vidsend.Sender(transport=term.send, receive=term.recv,
src=HOST_MAC, clock=clock.now,
sleep=clock.sleep, rows=8, stretch=128,
frames=64, fps=0.0, status_wait=0.0, out=out,
err=err)
assert sender.run() == 1
assert "drops=1" in err.getvalue()
assert "FAIL" in err.getvalue()
+481
View File
@@ -0,0 +1,481 @@
#!/usr/bin/env python3
"""vidsend.py -- the host sender: paced strip frames and the status back-channel.
One frame *n* leaves at ``t0 + n/fps`` (an absolute schedule, so a slow frame is
caught up on rather than shifting every later frame), the frame is chunked at
1450 bytes and carried in Ethernet frames of the stream's own type
(:data:`harness.udpstream.frame.ETHERTYPE`), and every 30 frames one CSV line is
printed. In the other direction the script listens for the terminal's
``KIND_STATUS`` frames and decodes them.
sudo python3 harness/udpstream/vidsend.py --iface vt1200 --frames 200
python3 harness/udpstream/vidsend.py --dry-run --frames 200 # no NIC
Exit status is 0 only when a status frame was received and the last one reports
``drops == 0`` and ``crc_err == 0``. "No status" is a failure: the two
conditions were never observed, and a sender that cannot see the terminal must
not claim the terminal is happy.
WHAT THIS CANNOT PROVE HERE. The MAME driver creates no
``device_network_interface`` (ruling R11), so the emulated LANCE is not attached
to a host NIC: this script cannot be pointed at the emulator. It is exercised
three ways instead, and none of them is a network run:
* the pacing, chunking, padding and status decode are unit-tested through an
injected ``send`` callable and an injected clock
(harness/udpstream/tests/test_vidsend.py);
* ``--dry-run`` runs the real 200-frame, 30 fps schedule against a counting
transport, which is what makes a wall-clock pacing check possible without a
socket -- it prints ``RESULT DRY-RUN`` and never claims a delivery;
* the terminal's own status emission is checked in MAME
(``VT1200_MODE=stream harness/mame/run-vidstream.sh``), where the payload's
LANCE transmit path is exercised and the bytes are decoded by
``harness/mame/vidstream_status_check.py`` with the same wire format this
script parses.
"""
from __future__ import annotations
import argparse
import os
import socket
import struct
import sys
import time
if __package__ in (None, ""): # run as a script, not a module
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))))
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
__all__ = [
"CSV_HEADER", "Sender", "bars", "split", "PATTERNS",
"decode_status_frame", "RawEthernet", "parse_mac",
]
#: One CSV line is printed every this many frames.
REPORT_EVERY = 30
#: The CSV columns, in this order: frame number, elapsed seconds, achieved fps,
#: bytes handed to the transport so far. The CSV goes to stdout and the prose
#: (status lines, the verdict) to stderr, so a run's stdout is a clean table.
CSV_HEADER = "frame,elapsed_s,fps,bytes"
#: Bars per screen: a source row is ink while ``(r + n) % 16 < 8``.
BAR_PERIOD = 16
# --- patterns -------------------------------------------------------------
def bars(rows: int, frame_no: int) -> bytes:
"""Packed rows of horizontal bars, drifting up one row per frame.
Two row classes per frame (ink and blank), so a decoder that reused one
source row cannot produce the picture, and the pattern moves between
frames, so a frozen stream is distinguishable from a live one.
"""
out = bytearray(rows * ROW_BYTES)
for r in range(rows):
if (r + frame_no) % BAR_PERIOD < BAR_PERIOD // 2:
start = r * ROW_BYTES
out[start:start + ROW_BYTES] = b"\xff" * ROW_BYTES
return bytes(out)
def split(rows: int, frame_no: int) -> bytes:
"""Packed rows of a split field with a vertical edge that moves per frame.
A second pattern whose answer is known analytically: the left 640 pixels of
every row are ink, and the right side is ink only above a moving horizon.
"""
out = bytearray(rows * ROW_BYTES)
horizon = (frame_no * 3) % max(rows, 1)
for r in range(rows):
start = r * ROW_BYTES
out[start:start + ROW_BYTES // 2] = b"\xff" * (ROW_BYTES // 2)
if r < horizon:
out[start + ROW_BYTES // 2:start + ROW_BYTES] = \
b"\xff" * (ROW_BYTES // 2)
return bytes(out)
PATTERNS = {"bars": bars, "split": split}
# --- the status back-channel ---------------------------------------------
def decode_status_frame(data: bytes):
"""Decode one received Ethernet frame, or return None.
None means "not a status frame of ours": a different ethertype, too short,
not a ``VS`` frame, or a different kind. A status frame whose header CRC is
wrong is *returned* with ``crc_ok`` False, so the caller can count it
instead of silently treating it as noise.
"""
if len(data) < 14 + HEADER_SIZE + STATUS_SIZE:
return None
if data[12:14] != struct.pack(">H", ETHERTYPE):
return None
body = data[14:]
try:
h = unpack_header(body)
except ValueError:
return None
if h["magic"] != MAGIC or h["kind"] != KIND_STATUS:
return None
if h["payload_len"] != STATUS_SIZE:
return None
out = unpack_status(body[HEADER_SIZE:HEADER_SIZE + STATUS_SIZE])
out["kind"] = h["kind"]
out["frame_no"] = h["frame_no"]
out["crc_ok"] = h["crc_ok"]
return out
# --- pacing ---------------------------------------------------------------
class Sender:
"""Send ``rows``-row strip frames at a fixed rate and collect status frames.
Everything that would touch the world is injected, so the schedule and the
verdict can be tested without a socket:
``transport(data: bytes)`` one Ethernet frame, header included
``receive()`` zero or more received Ethernet frames
``clock()`` / ``sleep(s)`` the time base the schedule is measured on
"""
def __init__(self, transport, src, receive=None, rows=128, stretch=8,
fps=30.0, frames=None, pattern="bars", chunk=CHUNK,
dst=BROADCAST, clock=time.monotonic, sleep=time.sleep,
report_every=REPORT_EVERY, status_wait=1.0, out=None,
err=None):
if pattern not in PATTERNS:
raise ValueError("unknown pattern %r; choose from %s"
% (pattern, ", ".join(sorted(PATTERNS))))
if chunk < HEADER_SIZE + 1:
raise ValueError("chunk %d is too small for a frame" % chunk)
if fps < 0:
raise ValueError("fps must be >= 0 (0 = unpaced): %r" % (fps,))
self.source = StripSource(rows, stretch)
self.pattern = pattern
self.rows = rows
self.stretch = stretch
self.fps = float(fps)
self.frames = frames
self.chunk = chunk
self.dst = dst
self.src = src
self.transport = transport
self.receive = receive
self.clock = clock
self.sleep = sleep
self.report_every = report_every
self.status_wait = status_wait
self.out = out if out is not None else sys.stdout
self.err = err if err is not None else sys.stderr
self.frame_offset = 0
self.sent_frames = 0
self.sent_datagrams = 0
self.sent_bytes = 0
self.statuses = []
self.bad_status = 0
self.t0 = 0.0
# -- output --------------------------------------------------------
def say(self, text):
"""One CSV line: stdout, because it is a table."""
print(text, file=self.out, flush=True)
def log(self, text):
"""Prose: stderr, so it cannot be mistaken for a CSV row."""
print(text, file=self.err, flush=True)
# -- one frame -----------------------------------------------------
def frame_bytes(self, n: int) -> bytes:
"""The complete wire frame (header + body) for frame *n*."""
body = PATTERNS[self.pattern](self.rows, n)
no = (self.frame_offset + n) & 0xFFFF
return b"".join(self.source.encode(no, body))
def datagrams(self, n: int) -> list[bytes]:
"""The frame split into datagrams, only the last padded to the minimum.
Padding matters: the 60-byte Ethernet minimum must be reached at the
*end* of a frame. Padding a middle chunk would insert bytes the sender
never meant between two chunks of one frame, and the receiver would see
a body shifted by however many pads were inserted.
"""
frame = self.frame_bytes(n)
chunks = [frame[i:i + self.chunk]
for i in range(0, len(frame), self.chunk)]
out = []
for i, chunk in enumerate(chunks):
out.append(ethernet_frame(chunk, src=self.src, dst=self.dst,
pad=(i == len(chunks) - 1)))
return out
def send_frame(self, n: int):
for data in self.datagrams(n):
self.transport(data)
self.sent_datagrams += 1
self.sent_bytes += len(data)
self.sent_frames += 1
# -- status --------------------------------------------------------
def poll(self):
if self.receive is None:
return
for data in (self.receive() or [])[:64]:
st = decode_status_frame(data)
if st is None:
continue
if not st["crc_ok"]:
self.bad_status += 1
self.log("status: header CRC bad, ignored (frames_rx=%d)"
% st["frames_rx"])
continue
self.statuses.append(st)
self.log("status %d: frame_no=%d frames_rx=%d crc_err=%d drops=%d "
"csr0=0x%04X heap_free_kb=%d"
% (len(self.statuses), st["frame_no"], st["frames_rx"],
st["crc_err"], st["drops"], st["csr0"],
st["heap_free_kb"]))
# -- the run -------------------------------------------------------
def run(self, judge=True) -> int:
"""Send the run; ``judge`` False skips the verdict (the dry run's case)."""
self.t0 = self.clock()
if self.report_every:
self.say(CSV_HEADER)
n = 0
try:
while self.frames is None or n < self.frames:
self.wait_for(n)
self.send_frame(n)
n += 1
self.poll()
if self.report_every and n % self.report_every == 0:
self.report(n)
except KeyboardInterrupt:
self.log("interrupted after %d frames" % n)
if judge:
self.drain()
return self.verdict()
return 0
def wait_for(self, n: int):
"""Sleep until ``t0 + n/fps``; the first frame does not sleep."""
if self.fps <= 0:
return
delta = self.t0 + n / self.fps - self.clock()
if delta > 0:
self.sleep(delta)
def report(self, n: int):
elapsed = self.clock() - self.t0
rate = n / elapsed if elapsed > 0 else 0.0
self.say("%d,%.3f,%.2f,%d" % (n, elapsed, rate, self.sent_bytes))
def drain(self, limit=None):
"""Keep listening after the last frame, for a status still in flight.
The terminal reports every 64 accepted frames, so the report for the
last few frames of a short run may never come; the wait is bounded and
ends early only when a report covers everything that was sent.
"""
if self.receive is None or self.status_wait <= 0:
return
deadline = self.clock() + (self.status_wait if limit is None else limit)
while self.clock() < deadline:
self.poll()
if (self.statuses and self.frames is not None
and self.statuses[-1]["frames_rx"] >= self.frames):
return
self.sleep(0.02)
def verdict(self) -> int:
elapsed = self.clock() - self.t0
self.log("sent %d frames in %d datagrams, %d bytes, %.1f fps"
% (self.sent_frames, self.sent_datagrams, self.sent_bytes,
(self.sent_frames / elapsed) if elapsed > 0 else 0.0))
if not self.statuses:
self.log("FAIL: no status frame received (the terminal said "
"nothing, so drops and crc_err are unknown)")
return 1
last = self.statuses[-1]
if self.bad_status:
self.log("FAIL: %d status frame(s) had a bad header CRC"
% self.bad_status)
return 1
if last["drops"] or last["crc_err"]:
self.log("FAIL: terminal reports drops=%d crc_err=%d"
% (last["drops"], last["crc_err"]))
return 1
lag = (self.sent_frames - last["frames_rx"]
if self.frames is not None else 0)
self.log("PASS: %d status frames, last frames_rx=%d "
"(drops=0 crc_err=0), %d frame(s) sent since that report"
% (len(self.statuses), last["frames_rx"], max(lag, 0)))
return 0
# --- transports -----------------------------------------------------------
def parse_mac(text: str) -> bytes:
parts = text.split(":")
if len(parts) != 6:
raise argparse.ArgumentTypeError("expected aa:bb:cc:dd:ee:ff, got %r"
% text)
try:
return bytes(int(p, 16) for p in parts)
except ValueError:
raise argparse.ArgumentTypeError("not a MAC address: %r" % text)
def mac_of(iface: str) -> bytes:
with open("/sys/class/net/%s/address" % iface) as f:
return parse_mac(f.read().strip())
class RawEthernet:
"""AF_PACKET transport: send on ``iface`` and receive the stream's type.
Two sockets, because they answer two questions: the TX socket sends a frame
the caller built (header included), the RX socket only queues frames whose
ethertype is the stream's, so the status frames arrive without the host
having to sift the interface's whole traffic. Both need CAP_NET_RAW.
"""
def __init__(self, iface: str):
self.iface = iface
self.tx = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
self.tx.bind((iface, 0))
self.rx = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
socket.htons(ETHERTYPE))
self.rx.bind((iface, 0))
self.rx.setblocking(False)
self.src = mac_of(iface)
def send(self, data: bytes):
self.tx.send(data)
def recv(self):
out = []
while True:
try:
out.append(self.rx.recv(2048))
except BlockingIOError:
return out
if len(out) >= 64:
return out
def close(self):
self.tx.close()
self.rx.close()
class CountingTransport:
"""``--dry-run``: count what the schedule would send, touch no hardware."""
def __init__(self):
self.datagrams = 0
self.bytes = 0
def __call__(self, data: bytes):
self.datagrams += 1
self.bytes += len(data)
def main(argv=None) -> int:
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--iface", help="interface to send on (needs CAP_NET_RAW)")
ap.add_argument("--rows", type=int, default=128,
help="unique source rows a frame carries (default 128; "
"128x8 fills the screen, ruling R3)")
ap.add_argument("--stretch", type=int, default=8,
help="screen rows each source row paints (default 8)")
ap.add_argument("--fps", type=float, default=30.0,
help="frames per second; 0 = as fast as possible")
ap.add_argument("--frames", type=int, default=0,
help="stop after N frames (0 = until interrupted)")
ap.add_argument("--pattern", default="bars", choices=sorted(PATTERNS),
help="test picture (default bars)")
ap.add_argument("--chunk", type=int, default=CHUNK,
help="bytes of frame per datagram (default %d)" % CHUNK)
ap.add_argument("--dst", type=parse_mac, default=BROADCAST,
help="destination MAC (default broadcast: the terminal's "
"LANCE accepts broadcast without a filter entry)")
ap.add_argument("--src", type=parse_mac, default=None,
help="source MAC (default: the interface's own)")
ap.add_argument("--report-every", type=int, default=REPORT_EVERY,
help="CSV line every N frames (default %d)"
% REPORT_EVERY)
ap.add_argument("--status-wait", type=float, default=1.0,
help="seconds to keep listening for a status after the "
"last frame (default 1.0)")
ap.add_argument("--dry-run", action="store_true",
help="run the schedule against a counter, no socket")
args = ap.parse_args(argv)
# The geometry is vidsrc's, not this script's: `stretch` must divide the
# 1024-row screen or the encoder refuses the frame, and --rows 80
# --stretch 12 (the plan's own example) is such a pair -- 80*12 = 960
# rows, the other 64 scanlines unpainted (ruling R3). Report that as a
# usage error instead of a traceback.
try:
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)
return 2
if args.dry_run:
transport = CountingTransport()
sender = Sender(transport, src=b"\x02\x00\x00\x00\x00\x01",
rows=args.rows, stretch=args.stretch, fps=args.fps,
frames=args.frames or None, pattern=args.pattern,
chunk=args.chunk, dst=args.dst, receive=None,
report_every=args.report_every, status_wait=0.0)
sender.run(judge=False)
# stdout holds the CSV table only; the verdict of a dry run is prose.
print("RESULT DRY-RUN: %d frames, %d datagrams, %d bytes counted; "
"nothing left the host and no status was received"
% (sender.sent_frames, sender.sent_datagrams, sender.sent_bytes),
file=sys.stderr)
return 0
if not args.iface:
ap.error("--iface is required unless --dry-run is given")
try:
net = RawEthernet(args.iface)
except PermissionError:
print("vidsend.py: %s needs CAP_NET_RAW (run as root, or use "
"--dry-run)" % args.iface, file=sys.stderr)
return 2
except OSError as e:
print("vidsend.py: cannot open %s: %s" % (args.iface, e), file=sys.stderr)
return 2
src = args.src if args.src is not None else net.src
sender = Sender(net.send, src=src, receive=net.recv, rows=args.rows,
stretch=args.stretch, fps=args.fps,
frames=args.frames or None, pattern=args.pattern,
chunk=args.chunk, dst=args.dst,
report_every=args.report_every,
status_wait=args.status_wait)
try:
return sender.run()
finally:
net.close()
if __name__ == "__main__":
sys.exit(main())
+407 -3
View File
@@ -7,7 +7,9 @@
* fp_parse() (frame_parse.c, shared with the host unit test) and counts what
* it accepted. Task 5 blits an accepted strip frame to the screen: one
* PIXBLT B,XY per source row, repeated down the row `stretch` times by
* SPTCH=0 (vram_b.asm).
* SPTCH=0 (vram_b.asm). Task 6 adds the back-channel: the LANCE is taken over
* for transmit and a KIND_STATUS frame is built and sent every 64 accepted
* frames (send_status() below).
*
* Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB):
*
@@ -15,7 +17,12 @@
* +0x06000 frame slot B 24576 bytes
* +0x0C000 framebuffer 163840 bytes (scheme A, Task 5)
* +0x34000 dictionary 32768 bytes
* +0x3C000 slack 4096 bytes
* +0x3C000 LANCE block 4096 bytes (Task 6: init block, TX ring,
* status frame)
*
* The LANCE block is what was slack. Its ring base is aligned at run time
* (lance_start() explains why the offset alone is not enough), and the status
* frame sits right after the 8 descriptors.
*
* A slot holds one complete frame -- 16-byte header and payload -- exactly as
* it arrives on the wire, so the bytes fp_parse() validates are the bytes the
@@ -85,6 +92,66 @@
*/
#define SLOT_BYTES 24576
/*
* Task 6: the terminal's status frame.
*
* The payload takes the LANCE over -- STOP, its own init block and TX ring,
* INIT, IDON, STRT -- and every 64 accepted frames builds a 60-byte Ethernet
* frame in its own buffer and hands it to the chip with a TX descriptor. The
* descriptor setup is lance.c's (harness/udpstream/lance.c): the ring words are
* TMD0/TMD1/TMD2/TMD3 at byte offsets 0/2/4/6, the buffer address's high byte
* lives in TMD1, and the length is (0x10000 - bytes) in TMD2. This is NOT a
* second driver: it is the same init/descriptor sequence, with one buffer
* instead of a loopback probe's.
*
* Where the pieces live: the pool's last 4 KB was slack, so the LANCE block
* goes there -- 24 bytes of init block, 64 bytes of TX ring (8 descriptors) and
* the 60-byte status frame, all aligned at run time in lance_start(). All of
* it is memory this payload owns, which is the point: the firmware's own rings
* are in low memory with 32 descriptors (see lance.c's note on the OS init
* block), and arming a slot in a ring whose position the chip owns is not
* something this payload can do reliably.
*
* The receiver is DISABLED (MODE_DRX) on purpose, and that is a gap rather
* than a design: the receive path in this task is the harness injection
* interface (RX_READY_A/B), the real transport chunks a frame into 15
* datagrams, and a ring of one-descriptor-per-datagram that reassembles them
* does not exist yet (ruling R11 covers why MAME cannot exercise it either).
* Arming an RX ring we have no buffers for would let the chip DMA a datagram
* into address 0, so disabling the receiver is the honest state.
*
* So `drops` in the status body is wired to the chip's own counter -- CSR0's
* MISS bit, one per missed-packet event, cleared after each report -- and with
* the receiver disabled it can only be 0 here. What it must not be is
* invented: the field is the chip's, so an RX ring inherits it unchanged.
*/
#define LANCE_RAP 0x05900000 /* bit address of the register-select port */
#define LANCE_RDP 0x05800000 /* bit address of the data port */
#define CSR0_INIT 0x0001
#define CSR0_STRT 0x0002
#define CSR0_STOP 0x0004
#define CSR0_TDMD 0x0008
#define CSR0_IDON 0x0100
#define CSR0_MISS 0x1000 /* no RX descriptor: a missed-packet event */
#define MODE_DRX 0x0001 /* disable the receiver (see above) */
#define TMD1_OWN 0x8000
#define TMD1_ENP 0x0100
#define TMD1_STP 0x0200
#define L_INIT_OFF 0x3C000 /* 12-word init block (24 bytes) */
#define L_RING_OFF 0x3C040 /* 8 TX descriptors, 8 bytes each */
#define L_RING_LEN 8 /* TLEN code 3 = 2^3 descriptors */
#define STATUS_EVERY 64 /* frames per status report */
#define STATUS_LEN 60 /* Ethernet minimum; the chip adds the FCS */
/* Offset of the VS header inside the Ethernet frame, and of the body. */
#define ETH_HDR_LEN 14
#define STAT_BODY (ETH_HDR_LEN + FP_HEADER_SIZE)
/*
* Static fallback. pool_static is global so the linker map names it and the
* MAME check can assert the exact address a failed malloc falls back to.
@@ -118,6 +185,18 @@ static unsigned int heap_bytes()
return *(unsigned int *)HEAP_FREE / 8;
}
/*
* Task 6 state. The toolchain never runs .cinit, so main() clears these
* before they are used (same rule as the markers).
*/
static unsigned int lance_ok; /* 1 once the LANCE is ours */
static unsigned int lance_txring; /* bit address of the TX ring */
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 */
/*
* blit_row(src, daddr_xy, dydx)
*
@@ -191,6 +270,290 @@ fp_header *h;
vid_eint_();
}
/*
* The LANCE, transmit only. The sequence is lance.c's, and the comment on
* each step names what makes it necessary (harness/mame/README.md rule 4: a
* step taken from another driver carries its source).
*/
static void w16(bit, v)
unsigned int bit, v;
{
*(unsigned short *)bit = v;
}
static unsigned int r16(bit)
unsigned int bit;
{
return *(unsigned short *)bit;
}
static void wb(bit, v)
unsigned int bit, v;
{
*(unsigned char *)bit = v;
}
/*
* The chip's register file takes a select write (RAP) then a data access
* (RDP), and needs a settle between them: am79c90.cpp's regs_w consumes the
* address register on the next access, and lance.c measured that a store
* immediately after the select lands in the wrong register. Same 30-pass
* settle as lance.c.
*/
static void lance_settle()
{
unsigned int i;
for (i = 0; i < 30; i++) {
}
}
static void lance_csr_w(reg, v)
unsigned int reg, v;
{
*(unsigned short *)LANCE_RAP = reg;
lance_settle();
*(unsigned short *)LANCE_RDP = v;
lance_settle();
}
static unsigned int lance_csr_r(reg)
unsigned int reg;
{
unsigned int v;
*(unsigned short *)LANCE_RAP = reg;
lance_settle();
v = *(unsigned short *)LANCE_RDP;
lance_settle();
return v;
}
/*
* lance_start(ptr) -- take the LANCE over for transmit. 1 on success.
*
* STOP comes first because the init block address (CSR1/CSR2/CSR3) is only
* writable while the chip is stopped -- am79c90.cpp regs_w guards each of them
* with "if (m_csr[0] & CSR0_STOP)" -- and because STOP resets the chip, which
* clears INEA: the network interrupt is off before this payload touches
* anything, so the firmware's ISR cannot act on a half-programmed ring. The
* caller also masks interrupts for the whole sequence.
*
* The init block's words are the C-LANCE's own order: MODE, PADR (three
* words), LADRF (four), RX ring address/length, TX ring address/length. A
* ring word is the address's bits 16-23 in its low byte and the length code
* (2^code descriptors) in bits 13-15; the pool sits above 64 KB, so unlike
* lance.c's low-memory rings the high byte is not zero and has to be written.
*/
static int lance_start(ptr)
unsigned int ptr;
{
unsigned int init, ring, buf, cb, i, idon;
/*
* Alignment is the CHIP's requirement, not a convenience. The Am7990
* masks what it is given: INIT_ADDR_MASK 0xFFFFFE for the init block and
* RING_ADDR_MASK 0xFFFFF8 for a ring (am79c90.h), because a descriptor
* is eight bytes. The pool base is only word-aligned (0x0090C8D0 is
* byte 0x12191A), so a fixed offset does NOT give a ring the chip can
* find: the first run of this code put the ring at byte 0x15D93A, the
* chip masked that to 0x15D938 and read our TMD0 word as TMD1 -- it never
* fetched the status buffer at all (proved from VT1200_LANCE_DMA_LOG:
* no read of the buffer address, and the chip writing 0x599A into our
* TMD0 words). Round up to the alignment the chip will keep.
*/
init = (ptr + L_INIT_OFF * 8 + 0x0F) & 0xFFFFFFF0; /* 16-bit words */
ring = (ptr + L_RING_OFF * 8 + 0x3F) & 0xFFFFFFC0; /* 8-byte */
buf = ring + L_RING_LEN * 64; /* after the ring */
lance_txring = ring;
status_buf = buf;
lance_csr_w(0, CSR0_STOP);
w16(init + 0 * 16, MODE_DRX); /* no loopback, FCS appended */
w16(init + 1 * 16, 0x0008); /* PADR 08:00:2b:16:4b:5a */
w16(init + 2 * 16, 0x162b);
w16(init + 3 * 16, 0x5a4b);
w16(init + 4 * 16, 0); /* LADRF: physical + broadcast */
w16(init + 5 * 16, 0);
w16(init + 6 * 16, 0);
w16(init + 7 * 16, 0);
w16(init + 8 * 16, 0); /* RX ring: none (MODE_DRX) */
w16(init + 9 * 16, 0);
cb = ring >> 3; /* BYTE address of the ring */
w16(init + 10 * 16, cb & 0xFFFF);
w16(init + 11 * 16, ((cb >> 16) & 0xFF) | (3 << 13));
/* Every descriptor points at the one status buffer; TMD1's OWN stays
* clear until a frame is ready, and TMD2 holds the length as a two's
* complement (0x10000 - 60), lance.c's descriptor layout. */
cb = buf >> 3;
for (i = 0; i < L_RING_LEN; i++) {
w16(ring + i * 64 + 0, cb & 0xFFFF);
w16(ring + i * 64 + 16, (cb >> 16) & 0xFF);
w16(ring + i * 64 + 32, (0x10000 - STATUS_LEN) & 0xFFFF);
w16(ring + i * 64 + 48, 0);
}
for (i = 0; i < STATUS_LEN; i++) {
wb(buf + i * 8, 0);
}
lance_csr_w(1, (init >> 3) & 0xFFFF);
lance_csr_w(2, ((init >> 3) >> 16) & 0xFF);
lance_csr_w(3, 0);
lance_csr_w(0, CSR0_INIT);
idon = 0;
for (i = 0; i < 20000; i++) {
idon = lance_csr_r(0);
if (idon & CSR0_IDON) {
break;
}
}
*(unsigned int *)ST_INIT = idon;
if (!(idon & CSR0_IDON)) {
return 0;
}
/* STRT turns the transmitter on; no INEA, so no interrupt reaches the
* firmware's network ISR while this payload owns the chip. */
lance_csr_w(0, CSR0_STRT);
for (i = 0; i < 30; i++) {
}
return 1;
}
/*
* send_status(frames_rx) -- build and transmit one status frame.
*
* The frame is an ordinary Ethernet frame: broadcast destination (the LANCE
* accepts broadcast with an empty LADRF, and the host's capture sees it), the
* terminal's own address from its Ethernet ROM, the stream's ethertype 0x88B7
* (frame.py ETHERTYPE), then the same 16-byte VS header every other frame
* carries, with rows and stretch zero because a status has no geometry. The
* body is 12 bytes, LITTLE-endian as frame.py's pack_status() writes it:
* frames_rx u32, crc_err u16, drops u16, csr0 u16, heap_free_kb u16. (The
* command protocol in lance_cmd.c is big-endian; this one is not, and getting
* it backwards reads 64 frames as 0x40000000 -- which is how the Task 6
* MAME run first found it.)
*
* 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.
*
* 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
* owns is what every reference driver refuses to do (Linux stops the queue,
* NetBSD prints "missing buffer"), and the caller must not count a report it
* never sent: it advances status_seen only on 1.
*/
static int send_status(frames_rx)
unsigned int frames_rx;
{
unsigned int i, crc, csr0, tmd1, off, hb, miss, bad, free_kb;
hb = status_buf;
/* Ethernet header. */
for (i = 0; i < 6; i++) {
wb(hb + i * 8, 0xFF);
}
wb(hb + 6 * 8, 0x08); wb(hb + 7 * 8, 0x00);
wb(hb + 8 * 8, 0x2B); wb(hb + 9 * 8, 0x16);
wb(hb + 10 * 8, 0x4B); wb(hb + 11 * 8, 0x5A);
wb(hb + 12 * 8, 0x88); wb(hb + 13 * 8, 0xB7);
/* The VS header, laid out exactly as frame.py's pack_header() does. */
off = hb + ETH_HDR_LEN * 8;
wb(off + 0 * 8, FP_MAGIC0);
wb(off + 1 * 8, FP_MAGIC1);
wb(off + 2 * 8, FP_VERSION);
wb(off + 3 * 8, FP_KIND_STATUS);
wb(off + 4 * 8, status_seq & 0xFF);
wb(off + 5 * 8, (status_seq >> 8) & 0xFF);
wb(off + 6 * 8, FP_STATUS_SIZE);
wb(off + 7 * 8, 0);
wb(off + 8 * 8, 0); /* rows: none */
wb(off + 9 * 8, 0); /* stretch: none */
wb(off + 10 * 8, 0); /* reserved */
wb(off + 11 * 8, 0);
crc = fp_crc((unsigned char *)off, FP_PREFIX_SIZE);
wb(off + 12 * 8, crc & 0xFF);
wb(off + 13 * 8, (crc >> 8) & 0xFF);
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). */
csr0 = lance_csr_r(0);
miss = 0;
if (csr0 & CSR0_MISS) {
drops_total = drops_total + 1;
miss = 1;
}
bad = *(unsigned int *)RX_BAD;
free_kb = heap_bytes() >> 10;
off = hb + STAT_BODY * 8;
wb(off + 0 * 8, frames_rx & 0xFF);
wb(off + 1 * 8, (frames_rx >> 8) & 0xFF);
wb(off + 2 * 8, (frames_rx >> 16) & 0xFF);
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 & 0xFF);
wb(off + 7 * 8, (drops_total >> 8) & 0xFF);
wb(off + 8 * 8, csr0 & 0xFF);
wb(off + 9 * 8, (csr0 >> 8) & 0xFF);
wb(off + 10 * 8, free_kb & 0xFF);
wb(off + 11 * 8, (free_kb >> 8) & 0xFF);
/* Arm the next descriptor: TMD3, then the length, then ownership last,
* so the chip cannot take a descriptor whose length is still stale
* (lance_cmd.c does the same, and NetBSD's am7990_rint() says why). */
off = lance_txring + lance_txslot * 64;
for (i = 0; i < 10000; i++) {
if (!(r16(off + 16) & TMD1_OWN)) {
break;
}
}
if (r16(off + 16) & TMD1_OWN) {
return 0; /* still the chip's: arm nothing */
}
w16(off + 48, 0);
w16(off + 32, (0x10000 - STATUS_LEN) & 0xFFFF);
w16(off + 16, TMD1_OWN | TMD1_STP | TMD1_ENP
| ((status_buf >> 3 >> 16) & 0xFF));
/* Where the harness must look to watch the chip finish this frame. */
*(unsigned int *)ST_TMLAST = off + 16;
/* One write: a bare TDMD would clear STRT (CSR0 is a command shadow;
* lance.c's comment and every reference driver agree). */
lance_csr_w(0, CSR0_STRT | CSR0_TDMD);
tmd1 = 0;
for (i = 0; i < 20000; i++) {
tmd1 = r16(off + 16);
if (!(tmd1 & TMD1_OWN)) {
break;
}
}
lance_txslot = (lance_txslot + 1) & (L_RING_LEN - 1);
status_seq = status_seq + 1;
*(unsigned int *)ST_FRAMES = frames_rx;
*(unsigned int *)ST_TMD1 = tmd1;
/* ST_SENT last: a reader that sees it move reads a frame that was
* built, transmitted and recorded, never one in progress. */
*(unsigned int *)ST_SENT = *(unsigned int *)ST_SENT + 1;
if (miss) {
lance_csr_w(0, CSR0_MISS);
}
return 1;
}
/*
* Handle one slot if the receive path has raised its ready flag.
*
@@ -254,7 +617,7 @@ unsigned int slot;
int main()
{
unsigned int before, after, ptr;
unsigned int before, after, ptr, frames, sent;
*(unsigned int *)VID_ALIVE = 0;
*(unsigned int *)POOL_PTR = 0;
@@ -301,9 +664,50 @@ int main()
*(unsigned int *)RX_SLOT_B = ptr + SLOT_BYTES * 8;
*(unsigned int *)POOL_PTR = ptr; /* publish last: a complete record */
/* Task 6: the status back-channel. The record starts empty, then the
* LANCE is taken over for transmit with interrupts masked -- the OS's
* drivers were using this chip a moment ago, and a network ISR running
* in the middle of the ring setup would see a half-written descriptor. */
*(unsigned int *)RX_EOF = 0;
*(unsigned int *)ST_SENT = 0;
*(unsigned int *)ST_FRAMES = 0;
*(unsigned int *)ST_BUF = 0;
*(unsigned int *)ST_TMD1 = 0;
*(unsigned int *)ST_INIT = 0;
*(unsigned int *)ST_TMLAST = 0;
lance_txslot = 0;
status_seq = 0;
status_seen = 0;
drops_total = 0;
vid_dint_();
lance_ok = lance_start(ptr);
vid_eint_();
*(unsigned int *)ST_BUF = status_buf;
for (;;) {
*(unsigned int *)VID_ALIVE = *(unsigned int *)VID_ALIVE + 1;
rx_poll(0);
rx_poll(1);
/* Report every STATUS_EVERY accepted frames, and -- once the
* harness says the sender has stopped -- for every further
* accepted frame, so a finite run ends with a report whose
* frames_rx is the final count rather than the last multiple
* of 64 (RX_EOF is harness-only; see frame_parse.h). */
frames = *(unsigned int *)RX_OK;
if (!lance_ok) {
continue;
}
if (frames - status_seen >= STATUS_EVERY
|| (*(unsigned int *)RX_EOF != 0 && frames != status_seen)) {
vid_dint_();
sent = send_status(frames);
vid_eint_();
if (sent) {
status_seen = frames;
}
}
}
}