forked from arti/vt1200
feat(stream): paced sender and terminal status frames
This commit is contained in:
1 parent
5cd708460a
commit
53b1580abd
11 files changed
+2225
-15
No files matched your search
@@ -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
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user