diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | c/gbos.h | 2 | ||||
| -rw-r--r-- | c/libc.s | 15 | ||||
| -rw-r--r-- | c/necho.c | 44 | ||||
| -rw-r--r-- | include/gbos.inc | 5 | ||||
| -rw-r--r-- | src/net.asm | 41 | ||||
| -rw-r--r-- | src/programs.asm | 7 | ||||
| -rw-r--r-- | src/syscall.asm | 2 | ||||
| -rw-r--r-- | tools/gateway.py | 69 |
9 files changed, 184 insertions, 3 deletions
@@ -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 @@ -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 */ @@ -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() |
