aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-17 01:28:23 +0200
committeruser <user@clank>2026-07-17 01:28:23 +0200
commitb5544e84232b80351e7c58501c1632080a354d12 (patch)
treea4087d5c9df0589b613bcb6696962825099ce8b7
parentpipe: PIPE_MAX 2->4 (5-stage pipelines) + fix cross-yield byte corruption (diff)
downloadgbos-b5544e84232b80351e7c58501c1632080a354d12.tar.gz
gbos-b5544e84232b80351e7c58501c1632080a354d12.tar.xz
gbos-b5544e84232b80351e7c58501c1632080a354d12.zip
net: SLIP framing over the link port + a host gateway (echo round-trip)
First step of link-port networking. The LCD terminal + OSK freed the serial port from console duty, so it can be the network link. Kernel (src/net.asm): raw link-port serial that bypasses the console/ terminal - sys_ssend (transmit, GB drives the clock) and sys_srecv (receive, GB slave, blocks by yielding). Syscalls 29/30. Userland: libc ssend()/srecv(); c/necho.c does SLIP (RFC 1055) framing over them - send a packet, receive the reply, print it. Host: tools/gateway.py wraps the emulator, owns its link serial, speaks SLIP, and (for now) echoes every frame back - the "link cable adapter". Console (ASCII) bytes on the same channel are printed for visibility. Verified: `necho` sends a SLIP frame, the gateway decodes+echoes it, and gbos prints the reply - a real framed round-trip over the Game Boy link port. Next: swap the echo for actual network ops (DNS/HTTP or IRC/chat).
-rw-r--r--Makefile2
-rw-r--r--c/gbos.h2
-rw-r--r--c/libc.s15
-rw-r--r--c/necho.c44
-rw-r--r--include/gbos.inc5
-rw-r--r--src/net.asm41
-rw-r--r--src/programs.asm7
-rw-r--r--src/syscall.asm2
-rw-r--r--tools/gateway.py69
9 files changed, 184 insertions, 3 deletions
diff --git a/Makefile b/Makefile
index ceb9d51..4da1b2b 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ $(BUILD)/%.o: src/%.asm | $(BUILD)
# C programs: compiled with SDCC into ROM-bank blobs and INCBIN'd by programs.asm
CBLOBS := c/chello.bin c/echo.bin c/true.bin c/false.bin c/uname.bin \
c/pid.bin c/cat.bin c/wc.bin c/head.bin c/args.bin \
- c/ls.bin c/save.bin c/rm.bin c/sh.bin c/ps.bin c/kill.bin c/spin.bin c/blk.bin c/mkdir.bin c/count.bin c/ptest.bin
+ c/ls.bin c/save.bin c/rm.bin c/sh.bin c/ps.bin c/kill.bin c/spin.bin c/blk.bin c/mkdir.bin c/count.bin c/ptest.bin c/necho.bin
$(BUILD)/programs.o: $(CBLOBS)
# C programs also depend on the shared libc sources
diff --git a/c/gbos.h b/c/gbos.h
index a0619cc..d207a75 100644
--- a/c/gbos.h
+++ b/c/gbos.h
@@ -13,6 +13,8 @@ char readc(void); /* one input byte, EOF at end */
void sexit(unsigned char code);
unsigned char getpid(void);
unsigned char pipe(void); /* -> read fd; write end is read+1 */
+void ssend(unsigned char b); /* link-port: transmit one byte */
+unsigned char srecv(void); /* link-port: receive one byte (blocks) */
/* helpers (c/libc.c) */
void putu(unsigned int n); /* print decimal */
diff --git a/c/libc.s b/c/libc.s
index ec59bb7..50d7860 100644
--- a/c/libc.s
+++ b/c/libc.s
@@ -1,6 +1,6 @@
.module libc
.globl _writes, _putc, _puts, _nl, _strlen, _readc, _sexit, _getpid, _getargs
- .globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe
+ .globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe, _ssend, _srecv
;; SDCC sm83 sdcccall(1): 1st arg -> A (byte) or DE (pointer); ret A / BC.
;; rst $30 (the trap) clobbers BC/DE/HL; SDCC treats them caller-saved, so
;; wrappers need not preserve them - but we compute cleanly regardless.
@@ -92,6 +92,19 @@ _pipe:
rst #0x30
ret
+ ;; void ssend(unsigned char b /*A*/) -- link-port transmit
+_ssend:
+ ld e, a
+ ld c, #29 ; SYS_SSEND
+ rst #0x30
+ ret
+
+ ;; unsigned char srecv(void) -> A -- link-port receive (blocks)
+_srecv:
+ ld c, #30 ; SYS_SRECV
+ rst #0x30
+ ret
+
;; unsigned char open(const char *name /*DE*/, unsigned char mode /*A*/) -> A(fd)
_open:
ld b, a ; B = mode
diff --git a/c/necho.c b/c/necho.c
new file mode 100644
index 0000000..b9dc9d5
--- /dev/null
+++ b/c/necho.c
@@ -0,0 +1,44 @@
+#include "gbos.h"
+/* necho: send a SLIP-framed packet over the link port and print the reply.
+ Talks to a host gateway (tools/gateway.py) that echoes frames back. */
+
+#define SLIP_END 0xC0
+#define SLIP_ESC 0xDB
+#define SLIP_ESC_END 0xDC
+#define SLIP_ESC_ESC 0xDD
+
+static void slip_send(const char *buf, unsigned char len) {
+ unsigned char i, c;
+ ssend(SLIP_END);
+ for (i = 0; i < len; i++) {
+ c = (unsigned char)buf[i];
+ if (c == SLIP_END) { ssend(SLIP_ESC); ssend(SLIP_ESC_END); }
+ else if (c == SLIP_ESC) { ssend(SLIP_ESC); ssend(SLIP_ESC_ESC); }
+ else ssend(c);
+ }
+ ssend(SLIP_END);
+}
+
+static unsigned char slip_recv(char *buf, unsigned char max) {
+ unsigned char len = 0, c;
+ for (;;) {
+ c = srecv();
+ if (c == SLIP_END) { if (len) return len; continue; }
+ if (c == SLIP_ESC) {
+ c = srecv();
+ if (c == SLIP_ESC_END) c = SLIP_END;
+ else if (c == SLIP_ESC_ESC) c = SLIP_ESC;
+ }
+ if (len < max) buf[len++] = (char)c;
+ }
+}
+
+void main(void) {
+ char buf[64];
+ unsigned char len;
+ puts("net: send 'hello from gbos'"); nl();
+ slip_send("hello from gbos", 15);
+ len = slip_recv(buf, 63);
+ buf[len] = 0;
+ puts("net: recv '"); puts(buf); puts("'"); nl();
+}
diff --git a/include/gbos.inc b/include/gbos.inc
index bd7fbc2..71d6fa0 100644
--- a/include/gbos.inc
+++ b/include/gbos.inc
@@ -126,7 +126,9 @@ DEF SYS_MKDIR EQU 25 ; make a directory (DE=path -> A=0/$FF)
DEF SYS_CHDIR EQU 26 ; change directory (DE=path -> A=0/$FF)
DEF SYS_OPENDIR EQU 27 ; point ls at a directory (DE=path -> A=0/$FF)
DEF SYS_PIPE EQU 28 ; make a pipe (-> A=read fd; write=A+1)
-DEF SYS_MAX EQU 29
+DEF SYS_SSEND EQU 29 ; link-port: transmit one byte (E=byte)
+DEF SYS_SRECV EQU 30 ; link-port: receive one byte (-> A=byte, blocks)
+DEF SYS_MAX EQU 31
; SYS_KILL (9) is now implemented (B=pid -> A=0/$FF; pid 1 is immortal)
; (SYS_OPEN=4 / SYS_CLOSE=5 are now implemented by the filesystem)
@@ -211,5 +213,6 @@ DEF PROG_BLK EQU 19
DEF PROG_MKDIR EQU 20
DEF PROG_COUNT EQU 21
DEF PROG_PTEST EQU 22
+DEF PROG_NECHO EQU 23
ENDC
diff --git a/src/net.asm b/src/net.asm
new file mode 100644
index 0000000..939b975
--- /dev/null
+++ b/src/net.asm
@@ -0,0 +1,41 @@
+; =============================================================================
+; net.asm - raw link-port serial for networking (bypasses the console/terminal).
+;
+; The Game Boy link port is one 8-bit shift register. These two calls move a
+; byte at a time: ssend transmits with the GB as clock master; srecv waits for
+; a byte with the GB as slave (external clock), yielding while it blocks. On top
+; of these, userland runs SLIP framing and talks to a host-side gateway.
+;
+; The LCD terminal + on-screen keyboard mean the console no longer needs the
+; link port, so it is free to be the network.
+; =============================================================================
+INCLUDE "include/gbos.inc"
+
+SECTION "net", ROM0
+
+; sys_ssend(E = byte) - transmit one byte (GB drives the clock).
+sys_ssend::
+ ld a, e
+ ld [rSB], a
+ ld a, $81 ; start transfer, internal clock
+ ld [rSC], a
+.wait
+ ld a, [rSC]
+ bit 7, a
+ jr nz, .wait ; bit7 clears when the byte has shifted out
+ ret
+
+; sys_srecv() -> A = received byte. GB is the slave; blocks (yields) until the
+; peer clocks a byte in.
+sys_srecv::
+.poll
+ ld a, $80 ; start transfer, external clock (receive)
+ ld [rSC], a
+ ld a, [rSC]
+ bit 7, a
+ jr z, .got ; bit7 clear => a byte arrived
+ call SchedYield
+ jr .poll
+.got
+ ld a, [rSB]
+ ret
diff --git a/src/programs.asm b/src/programs.asm
index e659cee..a0bef96 100644
--- a/src/programs.asm
+++ b/src/programs.asm
@@ -109,6 +109,9 @@ ProgCount:
SECTION "prog_ptest", ROMX[$4000], BANK[24]
ProgPtest:
INCBIN "c/ptest.bin"
+SECTION "prog_necho", ROMX[$4000], BANK[25]
+ProgNecho:
+ INCBIN "c/necho.bin"
; -----------------------------------------------------------------------------
; PROG_SH (bank 3) - the shell, now written in C (c/sh.c): parses >/</| and
@@ -197,6 +200,8 @@ ProgramTable::
dw ProgCount
db LOW(BANK(ProgPtest)), HIGH(BANK(ProgPtest))
dw ProgPtest
+ db LOW(BANK(ProgNecho)), HIGH(BANK(ProgNecho))
+ dw ProgNecho
; -----------------------------------------------------------------------------
; NameTable (ROM0) - command name -> program id, searched by sys_lookup.
@@ -249,4 +254,6 @@ NameTable::
db PROG_COUNT
db "ptest", 0
db PROG_PTEST
+ db "necho", 0
+ db PROG_NECHO
db 0
diff --git a/src/syscall.asm b/src/syscall.asm
index c602097..cb16bbe 100644
--- a/src/syscall.asm
+++ b/src/syscall.asm
@@ -63,6 +63,8 @@ SyscallTable:
dw sys_chdir ; 26 CHDIR
dw sys_opendir ; 27 OPENDIR
dw sys_pipe ; 28 PIPE
+ dw sys_ssend ; 29 SSEND
+ dw sys_srecv ; 30 SRECV
; -----------------------------------------------------------------------------
sys_nosys:
diff --git a/tools/gateway.py b/tools/gateway.py
new file mode 100644
index 0000000..81a70af
--- /dev/null
+++ b/tools/gateway.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+"""gateway.py - a host-side "link cable adapter" for gbos networking.
+
+Wraps the emulator, owns its link-port serial (stdio), and speaks SLIP. This
+milestone just ECHOES every frame back (loopback), so `necho` on gbos can prove
+a framed round-trip. Console (ASCII) bytes on the same channel are printed.
+
+usage: tools/gateway.py [rom]
+"""
+import os, sys, subprocess, threading, time
+
+EMU = os.path.expanduser("~/dev/gbc/build/sl0pboy")
+ROM = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(__file__), "..", "gbos.gb")
+
+END, ESC, ESC_END, ESC_ESC = 0xC0, 0xDB, 0xDC, 0xDD
+
+def slip_encode(payload):
+ out = bytearray([END])
+ for c in payload:
+ if c == END: out += bytes([ESC, ESC_END])
+ elif c == ESC: out += bytes([ESC, ESC_ESC])
+ else: out.append(c)
+ out.append(END)
+ return bytes(out)
+
+def handle_frame(payload):
+ """The 'network'. For now: echo. (Later: DNS/TCP/HTTP/IRC.)"""
+ sys.stderr.write("\n[gw] frame in : %r\n" % bytes(payload))
+ sys.stderr.write("[gw] frame out: %r (echo)\n" % bytes(payload))
+ return bytes(payload)
+
+def reader(out, inp):
+ buf, in_frame, esc = bytearray(), False, False
+ while True:
+ b = out.read(1)
+ if not b:
+ return
+ c = b[0]
+ if c == END:
+ if in_frame and buf:
+ reply = handle_frame(buf)
+ inp.write(slip_encode(reply)); inp.flush()
+ buf, in_frame, esc = bytearray(), False, False
+ else:
+ buf, in_frame, esc = bytearray(), True, False
+ elif in_frame:
+ if esc:
+ c = {ESC_END: END, ESC_ESC: ESC}.get(c, c); esc = False; buf.append(c)
+ elif c == ESC:
+ esc = True
+ else:
+ buf.append(c)
+ else:
+ sys.stderr.write(chr(c) if 32 <= c < 127 else ("\n" if c == 10 else "."))
+ sys.stderr.flush()
+
+def main():
+ p = subprocess.Popen([EMU, "--headless", "--uncapped", ROM],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, bufsize=0)
+ threading.Thread(target=reader, args=(p.stdout, p.stdin), daemon=True).start()
+ time.sleep(1.0) # boot to the shell
+ p.stdin.write(b"necho\n"); p.stdin.flush()
+ time.sleep(2.0) # let the framed round-trip happen
+ p.stdin.write(b"exit\n"); p.stdin.flush()
+ try: p.wait(timeout=3)
+ except Exception: p.kill()
+
+if __name__ == "__main__":
+ main()