Files
vt1200/harness/udpstream/udpstream.c
T
arti 874d1fbbf5 networking RE report + LANCE payload variants
harness/mame/NETWORK_REPORT.md: how the VT1200 firmware drives the LANCE,
from ROM disassembly and MAME traces. 50 sections, each claim tied to a bit
address, a trace count or a quoted source line. Covers the driver device
struct (0x23CBF0), init (0x3F9D4BC0), descriptor/DMA encoding, the live
handler (0x3F9D5B40) with per-status-bit dispatch, the RX poll (0x3F9D62B0),
ring arming (0x3F9D36C0), TX completion (0x3F9D8AF0) and the single transmit
path every protocol uses (0x3F6084E0 -> 0x3F9DC890 -> 0x3F9D4BC0), plus the
IP protocol dispatch (0x3F6270F0), ICMP (343-count chain), TCP state tables
and live TIME_WAIT, the ARP responder (0x3F6119A0) and the socket 4-tuple
demux confirmed by a memory dump.

It also records what was withdrawn: five successive explanations for the
real-silicon CSR0=0x1BB / TMD1=0x8338 result, and the descriptor-length
encoding bug found by comparing Linux 7990.c and NetBSD am7990.c (length must
be 0xF000 | -len; MAME masks the difference in get_buf_length).

harness/udpstream: the LANCE loopback/takeover payload and the variants used
to test the init/kick orderings in MAME (bare TDMD, STOP before INIT, double
INIT, long wait with CSR0 sampling, IDON clear, corrected descriptor length).
build-lance.sh takes LANCE_SRC/LANCE_OUT so the variants build from one script.
2026-10-30 01:25:21 +02:00

162 lines
4.5 KiB
C

/*
* udpstream.c -- spike: receive full frames over UDP and blit them into VRAM.
*
* The terminal is the client; the host sends self-describing datagrams:
*
* +0 u16 start row (little endian)
* +2 u16 row count
* +4 row_count * 160 bytes of 1-bpp pixels (1280 px per row)
*
* Each row lands at bit 0x04000000 + row * 2048 (the real unit's 256-byte row
* pitch). Only the 160 visible bytes of a row are sent; the remaining 96 mask
* bytes are left as they are. 8 rows per datagram = 1284 bytes, under the
* 1500-byte Ethernet MTU.
*
* Markers at 0x018C8000 (32-bit cells):
* +0x000 stage 1 entered main, 2 socket ok, 3 bound, 4 receiving
* 0xEE socket failed, 0xEF bind failed
* +0x020 fd
* +0x040 bind() result
* +0x060 datagrams received
* +0x080 last datagram length
* +0x0A0 rows written
* +0x0C0 bytes blitted
*
* K&R C only; the ROM entry points and the sockaddr layout are in romapi.h.
*/
#include "romapi.h"
#define VRAM_BASE 0x04000000
#define ROW_BITS 2048 /* 256-byte row pitch on the real unit */
#define ROW_BYTES 160 /* 1280 visible pixels per row */
#define MAX_ROWS 8
#define PORT 7000
#define MARK 0x018C8000
#define LIMIT_CELL 0x018C9000 /* host-written datagram limit, 0 = unlimited */
#define PORT_CELL 0x018C9040 /* host-written UDP port, 0 = 7000 */
static unsigned char sa[16];
static unsigned char from[16];
static int fromlen;
static unsigned char buf[4 + MAX_ROWS * ROW_BYTES];
static unsigned int rset[8];
static unsigned int tv[2];
static void cell(off, v)
unsigned int off, v;
{
*(unsigned int *)(MARK + off) = v;
}
static unsigned int getcell(off)
unsigned int off;
{
return *(unsigned int *)(MARK + off);
}
int main()
{
int fd, r, n;
unsigned int limit, count, port;
unsigned int start, rows, row, i, dst, len;
cell(0x000, 1);
limit = *(unsigned int *)LIMIT_CELL;
count = 0;
port = *(unsigned int *)PORT_CELL;
if (port == 0) {
port = PORT;
}
/* The per-process user area (process+0x100 bits) is not initialised by
* create_process: on both the emulator and real hardware it reads as the
* heap fill 0x55. The syscall layer keeps its file table and syscall
* state there, so clear it before the first call. */
{
unsigned int proc, user, k;
proc = *(unsigned int *)0x1B3900;
cell(0x140, proc);
user = *(unsigned int *)(proc + 0x100);
cell(0x160, user);
for (k = 0; k < 0x100; k++) {
*(unsigned char *)(user + k * 8) = 0;
}
}
fd = socket_(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
cell(0x020, fd);
if (fd < 0) {
cell(0x000, 0xEE);
return 1;
}
cell(0x000, 2);
sa_any(sa, port);
cell(0x120, port);
r = bind_(fd, sa, SOCKADDR_LEN);
cell(0x040, r);
if (r < 0) {
cell(0x000, 0xEF);
close_(fd);
return 2;
}
cell(0x000, 3);
/* limit == 0xDEAD: bind and return without reading, so the host can test
* whether traffic to an unread socket is what kills the machine. */
if (limit == 0xDEAD) {
cell(0x000, 0xDD);
return 0;
}
cell(0x060, 0);
cell(0x0A0, 0);
cell(0x0C0, 0);
for (;;) {
fromlen = SOCKADDR_LEN;
n = recvfrom_(fd, buf, sizeof(buf), 0, from, &fromlen);
cell(0x0E0, (unsigned int)n);
if (n <= 0) {
close_(fd);
return 3;
}
if (n < 4) {
close_(fd);
return 4;
}
len = (unsigned int)n;
start = (unsigned int)buf[0] | ((unsigned int)buf[1] << 8);
rows = (unsigned int)buf[2] | ((unsigned int)buf[3] << 8);
if (rows > MAX_ROWS || start + rows > 1024) {
close_(fd);
return 5;
}
if (len < 4 + rows * ROW_BYTES) {
close_(fd);
return 6;
}
for (row = 0; row < rows; row++) {
dst = VRAM_BASE + (start + row) * ROW_BITS;
for (i = 0; i < ROW_BYTES; i++) {
*(unsigned char *)(dst + i * 8) =
buf[4 + row * ROW_BYTES + i];
}
}
cell(0x080, len);
cell(0x060, getcell(0x060) + 1);
cell(0x0A0, getcell(0x0A0) + rows);
cell(0x0C0, getcell(0x0C0) + rows * ROW_BYTES);
cell(0x000, 4);
count = count + 1;
if (limit != 0 && count >= limit) {
cell(0x100, count);
close_(fd);
return 0; /* bounded run: leave the OS to it */
}
}
}