BBC Basic in Z80 emulation on the mbed, USB serial terminal output only. LOAD and SAVE work on the local file system but there is no error signalling.
Diff: main.cpp
- Revision:
- 0:806c2f2a7d47
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.cpp Wed Jun 29 14:25:56 2011 +0000 @@ -0,0 +1,741 @@ +/* BBC Basic emulator on NXP mbed by Gert van der Knokke + * Emulation of the Z80 CPU + * The z80 emulator is heavily modified by me (Gert) + * original Copyright (C) 1994 Ian Collier. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "mbed.h" +#include "module.h" +#include "z80.h" + +#define DEBUG false + +// define serial port for debug +Serial linktopc(USBTX,USBRX); + + +// VDU queue +unsigned char vdu_queue[256]; // a somewhat large queue +unsigned char queue_bytes=0; // vdu queue pointer +unsigned char oldchar; // which VDU function needs the queue + +volatile unsigned char flag,oldy=0; + + +int background_color; // for clear screen +int cursor_xpos; +int cursor_ypos; +int last_line_processed; +int previous_line_found; + +int current_background_color; +int current_text_color; + + +// prepare mbed storage for local use +LocalFileSystem local("local"); // Create the local filesystem under the name "local" + +// clear the screen +void cls() { + putchar(0x0c); +} + +// dummy function +void set_cursor_position() { +} + +// home cursor +void home_cursor() { + cursor_xpos=0x00; // ypos + cursor_ypos=0x00; // xpos + set_cursor_position(); +} + +// prepare the screen +void init_screen() { + current_background_color=0; // set initial background color + current_text_color=7; // initial text color attributes + cls(); + home_cursor(); +} + +// handle VDU calls after all bytes are in queue +// Note! queue is working front to back (255 is first, 0 is last) +void do_handle_queue(unsigned char code) { + switch (code) { + case 0x11: // select text color + current_text_color=vdu_queue[255]; + break; + case 0x16: // Mode + current_text_color=7; + current_background_color=0; + init_screen(); + home_cursor(); + break; + case 0x17: // define character n + break; + case 0x1f: // move cursor to x,y + cursor_xpos=vdu_queue[254]; + cursor_ypos=vdu_queue[255]; + set_cursor_position(); + break; + } +} + +// VDU OSWRCHR +// this routine evaluates the stream characters +// and prepares the queue if needed +void printchar(unsigned char ch) { + + // do we need more queue bytes ? + if (queue_bytes) { + vdu_queue[queue_bytes++]=ch; + // all bytes in queue, now handle the call + if (queue_bytes==0) + do_handle_queue(oldchar); + + } else { + + switch (ch) { + case 0x00: // NULL + break; + case 0x08: // CURSOR LEFT + putchar(0x08); + if (cursor_xpos>0) { + cursor_xpos--; + set_cursor_position(); + } + break; + case 0x09: // CURSOR RIGHT + if (cursor_xpos<39) { + cursor_xpos++; + set_cursor_position(); + } + break; + case 0x0a: // CURSOR DOWN + if (cursor_ypos<23) { + cursor_ypos++; + set_cursor_position(); + } else { + cursor_ypos=23; + } + break; + case 0x0b: // CURSOR UP + cursor_ypos++; + if (cursor_ypos>0) { + cursor_ypos--; + set_cursor_position(); + } else { + cursor_ypos=0; + } + break; + case 0x0d: // Carriage Return + cursor_xpos=0; + putchar(0x0d); + putchar(0x0a); + set_cursor_position(); + break; + case 0x0c: // CLEAR TEXT WINDOW + case 0x10: // CLEAR GRAPHIC WINDOW + cls(); + cursor_xpos=0; + cursor_ypos=0; + set_cursor_position(); + break; + case 0x11: // set TEXT color + oldchar=ch; // this was the code of the initial call + queue_bytes=256-1; // for this call we need one more byte + break; + case 0x14: // reset colors + current_text_color=7; + current_background_color=0; + break; + case 0x16: // MODE select + oldchar=ch; + queue_bytes=256-1; + break; + case 0x17: // define character n + break; + case 0x1e: + cursor_xpos=0; + cursor_ypos=0; + set_cursor_position(); + break; + case 0x1f: // set cursor to position X,Y + oldchar=ch; // this was the code of the initial call + queue_bytes=256-2; // for this call we need two more bytes + break; + // all else is put on screen literally + default: + putchar(ch); + break; + } + } +} + + +void init_ramtop() { + + + // set all MOS calls to illegal opcode 0xed55 + // that code is trapped by a mbed routine + wrmem(OSWRCH,0xed); + wrmem(OSWRCH+1,0x55); + + wrmem(OSWORD,0xed); + wrmem(OSWORD+1,0x55); + + wrmem(OSBYTE,0xed); + wrmem(OSBYTE+1,0x55); + + wrmem(OSRDCH,0xed); + wrmem(OSRDCH+1,0x55); + + wrmem(OSNEWL,0xed); + wrmem(OSNEWL+1,0x55); + + wrmem(OSASCI,0xed); + wrmem(OSASCI+1,0x55); + + wrmem(OSRDCH,0xed); + wrmem(OSRDCH+1,0x55); + + wrmem(MAINVDU,0xed); + wrmem(MAINVDU+1,0x55); + + wrmem(GSINIT,0xed); + wrmem(GSINIT+1,0x55); + + wrmem(GSREAD,0xed); + wrmem(GSREAD+1,0x55); + + wrmem(OSRDRM,0xed); + wrmem(OSRDRM+1,0x55); + + wrmem(OSEVEN,0xed); + wrmem(OSEVEN+1,0x55); + + wrmem(OSCLI,0xed); + wrmem(OSCLI+1,0x55); + + wrmem(OSFILE,0xed); + wrmem(OSFILE+1,0x55); + + wrmem(CMDPTR,0xf0); // pointer to CR terminated string (auto start filename) + wrmem(CMDPTR+1,0x00); // it is set here to 0x00f0 + + wrmem(BRKV,0x00); // break vector + wrmem(BRKV+1,0x00); + + wrmem(FAULT,0x38); // fault vector + wrmem(FAULT+1,0x00); +} + +unsigned char getkey() { + unsigned char key=0x00; + if (linktopc.readable()) // get USB status + key=linktopc.getc(); // get ASCII key + return key; +} + +// crude getline routine +void mygets(int ad,int max,unsigned char minasc,unsigned char maxasc) { + int n=0; + int key=0; + + bool eflag=1; + + // show cursor + set_cursor_position(); + + while (eflag) { +// key=getkey(); +#if !DEBUG + // USB serial data ? then override key (only when NOT in debug mode) + if (linktopc.readable()) // get USB status + key=linktopc.getc(); // get ASCII key code + else key=0; + // key=getchar(); // BLOCKING MODE +#endif + + if (key) // printf("got key code: %02x\n\r",key); + + // only ASCII should arrive here + switch (key) { + case 0x00: // no key + break; + case 0x0d: // Enter or Return + wrmem(ad+n,key); + h=n; + // clear carry flag + anda(a); // z80 way of clearing carry... + eflag=0; // terminate the loop + printchar(key); + printchar(0x0a); + break; + case 0x1b: // Escape + wrmem(ad+n,key); + // set carry flag + f=(f&0xc4)|1|(a&0x28); + eflag=0; + h=n; + break; + case 0x15: // CTRL-U (clear input) + while (n) { + + printchar(0x08); + printchar(0x20); + printchar(0x08); + n--; + } + wrmem(ad,0x0d); + break; + case 0x7f: // DELETE + if (n) { + n--; + printchar(0x08); // cursor left + printchar(0x20); // delete character by printing a space + printchar(0x08); // cursor left + wrmem(ad+n,0x20); // overwrite with space + } + break; + default: // all else + // keep within maximum lenght and given ascii limits + if ((key>=minasc) && (key<=maxasc) && n<max) { + wrmem(ad+n,key); + n++; + printchar(key); + } + break; + } + } +} + + +// OSWORD +void do_osword() { + int ad,n; + char buf[40]; + time_t seconds; + + // printf("OSWORD called with PC=%04x A=%02x HL=%02x%02x\n\r",pc,a,h,l); + switch (a) { + + case 0x00: // get a line from the keyboard/input channel + ad=(h<<8)+l; +// printf("Control block:\n\r"); +// printf("buffer address %02x%02x\n\r",rdmem(ad+1),rdmem(ad)); +// printf("maximum length: %d\n\r",rdmem(ad+2)); +// printf("minimum ASCII value %d\n\r",rdmem(ad+3)); +// printf("maximum ASCII value %d\n\r",rdmem(ad+4)); + + mygets((rdmem(ad+1)<<8)+rdmem(ad),rdmem(ad+2),rdmem(ad+3),rdmem(ad+4)); + + break; + case 0x01: // read systemclock (long integer) + break; + case 0x02: // write systemclock (long integer) + break; + case 0x07: // emit a sound from the sound library (0-7) + break; + case 0x0e: // read TIME$ (not present in original BBC Basic) + ad=hl; // get pointer to function number + switch (rdmem(hl)) { // select function number + case 0: // return time string + seconds = time(NULL); // get current time from mbed RTC + strftime(buf,40, "%a,%d %m %Y.%H:%M:%S\r", localtime(&seconds)); // write to temporary buffer + n=0; + while (buf[n]) wrmem(ad++,buf[n++]); // copy to pointer from HL + break; + case 1: // return BCD values of clock + // On exit: + // XY?0=year (&00-&99) + // XY?1=month (&01-&12) + // XY?2=date (&01-&31) + // XY?3=day of week (&01-&07, Sun-Sat) + // XY?4=hours (&00-&23) + // XY?5=minutes (&00-&59) + // XY?6=seconds (&00-&59). + // + // A year value of &80-&99 represents 1980-1999, a value of + // &00-&79 represents 2000-2079. + // + break; + case 2: // Convert BCD to string. + // On entry: + // XY+1..7=BCD value + // On exit: + // XY+1..25=CR-terminated string + break; + } + break; + case 0x0f: // write TIME$ (not present in original BBC Basic) + // On entry: + // XY?0=function code + // XY+1.. value to use. + // Functions are: + // 8 - Set time to value in format "HH:MM:SS" + // 16 - Set date to value in format "Day,DD Mon Year" + // 24 - Set time and date to value in format + // "Day,DD Mon Year.HH:MM:SS" + n=0; + ad=hl; + // printf(" write time$ function %d\n\r",rdmem(ad)); + ad++; + while (rdmem(ad)!=0x0d) buf[n++]=rdmem(ad++); // copy timestring to buffer + buf[n]=0; + // printf("trying to set time from %s\n\r",buf); + break; + } +} + +// OSBYTE +// on 6502 A register and X,Y are used for Low,High parameters +// Z80 uses L and H (so X=L and H=Y <-> L=X and Y=H) +void do_osbyte() { + unsigned char temp; + + // printf("OSBYTE called with PC=%04x A=%02x H=%02x L=%02x\n\r",pc,a,h,l); + switch (a) { + case 0x00: // NULL + l=0xff; + break; + case 0x13: // ASCII 19 + break; + case 0x7c: // clear ESCAPE state + wrmem(0xff80,rdmem(0xff80)&0x7f); + break; + case 0x7d: // set ESCAPE state + wrmem(0xff80,rdmem(0xff80)|0x80); + break; + case 0x7e: // acknowledge ESCAPE state ? (CLOSE ALL!) + wrmem(0xff80,rdmem(0xff80)&0x7f); + break; + case 0x7f: // read EOF on channel + l=0x01; + break; + case 0x80: // read ADVAL(hl) + h=0; + l=0; + break; + case 0x81: // read key within time limit 'hl' centiseconds + temp=hl; // get timeout value (100th seconds) + do { + l=getkey(); + wait(0.01); + } while ((temp--) && (!l)); + + if (l) { // we got a key + if (l==0x1b) { // Escape ? + f=(f&0xc4)|1|(a&0x28); // set carry + h=0x1b; + } else { + anda(a); // clear carry + h=0x00; // clear h + } + } else { // timeout + f=(f&0xc4)|1|(a&0x28); // set carry + h=0xff; // signal no key + } + break; // scan keyboard etc + case 0x82: + h=l=0; + break; // Machine high order address + case 0x83: + h=PAGE>>8; + l=(PAGE & 0xff); + break; // lowest available memory address (PAGE) + case 0x84: // highest available memory address (HIMEM) + h=RAMEND>>8; + l=RAMEND & 0xff; + break; + case 0x85: // read bottom of display RAM for a specific mode + h=0x80; + l=0x00; + break; + case 0x86: // return H=VPOS, L=POS + h=cursor_ypos; + l=cursor_xpos; + break; + case 0x87: // L=character on screen at cursor position + h=7; + l=0; + break; + case 0xda: // read/write the number of items in the VDU queue + // on return H contains the new value, L the old + temp=queue_bytes; + queue_bytes=(queue_bytes & h)^l; + h=queue_bytes; + l=temp; + break; // read/write the number of items in the VDU queue + } +// show_registers(); +// wait(0.1); +} + +void do_osreadchar() { + a=getchar(); + // test for ESCAPE + if (a==27) { + f=(f&0xc4)|1|(a&0x28); + wrmem(0xff80,rdmem(0xff80)|0x80); + } +} + +void do_osnewline() { + printchar(0x0a); + printchar(0x0d); + a=0x0d; +} + +void do_osasci() { + if (a==0x0d) do_osnewline(); + else printchar(a); +} + +void do_mainvdu() { + printchar(a); +} + +void do_gsinit() { + printf("GSINIT called with PC=%04x A=%02x\n\r",pc,a); +} + +void do_gsread() { +} + +void do_osrdrm() { + printf("OSRDRM called with PC=%04x A=%02x H=%02x L=%02x\n\r",pc,a,h,l); +} + +void do_oseven() { + printf("OSEVEN called with PC=%04x A=%02x H=%02x L=%02x\n\r",pc,a,h,l); +} + +void do_oscli() { + printf("OSCLI called with PC=%04x A=%02x H=%02x L=%02x\n\r",pc,a,h,l); + do { + a=rdmem((h<<8)+l); + if (!++l)h++; + putchar(a); + } while (a!=0x0d); +} + +void do_osfile() { + char buffer[200]; + int n=0; + FILE *fp; + // get address of control block + int controlblock=hl; + int offset=rdmem(controlblock)+(rdmem(controlblock+1)<<8); + // create a kludgy pointer from this offset to the filename + char *filename=(char *)(ram+offset-RAMSTART); + long load_address=rdmem(controlblock+2)+(rdmem(controlblock+3)<<8)+(rdmem(controlblock+4)<<16)+(rdmem(controlblock+5)<<24); + long execution_address=rdmem(controlblock+6)+(rdmem(controlblock+7)<<8)+(rdmem(controlblock+8)<<16)+(rdmem(controlblock+9)<<24); + long start_or_length=rdmem(controlblock+10)+(rdmem(controlblock+11)<<8)+(rdmem(controlblock+12)<<16)+(rdmem(controlblock+13)<<24); + long end_address_or_attributes=rdmem(controlblock+14)+(rdmem(controlblock+15)<<8)+(rdmem(controlblock+16)<<16)+(rdmem(controlblock+17)<<24); + + + // printf("OSFILE called with PC=%04x A=%02x H=%02x L=%02x\n\r",pc,a,h,l); + + // for (n=0; n<=0x11; n++) printf("offset %02x value %02x\n\r",n,rdmem(controlblock+n)); + + n=0; + while (filename[n]!=0x0d) n++; + // overwrite 0x0d with end of string + filename[n]=0x00; + + // now determine what to do + switch (a) { + case 0x00: // save a section of memory as a named file + sprintf(buffer,"/local/%s",filename); + // printf("Saving file %s load adr:%08lx exec adr:%08lx start/length%08lx end/attr:%08lx\n\r",filename,load_address,execution_address,start_or_length,end_address_or_attributes); + fp=fopen(buffer, "w"); + if (fp) { + n=start_or_length; + while (n!=end_address_or_attributes) fputc(rdmem(n++),fp); + fclose(fp); + a=1; + } else { + // printf("could not write %s\n\r",buffer); + a=0; + pc=rdmem(0xfffa)+(rdmem(0xfffb)<<8); // do BRK + } + break; + case 0x01: // write the catalogue information (only) for the named file (disc only) + break; + case 0x02: // write the load address (only) for the named file (disc only) + break; + case 0x03: // write the execution address (only) for the named file (disc only) + break; + case 0x04: // write the attributes (only) for the named file (disc only) + break; + case 0x05: // read the named file's catalogue information, place file type in A (disc only) + break; + case 0x06: // delete the named file (disc only) + break; + case 0xff: // load the named file and read the named file's catalogue information + sprintf(buffer,"/local/%s",filename); + // printf("Loading file %s load adr:%08lx exec adr:%08lx start/length%08lx end/attr:%08lx\n\r",buffer,load_address,execution_address,start_or_length,end_address_or_attributes); + fp = fopen(buffer, "r"); + if (fp) { + n=load_address; + while (!feof(fp)) wrmem(n++,fgetc(fp)); + fclose(fp); + a=1; // file found + } else { + // printf("Could not open %s\n",buffer); + a=0; + pc=rdmem(0xfffa)+(rdmem(0xfffb)<<8); // do BRK + } + break; + } +} + +void terminalgets(char *buffer) { + int n=0; + int key=0; + do { + key=getchar(); + buffer[n]=key; + putchar(key); + if (n<16) n++; + } while (key != 0x0d); + buffer[n]=0; +} + + +// here the OS calls are recognized and +// diverted to the mbed routines. +// this needs to be replaced by native Z80 code +void do_mos() { + // printf("Entering MOS emulation with PC %04x\n\r",pc); + + // compensate pc for length of ED55 opcode + switch (pc-2) { + case OSBYTE: + do_osbyte(); + break; + case OSWORD: + do_osword(); + break; + case OSWRCH: + printchar(a); + break; + case OSRDCH: + do_osreadchar(); + break; + case OSNEWL: + do_osnewline(); + break; + case OSASCI: + do_osasci(); + break; + case MAINVDU: + do_mainvdu(); + break; + case GSINIT: + do_gsinit(); + break; + case GSREAD: + do_gsread(); + break; + case OSRDRM: + do_osrdrm(); + break; + case OSEVEN: + do_oseven(); + break; + case OSCLI: + do_oscli(); + break; + case OSFILE: + do_osfile(); + break; + } +} + +void listdir(void) { + DIR *d; + struct dirent *p; + + d = opendir("/sd"); + if (d != NULL) { + while ((p = readdir(d)) != NULL) { + printf(" - %s\r\n", p->d_name); + } + } else { + printf("Could not open directory!\n"); + } + closedir(d); +} + + + + + +// ================================================================ +// main loop +// ================================================================ +int main() { + + // serial port on at 115200 baud + linktopc.baud(115200); + setbuf(stdout, NULL); // no buffering for this filehandle + + // reset all Z80 registers to some initial values + a=f=b=c=d=e=h=l=a1=f1=b1=c1=d1=e1=h1=l1=i=iff1=iff2=im=r=0; + ixoriy=new_ixoriy=0; + ix=iy=sp=pc=0; + + queue_bytes=0; + + // init MOS vectors + init_ramtop(); + + init_screen(); + home_cursor(); + + // endless loop + while (1) { + // wait(0.01); + r++; + // this is some optimization for the IX and IY opcodes (DD/FD) + ixoriy=new_ixoriy; + new_ixoriy=0; + + // fetch opcode and execute, the include does all the heavy decoding + switch (fetch(pc++)) { +#include "z80ops.h" + } + + // next is the interrupt emulator (only IM1 mode) + // interrupt pending? if new_ixoriy is set or iff1==0 don't do interrupt yet (continue DD/FD opcode) + // if (intpend && !new_ixoriy && iff1) { + // are we HALT-ed ? then resume + // if (fetch(pc)==0x76) pc++; + + // block further interrupts for now + // iff1=0; + + // do the call to 00x38 + // push2(pc); // save old pc + // pc=0x0038; // setup new pc + // intpend=0; // release interrupt pending flag; + // } // if intpend + } // while +} \ No newline at end of file