From 13f935aa88c2d9ebf51308e25e7c9025024d5c7c Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Sun, 24 Nov 2019 10:24:15 -0500 Subject: [PATCH] basic: add fls command Also, add the new `bshell` emulated tool. BASIC is on its way to replace the shell. --- apps/basic/README.md | 21 ++++ apps/basic/fs.asm | 15 +++ apps/basic/glue.asm | 1 + apps/basic/main.asm | 66 ++++++++---- tools/emul/.gitignore | 1 + tools/emul/Makefile | 13 ++- tools/emul/README.md | 5 + tools/emul/bshell/glue.asm | 172 +++++++++++++++++++++++++++++++ tools/emul/bshell/shell.c | 203 +++++++++++++++++++++++++++++++++++++ tools/emul/bshell/user.h | 35 +++++++ 10 files changed, 509 insertions(+), 23 deletions(-) create mode 100644 apps/basic/fs.asm create mode 100644 tools/emul/bshell/glue.asm create mode 100644 tools/emul/bshell/shell.c create mode 100644 tools/emul/bshell/user.h diff --git a/apps/basic/README.md b/apps/basic/README.md index cad5c42..c6d9b83 100644 --- a/apps/basic/README.md +++ b/apps/basic/README.md @@ -25,6 +25,16 @@ Because the goal is not to provide a foundation for complex programs, I'm planning on intentionally crippling this BASIC dialect for the sake of simplicity. +## Glueing + +The `glue.asm` file in this folder represents the minimal basic system. There +are additional modules that can be added that aren't added by default, such +as `fs.asm` because they require kernel options that might not be available. + +To include these modules, you'll need to write your own glue file and to hook +extra commands through `BAS_FINDHOOK`. Look for examples in `tools/emul` and +in recipes. + ## Usage Upon launch, a prompt is presented, waiting for a command. There are two types @@ -123,3 +133,14 @@ output I/O on port 42 with value 3. **sleep**: Sleep a number of "units" specified by the supplied expression. A "unit" depends on the CPU clock speed. At 4MHz, it is roughly 8 microseconds. + +## Optional modules + +As explained in "glueing" section abolve, this folder contains optional modules. +Here's the documentation for them. + +### fs + +`fs.asm` provides those commands: + +**fls**: prints the list of files contained in the active filesystem. diff --git a/apps/basic/fs.asm b/apps/basic/fs.asm new file mode 100644 index 0000000..b9fed42 --- /dev/null +++ b/apps/basic/fs.asm @@ -0,0 +1,15 @@ +; FS-related basic commands + +basFLS: + ld iy, .iter + jp fsIter +.iter: + ld a, FS_META_FNAME_OFFSET + call addHL + call printstr + jp printcrlf + +basFSCmds: + .dw basFLS + .db "fls", 0, 0, 0 + .db 0xff, 0xff, 0xff ; end of table diff --git a/apps/basic/glue.asm b/apps/basic/glue.asm index 81777ef..b846746 100644 --- a/apps/basic/glue.asm +++ b/apps/basic/glue.asm @@ -7,6 +7,7 @@ .inc "user.h" .inc "err.h" + call basInit jp basStart ; RAM space used in different routines for short term processing. diff --git a/apps/basic/main.asm b/apps/basic/main.asm index 2fc3468..3abb284 100644 --- a/apps/basic/main.asm +++ b/apps/basic/main.asm @@ -7,16 +7,24 @@ ; Important note: this is **not** a line number, it's a pointer to a line index ; in buffer. If it's not zero, its a valid pointer. .equ BAS_PNEXTLN @+2 +; Points to a routine to call when a command isn't found in the "core" cmd +; table. This gives the opportunity to glue code to configure extra commands. +.equ BAS_FINDHOOK @+2 .equ BAS_RAMEND @+2 ; *** Code *** -basStart: +basInit: ld (BAS_INITSP), sp call varInit call bufInit xor a ld (BAS_PNEXTLN), a ld (BAS_PNEXTLN+1), a + ld hl, unsetZ + ld (BAS_FINDHOOK), hl + ret + +basStart: ld hl, .welcome call printstr call printcrlf @@ -48,10 +56,35 @@ basLoop: .sPrompt: .db "> ", 0 +; Tries to find command specified in (DE) (must be null-terminated) in cmd +; table in (HL). If found, sets IX to point to the associated routine. If +; not found, calls BAS_FINDHOOK so that we look through extra commands +; configured by glue code. +; Destroys HL. +; Z is set if found, unset otherwise. +basFindCmd: + ; cmd table starts with routine pointer, skip + inc hl \ inc hl +.loop: + call strcmp + jr z, .found + ld a, 8 + call addHL + ld a, (hl) + cp 0xff + jr nz, .loop + jp unsetZ +.found: + dec hl \ dec hl + call intoHL + push hl \ pop ix + ret + ; Call command in (HL) after having looked for it in cmd table in (DE). -; If found, jump to it. If not found, unset Z. We expect commands to set Z -; on success. Therefore, when calling basCallCmd results in NZ, we're not sure -; where the error come from, but well... +; If found, jump to it. If not found, try (BAS_FINDHOOK). If still not found, +; unset Z. We expect commands to set Z on success. Therefore, when calling +; basCallCmd results in NZ, we're not sure where the error come from, but +; well... basCallCmd: ; let's see if it's a variable assignment. call varTryAssign @@ -64,24 +97,17 @@ basCallCmd: ; cmd table in the stack, which we want in HL and we have the rest of ; the cmdline in (HL), which we want in the stack! ex (sp), hl - inc hl \ inc hl -.loop: - call strcmp - jr z, .found - ld a, 8 - call addHL - ld a, (hl) - cp 0xff - jr nz, .loop - ; not found - pop hl ; <-- lvl 1 - jp unsetZ -.found: - dec hl \ dec hl - call intoHL - push hl \ pop ix + call basFindCmd + jr z, .skip + ; not found, try BAS_FINDHOOK + ld ix, (BAS_FINDHOOK) + call callIX +.skip: + ; regardless of the result, we need to balance the stack. ; Bring back rest of the command string from the stack pop hl ; <-- lvl 1 + ret nz + ; cmd found, skip whitespace and then jump! call rdSep jp (ix) diff --git a/tools/emul/.gitignore b/tools/emul/.gitignore index 63baf15..2b9ca3c 100644 --- a/tools/emul/.gitignore +++ b/tools/emul/.gitignore @@ -1,4 +1,5 @@ /shell/shell +/bshell/shell /zasm/zasm /runbin/runbin /*/*-bin.h diff --git a/tools/emul/Makefile b/tools/emul/Makefile index e5c3b33..01daf6f 100644 --- a/tools/emul/Makefile +++ b/tools/emul/Makefile @@ -1,10 +1,10 @@ CFSPACK = ../cfspack/cfspack -TARGETS = shell/shell zasm/zasm runbin/runbin +TARGETS = shell/shell bshell/shell zasm/zasm runbin/runbin KERNEL = ../../kernel APPS = ../../apps ZASMBIN = zasm/zasm ZASMSH = ../zasm.sh -SHELLAPPS = $(addprefix cfsin/, zasm ed basic) +SHELLAPPS = $(addprefix cfsin/, zasm ed) CFSIN_CONTENTS = $(SHELLAPPS) cfsin/user.h .PHONY: all @@ -17,6 +17,12 @@ shell/shell.bin: $(APPS)/shell/glue.asm $(ZASMBIN) shell/kernel-bin.h: shell/glue.asm shell/shell.bin $(ZASMBIN) $(ZASMSH) $(KERNEL) shell/shell.bin < $< | ./bin2c.sh KERNEL | tee $@ > /dev/null +bshell/shell.bin: bshell/glue.asm $(ZASMBIN) + $(ZASMSH) $(KERNEL) bshell/user.h $(APPS) < $< | tee $@ > /dev/null + +bshell/shell-bin.h: bshell/shell.bin + ./bin2c.sh KERNEL < $< | tee $@ > /dev/null + zasm/kernel-bin.h: zasm/kernel.bin ./bin2c.sh KERNEL < $< | tee $@ > /dev/null @@ -24,6 +30,7 @@ zasm/zasm-bin.h: zasm/zasm.bin ./bin2c.sh USERSPACE < $< | tee $@ > /dev/null shell/shell: shell/shell.c libz80/libz80.o shell/kernel-bin.h +bshell/shell: bshell/shell.c libz80/libz80.o bshell/shell-bin.h $(ZASMBIN): zasm/zasm.c libz80/libz80.o zasm/kernel-bin.h zasm/zasm-bin.h $(CFSPACK) runbin/runbin: runbin/runbin.c libz80/libz80.o $(TARGETS): @@ -50,4 +57,4 @@ updatebootstrap: $(ZASMBIN) $(INCCFS) .PHONY: clean clean: - rm -f $(TARGETS) $(SHELLAPPS) {zasm,shell}/*-bin.h + rm -f $(TARGETS) $(SHELLAPPS) zasm/*-bin.h shell/*-bin.h diff --git a/tools/emul/README.md b/tools/emul/README.md index 862da47..1860efd 100644 --- a/tools/emul/README.md +++ b/tools/emul/README.md @@ -25,6 +25,11 @@ We don't try to emulate real hardware to ease the development of device drivers because so far, I don't see the advantage of emulation versus running code on the real thing. +## bshell + +The `basic` app is on its way to replace the shell. It is wrapped in the z80 +emulator in the same way that the shell is and interacts with `cfsin` similarly. + ## zasm `zasm/zasm` is `apps/zasm` wrapped in an emulator. It is quite central to the diff --git a/tools/emul/bshell/glue.asm b/tools/emul/bshell/glue.asm new file mode 100644 index 0000000..939d4e3 --- /dev/null +++ b/tools/emul/bshell/glue.asm @@ -0,0 +1,172 @@ +.inc "blkdev.h" +.inc "fs.h" +.inc "err.h" +.inc "ascii.h" +.equ RAMSTART 0x4000 +.equ STDIO_PORT 0x00 +.equ FS_DATA_PORT 0x01 +.equ FS_ADDR_PORT 0x02 + + jp init + +; *** JUMP TABLE *** + jp strncmp + jp upcase + jp findchar + jp blkSelPtr + jp blkSel + jp blkSet + jp blkSeek + jp blkTell + jp blkGetB + jp blkPutB + jp fsFindFN + jp fsOpen + jp fsGetB + jp fsPutB + jp fsSetSize + jp fsOn + jp fsIter + jp fsAlloc + jp fsDel + jp fsHandle + jp printstr + jp printnstr + jp _blkGetB + jp _blkPutB + jp _blkSeek + jp _blkTell + jp printcrlf + jp stdioGetC + jp stdioPutC + jp stdioReadLine + +.inc "core.asm" +.inc "str.asm" + +.equ BLOCKDEV_RAMSTART RAMSTART +.equ BLOCKDEV_COUNT 4 +.inc "blockdev.asm" +; List of devices +.dw fsdevGetB, fsdevPutB +.dw stdoutGetB, stdoutPutB +.dw stdinGetB, stdinPutB +.dw mmapGetB, mmapPutB + + +.equ MMAP_START 0xe000 +.inc "mmap.asm" + +.equ STDIO_RAMSTART BLOCKDEV_RAMEND +.equ STDIO_GETC emulGetC +.equ STDIO_PUTC emulPutC +.inc "stdio.asm" + +.equ FS_RAMSTART STDIO_RAMEND +.equ FS_HANDLE_COUNT 2 +.inc "fs.asm" + +; *** BASIC *** + +; RAM space used in different routines for short term processing. +.equ SCRATCHPAD_SIZE 0x20 +.equ SCRATCHPAD FS_RAMEND +.inc "lib/util.asm" +.inc "lib/ari.asm" +.inc "lib/parse.asm" +.inc "lib/fmt.asm" +.equ EXPR_PARSE parseLiteralOrVar +.inc "lib/expr.asm" +.inc "basic/util.asm" +.inc "basic/parse.asm" +.inc "basic/tok.asm" +.equ VAR_RAMSTART SCRATCHPAD+SCRATCHPAD_SIZE +.inc "basic/var.asm" +.equ BUF_RAMSTART VAR_RAMEND +.inc "basic/buf.asm" +.equ BAS_RAMSTART BUF_RAMEND +.inc "basic/main.asm" + +; Extra cmds +.inc "basic/fs.asm" + +init: + di + ; setup stack + ld sp, 0xffff + call fsInit + ld a, 0 ; select fsdev + ld de, BLOCKDEV_SEL + call blkSel + call fsOn + call basInit + ld hl, basFindCmdExtra + ld (BAS_FINDHOOK), hl + jp basStart + +basFindCmdExtra: + ld hl, basFSCmds + jp basFindCmd + +emulGetC: + ; Blocks until a char is returned + in a, (STDIO_PORT) + cp a ; ensure Z + ret + +emulPutC: + out (STDIO_PORT), a + ret + +fsdevGetB: + ld a, e + out (FS_ADDR_PORT), a + ld a, h + out (FS_ADDR_PORT), a + ld a, l + out (FS_ADDR_PORT), a + in a, (FS_ADDR_PORT) + or a + ret nz + in a, (FS_DATA_PORT) + cp a ; ensure Z + ret + +fsdevPutB: + push af + ld a, e + out (FS_ADDR_PORT), a + ld a, h + out (FS_ADDR_PORT), a + ld a, l + out (FS_ADDR_PORT), a + in a, (FS_ADDR_PORT) + cp 2 ; only A > 1 means error + jr nc, .error ; A >= 2 + pop af + out (FS_DATA_PORT), a + cp a ; ensure Z + ret +.error: + pop af + jp unsetZ ; returns + +.equ STDOUT_HANDLE FS_HANDLES + +stdoutGetB: + ld ix, STDOUT_HANDLE + jp fsGetB + +stdoutPutB: + ld ix, STDOUT_HANDLE + jp fsPutB + +.equ STDIN_HANDLE FS_HANDLES+FS_HANDLE_SIZE + +stdinGetB: + ld ix, STDIN_HANDLE + jp fsGetB + +stdinPutB: + ld ix, STDIN_HANDLE + jp fsPutB diff --git a/tools/emul/bshell/shell.c b/tools/emul/bshell/shell.c new file mode 100644 index 0000000..ae7c038 --- /dev/null +++ b/tools/emul/bshell/shell.c @@ -0,0 +1,203 @@ +#include +#include +#include +#include "../libz80/z80.h" +#include "shell-bin.h" + +/* Collapse OS shell with filesystem + * + * On startup, if "cfsin" directory exists, it packs it as a afke block device + * and loads it in. Upon halting, unpcks the contents of that block device in + * "cfsout" directory. + * + * Memory layout: + * + * 0x0000 - 0x3fff: ROM code from shell.asm + * 0x4000 - 0x4fff: Kernel memory + * 0x5000 - 0xffff: Userspace + * + * I/O Ports: + * + * 0 - stdin / stdout + * 1 - Filesystem blockdev data read/write. Reads and write data to the address + * previously selected through port 2 + */ + +//#define DEBUG +#define MAX_FSDEV_SIZE 0x20000 + +// in sync with shell.asm +#define RAMSTART 0x4000 +#define STDIO_PORT 0x00 +#define FS_DATA_PORT 0x01 +// Controls what address (24bit) the data port returns. To select an address, +// this port has to be written to 3 times, starting with the MSB. +// Reading this port returns an out-of-bounds indicator. Meaning: +// 0 means addr is within bounds +// 1 means that we're equal to fsdev size (error for reading, ok for writing) +// 2 means more than fsdev size (always invalid) +// 3 means incomplete addr setting +#define FS_ADDR_PORT 0x02 + +static Z80Context cpu; +static uint8_t mem[0x10000] = {0}; +static uint8_t fsdev[MAX_FSDEV_SIZE] = {0}; +static uint32_t fsdev_size = 0; +static uint32_t fsdev_ptr = 0; +// 0 = idle, 1 = received MSB (of 24bit addr), 2 = received middle addr +static int fsdev_addr_lvl = 0; +static int running; + +static uint8_t io_read(int unused, uint16_t addr) +{ + addr &= 0xff; + if (addr == STDIO_PORT) { + int c = getchar(); + if (c == EOF) { + running = 0; + } + return (uint8_t)c; + } else if (addr == FS_DATA_PORT) { + if (fsdev_addr_lvl != 0) { + fprintf(stderr, "Reading FSDEV in the middle of an addr op (%d)\n", fsdev_ptr); + return 0; + } + if (fsdev_ptr < fsdev_size) { +#ifdef DEBUG + fprintf(stderr, "Reading FSDEV at offset %d\n", fsdev_ptr); +#endif + return fsdev[fsdev_ptr]; + } else { + // don't warn when ==, we're not out of bounds, just at the edge. + if (fsdev_ptr > fsdev_size) { + fprintf(stderr, "Out of bounds FSDEV read at %d\n", fsdev_ptr); + } + return 0; + } + } else if (addr == FS_ADDR_PORT) { + if (fsdev_addr_lvl != 0) { + return 3; + } else if (fsdev_ptr > fsdev_size) { + fprintf(stderr, "Out of bounds FSDEV addr request at %d / %d\n", fsdev_ptr, fsdev_size); + return 2; + } else if (fsdev_ptr == fsdev_size) { + return 1; + } else { + return 0; + } + } else { + fprintf(stderr, "Out of bounds I/O read: %d\n", addr); + return 0; + } +} + +static void io_write(int unused, uint16_t addr, uint8_t val) +{ + addr &= 0xff; + if (addr == STDIO_PORT) { + if (val == 0x04) { // CTRL+D + running = 0; + } else { + putchar(val); + } + } else if (addr == FS_DATA_PORT) { + if (fsdev_addr_lvl != 0) { + fprintf(stderr, "Writing to FSDEV in the middle of an addr op (%d)\n", fsdev_ptr); + return; + } + if (fsdev_ptr < fsdev_size) { +#ifdef DEBUG + fprintf(stderr, "Writing to FSDEV (%d)\n", fsdev_ptr); +#endif + fsdev[fsdev_ptr] = val; + } else if ((fsdev_ptr == fsdev_size) && (fsdev_ptr < MAX_FSDEV_SIZE)) { + // We're at the end of fsdev, grow it + fsdev[fsdev_ptr] = val; + fsdev_size++; +#ifdef DEBUG + fprintf(stderr, "Growing FSDEV (%d)\n", fsdev_ptr); +#endif + } else { + fprintf(stderr, "Out of bounds FSDEV write at %d\n", fsdev_ptr); + } + } else if (addr == FS_ADDR_PORT) { + if (fsdev_addr_lvl == 0) { + fsdev_ptr = val << 16; + fsdev_addr_lvl = 1; + } else if (fsdev_addr_lvl == 1) { + fsdev_ptr |= val << 8; + fsdev_addr_lvl = 2; + } else { + fsdev_ptr |= val; + fsdev_addr_lvl = 0; + } + } else { + fprintf(stderr, "Out of bounds I/O write: %d / %d (0x%x)\n", addr, val, val); + } +} + +static uint8_t mem_read(int unused, uint16_t addr) +{ + return mem[addr]; +} + +static void mem_write(int unused, uint16_t addr, uint8_t val) +{ + if (addr < RAMSTART) { + fprintf(stderr, "Writing to ROM (%d)!\n", addr); + } + mem[addr] = val; +} + +int main() +{ + // Setup fs blockdev + FILE *fp = popen("../cfspack/cfspack cfsin", "r"); + if (fp != NULL) { + printf("Initializing filesystem\n"); + int i = 0; + int c = fgetc(fp); + while (c != EOF) { + fsdev[i] = c & 0xff; + i++; + c = fgetc(fp); + } + fsdev_size = i; + pclose(fp); + } else { + printf("Can't initialize filesystem. Leaving blank.\n"); + } + + // Turn echo off: the shell takes care of its own echoing. + struct termios termInfo; + if (tcgetattr(0, &termInfo) == -1) { + printf("Can't setup terminal.\n"); + return 1; + } + termInfo.c_lflag &= ~ECHO; + termInfo.c_lflag &= ~ICANON; + tcsetattr(0, TCSAFLUSH, &termInfo); + + + // initialize memory + for (int i=0; i