aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--README.md45
-rw-r--r--c/cat.c18
-rw-r--r--c/gbos.h11
-rw-r--r--c/head.c27
-rw-r--r--c/libc.s48
-rw-r--r--c/ls.c9
-rw-r--r--c/rm.c7
-rw-r--r--c/save.c19
-rw-r--r--c/wc.c21
-rw-r--r--include/gbos.inc30
-rw-r--r--src/boot.asm1
-rw-r--r--src/fs.asm451
-rw-r--r--src/kdata.asm7
-rw-r--r--src/programs.asm21
-rw-r--r--src/syscall.asm8
16 files changed, 697 insertions, 29 deletions
diff --git a/Makefile b/Makefile
index 857cc0a..8c38b3c 100644
--- a/Makefile
+++ b/Makefile
@@ -19,7 +19,8 @@ $(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/pid.bin c/cat.bin c/wc.bin c/head.bin c/args.bin \
+ c/ls.bin c/save.bin c/rm.bin
$(BUILD)/programs.o: $(CBLOBS)
# C programs also depend on the shared libc sources
diff --git a/README.md b/README.md
index 84c5c72..85a01d2 100644
--- a/README.md
+++ b/README.md
@@ -214,15 +214,20 @@ unsigned char argc = argv_parse(argv, 8); /* args one two -> argc=3 */
| tool | what it does |
|------|--------------|
| `echo` | print its arguments (`echo hello world`) |
-| `cat` | copy stdin to stdout until EOF (Ctrl-D / pipe end) |
-| `wc` | count lines / words / chars of stdin |
-| `head` | print the first *n* lines of stdin (`head 5`, default 10) |
+| `cat` | print a file (`cat readme`) or copy stdin to stdout |
+| `wc` | count lines/words/chars of a file or stdin (`wc -l readme`) |
+| `head` | first *n* lines of a file or stdin (`head -n 5 readme`) |
+| `ls` | list files |
+| `save` | read a line from stdin into a file (`save notes`) |
+| `rm` | delete a file (`rm notes`) |
| `args` | argc/argv demo (`args one two three`) |
| `uname`| print the system name |
| `pid` | print the process's pid (decimal) |
| `true` / `false` | exit 0 / 1 |
| `chello` | the C "hello" demo |
+Options use `hasflag`/`optval` in libc: `wc -l`/`-w`/`-c`, `head -n N`.
+
```
$ echo C tools on a Game Boy
C tools on a Game Boy
@@ -247,6 +252,35 @@ so we use SDCC's native asxxxx path and INCBIN the blob instead. String literals
with embedded control chars are also mangled in rgbds mode; C programs pass
explicit lengths and emit newlines via `nl()`.
+## Filesystem
+
+A tiny **RAM filesystem** lives in **WRAMX (`$D000-$DFFF`, 4 KiB)** — always
+mapped in DMG mode and simultaneously accessible with a process's `$A000` data
+bank, so the kernel copies between file and process memory with no bounce
+buffer. 8 fixed slots of 512 bytes: `name[8]` + `len[2]` + `data[502]`. Seeded
+with a `readme` at boot.
+
+Syscalls: `open` (4), `close` (5), `getb` (12), `putb` (13, byte in `E`),
+`list` (14), `remove` (15). Libc: `open`/`close`/`fgetc`/`fputc`/`flist`/
+`fremove` (`c/libc.s`).
+
+```
+$ save notes
+gbos has a filesystem now
+$ cat notes
+gbos has a filesystem now
+$ wc notes
+1 5 26
+$ ls
+readme
+notes
+$ rm notes
+```
+
+> **ABI note:** the syscall trap indexes its jump table with `A`, so syscall
+> args go in `B`/`DE`/`E` (never `A`) and results come back in `A`. That's why
+> `putb` takes its byte in `E`.
+
## Roadmap
- [x] `fork`: copy parent's 8 KiB RAM bank → free bank, child returns 0
@@ -259,7 +293,10 @@ explicit lengths and emit newlines via `nl()`.
- [x] CLI tools in C: `echo`, `cat`, `uname`, `pid`, `true`, `false`
- [x] Makefile builds all C programs (a `CBLOBS` list)
- [x] `wc`, `head`, and an `argc/argv` demo (`args`) + `argv_parse` in libc
-- [ ] more tools (`rev`, `grep`), option parsing (`-n`), a RAM filesystem
+- [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 (`>`/`|`)
+- [ ] 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
- [ ] `brk`/heap allocator inside the task bank (heap up, stack down, collision = ENOMEM)
diff --git a/c/cat.c b/c/cat.c
index 4092778..dde58cd 100644
--- a/c/cat.c
+++ b/c/cat.c
@@ -1,10 +1,16 @@
#include "gbos.h"
-/* cat: copy stdin to stdout until EOF (Ctrl-D / pipe end). */
+/* cat [file]: print a file, or copy stdin to stdout until EOF. */
void main(void) {
- char c;
- for (;;) {
- c = readc();
- if (c == EOF) break;
- putc(c);
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ if (argc > 0) {
+ unsigned char fd = open(argv[0], O_READ);
+ int ch;
+ if (fd == NOFD) { puts("cat: no such file"); nl(); return; }
+ while ((ch = fgetc(fd)) >= 0) putc((char)ch);
+ close(fd);
+ } else {
+ char c;
+ for (;;) { c = readc(); if (c == EOF) break; putc(c); }
}
}
diff --git a/c/gbos.h b/c/gbos.h
index 47f75ad..d8c8eb3 100644
--- a/c/gbos.h
+++ b/c/gbos.h
@@ -19,4 +19,15 @@ unsigned char atou(const char *s); /* parse decimal */
unsigned char argv_parse(char **argv, unsigned char maxv); /* argc/argv */
unsigned char hasflag(char **argv, unsigned char argc, char f); /* -x flag */
char *optval(char **argv, unsigned char argc, char opt); /* -x VALUE */
+
+/* filesystem (c/libc.s -> kernel syscalls) */
+unsigned char open(const char *name, unsigned char mode); /* 0=read 1=write */
+void close(unsigned char fd);
+int fgetc(unsigned char fd); /* a byte, or -1 at EOF */
+void fputc(unsigned char fd, char ch);
+unsigned char flist(unsigned char slot, char *namebuf); /* 1 if slot used */
+void fremove(const char *name);
+#define O_READ 0
+#define O_WRITE 1
+#define NOFD 0xFF
#endif
diff --git a/c/head.c b/c/head.c
index 952d4c5..e8bf846 100644
--- a/c/head.c
+++ b/c/head.c
@@ -1,17 +1,28 @@
#include "gbos.h"
-/* head [-n N] [N]: print the first N lines of stdin (default 10). */
+/* head [-n N] [N] [file]: first N lines of a file or stdin (default 10). */
void main(void) {
char *argv[8];
unsigned char argc = argv_parse(argv, 8);
char *nv = optval(argv, argc, 'n');
- unsigned char n = 10, lines = 0;
- char c;
+ unsigned char n = 10, lines = 0, i, fd = NOFD;
+ char *fname = 0;
+ int ch;
if (nv) n = atou(nv);
- else if (argc > 0 && argv[0][0] >= '0' && argv[0][0] <= '9') n = atou(argv[0]);
+ for (i = 0; i < argc; i++) {
+ if (argv[i][0] == '-') continue;
+ if (nv == argv[i]) continue;
+ if (!nv && argv[i][0] >= '0' && argv[i][0] <= '9') { n = atou(argv[i]); continue; }
+ fname = argv[i]; break;
+ }
+ if (fname) {
+ fd = open(fname, O_READ);
+ if (fd == NOFD) { puts("head: no such file"); nl(); return; }
+ }
for (;;) {
- c = readc();
- if (c == EOF) break;
- if (lines < n) putc(c);
- if (c == '\n') lines++;
+ if (fd == NOFD) { char c = readc(); if (c == EOF) break; ch = c; }
+ else { ch = fgetc(fd); if (ch < 0) break; }
+ if (lines < n) putc((char)ch);
+ if (ch == '\n') lines++;
}
+ if (fd != NOFD) close(fd);
}
diff --git a/c/libc.s b/c/libc.s
index 29031e9..a26042e 100644
--- a/c/libc.s
+++ b/c/libc.s
@@ -1,5 +1,6 @@
.module libc
.globl _writes, _putc, _puts, _nl, _strlen, _readc, _sexit, _getpid, _getargs
+ .globl _open, _close, _fgetc, _fputc, _flist, _fremove
;; 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.
@@ -84,3 +85,50 @@ _getpid:
ld c, #8 ; SYS_GETPID
rst #0x30
ret
+
+ ;; unsigned char open(const char *name /*DE*/, unsigned char mode /*A*/) -> A(fd)
+_open:
+ ld b, a ; B = mode
+ ld c, #4 ; SYS_OPEN
+ rst #0x30
+ ret
+
+ ;; void close(unsigned char fd /*A*/)
+_close:
+ ld b, a
+ ld c, #5 ; SYS_CLOSE
+ rst #0x30
+ ret
+
+ ;; int fgetc(unsigned char fd /*A*/) -> BC (byte, or -1 at EOF)
+_fgetc:
+ ld b, a
+ ld c, #12 ; SYS_GETB
+ rst #0x30
+ jr c, 1$
+ ld c, a
+ ld b, #0
+ ret
+1$: ld bc, #0xffff
+ ret
+
+ ;; void fputc(unsigned char fd /*A*/, char ch /*E*/)
+ ;; byte goes to the kernel in E (the trap clobbers A during dispatch)
+_fputc:
+ ld b, a ; B = fd (ch stays in E)
+ ld c, #13 ; SYS_PUTB
+ rst #0x30
+ ret
+
+ ;; unsigned char flist(unsigned char slot /*A*/, char *namebuf /*DE*/) -> A
+_flist:
+ ld b, a
+ ld c, #14 ; SYS_LIST
+ rst #0x30
+ ret
+
+ ;; void fremove(const char *name /*DE*/)
+_fremove:
+ ld c, #15 ; SYS_REMOVE
+ rst #0x30
+ ret
diff --git a/c/ls.c b/c/ls.c
new file mode 100644
index 0000000..f7e5161
--- /dev/null
+++ b/c/ls.c
@@ -0,0 +1,9 @@
+#include "gbos.h"
+/* ls: list files in the RAM filesystem. */
+void main(void) {
+ char name[9];
+ unsigned char i;
+ for (i = 0; i < 8; i++) {
+ if (flist(i, name)) { name[8] = 0; puts(name); nl(); }
+ }
+}
diff --git a/c/rm.c b/c/rm.c
new file mode 100644
index 0000000..25f3004
--- /dev/null
+++ b/c/rm.c
@@ -0,0 +1,7 @@
+#include "gbos.h"
+/* rm <name>: delete a file. */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ if (argc > 0) fremove(argv[0]);
+}
diff --git a/c/save.c b/c/save.c
new file mode 100644
index 0000000..dd2b8cd
--- /dev/null
+++ b/c/save.c
@@ -0,0 +1,19 @@
+#include "gbos.h"
+/* save <name>: read one line from stdin and store it as a file. */
+void main(void) {
+ char *argv[4];
+ unsigned char argc = argv_parse(argv, 4);
+ unsigned char fd;
+ char c;
+ if (argc < 1) { puts("usage: save <name>"); nl(); return; }
+ fd = open(argv[0], O_WRITE);
+ if (fd == NOFD) { puts("save: cannot create"); nl(); return; }
+ for (;;) {
+ c = readc();
+ if (c == EOF) break;
+ putc(c); /* echo */
+ fputc(fd, c);
+ if (c == '\n') break;
+ }
+ close(fd);
+}
diff --git a/c/wc.c b/c/wc.c
index 86c3aed..424876d 100644
--- a/c/wc.c
+++ b/c/wc.c
@@ -1,5 +1,5 @@
#include "gbos.h"
-/* wc [-l] [-w] [-c]: count lines, words, chars of stdin (default: all). */
+/* wc [-l][-w][-c] [file]: count lines/words/chars of a file or stdin. */
void main(void) {
char *argv[8];
unsigned char argc = argv_parse(argv, 8);
@@ -7,17 +7,24 @@ void main(void) {
unsigned char ww = hasflag(argv, argc, 'w');
unsigned char wch = hasflag(argv, argc, 'c');
unsigned int lines = 0, words = 0, chars = 0;
- unsigned char inword = 0, first = 1;
- char c;
+ unsigned char inword = 0, first = 1, i, fd = NOFD;
+ char *fname = 0;
+ int ch;
if (!wl && !ww && !wch) { wl = ww = wch = 1; }
+ for (i = 0; i < argc; i++) if (argv[i][0] != '-') { fname = argv[i]; break; }
+ if (fname) {
+ fd = open(fname, O_READ);
+ if (fd == NOFD) { puts("wc: no such file"); nl(); return; }
+ }
for (;;) {
- c = readc();
- if (c == EOF) break;
+ if (fd == NOFD) { char c = readc(); if (c == EOF) break; ch = c; }
+ else { ch = fgetc(fd); if (ch < 0) break; }
chars++;
- if (c == '\n') lines++;
- if (c == ' ' || c == '\n' || c == '\r' || c == '\t') inword = 0;
+ if (ch == '\n') lines++;
+ if (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') inword = 0;
else if (!inword) { inword = 1; words++; }
}
+ if (fd != NOFD) close(fd);
if (wl) { putu(lines); first = 0; }
if (ww) { if (!first) putc(' '); putu(words); first = 0; }
if (wch) { if (!first) putc(' '); putu(chars); }
diff --git a/include/gbos.inc b/include/gbos.inc
index 156876b..7c3f325 100644
--- a/include/gbos.inc
+++ b/include/gbos.inc
@@ -84,7 +84,32 @@ DEF SYS_GETPID EQU 8
DEF SYS_KILL EQU 9
DEF SYS_BRK EQU 10
DEF SYS_YIELD EQU 11
-DEF SYS_MAX EQU 12
+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
+; (SYS_OPEN=4 / SYS_CLOSE=5 are now implemented by the filesystem)
+
+; -----------------------------------------------------------------------------
+; RAM filesystem (lives in WRAMX $D000-$DFFF, 4 KiB; fixed in DMG mode).
+; 8 files x 512 bytes: name[8] (first byte 0 = free) + len[2] + data[502].
+; -----------------------------------------------------------------------------
+DEF FS_BASE EQU $D000
+DEF FS_MAX_FILES EQU 8
+DEF FS_FILE_SIZE EQU 512
+DEF FS_NAME EQU 0
+DEF FS_LEN EQU 8
+DEF FS_DATA EQU 10
+DEF FS_MAX_DATA EQU 502
+
+; open-file table (kernel WRAM0): inuse, slot, mode, pos(2)
+DEF OF_MAX EQU 4
+DEF OF_INUSE EQU 0
+DEF OF_SLOT EQU 1
+DEF OF_MODE EQU 2
+DEF OF_POS EQU 3
+DEF OF_SIZE EQU 5
; -----------------------------------------------------------------------------
; Program table indices (see programs.asm). exec() takes one of these in B.
@@ -102,5 +127,8 @@ DEF PROG_CAT EQU 9
DEF PROG_WC EQU 10
DEF PROG_HEAD EQU 11
DEF PROG_ARGS EQU 12
+DEF PROG_LS EQU 13
+DEF PROG_SAVE EQU 14
+DEF PROG_RM EQU 15
ENDC
diff --git a/src/boot.asm b/src/boot.asm
index 73ea860..7c175ce 100644
--- a/src/boot.asm
+++ b/src/boot.asm
@@ -49,6 +49,7 @@ KernelInit:
call ClearKernelRAM
call CopyHramCode ; move context-switch trampoline into HRAM
call ProcInit ; wipe process table
+ call fs_init ; set up the RAM filesystem
; --- spawn the init task (pid 1); it fork()s the rest ---
ld hl, TaskInit
diff --git a/src/fs.asm b/src/fs.asm
new file mode 100644
index 0000000..692fb45
--- /dev/null
+++ b/src/fs.asm
@@ -0,0 +1,451 @@
+; =============================================================================
+; fs.asm - a tiny RAM filesystem in WRAMX ($D000-$DFFF, 4 KiB).
+;
+; 8 fixed slots of 512 bytes: name[8] (first byte 0 = free) + len[2] + data[502].
+; Slot i base = $D000 + i*512 -> high = $D0 + i*2, low = $00.
+; Because WRAMX is always mapped (DMG) and the caller's buffers live in the
+; mapped $A000 window, the kernel can copy between them directly.
+; =============================================================================
+INCLUDE "include/gbos.inc"
+
+SECTION "fs", ROM0
+
+; -----------------------------------------------------------------------------
+; FsSlotBase - in: A = slot (0..7) out: HL = slot base. (A clobbered)
+; -----------------------------------------------------------------------------
+FsSlotBase::
+ add a ; slot*2
+ add HIGH(FS_BASE) ; + $D0
+ ld h, a
+ ld l, 0
+ ret
+
+; -----------------------------------------------------------------------------
+; FsNameEq - compare the <=8-byte NUL-terminated names at DE and HL.
+; out: Z set if equal. preserves DE, HL.
+; -----------------------------------------------------------------------------
+FsNameEq::
+ push de
+ push hl
+ ld b, 8
+.cmp
+ ld a, [de]
+ cp [hl]
+ jr nz, .ne
+ or a ; equal byte; if NUL, strings ended equal
+ jr z, .eq
+ inc de
+ inc hl
+ dec b
+ jr nz, .cmp
+.eq
+ pop hl
+ pop de
+ xor a ; Z set
+ ret
+.ne
+ pop hl
+ pop de
+ ld a, 1
+ and a ; Z clear
+ ret
+
+; -----------------------------------------------------------------------------
+; FsFind - in: DE = name out: A = slot (0..7) or $FF.
+; -----------------------------------------------------------------------------
+FsFind::
+ ld c, 0
+.l
+ ld a, c
+ call FsSlotBase ; HL = base
+ ld a, [hl] ; name[0]
+ or a
+ jr z, .next ; free slot
+ call FsNameEq ; DE vs HL
+ jr z, .found
+.next
+ inc c
+ ld a, c
+ cp FS_MAX_FILES
+ jr c, .l
+ ld a, $FF
+ ret
+.found
+ ld a, c
+ ret
+
+; -----------------------------------------------------------------------------
+; FsAllocSlot - out: A = free slot or $FF.
+; -----------------------------------------------------------------------------
+FsAllocSlot::
+ ld c, 0
+.l
+ ld a, c
+ call FsSlotBase
+ ld a, [hl]
+ or a
+ jr z, .found
+ inc c
+ ld a, c
+ cp FS_MAX_FILES
+ jr c, .l
+ ld a, $FF
+ ret
+.found
+ ld a, c
+ ret
+
+; -----------------------------------------------------------------------------
+; FsSetName - copy the name at DE into slot wFsSlot (<=8 bytes, incl. NUL).
+; -----------------------------------------------------------------------------
+FsSetName::
+ ld a, [wFsSlot]
+ call FsSlotBase ; HL = base (= name)
+ push de
+ ld b, 8
+.cp
+ ld a, [de]
+ ld [hl+], a
+ or a
+ jr z, .done
+ inc de
+ dec b
+ jr nz, .cp
+.done
+ pop de
+ ret
+
+; -----------------------------------------------------------------------------
+; OFPtr - in: A = fd out: HL = &open-file entry. preserves DE.
+; -----------------------------------------------------------------------------
+OFPtr::
+ push de
+ ld h, 0
+ ld l, a
+ ld d, h
+ ld e, l ; DE = fd
+ add hl, hl ; 2
+ add hl, hl ; 4
+ add hl, de ; 5*fd
+ ld de, wOFTable
+ add hl, de
+ pop de
+ ret
+
+; =============================================================================
+; sys_open(DE = name, B = mode 0=read/1=write) -> A = fd or $FF
+; =============================================================================
+sys_open::
+ ld a, b
+ ld [wFsMode], a
+ call FsFind ; A = slot or $FF (DE = name)
+ ld [wFsSlot], a
+ ld a, [wFsMode]
+ or a
+ jr nz, .write
+ ; --- read mode: must exist ---
+ ld a, [wFsSlot]
+ cp $FF
+ jr z, .fail
+ jr .alloc
+.write
+ ; --- write mode: create or truncate ---
+ ld a, [wFsSlot]
+ cp $FF
+ jr nz, .trunc
+ call FsAllocSlot
+ cp $FF
+ jr z, .fail
+ ld [wFsSlot], a
+ call FsSetName ; DE still = name
+.trunc
+ ld a, [wFsSlot]
+ call FsSlotBase
+ ld a, l
+ add FS_LEN
+ ld l, a ; HL = &len
+ xor a
+ ld [hl+], a
+ ld [hl], a ; len = 0
+.alloc
+ ; --- allocate an open-file entry ---
+ ld c, 0
+.of
+ ld a, c
+ call OFPtr ; HL = &OF[c]
+ ld a, [hl]
+ or a
+ jr z, .gotof
+ inc c
+ ld a, c
+ cp OF_MAX
+ jr c, .of
+.fail
+ ld a, $FF
+ ret
+.gotof
+ ld a, 1
+ ld [hl+], a ; inuse
+ ld a, [wFsSlot]
+ ld [hl+], a ; slot
+ ld a, [wFsMode]
+ ld [hl+], a ; mode
+ xor a
+ ld [hl+], a ; pos lo
+ ld [hl], a ; pos hi
+ ld a, c
+ ret
+
+; =============================================================================
+; sys_close(B = fd)
+; =============================================================================
+sys_close::
+ ld a, b
+ cp OF_MAX
+ jr nc, .done
+ call OFPtr
+ xor a
+ ld [hl], a ; inuse = 0
+.done
+ xor a
+ ret
+
+; =============================================================================
+; sys_getb(B = fd) -> A = byte, CF set on EOF
+; =============================================================================
+sys_getb::
+ ld a, b
+ cp OF_MAX
+ jr nc, .eof
+ call OFPtr ; HL = OF entry
+ ld a, [hl]
+ or a
+ jr z, .eof
+ ld a, l
+ ld [wFsOFPtr], a
+ ld a, h
+ ld [wFsOFPtr+1], a
+ inc hl ; OF_SLOT
+ ld a, [hl]
+ ld [wFsSlot], a
+ inc hl ; OF_MODE
+ inc hl ; OF_POS lo
+ ld a, [hl+]
+ ld e, a
+ ld a, [hl]
+ ld d, a ; DE = pos
+ ; file length -> BC
+ ld a, [wFsSlot]
+ call FsSlotBase ; HL = base
+ ld a, l
+ add FS_LEN
+ ld l, a
+ ld a, [hl+]
+ ld c, a
+ ld a, [hl]
+ ld b, a ; BC = len
+ ; pos >= len ? EOF
+ ld a, e
+ sub c
+ ld a, d
+ sbc b
+ jr nc, .eof
+ ; addr = base + FS_DATA + pos
+ ld a, [wFsSlot]
+ call FsSlotBase ; HL = base (low = 0)
+ ld a, FS_DATA
+ add e
+ ld l, a
+ ld a, h
+ adc d
+ ld h, a ; HL = &data[pos]
+ ld a, [hl]
+ ld [wFsByte], a
+ inc de ; pos++
+ ; store pos back
+ ld a, [wFsOFPtr]
+ add OF_POS
+ ld l, a
+ ld a, [wFsOFPtr+1]
+ adc 0
+ ld h, a
+ ld a, e
+ ld [hl+], a
+ ld a, d
+ ld [hl], a
+ ld a, [wFsByte]
+ and a ; CF = 0 (success)
+ ret
+.eof
+ scf
+ ret
+
+; =============================================================================
+; sys_putb(B = fd, E = byte) -- byte is in E, not A: the syscall dispatch
+; clobbers A (it indexes the jump table with it).
+; =============================================================================
+sys_putb::
+ ld a, e
+ ld [wFsByte], a
+ ld a, b
+ cp OF_MAX
+ jr nc, .done
+ call OFPtr
+ ld a, [hl]
+ or a
+ jr z, .done
+ ld a, l
+ ld [wFsOFPtr], a
+ ld a, h
+ ld [wFsOFPtr+1], a
+ inc hl
+ ld a, [hl]
+ ld [wFsSlot], a
+ inc hl
+ inc hl ; OF_POS lo
+ ld a, [hl+]
+ ld e, a
+ ld a, [hl]
+ ld d, a ; DE = pos
+ ; pos >= FS_MAX_DATA ? full
+ ld a, e
+ sub LOW(FS_MAX_DATA)
+ ld a, d
+ sbc HIGH(FS_MAX_DATA)
+ jr nc, .done
+ ; addr = base + FS_DATA + pos
+ ld a, [wFsSlot]
+ call FsSlotBase
+ ld a, FS_DATA
+ add e
+ ld l, a
+ ld a, h
+ adc d
+ ld h, a
+ ld a, [wFsByte]
+ ld [hl], a ; write byte
+ inc de ; pos++
+ ; len = pos (sequential append)
+ ld a, [wFsSlot]
+ call FsSlotBase
+ ld a, l
+ add FS_LEN
+ ld l, a
+ ld a, e
+ ld [hl+], a
+ ld a, d
+ ld [hl], a
+ ; store pos back
+ ld a, [wFsOFPtr]
+ add OF_POS
+ ld l, a
+ ld a, [wFsOFPtr+1]
+ adc 0
+ ld h, a
+ ld a, e
+ ld [hl+], a
+ ld a, d
+ ld [hl], a
+.done
+ xor a
+ ret
+
+; =============================================================================
+; sys_list(B = slot, DE = namebuf[8]) -> A = 1 if used (name copied), else 0
+; =============================================================================
+sys_list::
+ ld a, b
+ cp FS_MAX_FILES
+ jr nc, .no
+ call FsSlotBase ; HL = base (name)
+ ld a, [hl]
+ or a
+ jr z, .no
+ ld b, 8
+.cp
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec b
+ jr nz, .cp
+ ld a, 1
+ ret
+.no
+ xor a
+ ret
+
+; =============================================================================
+; sys_remove(DE = name) -> A = 0 ok / $FF not found
+; =============================================================================
+sys_remove::
+ call FsFind
+ cp $FF
+ jr z, .no
+ call FsSlotBase
+ xor a
+ ld [hl], a ; name[0] = 0 -> free
+ xor a
+ ret
+.no
+ ld a, $FF
+ ret
+
+; =============================================================================
+; fs_init - mark all slots free, clear the OF table, seed a "readme" file.
+; =============================================================================
+fs_init::
+ ld c, 0
+.clr
+ ld a, c
+ call FsSlotBase
+ xor a
+ ld [hl], a ; name[0] = 0
+ inc c
+ ld a, c
+ cp FS_MAX_FILES
+ jr c, .clr
+ ; zero the open-file table
+ ld hl, wOFTable
+ ld b, OF_MAX * OF_SIZE
+ xor a
+.zof
+ ld [hl+], a
+ dec b
+ jr nz, .zof
+ ; seed slot 0 = "readme"
+ xor a
+ ld [wFsSlot], a
+ ld de, .rname
+ call FsSetName
+ ; copy content -> data
+ xor a
+ call FsSlotBase ; HL = base
+ ld a, FS_DATA
+ ld l, a ; HL = &data (base low = 0)
+ ld de, .rtext
+ ld bc, .rtext_end - .rtext
+.ccpy
+ ld a, [de]
+ ld [hl+], a
+ inc de
+ dec bc
+ ld a, b
+ or c
+ jr nz, .ccpy
+ ; set len
+ xor a
+ call FsSlotBase
+ ld a, l
+ add FS_LEN
+ ld l, a
+ ld a, LOW(.rtext_end - .rtext)
+ ld [hl+], a
+ ld a, HIGH(.rtext_end - .rtext)
+ ld [hl], a
+ ret
+.rname
+ db "readme", 0
+.rtext
+ db "welcome to gbos!", $0D, $0A
+ db "files live in RAM.", $0D, $0A
+.rtext_end
diff --git a/src/kdata.asm b/src/kdata.asm
index 5b91030..9aca466 100644
--- a/src/kdata.asm
+++ b/src/kdata.asm
@@ -38,3 +38,10 @@ wExitParentPid:: DS 1
wWaitMyPid:: DS 1
wWaitRetPid:: DS 1
wWaitRetCode:: DS 1
+
+; filesystem: open-file table + scratch
+wOFTable:: DS OF_MAX * OF_SIZE
+wFsMode:: DS 1
+wFsSlot:: DS 1
+wFsByte:: DS 1
+wFsOFPtr:: DS 2
diff --git a/src/programs.asm b/src/programs.asm
index a3a6db2..161ee2f 100644
--- a/src/programs.asm
+++ b/src/programs.asm
@@ -79,6 +79,15 @@ ProgHead:
SECTION "prog_args", ROMX[$4000], BANK[14]
ProgArgs:
INCBIN "c/args.bin"
+SECTION "prog_ls", ROMX[$4000], BANK[15]
+ProgLs:
+ INCBIN "c/ls.bin"
+SECTION "prog_save", ROMX[$4000], BANK[16]
+ProgSave:
+ INCBIN "c/save.bin"
+SECTION "prog_rm", ROMX[$4000], BANK[17]
+ProgRm:
+ INCBIN "c/rm.bin"
; -----------------------------------------------------------------------------
; PROG_SH (bank 3) - a tiny shell.
@@ -217,6 +226,12 @@ ProgSh:
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
; -----------------------------------------------------------------------------
@@ -277,3 +292,9 @@ ProgramTable::
dw ProgHead
db LOW(BANK(ProgArgs)), HIGH(BANK(ProgArgs))
dw ProgArgs
+ db LOW(BANK(ProgLs)), HIGH(BANK(ProgLs))
+ dw ProgLs
+ db LOW(BANK(ProgSave)), HIGH(BANK(ProgSave))
+ dw ProgSave
+ db LOW(BANK(ProgRm)), HIGH(BANK(ProgRm))
+ dw ProgRm
diff --git a/src/syscall.asm b/src/syscall.asm
index bf75865..5ed3d6f 100644
--- a/src/syscall.asm
+++ b/src/syscall.asm
@@ -38,14 +38,18 @@ SyscallTable:
dw sys_fork ; 1 FORK
dw sys_read ; 2 READ
dw sys_write ; 3 WRITE
- dw sys_nosys ; 4 OPEN (TODO)
- dw sys_nosys ; 5 CLOSE (TODO)
+ dw sys_open ; 4 OPEN
+ dw sys_close ; 5 CLOSE
dw sys_exec ; 6 EXEC
dw sys_wait ; 7 WAIT
dw sys_getpid ; 8 GETPID
dw sys_nosys ; 9 KILL (TODO)
dw sys_nosys ; 10 BRK (TODO)
dw sys_yield ; 11 YIELD
+ dw sys_getb ; 12 GETB
+ dw sys_putb ; 13 PUTB
+ dw sys_list ; 14 LIST
+ dw sys_remove ; 15 REMOVE
; -----------------------------------------------------------------------------
sys_nosys: