From 09f86074667fdac7f4a33fb80241b78b15682ee4 Mon Sep 17 00:00:00 2001 From: SDD Date: Sun, 18 Oct 2026 17:12:00 +0300 Subject: [PATCH] dwt/tcpip: LAN TFTP client (100.103.255.254) and a verified self-contained fetch + load on the real unit --- .../real-unit-tftp/hostload_tftp_xhello.png | Bin 0 -> 7099 bytes dwt/notes/REAL_UNIT.md | 22 ++ dwt/tcpip/tftpl.asm | 256 ++++++++++++++++++ 3 files changed, 278 insertions(+) create mode 100644 dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png create mode 100644 dwt/tcpip/tftpl.asm diff --git a/dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png b/dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png new file mode 100644 index 0000000000000000000000000000000000000000..05299ff6c3e99bf6c91e334a4f9a9acb8c15c792 GIT binary patch literal 7099 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 literal 0 HcmV?d00001 diff --git a/dwt/notes/REAL_UNIT.md b/dwt/notes/REAL_UNIT.md index bbb3764..050e30d 100644 --- a/dwt/notes/REAL_UNIT.md +++ b/dwt/notes/REAL_UNIT.md @@ -282,3 +282,25 @@ The LANCE address ROM at bit `0x6800000` reads (low bytes of the words): byte-identical to the MAME model. The `Ethernet Address ROM Test` passing is consistent with this. + +### Self-contained TFTP loader on the real unit + +Retargeted `tftpc.asm` to the LAN (`100.103.255.254:69`) as +`dwt/tcpip/tftpl.asm`; the RRQ name stays `dwt/tcpip/probe.bin` because the +server serves the chosen COFF for any requested name. The DOS 8.3 build name +must be <= 8 characters: `tftpc_lan` silently truncates and GSPLNK produces no +`TFTPC_LAN.OUT`; `tftpl` is safe. + +``` +server: dwt/tcpip/serve_probe.py 100.103.255.254 --file dwt/loader/build/xhello.coff +RRQ: 100.103.100.103:1036 requested 'dwt/tcpip/probe.bin' (774 bytes) +fetch: sha256 a8b3014d27deb97adcebb418a5d14da51fc3c11559e84923ede72e260d3e7271 +loader: status 0, entry 0x01840000, pcb 0x00849a10 +app: xhello stage=6 strlen=4 +``` + +Evidence: `dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png`. + +This is the first fully self-contained delivery on the real unit: the terminal +fetched the COFF over TFTP into `malloc` memory and ran it in a created +process, with only the rdebug jump used to start the fetch. diff --git a/dwt/tcpip/tftpl.asm b/dwt/tcpip/tftpl.asm new file mode 100644 index 0000000..0848313 --- /dev/null +++ b/dwt/tcpip/tftpl.asm @@ -0,0 +1,256 @@ +; tftpc.asm -- minimal TFTP read client built on the firmware's socket API. +; +; The firmware's own tftp_open() calls a resolver that does not return when +; called from the rdebug process (dwt/notes/TCPIP.md), so this payload speaks +; RFC 783 directly: RRQ, DATA/ACK, 512-byte blocks, one file. +; +; socket = 3F5C9A00h socket(2,2,0) -> fd +; sendto = 3F5D3D70h sendto(fd, buf, len, flags, to, tolen) -> n +; recvfrom = 3F5D7D50h recvfrom(fd, buf, len, flags, from, fromlen) -> n +; close = 3F5E8930h close(fd) +; malloc = 3F701230h malloc(bytes) -> bit pointer +; +; The transfer is 600 bytes, so DATA 1 is 512 (full) and DATA 2 is 88 +; (short); the short block ends the transfer. ACKs go to the sockaddr that +; recvfrom reports, not to a hard-coded port. +; +; Evidence cells (32-bit, bit addresses): +; 0187C000 stage (1 malloc, 2 socket, 3 receiving, 4 closed, 0FFh failed) +; 0187C020 fd +; 0187C040 destination buffer (malloc) +; 0187C060 total bytes fetched +; 0187C080 last recvfrom length +; 0187C0A0 last DATA block word as read from the wire +; 0187C0C0 marker 05A5A5A5h when the fetch completed +; 0187C200 entry SP +; 0187C220 entry A14 +; +; Scratch (fixed bit addresses, above this image): +; 01875000 argument area +; 01876000 receive buffer (2048 bytes) +; 0187A000 recvfrom source sockaddr +; 0187A080 recvfrom sockaddr length +; 0187A100 ACK packet (4 bytes) + + .text +_start: + setf 16, 0, 0 + setf 32, 0, 1 + mmtm sp, a0,a1,a2,a3,a4,a5,a6,a7 + move sp, @0187C200h, 1 + move a14, @0187C220h, 1 + + movi 1, a0, L + move a0, @0187C000h, 1 + + ; ---- buf = malloc(010000h) ------------------------------------ + move a14, -*sp, 1 + movi 01875000h, a14, L + movi 010000h, a0, L + move a0, *a14+, 1 + calla 3F701230h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, @0187C040h, 1 + move a8, a11 ; dest cursor (callee-saved) + move a8, a8 + jreq fail + + movi 2, a0, L + move a0, @0187C000h, 1 + + ; ---- fd = socket(2,2,0) -------------------------------------- + movi 01875000h, a14, L + movi 0, a0, L + move a0, *a14+, 1 + movi 2, a0, L + move a0, *a14+, 1 + movi 2, a0, L + move a0, *a14+, 1 + calla 3F5C9A00h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, @0187C020h, 1 + move a8, a9 ; fd (callee-saved) + move a8, a8 + jrlt fail + + ; ---- sendto(fd, RRQ, 28, 0, SADDR, 16) ------------------------ + movi 01875000h, a14, L + movi 16, a0, L + move a0, *a14+, 1 + movi SADDR, a0, L + move a0, *a14+, 1 + movi 0, a0, L + move a0, *a14+, 1 + movi 28, a0, L + move a0, *a14+, 1 + movi RRQ, a0, L + move a0, *a14+, 1 + move a9, *a14+, 1 + calla 3F5D3D70h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, a8 + jrlt fail + + movi 3, a0, L + move a0, @0187C000h, 1 + sub a0, a0 + move a0, @0187C060h, 1 ; total = 0 + +recv_loop: + ; ---- recvfrom(fd, BUF, 2048, 0, &FROM, &FROMLEN) ------------- + ; arg5 is a POINTER to the socklen (as tftp_read passes it); the + ; value is initialised to 16 first. + movi 16, a0, L + move a0, @0187A080h, 1 ; fromlen = 16 + movi 01875000h, a14, L + movi 0187A080h, a0, L + move a0, *a14+, 1 ; arg5 = &fromlen + movi 0187A000h, a0, L + move a0, *a14+, 1 ; arg4 = &from + movi 0, a0, L + move a0, *a14+, 1 ; arg3 = flags + movi 00800h, a0, L + move a0, *a14+, 1 ; arg2 = 2048 + movi 01876000h, a0, L + move a0, *a14+, 1 ; arg1 = receive buffer + move a9, *a14+, 1 ; arg0 = fd + calla 3F5D7D50h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + + move a8, @0187C080h, 1 ; last length + move a8, a8 + jrle fail + + ; opcode: wire 00 03 -> little-endian word 0300h + move @01876000h, a0, 0 + cmpi 0300h, a0 + jrne fail ; only DATA is accepted + + move @01876010h, a4, 0 ; block number, wire order + move a4, @0187C0A0h, 1 + + move a8, a3 + subk 4, a3 ; payload length + + ; ---- total += payload, dest += payload ----------------------- + move @0187C060h, a1, 1 + add a3, a1 + move a1, @0187C060h, 1 ; total += payload (bytes) + move a11, a6 ; copy destination + move a3, a2 + sll 3, a2 ; bytes -> bits: pointers are bit addresses + add a2, a11 ; dest cursor advances now (do NOT add bytes) + + ; ---- copy payload from BUF+4 to a6, one byte at a time ------ + ; Interrupts are disabled for the copy so a 512-byte copy is atomic + ; with respect to the firmware's ISRs. + dint + movi 01876020h, a5, L + move a3, a2 + move a3, @0187C0E0h, 1 ; payload length + move a2, @0187C140h, 1 ; copy count + move a6, @0187C120h, 1 ; write start + move a11, @0187C100h, 1 ; cursor after advance + move a5, @0187C160h, 1 ; source + jreq copy_done +copy_bytes: + movb *a5, a0 + movb a0, *a6 + addk 8, a5 ; one byte + addk 8, a6 + dsjs a2, copy_bytes +copy_done: + eint + + ; ---- ACK: 00 04 -------------------------- + movi 00400h, a0, L + move a0, @0187A100h, 0 + move a4, @0187A110h, 0 + + movi 01875000h, a14, L + movi 16, a0, L + move a0, *a14+, 1 + movi 0187A000h, a0, L + move a0, *a14+, 1 + movi 0, a0, L + move a0, *a14+, 1 + movi 4, a0, L + move a0, *a14+, 1 + movi 0187A100h, a0, L + move a0, *a14+, 1 + move a9, *a14+, 1 + calla 3F5D3D70h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, a8 + jrlt fail + + ; short block ends the transfer; full block means read again + move @0187C080h, a2, 1 + subk 4, a2 + cmpi 512, a2 + jrlt close_it + jruc recv_loop + +close_it: + movi 4, a0, L + move a0, @0187C000h, 1 + movi 01875000h, a14, L + move a9, *a14+, 1 + calla 3F5E8930h ; close(fd) + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + movi 05A5A5A5h, a0, L + move a0, @0187C0C0h, 1 + sub a8, a8 + jruc epilogue + +fail: + movi 0FFh, a0, L + move a0, @0187C000h, 1 + movi 0FFFFFFFFh, a8, L +epilogue: + mmfm sp, a0,a1,a2,a3,a4,a5,a6,a7 + rets + +RRQ: + .word 00100h ; opcode 1 (00 01) + .word 07764h ; "dw" + .word 02F74h ; "t/" + .word 06374h ; "tc" + .word 06970h ; "pi" + .word 02F70h ; "p/" + .word 07270h ; "pr" + .word 0626Fh ; "ob" + .word 02E65h ; "e." + .word 06962h ; "bi" + .word 0006Eh ; "n\0" + .word 0636Fh ; "oc" + .word 06574h ; "te" + .word 00074h ; "t\0" +SADDR: + .word 00002h ; sin_family = AF_INET + .word 04500h ; sin_port = htons(69) + .word 06764h ; 100.103 + .word 0FEFFh ; .255.254 + .word 0 + .word 0 + .word 0 + .word 0 + .end