basic: add fls command

Also, add the new `bshell` emulated tool. BASIC is on its way to replace the
shell.
This commit is contained in:
Virgil Dupras 2019-11-24 10:24:15 -05:00
parent aad8efeff7
commit 13f935aa88
10 changed files with 509 additions and 23 deletions

View File

@ -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.

15
apps/basic/fs.asm Normal file
View File

@ -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

View File

@ -7,6 +7,7 @@
.inc "user.h"
.inc "err.h"
call basInit
jp basStart
; RAM space used in different routines for short term processing.

View File

@ -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)

View File

@ -1,4 +1,5 @@
/shell/shell
/bshell/shell
/zasm/zasm
/runbin/runbin
/*/*-bin.h

View File

@ -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

View File

@ -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

172
tools/emul/bshell/glue.asm Normal file
View File

@ -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

203
tools/emul/bshell/shell.c Normal file
View File

@ -0,0 +1,203 @@
#include <stdint.h>
#include <stdio.h>
#include <termios.h>
#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<sizeof(KERNEL); i++) {
mem[i] = KERNEL[i];
}
// Run!
running = 1;
Z80RESET(&cpu);
cpu.ioRead = io_read;
cpu.ioWrite = io_write;
cpu.memRead = mem_read;
cpu.memWrite = mem_write;
while (running && !cpu.halted) {
Z80Execute(&cpu);
}
printf("Done!\n");
termInfo.c_lflag |= ECHO;
termInfo.c_lflag |= ICANON;
tcsetattr(0, TCSAFLUSH, &termInfo);
return 0;
}

35
tools/emul/bshell/user.h Normal file
View File

@ -0,0 +1,35 @@
.equ SHELL_RAMSTART 0x4100
.equ USER_CODE 0x4200
; *** JUMP TABLE ***
.equ strncmp 0x03
.equ upcase @+3
.equ findchar @+3
.equ blkSelPtr @+3
.equ blkSel @+3
.equ blkSet @+3
.equ blkSeek @+3
.equ blkTell @+3
.equ blkGetB @+3
.equ blkPutB @+3
.equ fsFindFN @+3
.equ fsOpen @+3
.equ fsGetB @+3
.equ fsPutB @+3
.equ fsSetSize @+3
.equ fsOn @+3
.equ fsIter @+3
.equ fsAlloc @+3
.equ fsDel @+3
.equ fsHandle @+3
.equ printstr @+3
.equ printnstr @+3
.equ _blkGetB @+3
.equ _blkPutB @+3
.equ _blkSeek @+3
.equ _blkTell @+3
.equ printcrlf @+3
.equ stdioGetC @+3
.equ stdioPutC @+3
.equ stdioReadLine @+3