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.

Dependencies:   mbed

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
}