Showing posts with label retro computer. Show all posts
Showing posts with label retro computer. Show all posts

Saturday, July 11, 2015

Arduino Retro Computer: v1.0

I'm calling the current state of the Arduino Retro Computer version 1.0. There are still a ton of improvements on the to-do list, but the computer as it stands is in a usable state.


I've made 2 short programs to show it can do something useful...

Guess the Random Number


10 let guessNum% rnd 100
20 print "Guess the number between 0 and 100"
30 input guessVal%
35 print guessVal%
40 if guessVal% < guessNum%
50 print "Too low."
60 if guessVal% > guessNum%
70 print "Too high."
80 if guessVal% == guessNum%
90 goto 200
100 goto 20
200 print "You got it!"


Simple RPG Battle (Using Joystick)


1 let playerHP! = 40
2 let playerSP! = 5
3 let monsterHP! = 40
4 if playerHP! <= 0
5 goto 39
6 if monsterHP! <= 0
7 goto 41
8 print "You face a troll."
9 print "Your health:"
10 print playerHP!
11 print "Your spell points:"
12 print playerSP!
13 print "Command?"
14 read joystick 1
15 if j1left% = 1
16 goto 25
17 if j1right% = 1
18 goto 30
19 goto 14
20 let mDamage! rnd 20
21 let playerHP! -= mDamage!
22 print "Troll hits you for:"
23 print mDamage!
24 goto 4
25 let pDamage! rnd 20
26 let monsterHP! -= pDamage!
27 print "You hit troll for:"
28 print pDamage!
29 goto 20
30 if playerSP! <= 0
31 goto 20
32 let pSpell! rnd 15
33 let pSpell! += 10
34 let monsterHP! -= pSpell!
35 print "You cast fireball at troll for:"
36 print pSpell!
37 let playerSP! -= 1
38 goto 20
39 print "You have died."
40 end
41 print "You have defeated the troll."


Video of Computer


Here's a video of me making a quick program and demonstrating the multitasking. (Multitasking works great, but you can see it slows things down!)


What's Next?


Version 2.0 will probably be refining the BASIC interpreter along with some refactoring of the code.

Some ideas for it:
- Cleanup how I handle assigning variables (quotes and = signs).
- Be able to manage variables without the let statement.
- Make the if statement follow standard behavior. (Support "end if".)
- Support printing text and variables in the same print statement.
- Add a REM statement.
- Support single key input (similar to C's getchar() function).
- Fixing bugs. I currently have a mixture of long & int. Arduino Int's are only 2 bytes. I want to standardize on the 4-byte integer (Arduino Long).


Hopefully some of these posts could be a useful reference for your own project. I've got another project that I'm excited to start, so I figure I'm at a good "pause" point on the computer. Looking back at my intro post, it looks like I reached step #5. The BASIC interpreter could use many more features, but I'm way ahead of schedule to make it an educational first computer for my son (now 9 months old).


Wednesday, July 8, 2015

Arduino Retro Computer: BASIC Interpreter (Let and If)

The following two statements, Let and If, are going to wrap up the minimum I need in my BASIC Interpreter. I'm looking forward to making an Arduino Retro Computer v1.0 post!


Let Statement


The Let Statement is how all variables are assigned - both initial assignment and any changes. Some of the options in the Let statement for integers and floats are:

= <value>  assign the value
++  increment by 1
--  decrement by 1
+= <value>  increment by value
-= <value>  decrement by value
rnd <value>  pick a random value between 0 and value

Strings only support the = <value> option.

Ultimately I'll make it so that if the first word in the command string doesn't match any of the predefined statements, then the interpreter will just consider that unknown word as a variable name (like other languages).

  bool parseProgramString(char *programString)
  {
    .
    .
    else if (!strncmp(programString, "let ", 4))
    {
      parseProgramLet(programString + 4);
    }
    .
    .
  }

  bool parseProgramLet(char *commandString)
  {
    // Up to the first 10 characters (or the first space) is the variable name.
    char variableName[sizeOfVariableName+1];
    memset(variableName, 0, sizeOfVariableName+1);
    for (int variableNameLoop = 0; variableNameLoop < sizeOfVariableName; variableNameLoop++)
    {
      if (*commandString == 0)
      {
        // Should not have reached null terminate yet, fail out.
        advanceNextProgramMemoryAddress();
        return false;
      }
      else if (*commandString == ' ')
      {
        // Done with variable name.
        commandString++;
        break;      
      }
      else
      {
        variableName[variableNameLoop] = *commandString;
        commandString++;
      }
    }

    // Skip over empty spaces.
    while (*commandString == ' ')
    {
      commandString++;
    }
   
    // The remaining characters are the value.
    switch (getProgramVariableType(variableName))
    {
      case stringType:
        {
          char stringValue[sizeOfCommandArray];
          memset(stringValue, 0, sizeOfCommandArray);
          if (!strncmp(commandString, "=", 1))
          {
            commandString++;
            strcpy(stringValue, commandString);
          }
          programVariableWriteString(variableName, stringValue);
        }
        break;
      case integerType:
        {
          long integerValue;
          if (!strcmp(commandString, "++"))
          {
            // Increment the variable value by 1.
            if (programVariableReadInt32(variableName, &integerValue))
            {
              // Successfully found integer value, increment it.
              integerValue ++;
            }
            else
            {
              // Integer not found, just assign to 1.
              integerValue = 1;
            }
          }
          else if (!strncmp(commandString, "+=", 2))
          {
            // Addition operation.
            // Skip past "+=" string.
            commandString+=2;

            // Skip over empty spaces.
            while (*commandString == ' ')
            {
              commandString++;
            }

            // Read the existing value of the integer.
            if (!programVariableReadInt32(variableName, &integerValue))
            {
              // Integer not found, consider it 0.
              integerValue = 0;
            }
            // Then read the amount to add from the command and add it.
            long valueToAdd = 0;
            if (!programVariableReadInt32(commandString, &valueToAdd))
            {
              // Variable not found, interpret the string as a number.
              valueToAdd = strtol(commandString, &commandString, 10);
            }
            integerValue += valueToAdd;
          }
          else if (!strcmp(commandString, "--"))
          {
            // Decrement the variable value by 1.
            if (programVariableReadInt32(variableName, &integerValue))
            {
              // Successfully found integer value, decrement it.
              integerValue --;
            }
            else
            {
              // Integer not found, just assign to -1.
              integerValue = -1;
            }
          }
          else if (!strncmp(commandString, "-=", 2))
          {
            // Subtraction operation.
            // Skip past "-=" string.
            commandString+=2;

            // Skip over empty spaces.
            while (*commandString == ' ')
            {
              commandString++;
            }

            // Read the existing value of the integer.
            if (!programVariableReadInt32(variableName, &integerValue))
            {
              // Integer not found, consider it 0.
              integerValue = 0;
            }
            // Then read the amount to subtract from the command and subtract it.
            long valueToSubtract = 0;
            if (!programVariableReadInt32(commandString, &valueToSubtract))
            {
              // Variable not found, interpret the string as a number.
              valueToSubtract = strtol(commandString, &commandString, 10);
            }
            integerValue -= valueToSubtract;
          }
          else if (!strncmp(commandString, "rnd", 3))
          {
            // Get a random number.
            // Skip past the "rnd" string.
            commandString+=3;
            // Assign a random value between 0 and the given number.
            integerValue = random(strtol(commandString, &commandString, 10));
          }
          else if (!strncmp(commandString, "=", 1))
          {
            // Specific value given.
            commandString++;
            integerValue = strtol(commandString, &commandString, 10);
          }
          // Write value to the variable.
          programVariableWriteInt32(variableName, integerValue);
        }
        break;
      case floatType:
        {
          float floatValue;
          if (!strcmp(commandString, "++"))
          {
            // Increment the variable value by 1.
            if (programVariableReadFloat32(variableName, &floatValue))
            {
              // Successfully found float value, increment it.
              floatValue ++;
            }
            else
            {
              // Float not found, just assign to 1.0.
              floatValue = 1.0;
            }
          }
          else if (!strncmp(commandString, "+=", 2))
          {
            // Addition operation.
            // Skip past "+=" string.
            commandString+=2;

            // Skip over empty spaces.
            while (*commandString == ' ')
            {
              commandString++;
            }

            // Read the existing value of the float.
            if (!programVariableReadFloat32(variableName, &floatValue))
            {
              // Float not found, consider it 0.0.
              floatValue = 0.0;
            }
            // Then read the amount to add from the command and add it.
            float valueToAdd = 0.0;
            if (!programVariableReadFloat32(commandString, &valueToAdd))
            {
              // Variable not found, interpret the string as a number.
              valueToAdd = strtod(commandString, &commandString);
            }
            floatValue += valueToAdd;
          }
          else if (!strcmp(commandString, "--"))
          {
            // Decrement the variable value by 1.
            if (programVariableReadFloat32(variableName, &floatValue))
            {
              // Successfully found float value, decrement it.
              floatValue --;
            }
            else
            {
              // Float not found, just assign to -1.0.
              floatValue = -1.0;
            }
          }
          else if (!strncmp(commandString, "-=", 2))
          {
            // Subtraction operation.
            // Skip past "-=" string.
            commandString+=2;

            // Skip over empty spaces.
            while (*commandString == ' ')
            {
              commandString++;
            }

            // Read the existing value of the float.
            if (!programVariableReadFloat32(variableName, &floatValue))
            {
              // Float not found, consider it 0.
              floatValue = 0.0;
            }
            // Then read the amount to subtract from the command and subtract it.
            float valueToSubtract = 0.0;
            if (!programVariableReadFloat32(commandString, &valueToSubtract))
            {
              // Variable not found, interpret the string as a number.
              valueToSubtract = strtod(commandString, &commandString);
            }
            floatValue -= valueToSubtract;
          }
          else if (!strncmp(commandString, "rnd", 3))
          {
            // Get a random number.
            // Skip past the "rnd" string.
            commandString+=3;
            // Assign a random value between 0 and the given number.
            floatValue = (float)random(strtol(commandString, &commandString, 10));
          }
          else if (!strncmp(commandString, "=", 1))
          {
            // Specific value given.
            commandString++;
            floatValue = (float)strtod(commandString, &commandString);
          }
          // Write value to the variable.
          programVariableWriteFloat32(variableName, floatValue);
        }
        break;
    }

    advanceNextProgramMemoryAddress();
   
    return true;
  }



If Statement


My If statements are a little non-standard now, but they are functional. Rather than having a closing "End If", they will just execute the next line of code if the condition is true or skip it if the condition is false. To have an If condition that executes several lines of code would require the program to goto another section of code then goto back. Less than ideal, but it's something I can improve later.

The If statement does have the standard functionality of comparing with = (or ==), <=, <, >=, and != (or <>).

Also, I know my parseProgramIf function is a monster. It's on the to-do list to refactor the code. :-)

  enum comparisonType
  {
    comparisonEquals = 0,
    comparisonNotEquals = 1,
    comparisonLessThanEquals = 2,
    comparisonGreaterThanEquals = 3,
    comparisonLessThan = 4,
    comparisonGreaterThan = 5
  };

  bool parseProgramString(char *programString)
  {
    .
    .
    else if (!strncmp(programString, "if ", 3))
    {
      parseProgramIf(programString + 3);
    }
    .
    .
  }

  bool parseProgramIf(char *commandString)
  {
    // Up to the first 10 characters (or the first space) is the variable name.
    char variableName[sizeOfVariableName+1];
    memset(variableName, 0, sizeOfVariableName+1);
    for (int variableNameLoop = 0; variableNameLoop < sizeOfVariableName; variableNameLoop++)
    {
      if (*commandString == 0)
      {
        // Should not have reached null terminate yet, fail out.
        advanceNextProgramMemoryAddress();
        advanceNextProgramMemoryAddress();
        return false;
      }
      else if (*commandString == ' ')
      {
        // Done with variable name.
        commandString++;
        break;      
      }
      else
      {
        variableName[variableNameLoop] = *commandString;
        commandString++;
      }
    }

    // Skip over empty spaces.
    while (*commandString == ' ')
    {
      commandString++;
    }
   
    int comparisonType = comparisonEquals; // Default to equals.
    if (!strncmp(commandString, "==", 2))
    {
      comparisonType = comparisonEquals;
      commandString +=2;
    }
    else if (!strncmp(commandString, "=", 1))
    {
      comparisonType = comparisonEquals;
      commandString ++;
    }
    else if (!strncmp(commandString, "!=", 2))
    {
      comparisonType = comparisonNotEquals;
      commandString +=2;
    }
    else if (!strncmp(commandString, "<>", 2))
    {
      comparisonType = comparisonNotEquals;
      commandString +=2;
    }
    else if (!strncmp(commandString, "<=", 2))
    {
      comparisonType = comparisonLessThanEquals;
      commandString +=2;
    }
    else if (!strncmp(commandString, ">=", 2))
    {
      comparisonType = comparisonGreaterThanEquals;
      commandString +=2;
    }
    else if (!strncmp(commandString, "<", 1))
    {
      comparisonType = comparisonLessThan;
      commandString ++;
    }
    else if (!strncmp(commandString, ">", 1))
    {
      comparisonType = comparisonGreaterThan;
      commandString ++;
    }
   
    while (*commandString == ' ')
    {
      commandString++;
    }
   
    if (*commandString == 0)
    {
      // Should not have reached null terminate yet, fail out.
      advanceNextProgramMemoryAddress();
      advanceNextProgramMemoryAddress();
      return false;
    }
 
    // The remaining characters are the value.
    bool errorDetected = false;
    bool matchFound = false;
    switch (getProgramVariableType(variableName))
    {
      case stringType:
        char stringValue1[sizeOfCommandArray];
        if (programVariableReadString(variableName, stringValue1))
        {
          char stringValue2[sizeOfCommandArray];
          if (!programVariableReadString(commandString, stringValue2))
          {
            // Could not find a variable with the string given,
            // use the string itself.
            strcpy(stringValue2, commandString);
          }
          switch (comparisonType)
          {
            case comparisonEquals: // ==
              if (!strcmp(stringValue2, stringValue1))
              {
                matchFound = true;
              }
              break;
            case comparisonNotEquals: // !=
              if (strcmp(stringValue2, stringValue1))
              {
                matchFound = true;
              }
              break;
          }
        }
        else
        {
          errorDetected = true;
        }
        break;
      case integerType:
        long integerValue1;
        if (programVariableReadInt32(variableName, &integerValue1))
        {
          long integerValue2;
          if (!programVariableReadInt32(commandString, &integerValue2))
          {
            // Could not find a variable with the string given,
            // let's get the numeric value of it.
            integerValue2 = strtol(commandString, &commandString, 10);
          }
         
          switch (comparisonType)
          {
            case comparisonEquals: // ==
              if (integerValue1 == integerValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonNotEquals: // !=
              if (integerValue1 != integerValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonLessThanEquals: // <=
              if (integerValue1 <= integerValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonGreaterThanEquals: // >=
              if (integerValue1 >= integerValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonLessThan: // <
              if (integerValue1 < integerValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonGreaterThan: // >
              if (integerValue1 > integerValue2)
              {
                matchFound = true;
              }
              break;
          }
        }
        else
        {
          errorDetected = true;
        }
        break;
      case floatType:
        float floatValue1;
        if (programVariableReadFloat32(variableName, &floatValue1))
        {
          float floatValue2;
          if (!programVariableReadFloat32(commandString, &floatValue2))
          {
            // Could not find a variable with the string given,
            // let's get the numeric value of it.
            floatValue2 = (float)strtod(commandString, &commandString);
          }

          switch (comparisonType)
          {
            case comparisonEquals: // ==
              if (floatValue1 == floatValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonNotEquals: // !=
              if (floatValue1 != floatValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonLessThanEquals: // <=
              if (floatValue1 <= floatValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonGreaterThanEquals: // >=
              if (floatValue1 >= floatValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonLessThan: // <
              if (floatValue1 < floatValue2)
              {
                matchFound = true;
              }
              break;
            case comparisonGreaterThan: // >
              if (floatValue1 > floatValue2)
              {
                matchFound = true;
              }
              break;
          }
        }
        else
        {
          errorDetected = true;
        }
        break;
      default:
        errorDetected = true;
        break;
    }
   
    if (errorDetected)
    {
      addOutputLine("-- Error if condition. --");
      advanceNextProgramMemoryAddress();
    }
    else
    {
      if (matchFound)
      {
        // Match
        // Advance to the next memory address and execute it.
        advanceNextProgramMemoryAddress();
        programExecuteCurrentMemoryAddress();
      }
      else
      {
        // No Match
        // Advance to the next memory address then skip over it.
        advanceNextProgramMemoryAddress();
        advanceNextProgramMemoryAddress();
      }
    }

    return true;
  }

Let & If Statement Example


Simple loop...


10 let i% = 0
20 let i% ++
30 print i%
40 if i% < 10
50 goto 20


Pick a random phrase...


10 let phraseNum% rnd 5
20 if phraseNum% = 0
25 let phraseStr$ =Good. Bad. I'm the guy with the gun.
30 if phraseNum% = 1
35 let phraseStr$ =Klaatu Barada Nikto
40 if phraseNum% = 2
45 let phraseStr$ =Hail to the king, baby.
50 if phraseNum% = 3
55 let phraseStr$ =I'll swallow your soul!
60 if phraseNum% = 4
65 let phraseStr$ =Whoa right there spinach chin!
70 if phraseNum% = 5
75 let phraseStr$ =Gimme some sugar, baby.
80 print phraseStr$


If statement that executes several lines...


10 let distance! = 10000.0
20 if distance! > 1000.0
30 goto 100
40 print "Returned from if code!"
50 end
100 print "If condition is true."
110 print "Let's execute several lines of code."
120 goto 40



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.

Monday, July 6, 2015

Arduino Retro Computer: BASIC Interpreter (Variable Input)

Now that my BASIC interpreter has the ability to store variables, I need methods for the user to input data into a program.


I've implemented two ways for the user to interact with his program: Keyboard and Joystick. All of the code below goes inside the ScreenModel.

Input via Keyboard


Each ScreenModel in my operating system supports an OS mode and Program mode. If in OS mode, the computer just waits for a command from the user. If in Program mode, the computer is continuously executing the next line of BASIC code.

I already accepted keyboard commands in OS mode, but I didn't have any way for the user of an active program to input data. I've now implemented the Input statement for the keyboard.

I needed a flag to halt execution of the BASIC code when we're waiting for keyboard input from the user. In comes the boolean pendingProgramInput. If true, the computer will stop processing statements until the user submits his or her command (pushes enter). This will be covered more later, but I also needed to know the name of the variable that will store the keyboard input: pendingProgramInputVariableName

  bool pendingProgramInput;
  char pendingProgramInputVariableName[sizeOfVariableName+1];

  bool init(byte newIndex)
  {
    .
    .
    pendingProgramInput = false;
    memset(pendingProgramInputVariableName, 0, sizeOfVariableName+1);
    .
    .
  }


The processEvents function is called continuously to execute the user's lines of code. If we're pending program input then skip executing the code.


  bool processEvents()
  {
    switch(operatingMode)
    {
      case operatingModeOS:
        // No events to do.
        break;
      case operatingModeProgram:
        if (!pendingProgramInput)
        {
          // We're not waiting for user input, process the next command.
          programExecuteCurrentMemoryAddress();
        }
        break;
    }
    
    return true;
  }

All keyboard presses go to inputKeyboard...

  // 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;
  }

Those keyboard presses then get funneled to inputKeyboardProgram if there is an active program. The ESC key is always supported to immediately stop execution of the program.

If however we're pending program input, then store the keys into the command string (similar to OS mode).

  // Keyboard input intended to be processed by the currently running program.
  void inputKeyboardProgram(char inputKey)
  {
    if (inputKey == PS2_ESC)
    {
      stopProgram();
    }
    else if (pendingProgramInput)
    {
      if (inputKey == PS2_ENTER)
      {
        // Submit the value to the variable.
        submitProgramKeyboardInput();
      }
      else if (inputKey == PS2_ESC)
      {
        // Clear the current command.
        clearCommand();
      }
      else if (inputKey == PS2_LEFTARROW)
      {
        // Navigate left through the command array.
        // Not yet implemented.
      }
      else if (inputKey == PS2_RIGHTARROW)
      {
        // Navigate right through the command array.
        // Not yet implemented.
      }
      else if (inputKey == PS2_DELETE)
      {
        // Delete the command array character.
        removeCharacter();
      }
      else if (inputKey >= 32 && inputKey <= 126)
      {
        // Type commands into the command array.
        addCharacter(inputKey);
      }
    }

    return;
  }

When the program starts, be sure to clear the pendingProgramInput flag. (Just in case the user aborted the last program while in an input!)

  bool startProgram()
  {
    .
    .
    // Clear input flag.
    pendingProgramInput = false;

    operatingMode = operatingModeProgram;
    .
    .
  }

I created one new statement, Input, that gets read along with all the other statements inside parseProgramString:

  bool parseProgramString(char *programString)
  {
    .
    .
    else if (!strncmp(programString, "input ", 6))
    {
      parseProgramInput(programString + 6);
    }
    .
    .
  }

Parsing the input statement is actually very simple. All it does is store the name of the variable that we'll write to and sets the pendingProgramInput flag to true.

  bool parseProgramInput(char *commandString)
  {
    if (getProgramVariableType(commandString)!=undefinedType)
    {
      // Valid variable name given.
      // Store in pending input variable name and set the flag that we're accepting
      // input for the program.
      strncpy(pendingProgramInputVariableName, commandString, sizeOfVariableName+1);
      pendingProgramInput = true;
    }
    else
    {
      pendingProgramInput = false;
    }
    
    advanceNextProgramMemoryAddress();
    
    return true;
  }

The real work happens when the user pushes ENTER. submitProgramKeyboardInput gets called which writes whatever the user had entered in the command line to the variable denoted in the Input statement. The pendingProgramInput flag is also cleared so the program may resume executing.

  bool submitProgramKeyboardInput()
  {
    // User submitted a keyboard command to be saved into a variable of the program.

    // Parse Command
    // The command array is not null terminated, so we'll define a commandFormatted
    // array that we'll ensure is null terminated. commandFormatted is what the actual
    // command processing will be executed against.
    char commandFormatted[sizeOfCommandArray + 1];
    memcpy(commandFormatted, commandArray, sizeOfCommandArray);
    // Add null terminated.
    if (cursorPosition == (sizeOfCommandArray - 1) &&
        commandArray[cursorPosition] != ' ')
    {
      // Special case if we completely filled the command array.
      commandFormatted[sizeOfCommandArray] = 0;
    }
    else
    {
      // Standard case.
      commandFormatted[cursorPosition] = 0;
    }
    char *inputString = &commandFormatted[0];
    
    switch(getProgramVariableType(pendingProgramInputVariableName))
    {
      case stringType:
        {
          char stringValue[sizeOfCommandArray+1];
          memset(stringValue, 0, sizeOfCommandArray);
          strncpy(stringValue, inputString, sizeOfCommandArray + 1);
          programVariableWriteString(pendingProgramInputVariableName, stringValue);
        }
        break;
      case integerType:
        {
          long integerValue = strtol(inputString, &inputString, 10);
          programVariableWriteInt32(pendingProgramInputVariableName, integerValue);
        }
        break;
      case floatType:
        {
          float floatValue = (float)strtod(inputString, &inputString);
          programVariableWriteFloat32(pendingProgramInputVariableName, floatValue);
        }
        break; 
    }
    
    clearCommand();
    
    // Done with keyboard input on this variable.
    pendingProgramInput = false;
    
    return true;
  }

Input via Keyboard Example


10 print "Enter your name:"
20 input name$
30 print "How old are you?"
40 input age%
50 print "Your name is:"
60 print name$
70 print "Your age is:"
80 print age%


(Note: I'm not automatically echoing what the user types to the screen. I think I like it that way, but I may change my mind.)

Input via Joystick


Input via the joysticks is actually much easier as we can piggy back off a lot of existing logic. One new statement, read joystick #, was added. When called it creates/updates variables that store the various states of the joystick.

All the programmer must do is call read joystick then use if or print commands as desired on the joystick variables:
j1select% (1 = Joystick 1 select button pressed)
j1left% (1 = Joystick 1 left button pressed)
j1top% (1 = Joystick 1 top button pressed)
j1right% (1 = Joystick 1 right button pressed)
j1bottom% (1 = Joystick 1 bottom button pressed)
j1x! (Joystick 1 X axis from -1.0 to 1.0)
j1y! (Joystick 1 Y axis from -1.0 to 1.0)

  bool parseProgramString(char *programString)
  {
    .
    .
    else if (!strncmp(programString, "read joystick ", 14))
    {
      parseProgramReadJoystick(programString + 14);
    }
    .
    .
  }

  bool parseProgramReadJoystick(char *readString)
  {
    switch(*readString)
    {
      case '1':
        {
          programVariableWriteInt32("j1select%", (int)joysticks[0].selectPressed());
          programVariableWriteInt32("j1left%", (int)joysticks[0].leftPressed());
          programVariableWriteInt32("j1top%", (int)joysticks[0].topPressed());
          programVariableWriteInt32("j1right%", (int)joysticks[0].rightPressed());
          programVariableWriteInt32("j1bottom%", (int)joysticks[0].bottomPressed());
          programVariableWriteFloat32("j1x!", joysticks[0].analogX());
          programVariableWriteFloat32("j1y!", joysticks[0].analogY());
        }
        break;
      case '2':
        {
          programVariableWriteInt32("j2select%", (int)joysticks[1].selectPressed());
          programVariableWriteInt32("j2left%", (int)joysticks[1].leftPressed());
          programVariableWriteInt32("j2top%", (int)joysticks[1].topPressed());
          programVariableWriteInt32("j2right%", (int)joysticks[1].rightPressed());
          programVariableWriteInt32("j2bottom%", (int)joysticks[1].bottomPressed());
          programVariableWriteFloat32("j2x!", joysticks[1].analogX());
          programVariableWriteFloat32("j2y!", joysticks[1].analogY());
        }
        break;
    }

    advanceNextProgramMemoryAddress();

    return true;
  }

Input Via Joystick Example


10 read joystick 1
20 print j1x!
30 print j1y!
40 goto 10



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.

Thursday, June 25, 2015

Arduino Retro Computer: BASIC Interpreter (Variables)

Design


The Arduino works by having a single large block of memory that is manipulated from both the beginning and end: the HEAP starts at the beginning and works forward, the stack starts at the end and works back. As long as the two never intersect, you're good.

I designed my memory to be the same. I didn't want to use any more of the onboard memory of the Arduino for program variables because it's already very limited (and I'm running out!). Instead, I have the variables written to the FRAM memory block.

Each variable name has a maximum of 10 characters. The name must include a symbol to denote what kind of variable it is (matching QBasic's convention):
$ String (100 characters)
% Integer (4 bytes)
! Float (4 bytes)

Rather than mess with memory fragments and/or shifting large amounts of memory left and right every time a string variable gets resized, I decided to make each string variable take up a 100 character block of memory. A little wasteful, but that can be readdressed later if necessary (ha ha).

The memory structure (from the end back):
<Length of Data><Name of variable with symbol, 10 characters><Data>
.. repeat ..

A length of data of 0 denotes there are no more variables present.

(You'll see I'm using the long variable type for my integer. An int on the Arduino is only 16 bits, so you have to use a long for a 32 bit integer.)

Code


Before we dive into the new code, here's a quick recap of some existing code:

    // Programs are stored in a block of memory.
    // Code is stored from the beginning towards the end.
    // Variables are stored from the end towards the beginning.
    programMemoryAddressStart_bytes = screenIndex * programMemorySizePerScreen_bytes;
    programMemoryAddressEnd_bytes = programMemoryAddressStart_bytes + programMemorySizePerScreen_bytes - 1;

So inside the given ScreenModel, the variables will start at programMemoryAddressEnd_bytes, and work themselves back to programMemoryAddressStart_bytes.

New const at the top of the code:

const int sizeOfVariableName = 10;

The following code is all contained within my ever growing class ScreenModel. I'll eventually break these parts out into separate classes...

A simple enumeration to keep track of the variable type I'm working with:

  enum variableType
  {
    undefinedType = 0,
    stringType = 1,
    integerType = 2,
    floatType = 3
  };

I modified my startProgram() to automatically clear out all the program variables. I can see use cases for not doing this but that feature can be added later:

  bool startProgram()
  {
    programMemoryAddressCurrent_bytes = programMemoryAddressStart_bytes;

    // Clear Program Variables
    programVariablesClear();
    
    operatingMode = operatingModeProgram;

    return true;
  }

  bool programVariablesClear()
  {
    // Program memory starts at the end and works forward. Setting a name of
    // 0 length for the first program memory address effectively clears all memory.
    fram.write8(programMemoryAddressEnd_bytes, 0);
         
    return true;
  }

A function to find the next empty memory address spot:

  int getNextProgramVariableAddress_bytes()
  {
    int loopAddress_bytes = programMemoryAddressEnd_bytes;
    while (loopAddress_bytes > programMemoryAddressStart_bytes)
    {
      byte variableLength_bytes = fram.read8(loopAddress_bytes);
      if (variableLength_bytes == 0)
      {
        // Found next location for variable.
        return loopAddress_bytes;
      }
      else
      {
        // Decrement the loopAddress back (past the length).
        loopAddress_bytes -= 1;
        
        // Decrement the loopAddress back (past the variable name).
        loopAddress_bytes -= sizeOfVariableName;
        
        // Decrement the loopAddress back (past the variable contents).
        loopAddress_bytes -= variableLength_bytes;
      }
    }

    // No memory available.
    return -1;
  };

And a function to get the memory address of a variable:

  int getProgramVariableAddress_bytes(char *variableName)
  {
    int loopAddress_bytes = programMemoryAddressEnd_bytes;
    while (loopAddress_bytes > programMemoryAddressStart_bytes)
    {
      byte variableLength_bytes = fram.read8(loopAddress_bytes);
      if (variableLength_bytes == 0)
      {
        // Variable name length of 0, ran out of variables.
        return -1;
      }
      else
      {
        // Decrement the loopAddress back (past the length).
        loopAddress_bytes -= 1;

        // Found a variable in our loop, gets its name.
        char loopVariableName[11];
        for (int loopVariableNameIndex = 0; loopVariableNameIndex < sizeOfVariableName; loopVariableNameIndex++)
        {
          loopVariableName[loopVariableNameIndex] = fram.read8(loopAddress_bytes-loopVariableNameIndex);
        }
        // Null terminate.
        loopVariableName[sizeOfVariableName] = 0;

        // Decrement the loopAddress back (past the the variable name).
        loopAddress_bytes -= sizeOfVariableName;
        
        // Compare the variable name from the loop to what was passed in.        
        if (!strcmp(variableName, loopVariableName))
        {
          // A match!
          
          // Go back to the start of the variable block (the length field).
          loopAddress_bytes += 11;
          
          return loopAddress_bytes;
        }
        else
        {
          // Decrement the loopAddress back (past the variable contents).
          loopAddress_bytes -= variableLength_bytes;
        }
      }
    } // loopAddress_bytes
    
    // Never found a match.
    return -1;
  }

Each variable type is read slightly differently from memory:

  bool programVariableReadInt32(char *variableName, long *variableValue)
  {
    // Get the memory address of this variable.
    int memoryAddress_bytes = getProgramVariableAddress_bytes(variableName);
    
    if (memoryAddress_bytes < 0)
    {
      // Variable not found.
      return false;
    }
    
    // Skip over the variable length & name.
    memoryAddress_bytes -= 11;
    
    if ((memoryAddress_bytes - 3) < programMemoryAddressStart_bytes)
    {
      // Bad memory location.
      return false;
    }
    
    // Return the integer value!
    *variableValue = ((long)fram.read8(memoryAddress_bytes - 0) << 0) |
                     ((long)fram.read8(memoryAddress_bytes - 1) << 8) |
                     ((long)fram.read8(memoryAddress_bytes - 2) << 16) |
                     ((long)fram.read8(memoryAddress_bytes - 3) << 24);
       
    return true;
  }
  
  bool programVariableReadFloat32(char *variableName, float *variableValue)
  {
    // Get the memory address of this variable.
    int memoryAddress_bytes = getProgramVariableAddress_bytes(variableName);
    
    if (memoryAddress_bytes < 0)
    {
      // Variable not found.
      return false;
    }
    
    // Skip over the variable length & name.
    memoryAddress_bytes -= 11;
    
    if ((memoryAddress_bytes - 3) < programMemoryAddressStart_bytes)
    {
      // Bad memory location.
      return false;
    }
    
    // Return the float value!
    long integerValue = ((long)fram.read8(memoryAddress_bytes - 0) << 0) |
                        ((long)fram.read8(memoryAddress_bytes - 1) << 8) |
                        ((long)fram.read8(memoryAddress_bytes - 2) << 16) |
                        ((long)fram.read8(memoryAddress_bytes - 3) << 24);

    *variableValue = *(float*)&integerValue;
    
    return true;
  }
  
  bool programVariableReadString(char *variableName, char *variableValue)
  {
    // Get the memory address of this variable.
    int memoryAddress_bytes = getProgramVariableAddress_bytes(variableName);

    if (memoryAddress_bytes < 0)
    {
      // Variable not found.
      return false;
    }

    int variableLength_bytes = (int)fram.read8(memoryAddress_bytes); // sizeOfCommandArray;

    // Skip over the variable length & name.
    memoryAddress_bytes -= 11;
    
    if ((memoryAddress_bytes - variableLength_bytes) < programMemoryAddressStart_bytes)
    {
      // Bad memory location.
      return false;
    }
   
    // Read from the memory address back.
    for (int variableOffset_bytes = 0;
             variableOffset_bytes < variableLength_bytes;
             variableOffset_bytes ++)
    {
      // Store the character.
      *variableValue = (char)fram.read8(memoryAddress_bytes - variableOffset_bytes);
      if (*variableValue == 0)
      {
        // Null termination.
        break;
      }
      variableValue ++;
    }
    
    return true;
  }

Each variable type is also written slightly differently to memory:

  bool programVariableWriteInt32(char *variableName, long variableValue)
  {
    // Get the memory address of this variable.
    int memoryAddress_bytes = getProgramVariableAddress_bytes(variableName);
    
    if (memoryAddress_bytes < 0)
    {
      // Variable not found, create it.
      memoryAddress_bytes = getNextProgramVariableAddress_bytes();
    }
    
    if (memoryAddress_bytes < 0)
    {
      // No valid memory location found, give up.
      return false;
    }

    // Store the variable size. Integer is 4 bytes.
    byte variableSize_bytes = 4;
    fram.write8(memoryAddress_bytes, variableSize_bytes);

    // Store the variable name.
    for (int variableNameOffset_bytes = 0;
             variableNameOffset_bytes < sizeOfVariableName;
             variableNameOffset_bytes ++)
    {
      fram.write8((memoryAddress_bytes - variableNameOffset_bytes - 1), *variableName);
      if (*variableName!=0)
      {
        // Increment to the next variable name character (as long as we
        // haven't already reached the null terminator).
        variableName ++;
      }
    }

    // Store the variable value.
    // Take an 8 bit mask of the 32 bit variable, shift it to the right
    // to the position as necessary to write into FRAM.
    fram.write8((memoryAddress_bytes - 11), ((variableValue & 0xFF) >> 0));
    fram.write8((memoryAddress_bytes - 12), ((variableValue & 0xFF00) >> 8));
    fram.write8((memoryAddress_bytes - 13), ((variableValue & 0xFF0000) >> 16));
    fram.write8((memoryAddress_bytes - 14), ((variableValue & 0xFF000000) >> 24));
    
    return true;
  }
  
  bool programVariableWriteFloat32(char *variableName, float variableValue)
  {
    // Get the memory address of this variable.
    int memoryAddress_bytes = getProgramVariableAddress_bytes(variableName);
    
    if (memoryAddress_bytes < 0)
    {
      // Variable not found, create it.
      memoryAddress_bytes = getNextProgramVariableAddress_bytes();
    }
    
    if (memoryAddress_bytes < 0)
    {
      // No valid memory location found, give up.
      return false;
    }
    
    // Store the variable size. Float is 4 bytes.
    byte variableSize_bytes = 4;
    fram.write8(memoryAddress_bytes, variableSize_bytes);

    // Store the variable name.
    for (int variableNameOffset_bytes = 0;
             variableNameOffset_bytes < sizeOfVariableName;
             variableNameOffset_bytes ++)
    {
      fram.write8((memoryAddress_bytes - variableNameOffset_bytes - 1), *variableName);
      if (*variableName!=0)
      {
        // Increment to the next variable name character (as long as we
        // haven't already reached the null terminator).
        variableName ++;
      }
    }

    // Store the variable value.
    // Take an 8 bit mask of the 32 bit variable, shift it to the right
    // to the position as necessary to write into FRAM.
    long variableValueInt = *(long*)&variableValue;
    fram.write8((memoryAddress_bytes - 11), (variableValueInt & 0xFF) >> 0);
    fram.write8((memoryAddress_bytes - 12), (variableValueInt & 0xFF00) >> 8);
    fram.write8((memoryAddress_bytes - 13), (variableValueInt & 0xFF0000) >> 16);
    fram.write8((memoryAddress_bytes - 14), (variableValueInt & 0xFF000000) >> 24);

    return true;
  }
  
  bool programVariableWriteString(char *variableName, char *variableValue)
  {
    // Get the memory address of this variable.
    int memoryAddress_bytes = getProgramVariableAddress_bytes(variableName);
    
    if (memoryAddress_bytes < 0)
    {
      // Variable not found, create it.
      memoryAddress_bytes = getNextProgramVariableAddress_bytes();
    }
    
    if (memoryAddress_bytes < 0)
    {
      // No valid memory location found, give up.
      return false;
    }

    // Store the variable size.
    byte variableSize_bytes = sizeOfCommandArray;
    fram.write8(memoryAddress_bytes, variableSize_bytes);

    // Store the variable name.
    for (int variableNameOffset_bytes = 0;
             variableNameOffset_bytes < sizeOfVariableName;
             variableNameOffset_bytes ++)
    {
      fram.write8((memoryAddress_bytes - variableNameOffset_bytes - 1), *variableName);
      variableName ++;
    }

    // Store the variable value.
    for (int variableValueOffset_bytes = 0;
             variableValueOffset_bytes < sizeOfCommandArray;
             variableValueOffset_bytes ++)
    {
      fram.write8((memoryAddress_bytes - variableValueOffset_bytes - 11), *variableValue);
      if (*variableName!=0)
      {
        // Increment to the next variable name character (as long as we
        // haven't already reached the null terminator).
        variableName ++;
      }
    }

    return true;
  }


Within the existing parseProgramString function:

  bool parseProgramString(char *programString)
  {
    .
    .
    else if (!strncmp(programString, "print ", 6))
    {
      parseProgramPrintVariable(programString + 6);
    }
    else if (!strncmp(programString, "let ", 4))
    {
      parseProgramLet(programString + 4);
    }
    .
  }

A helper function so we know what type of variable we're dealing with based on its name:

  int getProgramVariableType(char *variableName)
  {
    while (*variableName != 0)
    {
      switch (*variableName)
      {
        case '$':
          return stringType;
        case '%':
          return integerType;
        case '!':
          return floatType;
      }
      variableName++;
    }
    return undefinedType;
  }

Using the let command to write a variable

  bool parseProgramLet(char *commandString)
  {
    // Up to the first 10 characters (or the first space) is the variable name.
    char variableName[sizeOfVariableName+1];
    memset(variableName, 0, sizeOfVariableName+1);
    for (int variableNameLoop = 0; variableNameLoop < sizeOfVariableName; variableNameLoop++)
    {
      if (*commandString == 0)
      {
        // Should not have reached null terminate yet, fail out.
        advanceNextProgramMemoryAddress();
        return false;
      }
      else if (*commandString == ' ')
      {
        // Done with variable name.
        commandString++;
        break;        
      }
      else
      {
        variableName[variableNameLoop] = *commandString;
        commandString++;
      }
    }

    // The remaining characters are the value.
    switch (getProgramVariableType(variableName))
    {
      case stringType:
        {
          char stringValue[sizeOfCommandArray];
          memset(stringValue, 0, sizeOfCommandArray);
          strcpy(stringValue, commandString);
          programVariableWriteString(variableName, stringValue);
        }
        break;
      case integerType:
        {
          long integerValue = (long)strtod(commandString, &commandString);
          programVariableWriteInt32(variableName, integerValue);
        }
        break;
      case floatType:
        {
          float floatValue = (float)strtod(commandString, &commandString);
          programVariableWriteFloat32(variableName, floatValue);
        }
        break;
    }  

    advanceNextProgramMemoryAddress();
    
    return true;
  }

Using the print command to display the variable's value:

  bool parseProgramPrintVariable(char *variableName)
  {
    bool errorDetected = false;
    switch (getProgramVariableType(variableName))
    {
      case stringType:
        char printString[sizeOfCommandArray];
        if (programVariableReadString(variableName, printString))
        {
          // Successfully found string value, print it.
          addOutputLine(printString);
        }
        else
        {
          errorDetected = true;
        }
        break;
      case integerType:
        long integerValue;
        if (programVariableReadInt32(variableName, &integerValue))
        {
          // Successfully found integer value, print it.
          char printString[sizeOfOutputColumnArray];
          snprintf(printString, sizeOfOutputColumnArray, "%ld", integerValue);
          addOutputLine(printString);
        }
        else
        {
          errorDetected = true;
        }
        break;
      case floatType:
        float floatValue;
        if (programVariableReadFloat32(variableName, &floatValue))
        {
          // Successfully found float value, print it.
          char printString[sizeOfOutputColumnArray];
          dtostrf(floatValue, sizeOfOutputColumnArray-1, 8, printString);
          addOutputLine(printString);
        }
        else
        {
          errorDetected = true;
        }
        break;
      default:
        errorDetected = true;
        break;
    }
    
    if (errorDetected)
    {
      addOutputLine("-- Error printing variable. --");
    }
    
    advanceNextProgramMemoryAddress();
    
    return true;
  }

Future Improvements 


When the program executes I could warn the user if the total number of "let <variable>" will eat into their program. And I can throw an out of memory error rather than overwrite code when that happens.

Store only the used length of a string. When that string variable is resized, shift the adjacent memory as necessary. Good for memory consumption, bad for performance.


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.