Tuesday, May 5, 2015

Arduino Retro Computer: BASIC Interpreter (Program Mode)

My first step for the BASIC interpreter was to get the "Program Mode" working. To do that I need to know when I'm in program mode, I need be able to switch how key presses are interpreted (OS vs program), I need to know which line number I'm processing, and I need to parse/process lines of code of running programs.

For initial code commands, I want to be able to print, goto, and end out of a running program.

Which will allow me to create the most important first program of all:

10 PRINT "Hello World!"
20 PRINT "..."
30 GOTO 10


Current Memory Address


I added a new variable to the ScreenMode to keep track of my current program memory address. I will execute code sequentially through the memory blocks (unless I reach a goto / similar loop command).

class ScreenModel
{
  .
  .
  int programMemoryAddressCurrent_bytes;

  bool init(byte newIndex)
  {
    .
    .
    programMemoryAddressCurrent_bytes = programMemoryAddressStart_bytes;
  }

Starting / Stopping Program Mode


Functions were created for starting and stopping the program mode. When called they change the operating mode of that particular ScreenModel, and in the case of execute, it resets the memory address to the start of the program.

Since the program mode and the current program memory address are stored in the ScreenModel, each screen can run (or not run) their own program. Multitasking!

  bool startProgram()
  {
    programMemoryAddressCurrent_bytes = programMemoryAddressStart_bytes;

    operatingMode = operatingModeProgram;

    return true;
  }

  bool stopProgram()
  {
    operatingMode = operatingModeOS;
   
    return true;
  }

To begin executing a program, the user will type "run" in OS mode. The submitCommand() of OS mode interprets the "run" and calls startProgram().

  bool submitCommand()
  {
    .
    .
    else if (strcmp(commandFormatted, "run") == 0)
    {
      startProgram();
    }
    .
    .
  }

When the program is running, the operating mode is operatingModeProgram and thus keyboard inputs get funneled to inputKeyboardProgram rather than inputKeyboardOS.

  // Called from the main loop() with the latest keyboard press.
  // Process the key in the OS or currently running program depending
  // on the current mode. 
  void inputKeyboard(char inputKey)
  {
    switch(operatingMode)
    {
      case operatingModeOS:
        inputKeyboardOS(inputKey);
        break;
      case operatingModeProgram:
        inputKeyboardProgram(inputKey);
        break;
    }
    return;
  }

For the user to halt execution of an active program, they can just push the ESC key.

  // Keyboard input intended to be processed by the currently running program.
  void inputKeyboardProgram(char inputKey)
  {
    if (inputKey == PS2_ESC)
    {
      stopProgram();
    }
    return;
  }

Executing Program Code


Within the main loop() function, I loop through all the screens and call their processEvents().

void loop()
{
  .
  .
  for (int screenIndex = 0; screenIndex < numberOfScreens; screenIndex++)
  {
    screen[screenIndex].processEvents();
  }
}

Within the ScreenModel:

  bool processEvents()
  {
    switch(operatingMode)
    {
      case operatingModeOS:
        // No events to do.
        break;
      case operatingModeProgram:
        programExecuteCurrentMemoryAddress();
        break;
    }
    return true;
  }

processEvents() of the ScreenModel calls programExecuteCurrentMemoryAddress() which gets the program memory string and calls parseProgramString to interpret it.

  bool programExecuteCurrentMemoryAddress()
  {
    char programString[sizeOfCommandArray];
    memset(programString, 0, sizeOfCommandArray);
    getProgramMemoryCommandString(programMemoryAddressCurrent_bytes, programString);

    if (programString[0] == 0)
    {
      addOutputLine("-- Program Ended --");
      stopProgram();
      return false;
    }
   
    return parseProgramString(programString);
  }

Interpreting the Code


parseProgramString increments past the line number and blank spaces to get to the actual command. It then goes through a series of string compares to determine what command the user is trying to do and processes it as appropriate.

  bool parseProgramString(char *programString)
  {
    if (strlen(programString)==0)
    {
      addOutputLine("-- Program Ended --");
      stopProgram();
      return false;
    }
   
    // Skip past the line number and blank spaces.
    char *digitIterator;
    for (digitIterator = programString; (isdigit(*digitIterator) || *digitIterator == ' '); ++digitIterator)
    {
      // Digit or space, we'll advance to the next.
    }
    // Reassign programString pointer to digitIterator (which has passed all digits and spaces).
    programString = digitIterator;
   
    // Convert the command of the program string to lowercase.
    for (char *charIterator = programString; *charIterator != '\0' && *charIterator != ' '; ++charIterator)
    {
      *charIterator = tolower(*charIterator);
    }
   
    if (!strncmp(programString, "print \"", 7))
    {
      parseProgramPrint(programString + 7);
    }
    else if (!strcmp(programString, "end"))
    {
      parseProgramEnd();
    }
    else if (!strncmp(programString, "goto ", 5))
    {
      parseProgramGoto(programString + 5);
    }
    else
    {
      char errorString[sizeOfOutputColumnArray];
      snprintf(errorString, sizeOfOutputColumnArray, "Bad statement, line: %i",
               getProgramMemoryLineNumber(programMemoryAddressCurrent_bytes));
      addOutputLine(errorString);
      stopProgram();
    }
   
    return true;
  }

  bool parseProgramPrint(char *printString)
  {
    char *printChar = printString;
    while (*printChar != 0)
    {
      if (*printChar == '"')
      {
        // Found closing quotes.
        // Clear the quote and break out of the while.
        *printChar = 0;
        break;
      }
      printChar++;
    }
    addOutputLine(printString);

    advanceNextProgramMemoryAddress();

    return true;
  }

  bool parseProgramEnd()
  {
    stopProgram();

    return true;
  }

  bool parseProgramGoto(char *gotoString)
  {
    int targetLineNumber = atoi(gotoString);
    if (targetLineNumber > 0)
    {
      int lastAddress_bytes = getLastProgramMemoryAddress(programMemoryAddressStart_bytes);

      int loopAddress_bytes = programMemoryAddressStart_bytes;
      while (loopAddress_bytes <= lastAddress_bytes)
      {
        byte loopCommandLength_bytes = fram.read8(loopAddress_bytes);

        // Check for end of program.    
        if (loopCommandLength_bytes == 0)
        {
          // End of program, failed goto.
          addOutputLine("Goto line does not exist.");
          stopProgram();
          break;
        }

        // Check if we're replacing or inserting at this line.
        int loopLineNumber = getProgramMemoryLineNumber(loopAddress_bytes);
     
        if (targetLineNumber == loopLineNumber)
        {
          // Goto to this memory location.
          programMemoryAddressCurrent_bytes = loopAddress_bytes;
          break;
        }
   
        // Increment to the next data line.
        loopAddress_bytes += 1 + 4 + loopCommandLength_bytes + 1;
      }
     
    }
    else
    {
      // Error parsing goto.
      addOutputLine("Goto line invalid.");
      stopProgram();
      //advanceNextProgramMemoryAddress();
    }
 
    return true;
  }

Most of the program lines will call advanceNextProgramMemoryAddress() so that the programMemoryAddressCurrent can be incremented to the next line of code. Exceptions would be goto, for, and while statements.

  bool advanceNextProgramMemoryAddress()
  {
    byte commandLength_bytes = fram.read8(programMemoryAddressCurrent_bytes);
    if (commandLength_bytes == 0)
    {
      return false;
    }
 
    // Advance to next program address.
    programMemoryAddressCurrent_bytes += 1 + 4 + commandLength_bytes + 1;

    return true;
  }

Seeing it in Action


Gotta start with Hello World!


Next is a simple demonstration of the goto command skipping a line of code.


And then using the goto command as a loop.


And finally using the end command to halt execution.


Additional Thoughts


I didn't demonstrate it here, but my Arduino computer is a functioning multitasking operating system. I can run my looping hello world program on one screen and then hit PgUp/PgDown to switch to the other screen to enter OS commands or run other programs.

My screen model has been expanding in functionality quite a bit. Ultimately I'll pull the BASIC interpreter code out of the screen model so it can act as a stand-alone useful class.

I've got a solid start on handling variables inside my BASIC programs. My blogging is lacking a bit behind my actual progress but hopefully an update will come soon.


Copyright (c) 2015 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

No comments:

Post a Comment