feat(stream): frame parser shared by host tests and payload

This commit is contained in:
opencode.energia.arti.ee committed 2026-10-30 23:16:48 +00:00
1 parent 213083f2e0
commit 487bb1789a
8 files changed
+797 -46

No files matched your search

+4 -1
View File
@@ -3,9 +3,12 @@
#
# harness/mame/run-vidstream.sh # build, then mode alloc
# BENCH=0 harness/mame/run-vidstream.sh # use the existing .bin
# VT1200_MODE=rx harness/mame/run-vidstream.sh # Task 4's mode
# VT1200_MODE=rx harness/mame/run-vidstream.sh # alloc + the receive path
# 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
# asserts RX_OK == 1 && RX_BAD == 1 (see vidstream.lua and the task-4 report).
#
# 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
+226 -10
View File
@@ -1,4 +1,5 @@
-- vidstream.lua -- load the streaming payload and check its startup allocation.
-- vidstream.lua -- load the streaming payload and check its startup allocation
-- and its receive path.
--
-- MECHANISM: debugger entry, not a delivery mechanism. The firmware is run
-- with VT1200_SKIP_LEVEL1=1 (a documented divergence) to the OS, because the
@@ -17,13 +18,27 @@
-- * on a failed malloc, POOL_PTR is the linked static buffer and the low bit
-- of POOL_FREE (before) marks the fallback, i.e. the payload keeps running.
--
-- Mode "rx" (Task 4) is "alloc" plus the receive path: it writes a VALID frame
-- into slot A and the same frame with one payload byte flipped into slot B,
-- raises each slot's ready flag (the role the LANCE receive interrupt plays on
-- the real transport), and asserts RX_OK == 1 && RX_BAD == 1, that RX_LAST_LEN
-- is the accepted frame's payload_len, that RX_SLOT names slot A, and that the
-- payload cleared both ready flags.
--
-- 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
-- AGENTS.md rule 4 requires to be quoted and independently read. The DMA path
-- is proven in Task 7, on the unit.
--
-- Marker page: bit 0x01810000. A marker named 0x018100NN in the plan is a
-- BYTE offset into that page (0x01810024 is four bits after 0x01810020 --
-- inside the same 32-bit cell -- so the literal reading cannot work). The C
-- side uses the same rule: bit = 0x01810000 + NN * 8.
-- side uses the same rule: bit = 0x01810000 + NN * 8. The full table,
-- including the RX_* cells below, is in harness/udpstream/frame_parse.h.
--
-- Env:
-- VT1200_MODE alloc (default; rx/draw arrive in later tasks)
-- VT1200_MODE alloc (default) or rx
-- VID_AT load/entry frame (default 2400, the OS phase)
-- VID_END inspection/exit frame (default VID_AT + 60)
-- VID_PAYLOAD raw .text image (default harness/udpstream/build/vidstream.bin)
@@ -65,12 +80,97 @@ local POOL_FREEB = MARK_PAGE + 0x24 * 8
local POOL_FREEA = MARK_PAGE + 0x28 * 8
local VID_ALIVE = MARK_PAGE + 0x2c * 8
if MODE ~= "alloc" then
print("VID ERROR: mode '" .. MODE .. "' is not implemented in Task 3")
-- Task 4: receive counters and the injection interface (frame_parse.h).
local RX_OK = MARK_PAGE + 0x30 * 8
local RX_BAD = MARK_PAGE + 0x34 * 8
local RX_LAST_LEN = MARK_PAGE + 0x38 * 8
local RX_SLOT = MARK_PAGE + 0x3c * 8
local RX_SLOT_A = MARK_PAGE + 0x60 * 8
local RX_SLOT_B = MARK_PAGE + 0x64 * 8
local RX_READY_A = MARK_PAGE + 0x68 * 8
local RX_LEN_A = MARK_PAGE + 0x6c * 8
local RX_READY_B = MARK_PAGE + 0x70 * 8
local RX_LEN_B = MARK_PAGE + 0x74 * 8
-- Must match vidstream.c.
local SLOT_BYTES = 24576
-- The injected frame: 8 rows of 1280 pixels, 1 bpp MSB-first (160 bytes a
-- row), each row repeated 8 times. 1280 B of payload -- the same geometry
-- vidsrc.py's ROW_BYTES produces, so Task 5 can blit it unchanged.
local RX_ROWS = 8
local RX_STRETCH = 8
local RX_ROW_BYTES = 160
local RX_PLEN = RX_ROWS * RX_ROW_BYTES
local RX_FRAME = 16 + RX_PLEN
if MODE ~= "alloc" and MODE ~= "rx" then
print("VID ERROR: mode '" .. MODE .. "' is not implemented")
machine:exit()
return
end
-- CCITT-FALSE (polynomial 0x1021, init 0xFFFF, not reflected, no final xor).
-- frame.py, frame_parse.c and this loop are three independent readings of the
-- same spec; the harness signs its own frames rather than reusing a value the
-- payload computed, so the C parser accepting them is a real check.
local function crc_add(crc, bytes, first, count)
for i = first, first + count - 1 do
crc = (crc ~ (bytes[i] << 8)) & 0xFFFF
for _ = 1, 8 do
if (crc & 0x8000) ~= 0 then
crc = ((crc << 1) ~ 0x1021) & 0xFFFF
else
crc = (crc << 1) & 0xFFFF
end
end
end
return crc
end
-- A complete frame, header and payload, laid out as frame.py writes it.
local function make_frame(kind, frame_no, rows, stretch, plen)
local b = {}
b[0] = 0x56 -- 'V'
b[1] = 0x53 -- 'S'
b[2] = 0x01 -- version
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
for i = 0, plen - 1 do
b[16 + i] = (i * 7 + 3) & 0xFF
end
local crc = crc_add(0xFFFF, b, 0, 12)
crc = crc_add(crc, b, 16, plen)
b[12] = crc & 0xFF
b[13] = (crc >> 8) & 0xFF
b[14] = 0
b[15] = 0
return b
end
local function poke(base, bytes, n)
for i = 0, n - 1 do
prog:write_u8(base + i * 8, bytes[i])
end
end
local function verify(base, bytes, n)
local bad = 0
for i = 0, n - 1 do
if prog:read_u8(base + i * 8) ~= bytes[i] then
bad = bad + 1
end
end
return bad == 0, bad
end
local payload = nil
do
local f, err = io.open(PATH, "rb")
@@ -91,6 +191,9 @@ local armed = false
local direct = false
local alive0 = nil
local alive_last = nil
local injected = false
local readback_a = false
local readback_b = false
local function load_payload()
local n = math.floor(#payload / 2)
@@ -127,7 +230,38 @@ local function enter_direct(reason)
frames, reason, hex(ENTRY_PTR)))
end
local function inspect()
-- Fill slot A with a valid frame and slot B with the same frame carrying one
-- flipped payload byte, then raise both ready flags. The payload has already
-- published the slot addresses by the time this runs; a zero there means the
-- malloc has not finished and the caller retries next frame.
local function inject()
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, 0x0021, RX_ROWS, RX_STRETCH, RX_PLEN)
poke(a, fa, RX_FRAME)
readback_a = verify(a, fa, RX_FRAME)
local fb = make_frame(1, 0x0022, RX_ROWS, RX_STRETCH, RX_PLEN)
fb[16 + 100] = fb[16 + 100] ~ 0x01 -- payload byte flipped, CRC left alone
poke(b, fb, RX_FRAME)
readback_b = verify(b, fb, RX_FRAME)
prog:write_u32(RX_LEN_A, RX_FRAME)
prog:write_u32(RX_LEN_B, RX_FRAME)
prog:write_u32(RX_READY_A, 1)
prog:write_u32(RX_READY_B, 1)
print(string.format("VID: frame %d: injected slot A (%s, %d B, valid) and slot B (%s, %d B, one payload byte flipped)",
frames, hex(a), RX_FRAME, hex(b), RX_FRAME))
return true
end
local function inspect_pool()
local ptr = prog:read_u32(POOL_PTR)
local freeb = prog:read_u32(POOL_FREEB)
local freea = prog:read_u32(POOL_FREEA)
@@ -208,7 +342,69 @@ local function inspect()
end
end
print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL")
return ok
end
local function inspect_rx()
local a = prog:read_u32(RX_SLOT_A)
local b = prog:read_u32(RX_SLOT_B)
local rxa = prog:read_u32(RX_READY_A)
local rxb = prog:read_u32(RX_READY_B)
local ok = true
if not injected then
print("VID: no frame was ever injected -> FAIL")
return false
end
print(string.format("VID: slots A=%s B=%s (delta %d bits = %d bytes, expected %d)",
hex(a), hex(b), b - a, (b - a) / 8, SLOT_BYTES))
if a == 0 or (b - a) ~= SLOT_BYTES * 8 then
print("VID: slot B does not follow slot A by SLOT_BYTES -> FAIL")
ok = false
end
if readback_a and readback_b then
print("VID: both injected frames read back byte for byte -> PASS")
else
print(string.format("VID: injected frame readback mismatch (A=%s B=%s) -> FAIL",
tostring(readback_a), tostring(readback_b)))
ok = false
end
local oka = prog:read_u32(RX_OK)
local bad = prog:read_u32(RX_BAD)
if oka == 1 and bad == 1 then
print(string.format("VID: RX_OK=%d RX_BAD=%d -> PASS", oka, bad))
else
print(string.format("VID: RX_OK=%d RX_BAD=%d -> FAIL (one valid frame, one corrupt)", oka, bad))
ok = false
end
local last = prog:read_u32(RX_LAST_LEN)
if last == RX_PLEN then
print(string.format("VID: RX_LAST_LEN=%d = payload_len of the accepted frame -> PASS", last))
else
print(string.format("VID: RX_LAST_LEN=%d, expected %d -> FAIL", last, RX_PLEN))
ok = false
end
local slot = prog:read_u32(RX_SLOT)
if slot == 0 then
print("VID: RX_SLOT=0 (slot A, where the valid frame went) -> PASS")
else
print(string.format("VID: RX_SLOT=%d, expected 0 (slot A) -> FAIL", slot))
ok = false
end
if rxa == 0 and rxb == 0 then
print("VID: the payload cleared both ready flags -> PASS")
else
print(string.format("VID: ready flags A=%d B=%d, both must be cleared -> FAIL", rxa, rxb))
ok = false
end
return ok
end
emu.register_frame_done(function()
@@ -234,6 +430,15 @@ emu.register_frame_done(function()
alive0 = prog:read_u32(VID_ALIVE)
end
-- Inject as soon as the payload has entered AND published the slots: the
-- slot addresses are written straight after the malloc, so a non-zero
-- value there means the receive loop is about to run.
if entered and MODE == "rx" and not injected then
if prog:read_u32(RX_SLOT_A) ~= 0 and prog:read_u32(RX_SLOT_B) ~= 0 then
injected = inject()
end
end
if entered and frames % 10 == 0 and frames < STOP_AT then
alive_last = prog:read_u32(VID_ALIVE)
end
@@ -243,10 +448,21 @@ emu.register_frame_done(function()
print("VID: payload never entered -> FAIL")
print("VID: RESULT FAIL")
else
local ok2, err = pcall(inspect)
if not ok2 then
print("VID: ERROR " .. tostring(err))
local ok, result = pcall(inspect_pool)
if not ok then
print("VID: ERROR " .. tostring(result))
print("VID: RESULT FAIL")
else
if MODE == "rx" then
local ok2, result2 = pcall(inspect_rx)
if not ok2 then
print("VID: ERROR " .. tostring(result2))
result = false
else
result = result and result2
end
end
print(result and "VID: RESULT PASS" or "VID: RESULT FAIL")
end
end
machine:exit()
+39 -15
View File
@@ -39,6 +39,13 @@ fi
SRC=${STREAM_SRC:-$DIR/vidstream.c}
cp "$SRC" "$WORK/VIDSTRM.C"
cp "$DIR/romapi.h" "$WORK/ROMAPI.H"
# The frame parser is a second translation unit: vidstream.c calls it and the
# host unit test (tests/test_frame_parse.c) compiles the same file. The 1987
# preprocessor cannot open a name with an 11-character base (Task 3 lost a
# build to VIDSTREAM.C the same way), so the work copies use 8.3 names and the
# C sources include "frmpsr.h" -- a symlink to frame_parse.h in the repo.
cp "$DIR/frame_parse.c" "$WORK/FRMPSR.C"
cp "$DIR/frame_parse.h" "$WORK/FRMPSR.H"
cp "$DIR/entry.asm" "$WORK/ENTRY.ASM"
cp "$DIR/vram.asm" "$WORK/VRAM.ASM"
@@ -63,6 +70,7 @@ printf '#define POOL_BYTES %s\n' "$POOL_BYTES" > "$WORK/POOLCFG.H"
printf '%s\n' '-m VIDSTRM.map'
printf '%s\n' '-e _start'
printf '%s\n' 'VIDSTRM.obj'
printf '%s\n' 'FRMPSR.obj'
printf '%s\n' 'ENTRY.obj'
printf '%s\n' 'VRAM.obj'
[ -z "$VRAMB_OBJ" ] || printf '%s\n' "$VRAMB_OBJ"
@@ -85,37 +93,52 @@ sed -i 's/$/\r/' "$WORK/VIDSTRM.CMD"
rm -f "$WORK"/VIDSTRM.OBJ "$WORK"/VIDSTRM.OUT "$WORK"/VIDSTRM.LST \
"$WORK"/VIDSTRM.MAP "$WORK"/VIDSTRM.ASM "$WORK"/VIDSTRM.CPP \
"$WORK"/VIDSTRM.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST \
"$WORK"/FRMPSR.OBJ "$WORK"/FRMPSR.LST "$WORK"/FRMPSR.ASM \
"$WORK"/FRMPSR.CPP "$WORK"/FRMPSR.IF \
"$WORK"/VRAM.OBJ "$WORK"/VRAM.LST "$WORK"/VRAMB.OBJ "$WORK"/VRAMB.LST
# The toolchain runs from a batch file, not from one -c per command. Once the
# build grew to a second translation unit the -c list stopped completing:
# FRMPSR.OBJ and ENTRY.OBJ were written, VRAM.OBJ and VIDSTRM.OUT never were,
# and DOSBox then sat at the prompt until the 240 s timeout (reproduced twice,
# with nothing in dosbox.log -- the DOS tools write to the screen, not stdout).
# DOSBox feeds each -c line through the keyboard buffer, which is the likeliest
# cause; a .BAT has no such limit, completes the same steps in ~2 s, and makes
# the step list one readable block. CRLF, because DOS.
{
printf 'gspcpp VIDSTRM\r\n'
printf 'gspcc VIDSTRM\r\n'
printf 'gspcg VIDSTRM\r\n'
printf 'gspa VIDSTRM VIDSTRM.obj VIDSTRM.lst -l\r\n'
printf 'gspcpp FRMPSR\r\n'
printf 'gspcc FRMPSR\r\n'
printf 'gspcg FRMPSR\r\n'
printf 'gspa FRMPSR FRMPSR.obj FRMPSR.lst -l\r\n'
printf 'gspa ENTRY ENTRY.obj ENTRY.lst -l\r\n'
printf 'gspa VRAM VRAM.obj VRAM.lst -l\r\n'
[ -z "$VRAMB_OBJ" ] || printf 'gspa VRAMB VRAMB.obj VRAMB.lst -l\r\n'
printf 'gsplnk VIDSTRM.cmd\r\n'
printf 'exit\r\n'
} > "$WORK/BUILD.BAT"
run_dosbox() {
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \
-c "mount c $WORK" -c "c:" \
-c "gspcpp VIDSTRM" \
-c "gspcc VIDSTRM" \
-c "gspcg VIDSTRM" \
-c "gspa VIDSTRM VIDSTRM.obj VIDSTRM.lst -l" \
-c "gspa ENTRY ENTRY.obj ENTRY.lst -l" \
-c "gspa VRAM VRAM.obj VRAM.lst -l" \
"$@" \
-c "gsplnk VIDSTRM.cmd" \
-c "BUILD.BAT" \
-c "exit" -exit > "$WORK/dosbox.log" 2>&1
}
if [ -n "$VRAMB_OBJ" ]; then
run_dosbox -c "gspa VRAMB VRAMB.obj VRAMB.lst -l"
else
run_dosbox
fi
run_dosbox
if [ ! -f "$WORK/VIDSTRM.OUT" ]; then
echo "build-stream.sh: GSPLNK produced no VIDSTRM.OUT" >&2
sed 's/\r$//' "$WORK/VIDSTRM.MAP" 2>/dev/null | tail -20 >&2 || true
for f in VIDSTRM.LST ENTRY.LST VRAM.LST $VRAMB_LST; do
for f in VIDSTRM.LST FRMPSR.LST ENTRY.LST VRAM.LST $VRAMB_LST; do
sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' | grep -vi 'no errors' >&2 || true
done
exit 1
fi
for f in VIDSTRM.LST ENTRY.LST VRAM.LST $VRAMB_LST; do
for f in VIDSTRM.LST FRMPSR.LST ENTRY.LST VRAM.LST $VRAMB_LST; do
if sed 's/\r$//' "$WORK/$f" | grep -i 'error' | grep -qvi 'no errors'; then
echo "build-stream.sh: $f diagnostics:" >&2
sed 's/\r$//' "$WORK/$f" | grep -in 'error' | grep -vi 'no errors' >&2
@@ -126,6 +149,7 @@ done
cp "$WORK/VIDSTRM.OUT" "$OUT/vidstream.coff"
sed 's/\r$//' "$WORK/VIDSTRM.MAP" > "$OUT/vidstream.map"
sed 's/\r$//' "$WORK/VIDSTRM.LST" > "$OUT/vidstream.lst"
sed 's/\r$//' "$WORK/FRMPSR.LST" > "$OUT/frmpsr.lst"
sed 's/\r$//' "$WORK/ENTRY.LST" > "$OUT/entry.lst"
sed 's/\r$//' "$WORK/VRAM.LST" > "$OUT/vram.lst"
if [ -n "$VRAMB_LST" ]; then
+111
View File
@@ -0,0 +1,111 @@
/*
* frame_parse.c -- receive-side frame check for the VT1200 video stream.
*
* fp_parse() answers one question: is the block of bytes the transport just
* delivered a complete, uncorrupted frame of the format frame.py writes? It
* checks, in this order, the length bound, the magic and version, and finally
* the CRC -- the CRC is the expensive part, so a truncated or foreign frame is
* rejected before it is computed.
*
* The CRC is CCITT-FALSE (polynomial 0x1021, initial value 0xFFFF, no input or
* output reflection, no final xor), the same one frame.py implements with a
* 256-entry table. It covers header bytes 0..11 followed by the payload; the
* stored crc16 at offset 12 is never part of its own input.
*
* This file is compiled twice: by the 1987 TI C compiler inside the payload
* build (build-stream.sh stages it as FRMPSR.C) and by the host cc for
* tests/test_frame_parse.c. So it is K&R C -- no prototypes in definitions,
* no declarations after statements, no '//', no 'const' -- and it must not
* assume a 32-bit int beyond the fields it masks.
*/
#include "frmpsr.h"
/*
* One byte through the CRC, bit by bit. frame.py builds a table from the
* identical loop; that is the whole reason this is written the long way, so
* the two can be compared line for line.
*/
static unsigned int fp_crc_byte(crc, byte)
unsigned int crc;
unsigned int byte;
{
int i;
crc = (crc ^ ((byte & 0xFF) << 8)) & 0xFFFF;
for (i = 0; i < 8; i++) {
if (crc & 0x8000) {
crc = ((crc << 1) ^ 0x1021) & 0xFFFF;
} else {
crc = (crc << 1) & 0xFFFF;
}
}
return crc;
}
/* Fold len bytes at buf into crc. */
static unsigned int fp_crc_add(crc, buf, len)
unsigned int crc;
unsigned char *buf;
unsigned int len;
{
unsigned int i;
for (i = 0; i < len; i++) {
crc = fp_crc_byte(crc, buf[i]);
}
return crc;
}
/*
* int fp_parse(unsigned char *buf, unsigned int len, fp_header *out)
*
* See frame_parse.h for the contract. A NULL buffer or header is a rejection
* rather than a crash: the payload calls this with a pointer it published
* itself, and a zero there means "no slot yet".
*/
int fp_parse(buf, len, out)
unsigned char *buf;
unsigned int len;
fp_header *out;
{
unsigned int plen, stored, crc;
if (buf == 0 || out == 0) {
return 0;
}
if (len < FP_HEADER_SIZE) {
return 0;
}
if (buf[0] != FP_MAGIC0 || buf[1] != FP_MAGIC1) {
return 0;
}
if (buf[2] != FP_VERSION) {
return 0;
}
plen = (unsigned int) buf[6] | ((unsigned int) buf[7] << 8);
/*
* Subtracting rather than adding: plen can be 0xFFFF, and
* FP_HEADER_SIZE + plen would still fit a 32-bit unsigned int but not a
* 16-bit one. len is known to be at least FP_HEADER_SIZE here.
*/
if (len - FP_HEADER_SIZE < plen) {
return 0;
}
stored = ((unsigned int) buf[FP_CRC_OFF + 1] << 8)
| (unsigned int) buf[FP_CRC_OFF];
crc = fp_crc_add(0xFFFF, buf, FP_PREFIX_SIZE);
crc = fp_crc_add(crc, buf + FP_HEADER_SIZE, plen);
if (crc != stored) {
return 0;
}
out->kind = (unsigned int) buf[3];
out->frame_no = (unsigned int) buf[4] | ((unsigned int) buf[5] << 8);
out->payload_len = plen;
out->rows = (unsigned int) buf[8];
out->stretch = (unsigned int) buf[9];
out->payload_off = FP_HEADER_SIZE;
return 1;
}
+129
View File
@@ -0,0 +1,129 @@
/*
* frame_parse.h -- the wire format and the receive-side frame check.
*
* This is the one place the wire format is written down in C; the host sender
* writes it down in harness/udpstream/frame.py. The two must agree byte for
* byte, and harness/udpstream/tests/test_frame_parse.c pins the agreement with
* a frame generated by frame.py (a golden vector, not a value this code
* produced itself).
*
* offset size field
* ------ ---- ------------------------------------------------------
* 0 2 magic, ASCII 'V','S'
* 2 1 version, 0x01
* 3 1 kind (KIND_STRIP = 1, KIND_STATUS = 5)
* 4 2 frame_no, u16 little-endian
* 6 2 payload_len, u16 little-endian
* 8 1 rows
* 9 1 stretch
* 10 2 reserved, zero
* 12 2 crc16, u16 little-endian
* 14 2 padding, zero
*
* The CRC is CCITT-FALSE (polynomial 0x1021, initial value 0xFFFF, no input or
* output reflection, no final xor) over header bytes 0..11 followed by the
* payload -- never the CRC field itself.
*
* ---------------------------------------------------------------------------
* Marker page: bit 0x01810000, byte 0x00302000. A cell below is one 32-bit
* word. Cells are 0x20 BITS apart (4 bytes), which is the rule Task 3
* established: "marker named 0x018100NN" means bit 0x01810000 + NN*8, because
* the TMS34010 is bit-addressed and four bits after a word would still be
* inside the same word. The plan numbers the page from 0x01810100, so both
* offsets are given.
*
* cell page offset bit address meaning
* ------------ ----------- ----------- --------------------------------
* POOL_PTR +0x00 0x01810100 bit pointer to the 244 KB pool
* POOL_FREEB +0x20 0x01810120 heap free bytes before malloc
* POOL_FREEA +0x40 0x01810140 heap free bytes after malloc
* VID_ALIVE +0x60 0x01810160 heartbeat while the loop runs
* RX_OK +0x80 0x01810180 frames accepted by fp_parse
* RX_BAD +0xA0 0x018101A0 frames rejected by fp_parse
* RX_LAST_LEN +0xC0 0x018101C0 payload_len of the last accepted
* RX_SLOT +0xE0 0x018101E0 slot of the last accepted (0=A)
*
* Debugger injection interface (Task 4; the harness plays the receive path):
*
* RX_SLOT_A +0x200 0x01810300 bit address of slot A
* RX_SLOT_B +0x220 0x01810320 bit address of slot B
* RX_READY_A +0x240 0x01810340 1 = slot A holds RX_LEN_A bytes
* RX_LEN_A +0x260 0x01810360 bytes in slot A
* RX_READY_B +0x280 0x01810380 1 = slot B holds RX_LEN_B bytes
* RX_LEN_B +0x2A0 0x018103A0 bytes in slot B
*
* The RX_READY_x and RX_LEN_x cells are NOT part of the wire protocol. In the real
* transport the LANCE receive interrupt fills a slot and raises that slot's
* 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.
* ---------------------------------------------------------------------------
*
* K&R C only. The 1987 toolchain in build-stream.sh compiles this header, so
* there are no ANSI prototypes, no '//' comments and no 'const' (romapi.h is
* written the same way): the parameter lists live in the comments and in the
* definitions in frame_parse.c.
*
* 8.3 names. The 1987 preprocessor cannot open an 11-character base name
* (Task 3 hit this with VIDSTREAM.C), so this file is also reachable as
* frmpsr.h -- a symlink to it -- and build-stream.sh stages it as FRMPSR.H.
* Host compiles may use either name.
*/
#ifndef FRAME_PARSE_H
#define FRAME_PARSE_H
#define FP_HEADER_SIZE 16 /* bytes of fixed header before the payload */
#define FP_PREFIX_SIZE 12 /* header bytes the CRC covers, before payload */
#define FP_CRC_OFF 12 /* offset of the stored crc16, low byte first */
#define FP_MAGIC0 0x56 /* 'V' */
#define FP_MAGIC1 0x53 /* 'S' */
#define FP_VERSION 1
#define FP_KIND_STRIP 1
#define FP_KIND_STATUS 5
/* Marker page cells; see the table above. */
#define MARK_PAGE 0x01810000
#define MARK_CELL(n) (MARK_PAGE + (n) * 8)
#define POOL_PTR MARK_CELL(0x20)
#define POOL_FREEB MARK_CELL(0x24)
#define POOL_FREEA MARK_CELL(0x28)
#define VID_ALIVE MARK_CELL(0x2c)
#define RX_OK MARK_CELL(0x30)
#define RX_BAD MARK_CELL(0x34)
#define RX_LAST_LEN MARK_CELL(0x38)
#define RX_SLOT MARK_CELL(0x3c)
#define RX_SLOT_A MARK_CELL(0x60)
#define RX_SLOT_B MARK_CELL(0x64)
#define RX_READY_A MARK_CELL(0x68)
#define RX_LEN_A MARK_CELL(0x6c)
#define RX_READY_B MARK_CELL(0x70)
#define RX_LEN_B MARK_CELL(0x74)
/* What a valid frame told us. payload_off is FP_HEADER_SIZE on success. */
typedef struct {
unsigned int kind;
unsigned int frame_no;
unsigned int rows;
unsigned int stretch;
unsigned int payload_len;
unsigned int payload_off;
} fp_header;
/*
* int fp_parse(unsigned char *buf, unsigned int len, fp_header *out)
*
* 1 and *out filled when buf[0..len) is a well-formed frame, 0 otherwise.
* A frame is well formed when len is at least FP_HEADER_SIZE, the magic and
* version match, len is at least FP_HEADER_SIZE + payload_len, and the stored
* crc16 equals the CCITT-FALSE CRC over buf[0..11] and the payload. Bytes
* beyond the frame are ignored: payload_len is authoritative (the transport
* hands us exactly what arrived, and a tail is not part of the frame).
*/
int fp_parse();
#endif /* FRAME_PARSE_H */
+1
View File
@@ -0,0 +1 @@
frame_parse.h
+167
View File
@@ -0,0 +1,167 @@
/*
* test_frame_parse.c -- host unit test for the receive-side frame check.
*
* cc -std=c89 -Wall -o /tmp/tfp harness/udpstream/tests/test_frame_parse.c \
* harness/udpstream/frame_parse.c && /tmp/tfp
*
* The frame under test is a GOLDEN VECTOR, not something this test or
* frame_parse.c computed: the bytes in `golden` came from frame.py, the
* sender's implementation of the same spec. That is what makes the test a
* cross-check of the C parser rather than a tautology: if the C CRC, the
* field offsets or the coverage (header bytes 0..11 plus the payload, never
* the CRC field) were wrong, the golden frame would be rejected.
*
* Regenerate the vector with:
*
* cd harness/udpstream && python3 -c '
* import frame
* pl = bytes((i*7+3) & 0xFF for i in range(80))
* d = frame.pack_header(1, 0x1234, len(pl), 1, 8, payload=pl) + pl
* print(len(d)); print(", ".join("0x%02X" % b for b in d))'
*
* (kind 1 = KIND_STRIP, frame_no 0x1234, rows 1, stretch 8, payload_len 80.)
*/
#include <stdio.h>
#include <string.h>
#include "../frame_parse.h"
static int checks;
static int failures;
static void check(int cond, const char *what)
{
checks++;
if (!cond) {
failures++;
printf("FAIL: %s\n", what);
}
}
static const unsigned char golden[] = {
0x56, 0x53, 0x01, 0x01, 0x34, 0x12, 0x50, 0x00,
0x01, 0x08, 0x00, 0x00, 0xB3, 0x3C, 0x00, 0x00,
0x03, 0x0A, 0x11, 0x18, 0x1F, 0x26, 0x2D, 0x34,
0x3B, 0x42, 0x49, 0x50, 0x57, 0x5E, 0x65, 0x6C,
0x73, 0x7A, 0x81, 0x88, 0x8F, 0x96, 0x9D, 0xA4,
0xAB, 0xB2, 0xB9, 0xC0, 0xC7, 0xCE, 0xD5, 0xDC,
0xE3, 0xEA, 0xF1, 0xF8, 0xFF, 0x06, 0x0D, 0x14,
0x1B, 0x22, 0x29, 0x30, 0x37, 0x3E, 0x45, 0x4C,
0x53, 0x5A, 0x61, 0x68, 0x6F, 0x76, 0x7D, 0x84,
0x8B, 0x92, 0x99, 0xA0, 0xA7, 0xAE, 0xB5, 0xBC,
0xC3, 0xCA, 0xD1, 0xD8, 0xDF, 0xE6, 0xED, 0xF4,
0xFB, 0x02, 0x09, 0x10, 0x17, 0x1E, 0x25, 0x2C
};
#define GOLDEN_LEN ((unsigned int) sizeof golden)
#define GOLDEN_PLEN 80
static unsigned char buf[256];
static fp_header h;
/* Copy the golden frame into the mutable buffer and return its length. */
static unsigned int load(void)
{
memcpy(buf, golden, sizeof golden);
return GOLDEN_LEN;
}
int main(void)
{
unsigned int len;
int rc;
/* ---- the well-formed frame is accepted, with every field read ---- */
len = load();
memset(&h, 0, sizeof h);
rc = fp_parse(buf, len, &h);
check(rc == 1, "golden frame accepted");
check(h.kind == FP_KIND_STRIP, "kind == KIND_STRIP");
check(h.frame_no == 0x1234, "frame_no == 0x1234 (little-endian)");
check(h.rows == 1, "rows == 1");
check(h.stretch == 8, "stretch == 8");
check(h.payload_len == GOLDEN_PLEN, "payload_len == 80");
check(h.payload_off == FP_HEADER_SIZE, "payload_off == 16");
/* ---- a flipped payload byte fails the CRC ---- */
len = load();
buf[FP_HEADER_SIZE + 40] = (unsigned char) (buf[FP_HEADER_SIZE + 40] ^ 0x01);
check(fp_parse(buf, len, &h) == 0, "flipped payload byte rejected");
/* ---- a flipped header byte inside the CRC prefix fails too ---- */
len = load();
buf[8] = (unsigned char) (buf[8] + 1);
check(fp_parse(buf, len, &h) == 0, "flipped header byte rejected");
/* ---- short buffers: one byte short of the frame, and below the header ---- */
len = load();
check(fp_parse(buf, GOLDEN_LEN - 1, &h) == 0,
"truncated payload (one byte short) rejected");
check(fp_parse(buf, FP_HEADER_SIZE - 1, &h) == 0,
"buffer shorter than the header rejected");
check(fp_parse(buf, 0, &h) == 0, "empty buffer rejected");
/* ---- wrong magic ---- */
len = load();
buf[0] = 'X';
check(fp_parse(buf, len, &h) == 0, "bad magic byte 0 rejected");
len = load();
buf[1] = 'X';
check(fp_parse(buf, len, &h) == 0, "bad magic byte 1 rejected");
/* ---- wrong version (0 and 2 both differ from 1) ---- */
len = load();
buf[2] = 0x00;
check(fp_parse(buf, len, &h) == 0, "version 0 rejected");
len = load();
buf[2] = 0x02;
check(fp_parse(buf, len, &h) == 0, "version 2 rejected");
/* ---- wrong stored CRC, either byte ---- */
len = load();
buf[FP_CRC_OFF] = (unsigned char) (buf[FP_CRC_OFF] + 1);
check(fp_parse(buf, len, &h) == 0, "corrupt crc low byte rejected");
len = load();
buf[FP_CRC_OFF + 1] = (unsigned char) (buf[FP_CRC_OFF + 1] + 1);
check(fp_parse(buf, len, &h) == 0, "corrupt crc high byte rejected");
/* ---- a header that understates payload_len cannot hide bad bytes ---- */
len = load();
buf[6] = 0x40; /* payload_len 0x40 instead of 0x50 */
check(fp_parse(buf, len, &h) == 0,
"payload_len shorter than the sealed payload rejected");
/* ---- payload_len is authoritative: a tail in the slot is not the frame ---- */
len = load();
buf[GOLDEN_LEN] = 0xFF;
check(fp_parse(buf, GOLDEN_LEN + 1, &h) == 1,
"trailing byte past the frame ignored");
/* ---- a bare header (payload_len 0) is a valid frame ...
*
* 56 53 01 01 09 00 00 00 00 00 00 00 AA D1 00 00 -- frame.py's
* pack_header(1, 9, 0, 0, 0, payload=b""), whose CRC covers the 12
* prefix bytes only. It pins the empty-payload case.
*/
{
static const unsigned char bare[] = {
0x56, 0x53, 0x01, 0x01, 0x09, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xAA, 0xD1, 0x00, 0x00
};
memcpy(buf, bare, sizeof bare);
memset(&h, 0, sizeof h);
rc = fp_parse(buf, (unsigned int) sizeof bare, &h);
check(rc == 1, "bare header (payload_len 0) accepted");
check(h.frame_no == 9, "bare header frame_no == 9");
check(h.payload_len == 0, "bare header payload_len == 0");
/* and one byte missing is not a header */
check(fp_parse(buf, (unsigned int) sizeof bare - 1, &h) == 0,
"bare header, one byte short, rejected");
}
printf("frame_parse: %d/%d ok\n", checks - failures, checks);
return failures != 0;
}
+120 -20
View File
@@ -1,38 +1,55 @@
/*
* vidstream.c -- terminal side of the VT1200 video stream (startup skeleton).
* vidstream.c -- terminal side of the VT1200 video stream.
*
* Task 3 only: claim the working set with ONE malloc at startup and publish
* where it landed. The receive ring (Task 4) and the blit loop (Task 5) build
* on the pointer this file leaves behind.
* Task 3 claimed the working set with ONE malloc at startup and published
* where it landed. Task 4 adds the receive side: two 24 KB frame slots at the
* head of that pool, and a poll loop that hands each completed slot to
* fp_parse() (frame_parse.c, shared with the host unit test) and counts what
* it accepted. Task 5 replaces the accept path's placeholder with the blit.
*
* Marker layout. The design plan names markers as 0x01810020, 0x01810024,
* 0x01810030, ... Those cannot be read literally as bit addresses: the
* TMS34010 is bit-addressed, so 0x01810024 is FOUR BITS after 0x01810020,
* inside the same 32-bit cell. They are byte offsets into the marker page
* (bit 0x01810000, byte 0x00302000), which is the only reading under which
* the plan's 4-byte spacing holds. So a named marker 0x018100NN lives at
* Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB):
*
* bit 0x01810000 + NN * 8
* +0x00000 frame slot A 24576 bytes
* +0x06000 frame slot B 24576 bytes
* +0x0C000 framebuffer 163840 bytes (scheme A, Task 5)
* +0x34000 dictionary 32768 bytes
* +0x3C000 slack 4096 bytes
*
* (POOL_PTR = 0x01810100, POOL_FREE before/after = 0x01810120/0x01810140).
* vidstream.lua uses the same rule. See the task-3 report.
* 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
* blit will read. Neither slot is ever split across a ring wrap: the two ring
* entries' descriptors are re-armed per slot at frame start.
*
* Marker layout: see the table in frame_parse.h. Markers are 32-bit cells on
* the byte-aligned page at bit 0x01810000, 0x20 bits (4 bytes) apart. The
* design plan names them 0x01810020, 0x01810024, ... but the TMS34010 is
* bit-addressed, so 0x01810024 is four BITS after 0x01810020 -- inside the
* same cell. The plan's numbers are byte offsets into the page; the rule is
*
* marker named 0x018100NN -> bit 0x01810000 + NN * 8
*
* (POOL_PTR = 0x01810100, ...). vidstream.lua uses the same rule.
*
* K&R C only: no prototypes, no '//', declarations before statements.
*/
#include "romapi.h"
#include "poolcfg.h" /* POOL_BYTES, written by build-stream.sh */
#define MARK_PAGE 0x01810000 /* bit address of the marker page */
#define POOL_PTR (MARK_PAGE + 0x20 * 8) /* u32 bit pointer to the pool */
#define POOL_FREEB (MARK_PAGE + 0x24 * 8) /* u32 heap free bytes, before */
#define POOL_FREEA (MARK_PAGE + 0x28 * 8) /* u32 heap free bytes, after */
#define VID_ALIVE (MARK_PAGE + 0x2c * 8) /* u32 heartbeat while spinning */
#include "frmpsr.h" /* frame_parse.h under an 8.3 name; see that file */
#define HEAP_FREE 0x1E8BA0 /* firmware free-byte counter, IN BITS */
#define MARK 0x01A00000 /* .text must end before this bit address */
#define STATIC_BYTES 249856 /* the designed working set, 244 KB */
/*
* The two frame slots. SLOT_BYTES is the plan's 24 KB: a 128-row strip frame
* (128 x 160 B + 16 B header = 20,496 B = 15 packets) fits, and so does any
* smaller strip. Slot addresses are bit addresses, because that is what a
* pointer holds in this toolchain (see harness/c-hello/README.md: "pointers
* are bit addresses").
*/
#define SLOT_BYTES 24576
/*
* 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.
@@ -60,6 +77,68 @@ static unsigned int heap_bytes()
return *(unsigned int *)HEAP_FREE / 8;
}
/*
* Handle one slot if the receive path has raised its ready flag.
*
* In the real transport the LANCE receive interrupt fills the slot and sets
* the flag; MAME cannot hand this emulated LANCE an Ethernet frame (the
* driver attaches no network interface -- ruling R11), so in the harness
* vidstream.lua writes the slot and sets the flag instead. The payload does
* not care which of the two did it.
*
* `len` is the number of bytes the transport actually delivered. fp_parse()
* rejects a frame whose payload_len wants more than that, which is the rule
* that makes payload_len plus the received length authoritative rather than a
* descriptor field.
*/
static void rx_poll(slot)
unsigned int slot;
{
unsigned int readyp, lenp, basep, base, len, ready;
fp_header h;
int ok;
if (slot == 0) {
readyp = RX_READY_A;
lenp = RX_LEN_A;
basep = RX_SLOT_A;
} else {
readyp = RX_READY_B;
lenp = RX_LEN_B;
basep = RX_SLOT_B;
}
ready = *(unsigned int *)readyp;
if (ready == 0) {
return;
}
base = *(unsigned int *)basep;
len = *(unsigned int *)lenp;
ok = 0;
if (base != 0) {
ok = fp_parse((unsigned char *)base, len, &h);
}
if (ok) {
*(unsigned int *)RX_OK = *(unsigned int *)RX_OK + 1;
*(unsigned int *)RX_LAST_LEN = h.payload_len;
*(unsigned int *)RX_SLOT = slot;
/* Task 5 blits the accepted frame from here: h.payload_off
* bytes into the slot, h.rows rows of 160 bytes (1280 pixels,
* 1 bpp, MSB first), h.stretch scanlines each. Nothing is
* drawn for a rejected frame, so a corrupt frame can never
* reach VRAM. */
} else {
*(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1;
}
/* Release the slot last: the checks read the counters, and a slot
* freed before its counter moved would look like a dropped frame. */
*(unsigned int *)readyp = 0;
}
int main()
{
unsigned int before, after, ptr;
@@ -69,6 +148,20 @@ int main()
*(unsigned int *)POOL_FREEB = 0;
*(unsigned int *)POOL_FREEA = 0;
/* Receive record: counters and the two slots are cleared before anything
* is published, so a reader never sees a stale ready flag or a slot
* address that has no allocation behind it. */
*(unsigned int *)RX_OK = 0;
*(unsigned int *)RX_BAD = 0;
*(unsigned int *)RX_LAST_LEN = 0;
*(unsigned int *)RX_SLOT = 0;
*(unsigned int *)RX_SLOT_A = 0;
*(unsigned int *)RX_SLOT_B = 0;
*(unsigned int *)RX_READY_A = 0;
*(unsigned int *)RX_LEN_A = 0;
*(unsigned int *)RX_READY_B = 0;
*(unsigned int *)RX_LEN_B = 0;
before = heap_bytes();
ptr = malloc_(POOL_BYTES); /* malloc_ takes BYTES, returns BITS */
after = heap_bytes();
@@ -82,9 +175,16 @@ int main()
*(unsigned int *)POOL_FREEB = before;
}
*(unsigned int *)POOL_FREEA = after;
*(unsigned int *)POOL_PTR = ptr; /* publish last: readers see a complete record */
/* Slot B follows slot A in the same pool. Both are published before
* POOL_PTR so the harness can find them from either one. */
*(unsigned int *)RX_SLOT_A = ptr;
*(unsigned int *)RX_SLOT_B = ptr + SLOT_BYTES * 8;
*(unsigned int *)POOL_PTR = ptr; /* publish last: a complete record */
for (;;) {
*(unsigned int *)VID_ALIVE = *(unsigned int *)VID_ALIVE + 1;
rx_poll(0);
rx_poll(1);
}
}