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.
main.cpp
- Committer:
- gertk
- Date:
- 2011-06-29
- Revision:
- 0:806c2f2a7d47
File content as of revision 0:806c2f2a7d47:
/* 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 }