Also, add the new `bshell` emulated tool. BASIC is on its way to replace the shell.pull/85/head
@@ -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. |
@@ -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 |
@@ -7,6 +7,7 @@ | |||
.inc "user.h" | |||
.inc "err.h" | |||
call basInit | |||
jp basStart | |||
; RAM space used in different routines for short term processing. | |||
@@ -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,22 +56,14 @@ basLoop: | |||
.sPrompt: | |||
.db "> ", 0 | |||
; 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... | |||
basCallCmd: | |||
; let's see if it's a variable assignment. | |||
call varTryAssign | |||
ret z ; Done! | |||
push de ; --> lvl 1. | |||
ld de, SCRATCHPAD | |||
call rdWord | |||
; cmdname to find in (DE) | |||
; How lucky, we have a legitimate use of "ex (sp), hl"! We have the | |||
; 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 | |||
; 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 | |||
@@ -73,15 +73,41 @@ basCallCmd: | |||
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 | |||
ret | |||
; Call command in (HL) after having looked for it in cmd table in (DE). | |||
; 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 | |||
ret z ; Done! | |||
push de ; --> lvl 1. | |||
ld de, SCRATCHPAD | |||
call rdWord | |||
; cmdname to find in (DE) | |||
; How lucky, we have a legitimate use of "ex (sp), hl"! We have the | |||
; 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 | |||
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) | |||
@@ -1,4 +1,5 @@ | |||
/shell/shell | |||
/bshell/shell | |||
/zasm/zasm | |||
/runbin/runbin | |||
/*/*-bin.h | |||
@@ -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 |
@@ -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 | |||
@@ -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 |
@@ -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; | |||
} |
@@ -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 | |||