forked from arti/vt1200
341 lines
18 KiB
C
341 lines
18 KiB
C
/*
|
|
* 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 (see the FP_KIND_* block below; the numbers are exactly
|
|
* frame.py's KINDS table)
|
|
* 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 ONLY (ruling R14);
|
|
* the payload is never part of it. A corrupted payload byte is therefore
|
|
* accepted -- the Ethernet FCS, checked by the LANCE per datagram, is the
|
|
* check for those bytes -- while a corrupted header byte is rejected. What
|
|
* catches a truncated frame is the `len >= FP_HEADER_SIZE + payload_len` bound
|
|
* in fp_parse()'s contract at the end of this file.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* 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
|
|
* ------------ ----------- ----------- --------------------------------
|
|
* RX_RING_BASE +0x080 0x01810080 bit address of the RX ring
|
|
* RX_RING_POS +0x0A0 0x018100A0 next descriptor to consume (the
|
|
* chip's ring position)
|
|
* RX_RING_ACC +0x0C0 0x018100C0 bytes of the frame arriving
|
|
* RX_RING_ARMED +0x0E0 0x018100E0 slot the ring is armed on (0=A)
|
|
* RX_RING_DROPS +0x200 0x01810200 frames too big for the ring
|
|
* RX_DGRAM +0x240 0x01810240 datagrams consumed
|
|
* RX_FCS_DROP +0x260 0x01810260 datagrams the chip flagged bad
|
|
* (no bytes delivered; each
|
|
* abandons its frame)
|
|
* DICT_READY +0x280 0x01810280 1 = the 32 KB dictionary is
|
|
* complete (Task 3)
|
|
* DICT_BYTES +0x2A0 0x018102A0 dictionary bytes accepted, a
|
|
* contiguous prefix from entry 0
|
|
* DICT_BAD +0x2C0 0x018102C0 dictionary chunks refused
|
|
* (gap/overlap/duplicate offset,
|
|
* overrun, or after ready)
|
|
* TILES_DROP +0x2E0 0x018102E0 tile frames that did not become
|
|
* pixels (no dictionary yet, or
|
|
* no tile paint in this tree)
|
|
* 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 that reached the screen
|
|
* (a strip painted; Task 4 adds
|
|
* tiles). A dictionary chunk
|
|
* is counted by DICT_BYTES, not
|
|
* here: it is not a picture.
|
|
* RX_BAD +0xA0 0x018101A0 frames the receiver refused
|
|
* (fp_parse, or a kind the
|
|
* dispatcher cannot act on)
|
|
* RX_LAST_LEN +0xC0 0x018101C0 payload_len of the last frame
|
|
* that reached the screen
|
|
* RX_SLOT +0xE0 0x018101E0 slot of that frame (0=A)
|
|
* OS_TOUCH +0x220 0x01810220 times the VRAM canary was not ours
|
|
*
|
|
* NOTE ON OWNERSHIP OF THE PAGE: nothing allocates this page. The firmware's
|
|
* large free block starts at byte 0x0012191A and grows upward, the page sits at
|
|
* byte 0x00302000 inside it and .text starts at byte 0x00318000 just above, so
|
|
* an allocator asked for enough memory -- or a long soak of small allocations --
|
|
* can hand these cells to someone else and the markers will be overwritten
|
|
* under the payload. Read them WHILE the payload runs (a live debugger peek,
|
|
* or a dump taken during the run); a marker page read after a soak is a claim
|
|
* about whatever now owns the memory, not about the payload.
|
|
*
|
|
* OS_TOUCH is Task 5's OS-repaint canary (design spec 8.9): the blit leaves one
|
|
* 16-bit sentinel word in the unused right edge of the last screen row, and
|
|
* OS_TOUCH counts how often that word was not what the payload left there. It
|
|
* is at cell 0x44 -- free between the RX_* counters and the injection cells
|
|
* below (ruling R12).
|
|
*
|
|
* Debugger injection interface (Task 4; retired by Task 7's ring):
|
|
*
|
|
* RX_SLOT_A +0x200 0x01810300 bit address of slot A
|
|
* RX_SLOT_B +0x220 0x01810320 bit address of slot B
|
|
*
|
|
* Task 4 received frames through a "slot ready" flag (RX_READY_A/B plus a byte
|
|
* count) that the debugger raised after poking a frame into a slot. Task 7
|
|
* removed those cells: the payload now owns a real LANCE RX ring and the
|
|
* handshake is the descriptor's OWN bit, per descriptor, with the chip clearing
|
|
* it and the payload setting it again only after reading the buffer. There is
|
|
* no level-triggered flag for two writers to race on, which was Task 4's
|
|
* Important finding (a).
|
|
*
|
|
* The harness therefore plays the CHIP: it writes a datagram into the buffer of
|
|
* the descriptor at RX_RING_POS (and the ones after it, for a multi-datagram
|
|
* frame) and completes it the way am79c90.cpp does -- RMD3 = delivered length
|
|
* including the FCS, RMD1 = STP | ENP with OWN clear. RX_ARMED says which slot
|
|
* those descriptors point into. The two slot addresses above remain published
|
|
* because they are the payload's own record of where its buffers are.
|
|
* ---------------------------------------------------------------------------
|
|
*
|
|
* 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 the retired injection
|
|
* flag was: 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.
|
|
* ---------------------------------------------------------------------------
|
|
*
|
|
* Task 3: the frame kinds and the dictionary upload.
|
|
*
|
|
* The kind byte is frame.py's KINDS table, written down twice -- there in
|
|
* Python, here in C -- so the numbers below must be changed in both places at
|
|
* once. harness/udpstream/tests/test_frame.py parses this file and compares
|
|
* the two tables, so a renumbering on one side fails a host test rather than
|
|
* silently mis-dispatching on the wire.
|
|
*
|
|
* FP_KIND_STRIP 1 packed 1-bpp rows, blitted (scheme A)
|
|
* FP_KIND_TILES_FULL 2 a full 40x32 screen of dictionary indices
|
|
* FP_KIND_TILES_DELTA 3 only the tiles that changed
|
|
* FP_KIND_DICT_CHUNK 4 one slice of the 32x32 tile dictionary
|
|
* FP_KIND_STATUS 5 the terminal's report, the other way
|
|
*
|
|
* A DICT_CHUNK's payload (tiledict.py writes it) is
|
|
*
|
|
* offset 2 u16 little-endian: dictionary position of the first body byte
|
|
* body n dictionary bytes, at most 1448 (1450-byte payload budget)
|
|
*
|
|
* The dictionary is 256 entries of 128 bytes (32 KB) and lives in the payload's
|
|
* own pool at POOL_PTR + 2 * SLOT_BYTES -- right after the slot pair, the
|
|
* placement Task 4 consumes. The assembler in vidstream.c refuses, and counts
|
|
* in DICT_BAD: an offset that is not the next contiguous byte (gaps,
|
|
* overlaps, duplicates, reordering), an offset inside an entry that is not a
|
|
* continuation of the accepted prefix (the 128-byte rule), a chunk that would
|
|
* overrun the last entry, and any chunk after DICT_READY (the dictionary is
|
|
* fixed for the clip -- ruling R1 of plan 2). A refused chunk leaves the
|
|
* prefix alone, so the sender's next good chunk still lands. DICT_READY is
|
|
* set by the chunk that makes DICT_BYTES exactly 32768: 256 whole entries, so
|
|
* a short upload is never complete and never paints anything.
|
|
*
|
|
* Until DICT_READY a TILES_FULL/TILES_DELTA frame cannot be decoded at all;
|
|
* it is refused without touching VRAM and counted in TILES_DROP. Those are
|
|
* dispatch rules of the receiver, not of fp_parse(): the header is well formed
|
|
* and fp_parse() accepts it, which is exactly why the dispatcher must not
|
|
* forget it.
|
|
* ---------------------------------------------------------------------------
|
|
*
|
|
* Task 4/5: the tile payloads, written down here because this is the wire
|
|
* format's one C statement (Task 4's review carried the TILES_FULL layout to
|
|
* this task for exactly this reason). Both kinds are decoded against the
|
|
* dictionary at POOL_PTR + 2 * SLOT_BYTES -- 32,768 bytes, 256 entries of 128
|
|
* bytes -- and only after DICT_READY. The screen is a 40 x 32 grid of 32x32
|
|
* tiles: 1,280 of them, tile (tx, ty) at row-major position ty * 40 + tx.
|
|
*
|
|
* TILES_FULL (kind 2) is the keyframe: the whole screen, one dictionary index
|
|
* per tile, in one byte each.
|
|
*
|
|
* offset size field
|
|
* 0 1280 index[ty * 40 + tx], dictionary entry 0..255
|
|
*
|
|
* The payload is EXACTLY 1,280 bytes. A short frame does not name every tile
|
|
* and a longer one is not this kind's wire form, so both are refused rather
|
|
* than painted as a partial screen. (A 256-entry dictionary is exactly what
|
|
* an index byte can name, which is why the entry count is fixed at 256 and not
|
|
* a per-clip parameter on the wire.)
|
|
*
|
|
* TILES_DELTA (kind 3) is the same grid with only the tiles that changed:
|
|
*
|
|
* offset size field
|
|
* 0 2 count, u16 little-endian, 1..1280
|
|
* 2 2 * count position[k], u16 little-endian, row-major 0..1279,
|
|
* STRICTLY ASCENDING
|
|
* 2 + 2*count count index[k], one byte each: the new dictionary entry
|
|
*
|
|
* and payload_len is EXACTLY 2 + 3 * count. The position is two bytes because
|
|
* the grid has 1,280 tiles while an entry needs one: 256 fits a byte, 1,280
|
|
* does not. The receiver refuses, in TILES_DROP and without painting anything,
|
|
* a count of 0 or above 1,280, a length that disagrees with the count (the list
|
|
* may not run past the frame), a position off the grid, a repeated or
|
|
* descending position, and (defensively: one byte cannot express it) an entry
|
|
* above 255. It validates the WHOLE list before the first blit, so one bad
|
|
* entry cannot leave half a delta on the screen; positions being ascending is
|
|
* what makes that possible with one cursor and no per-tile division.
|
|
*
|
|
* The keyframe cadence is the host's: KEYFRAME_EVERY = 60 frames (tiledict.py,
|
|
* ruling R2), plus a keyframe whenever the status frame's `drops` rises. The
|
|
* terminal does not choose it and has no "delta lost" signal of its own -- a
|
|
* delta that never arrives is silence.
|
|
* ---------------------------------------------------------------------------
|
|
*
|
|
* 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_TILES_FULL 2
|
|
#define FP_KIND_TILES_DELTA 3
|
|
#define FP_KIND_DICT_CHUNK 4
|
|
#define FP_KIND_STATUS 5
|
|
|
|
/* Task 4/5: the tile wire layout (see the comment block above). A keyframe is
|
|
* one index byte per tile; a delta opens with a u16 count and names each
|
|
* changed tile with a u16 position and a one-byte entry. */
|
|
#define FP_TILES_SCREEN 1280 /* 40 x 32 tiles, one index byte each */
|
|
#define FP_DELTA_HDR 2 /* bytes of a delta's u16 count */
|
|
|
|
/* 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 OS_TOUCH MARK_CELL(0x44)
|
|
|
|
/*
|
|
* Task 7: the RX ring record. Task 4's injection cells (0x68..0x74) are gone;
|
|
* these take the free page prefix R12 left for the RX work, at the SAME 4-byte
|
|
* spacing every other cell uses (n increases by 4 per cell -- 0x10, 0x14, 0x18
|
|
* and 0x1C are four cells, and a 32-bit store at 0x01810080 does not reach
|
|
* 0x01810088). Reading "n = 0x10..0x15" as five adjacent cells is the mistake
|
|
* this comment exists to prevent: those four cells would be ONE BYTE apart and
|
|
* the 32-bit stores would overlap into zeros.
|
|
*/
|
|
#define RX_RING_BASE MARK_CELL(0x10)
|
|
#define RX_RING_POS MARK_CELL(0x14)
|
|
#define RX_RING_ACC MARK_CELL(0x18)
|
|
#define RX_RING_ARMED MARK_CELL(0x1c)
|
|
#define RX_RING_DROPS MARK_CELL(0x40)
|
|
#define RX_DGRAM MARK_CELL(0x48)
|
|
#define RX_FCS_DROP MARK_CELL(0x4c)
|
|
|
|
/* Task 3: the dictionary record and the tile-frame drop count. Cells 0x50 to
|
|
* 0x5c fill the gap between RX_FCS_DROP and RX_SLOT_A at the same four-byte
|
|
* spacing every other cell uses. */
|
|
#define DICT_READY MARK_CELL(0x50)
|
|
#define DICT_BYTES MARK_CELL(0x54)
|
|
#define DICT_BAD MARK_CELL(0x58)
|
|
#define TILES_DROP MARK_CELL(0x5c)
|
|
|
|
#define RX_SLOT_A MARK_CELL(0x60)
|
|
#define RX_SLOT_B MARK_CELL(0x64)
|
|
|
|
/* 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, four u16, dict_state u8,
|
|
* reserved u8, decode_worst u16, little-endian. */
|
|
#define FP_STATUS_SIZE 16
|
|
|
|
/* 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]. 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();
|
|
|
|
/*
|
|
* 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 */
|