diff options
| author | user <user@clank> | 2026-07-16 08:25:35 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-16 08:25:35 +0200 |
| commit | 6c64d97e70984c77be3cd8d4e03e9adf717e02fb (patch) | |
| tree | fd55f0ef943fef12a833ab526ebfe6bc5740f728 | |
| parent | filesystem: a RAM FS (WRAMX) + ls/cat/save/rm; wc/head read files (diff) | |
| download | gbos-6c64d97e70984c77be3cd8d4e03e9adf717e02fb.tar.gz gbos-6c64d97e70984c77be3cd8d4e03e9adf717e02fb.tar.xz gbos-6c64d97e70984c77be3cd8d4e03e9adf717e02fb.zip | |
shell: I/O redirection (>/<) and pipes (|); rewrite shell in C
- kernel I/O routing: PCB gains PROC_STDIN/PROC_STDOUT (default console $FF),
inherited across fork. KGetc/KPutc route read/write/putc to the console or a
file fd. New syscalls: putc(16), setin(17), setout(18), lookup(19).
read/write now go through the routing; putc/puts/nl use SYS_PUTC.
- sys_lookup + NameTable move command-name resolution into the kernel.
- shell rewritten in C (c/sh.c): tokenizes a line, parses > / < / |, and drives
fork+setin/setout+exec+wait. Pipes run as 'a > __pipe ; b < __pipe' (temp
file). asm shell + cmdtab removed; StrEqual/SkipName kept for sys_lookup.
- libc: fork/exec/wait/setin/setout/lookup wrappers.
- verified: echo>file, cat file, wc<file, cat readme|wc -l, echo ...|wc.
Diffstat (limited to '')
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | README.md | 37 | ||||
| -rw-r--r-- | c/gbos.h | 8 | ||||
| -rw-r--r-- | c/libc.s | 67 | ||||
| -rw-r--r-- | c/sh.c | 99 | ||||
| -rw-r--r-- | include/gbos.inc | 10 | ||||
| -rw-r--r-- | src/kdata.asm | 1 | ||||
| -rw-r--r-- | src/proc.asm | 19 | ||||
| -rw-r--r-- | src/programs.asm | 181 | ||||
| -rw-r--r-- | src/syscall.asm | 146 |
10 files changed, 386 insertions, 184 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/ls.bin c/save.bin c/rm.bin c/sh.bin $(BUILD)/programs.o: $(CBLOBS) # C programs also depend on the shared libc sources @@ -58,22 +58,33 @@ across a syscall (`push hl` / `pop hl`). Libc syscall stubs handle this for C. | 2 | read | ✅ blocking console input (1 byte) via external-clock serial | | 4,5,9,10 | open/close/kill/brk | ⛔ `ENOSYS` | -### The shell (PROG_SH) +### The shell (`c/sh.c`) -init `exec`s a tiny shell: **prompt → read a line (with echo) → match a command -→ `fork`+`exec`+`wait`**. Commands live in a table (`worker`, `hello`). Runs -entirely over the headless serial console: +init `exec`s the shell (written in C). It reads a line, tokenizes it, and runs +commands with **I/O redirection and pipes**: ``` -$ hello -hello world! -$ worker -worker! -$ foo -? -$ +$ echo hello > note # redirect stdout to a file +$ cat note +hello +$ wc < note # redirect stdin from a file +1 1 6 +$ cat readme | wc -l # pipe (via a temp file) +2 +$ echo one two three | wc +1 3 14 ``` +Operators must be space-separated (`cat readme > out`). Command names are +resolved by the kernel (`sys_lookup` + `NameTable`). + +**How I/O routing works.** Each process has a `stdin`/`stdout` fd in its PCB +(default `$FF` = console). `read`/`write`/`putc` route through the kernel +(`KGetc`/`KPutc`): console when the fd is `$FF`, otherwise the filesystem +(`getb`/`putb`). The shell forks a child, the child `setin`/`setout`s to open +files, then `exec`s — so the program's I/O lands in the file. A pipe +`a | b` is run as `a > __pipe ; b < __pipe ; rm __pipe`. + ### Process lifecycle (exit / wait / reaping) The classic Unix zombie/reap dance, adapted to banked memory: @@ -295,7 +306,9 @@ $ rm notes - [x] `wc`, `head`, and an `argc/argv` demo (`args`) + `argv_parse` in libc - [x] option parsing (`hasflag`/`optval`): `wc -l/-w/-c`, `head -n N` - [x] a RAM filesystem (WRAMX) + `ls`/`cat`/`save`/`rm`; `wc`/`head` read files -- [ ] more tools (`rev`, `grep`, `tail`), shell redirection (`>`/`|`) +- [x] shell in C with redirection (`>`/`<`) and pipes (`|`, via a temp file) +- [x] per-process stdin/stdout routing (`KGetc`/`KPutc`, `setin`/`setout`) +- [ ] more tools (`rev`, `grep`, `tail`), true concurrent pipes - [ ] battery-backed files (move the FS to cart SRAM), directories - [ ] Preemptive scheduling: real context save in `TimerISR` → `hSwitchTo` - [ ] Use the `$D000-$DFFF` u-area (SVBK) for per-process kernel state / kstack @@ -30,4 +30,12 @@ void fremove(const char *name); #define O_READ 0 #define O_WRITE 1 #define NOFD 0xFF + +/* process control (c/libc.s) */ +unsigned char fork(void); +void exec(unsigned char id); /* replace image; never returns */ +unsigned char wait(void); +void setin(unsigned char fd); /* redirect stdin ($FF=console) */ +void setout(unsigned char fd); /* redirect stdout ($FF=console) */ +unsigned char lookup(const char *name); /* command name -> program id */ #endif @@ -25,11 +25,11 @@ _getargs: ld c, l ret - ;; void putc(char c /*A*/) + ;; void putc(char c /*A*/) -> stdout via SYS_PUTC _putc: - ld (0xff01), a - ld a, #0x81 - ld (0xff02), a + ld e, a + ld c, #16 ; SYS_PUTC + rst #0x30 ret ;; void puts(const char *s /*DE*/) - write until NUL (no newline) @@ -37,22 +37,22 @@ _puts: 1$: ld a, (de) or a ret z - ld (0xff01), a - ld a, #0x81 - ld (0xff02), a + push de + ld e, a + ld c, #16 ; SYS_PUTC + rst #0x30 + pop de inc de jr 1$ - ;; void nl(void) - CRLF + ;; void nl(void) - CRLF via stdout _nl: - ld a, #0x0d - ld (0xff01), a - ld a, #0x81 - ld (0xff02), a - ld a, #0x0a - ld (0xff01), a - ld a, #0x81 - ld (0xff02), a + ld e, #0x0d + ld c, #16 ; SYS_PUTC + rst #0x30 + ld e, #0x0a + ld c, #16 + rst #0x30 ret ;; unsigned char strlen(const char *s /*DE*/) -> A @@ -132,3 +132,38 @@ _fremove: ld c, #15 ; SYS_REMOVE rst #0x30 ret + + .globl _fork, _exec, _wait, _setin, _setout, _lookup + ;; unsigned char fork(void) -> A (child pid in parent, 0 in child) +_fork: + ld c, #1 ; SYS_FORK + rst #0x30 + ret + ;; void exec(unsigned char id /*A*/) - never returns +_exec: + ld b, a + ld c, #6 ; SYS_EXEC + rst #0x30 + ret + ;; unsigned char wait(void) -> A (reaped pid) +_wait: + ld c, #7 ; SYS_WAIT + rst #0x30 + ret + ;; void setin(unsigned char fd /*A*/) +_setin: + ld b, a + ld c, #17 ; SYS_SETIN + rst #0x30 + ret + ;; void setout(unsigned char fd /*A*/) +_setout: + ld b, a + ld c, #18 ; SYS_SETOUT + rst #0x30 + ret + ;; unsigned char lookup(const char *name /*DE*/) -> A (prog id or $FF) +_lookup: + ld c, #19 ; SYS_LOOKUP + rst #0x30 + ret @@ -0,0 +1,99 @@ +#include "gbos.h" +/* gbos shell: prompt -> read a line -> parse -> run, with I/O redirection + ( > file, < file ) and pipes ( cmd1 | cmd2, via a temp file ). + Operators must be space-separated (e.g. cat readme > out ). */ + +#define ARGV0 ((char *)0xA000) /* where a child reads its command line */ + +/* split s into space-separated tokens (NUL-terminated in place). */ +static unsigned char tokenize(char *s, char **tok, unsigned char max) { + unsigned char n = 0; + for (;;) { + while (*s == ' ') *s++ = 0; + if (*s == 0) break; + if (n < max) tok[n++] = s; + while (*s && *s != ' ') s++; + } + return n; +} + +static unsigned char isop(char *t, char c) { return t[0] == c && t[1] == 0; } + +/* build the child command line "name\0arg1 arg2\0" at 0xA000 */ +static void set_cmdline(char **tok, unsigned char start, unsigned char end) { + char *p = ARGV0, *t; + unsigned char i; + t = tok[start]; + while (*t) *p++ = *t++; + *p++ = 0; /* NUL after the command word */ + for (i = start + 1; i < end; i++) { + if (i > start + 1) *p++ = ' '; + t = tok[i]; + while (*t) *p++ = *t++; + } + *p = 0; +} + +/* run tok[start..end): parse its own >/< redirects, honor forced in/out. */ +static void run(char **tok, unsigned char start, unsigned char end, + unsigned char infd, unsigned char outfd) { + unsigned char i, cmdend = end, myin = NOFD, myout = NOFD, id, pid; + for (i = start; i < end; i++) { + if (isop(tok[i], '>')) { + if (i + 1 < end) myout = open(tok[i + 1], O_WRITE); + if (i < cmdend) cmdend = i; + } else if (isop(tok[i], '<')) { + if (i + 1 < end) myin = open(tok[i + 1], O_READ); + if (i < cmdend) cmdend = i; + } + } + id = lookup(tok[start]); + if (id == NOFD) { puts(tok[start]); puts(": not found"); nl(); goto done; } + set_cmdline(tok, start, cmdend); + pid = fork(); + if (pid == 0) { + if (myin != NOFD) setin(myin); + else if (infd != NOFD) setin(infd); + if (myout != NOFD) setout(myout); + else if (outfd != NOFD) setout(outfd); + exec(id); + } + wait(); +done: + if (myin != NOFD) close(myin); + if (myout != NOFD) close(myout); +} + +void main(void) { + char line[80]; + char *tok[16]; + for (;;) { + unsigned char nt, i, pipepos, c; + puts("$ "); + /* read a line (echoing), stop at newline; exit on EOF */ + i = 0; + for (;;) { + c = readc(); + if (c == EOF) return; + if (c == '\r' || c == '\n') { nl(); break; } + if (i < 79) { line[i++] = c; putc(c); } + } + line[i] = 0; + nt = tokenize(line, tok, 16); + if (nt == 0) continue; + pipepos = 0; + for (i = 1; i < nt; i++) + if (isop(tok[i], '|')) { pipepos = i; break; } + if (pipepos) { + unsigned char pw = open("__pipe", O_WRITE); + run(tok, 0, pipepos, NOFD, pw); + close(pw); + { unsigned char pr = open("__pipe", O_READ); + run(tok, pipepos + 1, nt, pr, NOFD); + close(pr); } + fremove("__pipe"); + } else { + run(tok, 0, nt, NOFD, NOFD); + } + } +} diff --git a/include/gbos.inc b/include/gbos.inc index 7c3f325..6aaaf73 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -67,8 +67,12 @@ DEF PROC_RAMB RB 1 ; data/stack cart-RAM bank DEF PROC_WRAMB RB 1 ; u-area SVBK bank (1..7) DEF PROC_PARENT RB 1 DEF PROC_EXIT RB 1 ; exit code (valid when PS_ZOMBIE) +DEF PROC_STDIN RB 1 ; $FF = console, else an open-file fd +DEF PROC_STDOUT RB 1 ; $FF = console, else an open-file fd DEF PROC_SIZE RB 0 +DEF STD_CONSOLE EQU $FF + ; ----------------------------------------------------------------------------- ; Syscall numbers (passed in C; args in DE/HL/B per call, ret in A) ; ----------------------------------------------------------------------------- @@ -88,7 +92,11 @@ DEF SYS_GETB EQU 12 ; read a byte from an open file (B=fd -> A, CF=EOF) DEF SYS_PUTB EQU 13 ; append a byte to an open file (B=fd, E=byte) DEF SYS_LIST EQU 14 ; list a directory slot (B=slot, DE=namebuf) DEF SYS_REMOVE EQU 15 ; delete a file (DE=name) -DEF SYS_MAX EQU 16 +DEF SYS_PUTC EQU 16 ; write a byte to stdout (E=byte) +DEF SYS_SETIN EQU 17 ; set current stdin fd (B=fd, $FF=console) +DEF SYS_SETOUT EQU 18 ; set current stdout fd (B=fd, $FF=console) +DEF SYS_LOOKUP EQU 19 ; program id for a command name (DE=name -> A=id/$FF) +DEF SYS_MAX EQU 20 ; (SYS_OPEN=4 / SYS_CLOSE=5 are now implemented by the filesystem) ; ----------------------------------------------------------------------------- diff --git a/src/kdata.asm b/src/kdata.asm index 9aca466..ea18756 100644 --- a/src/kdata.asm +++ b/src/kdata.asm @@ -45,3 +45,4 @@ wFsMode:: DS 1 wFsSlot:: DS 1 wFsByte:: DS 1 wFsOFPtr:: DS 2 +wKChar:: DS 1 ; scratch for KPutc diff --git a/src/proc.asm b/src/proc.asm index a2b1368..ab1c807 100644 --- a/src/proc.asm +++ b/src/proc.asm @@ -221,7 +221,10 @@ ProcCreate:: ld [hl+], a ; PROC_WRAMB xor a ld [hl+], a ; PROC_PARENT - ld [hl], a ; PROC_EXIT + ld [hl+], a ; PROC_EXIT + ld a, STD_CONSOLE + ld [hl+], a ; PROC_STDIN = console + ld [hl], a ; PROC_STDOUT = console ; build the initial stack frame in the task's RAM bank ld a, [wTmpEntry] @@ -438,7 +441,19 @@ sys_fork:: ld [hl+], a ; PROC_EXIT = 0 xor a - ld [hl], a + ld [hl+], a + ; PROC_STDIN / PROC_STDOUT = inherit from parent + ld a, [wCurProc] + add PROC_STDIN + ld e, a + ld a, [wCurProc+1] + adc 0 + ld d, a + ld a, [de] + ld [hl+], a ; child STDIN = parent STDIN + inc de + ld a, [de] + ld [hl], a ; child STDOUT = parent STDOUT ; ---- return to the parent with child pid ---- ld a, [wForkUserSP] diff --git a/src/programs.asm b/src/programs.asm index 161ee2f..1a7092e 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -90,149 +90,13 @@ ProgRm: INCBIN "c/rm.bin" ; ----------------------------------------------------------------------------- -; PROG_SH (bank 3) - a tiny shell. -; prompt -> read a line (with echo) -> match a command -> fork+exec+wait. +; PROG_SH (bank 3) - the shell, now written in C (c/sh.c): parses >/</| and +; drives fork+exec+wait with I/O redirection. Command lookup + StrEqual/SkipName +; live in the kernel now (sys_lookup + NameTable + shell_lib). ; ----------------------------------------------------------------------------- SECTION "prog_sh", ROMX[$4000], BANK[3] ProgSh: -.prompt - ld de, .ps1 - ld b, 2 - ld c, SYS_WRITE - rst $30 - - ; read a line into SH_LINEBUF, echoing as we go - ld hl, SH_LINEBUF -.rd - push hl ; syscalls clobber HL (trap uses it to dispatch) - ld c, SYS_READ - rst $30 ; A = input byte - pop hl - cp $04 - jr z, .eof ; Ctrl-D / EOF: exit the shell - cp $0D - jr z, .eol - cp $0A - jr z, .eol - ld [hl], a ; store in the line buffer (A preserved) - inc hl - ld [$FF01], a ; echo the char straight to serial - ld a, $81 - ld [$FF02], a - ld a, l - cp LOW(SH_LINEBUF) + SH_MAXLINE - 1 - jr c, .rd ; keep reading unless the buffer is full - -.eof - ld b, 0 - ld c, SYS_EXIT ; exit init/shell -> system idle - rst $30 - -.eol - ld [hl], 0 ; NUL-terminate - ld de, .crlf - ld b, 2 - ld c, SYS_WRITE - rst $30 - ld a, [SH_LINEBUF] - or a - jp z, .prompt ; empty line - - ; split off the first word: replace the first space with a NUL so command - ; matching sees just the command, and the args survive at SH_LINEBUF+n - ; (inherited by the child through fork, read back via main(char *args)). - ld hl, SH_LINEBUF -.split - ld a, [hl] - or a - jr z, .lookup ; no space -> no args - cp $20 - jr z, .cut - inc hl - jr .split -.cut - ld [hl], 0 ; isolate the command word - -.lookup - ; look the command up in the table - ld de, .cmdtab -.scan - ld a, [de] - or a - jr z, .unknown ; empty name = end of table - push de - ld hl, SH_LINEBUF - call StrEqual ; Z set if equal - pop de - jr z, .match - call SkipName ; DE -> NUL after this name - inc de ; -> program id - inc de ; -> next entry - jr .scan - -.match - call SkipName - inc de ; DE -> program id - ld a, [de] - ld [SH_PROGID], a - ld c, SYS_FORK - rst $30 - or a - jr z, .exec - ld c, SYS_WAIT ; parent: reap the command - rst $30 - jp .prompt -.exec - ld a, [SH_PROGID] - ld b, a - ld c, SYS_EXEC - rst $30 ; child becomes the command - -.unknown - ld de, .huh - ld b, 3 - ld c, SYS_WRITE - rst $30 - jp .prompt - -.ps1 - db "$ " -.crlf - db $0D, $0A -.huh - db "?", $0D, $0A -.cmdtab - db "worker", 0 - db PROG_WORKER - db "hello", 0 - db PROG_HELLO - db "chello", 0 - db PROG_CHELLO - db "echo", 0 - db PROG_ECHO - db "true", 0 - db PROG_TRUE - db "false", 0 - db PROG_FALSE - db "uname", 0 - db PROG_UNAME - db "pid", 0 - db PROG_PID - db "cat", 0 - db PROG_CAT - db "wc", 0 - db PROG_WC - db "head", 0 - db PROG_HEAD - db "args", 0 - db PROG_ARGS - db "ls", 0 - db PROG_LS - db "save", 0 - db PROG_SAVE - db "rm", 0 - db PROG_RM - db 0 ; terminator + INCBIN "c/sh.bin" ; ----------------------------------------------------------------------------- ; Shell string helpers (ROM0, always mapped). @@ -298,3 +162,40 @@ ProgramTable:: dw ProgSave db LOW(BANK(ProgRm)), HIGH(BANK(ProgRm)) dw ProgRm + +; ----------------------------------------------------------------------------- +; NameTable (ROM0) - command name -> program id, searched by sys_lookup. +; ----------------------------------------------------------------------------- +SECTION "nametab", ROM0 +NameTable:: + db "worker", 0 + db PROG_WORKER + db "hello", 0 + db PROG_HELLO + db "chello", 0 + db PROG_CHELLO + db "echo", 0 + db PROG_ECHO + db "true", 0 + db PROG_TRUE + db "false", 0 + db PROG_FALSE + db "uname", 0 + db PROG_UNAME + db "pid", 0 + db PROG_PID + db "cat", 0 + db PROG_CAT + db "wc", 0 + db PROG_WC + db "head", 0 + db PROG_HEAD + db "args", 0 + db PROG_ARGS + db "ls", 0 + db PROG_LS + db "save", 0 + db PROG_SAVE + db "rm", 0 + db PROG_RM + db 0 diff --git a/src/syscall.asm b/src/syscall.asm index 5ed3d6f..f149a76 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -50,6 +50,10 @@ SyscallTable: dw sys_putb ; 13 PUTB dw sys_list ; 14 LIST dw sys_remove ; 15 REMOVE + dw sys_putc ; 16 PUTC + dw sys_setin ; 17 SETIN + dw sys_setout ; 18 SETOUT + dw sys_lookup ; 19 LOOKUP ; ----------------------------------------------------------------------------- sys_nosys: @@ -64,33 +68,151 @@ sys_write: ld a, b or a ret z ; len 0 - ld c, b ; C = remaining .loop ld a, [de] - call PutChar + push de + push bc + ld e, a + call KPutc ; route the byte to this process's stdout + pop bc + pop de inc de - dec c + dec b jr nz, .loop - ld a, b ; return len ret ; ----------------------------------------------------------------------------- -; sys_read() -> A = one input byte from the console (blocking). -; Uses an external-clock serial transfer as a clean RX: it completes only when -; the host has a byte (no stdout echo). While waiting we yield so other procs -; run. Preserves BC/DE/HL (the context switch saves/restores them). +; CurStdin / CurStdout -> A = current process PROC_STDIN / PROC_STDOUT ; ----------------------------------------------------------------------------- -sys_read: +CurStdin: + ld a, [wCurProc] + add PROC_STDIN + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ret +CurStdout: + ld a, [wCurProc] + add PROC_STDOUT + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ret + +; ----------------------------------------------------------------------------- +; KGetc -> A = byte, CF set on EOF. Reads this process's stdin (console/file). +; ----------------------------------------------------------------------------- +KGetc:: + call CurStdin + cp STD_CONSOLE + jr z, .console + ld b, a + jp sys_getb ; B=fd -> A=byte, CF=EOF +.console .poll ld a, $80 - ld [rSC], a ; request a byte (external clock) + ld [rSC], a ld a, [rSC] bit 7, a - jr z, .got ; bit7 clear => transfer completed - call SchedYield ; nothing yet: let others run, then retry + jr z, .got + call SchedYield jr .poll .got ld a, [rSB] + and a ; CF = 0 + ret + +; ----------------------------------------------------------------------------- +; KPutc(E = byte) - write to this process's stdout (console/file). +; ----------------------------------------------------------------------------- +KPutc:: + ld a, e + ld [wKChar], a + call CurStdout + cp STD_CONSOLE + jr z, .console + ld b, a ; fd + ld a, [wKChar] + ld e, a + jp sys_putb ; B=fd, E=byte +.console + ld a, [wKChar] + ld [rSB], a + ld a, $81 + ld [rSC], a + ret + +; ----------------------------------------------------------------------------- +; sys_putc(E = byte), sys_setin/sys_setout(B = fd) +; ----------------------------------------------------------------------------- +sys_putc: + call KPutc + ret +sys_setin: + ld a, [wCurProc] + add PROC_STDIN + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, b + ld [hl], a + ret +sys_setout: + ld a, [wCurProc] + add PROC_STDOUT + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, b + ld [hl], a + ret + +; ----------------------------------------------------------------------------- +; sys_lookup(DE = name) -> A = program id, or $FF if not a known command. +; ----------------------------------------------------------------------------- +sys_lookup: + ld h, d + ld l, e ; HL = query name + ld de, NameTable +.l + ld a, [de] + or a + jr z, .none + push hl + push de + call StrEqual ; HL=query vs DE=table name -> Z if equal + pop de + pop hl + jr z, .match + call SkipName ; DE -> NUL + inc de ; -> id + inc de ; -> next entry + jr .l +.match + call SkipName + inc de ; -> id + ld a, [de] + ret +.none + ld a, $FF + ret + +; ----------------------------------------------------------------------------- +; sys_read() -> A = one input byte from the console (blocking). +; Uses an external-clock serial transfer as a clean RX: it completes only when +; the host has a byte (no stdout echo). While waiting we yield so other procs +; run. Preserves BC/DE/HL (the context switch saves/restores them). +; ----------------------------------------------------------------------------- +sys_read: + call KGetc ; A=byte, CF=EOF (file); console EOF is $04 + ret nc + ld a, $04 ; file EOF -> EOT ret ; ----------------------------------------------------------------------------- |
