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.

Tuesday, June 9, 2015

Arduino Retro Computer: Case

The computer was long overdue for protection from... the cat.


I used Google's SketchUp to design a simple case.



And then I used Repetier with my Printrbot to print it.




The printed case (with holes drilled for mounting the components)...



With the parts mounted...


I soldered everything that was on the breadboard onto a more permanent protoboard. I know the wires are long; I'm not too motivated to trim them just in case things get rearranged or replaced.

The joystick and keyboard ports are in the front, and the power, sound, and VGA connections are in the back.



And everything plugged in!


I'll call version 1.0 of the hardware done! Now I just need to finish the software.

Sunday, May 10, 2015

Electric Car 240V Charger

What happens when you buy an electric car?

You fill up the tank in your gas car so rarely wasps build a nest under the gas cap!


The Ford Focus electric came with a 120V charger that can plug into a standard U.S. outlet, but it takes 20 hours to charge the car from empty. We had a few times in which we couldn't take a second trip in the car because it was still charging; it was time to get a 240V charger.

I decided on the Juicebox Classic 30 Amp:
 - Rather than being hard wired into the house, it plugs into a standard NEMA 14-50 outlet (common among RV parks). You can also use an adapter to plug into typical dryer outlets. That gives me more options on where to charge.
 - The 30 Amp model meets/exceeds the ability of the car's onboard charger (without going overboard).
 - Long charging cable.
 - It's simple!
 - It's cheap!

Some more advanced chargers allow you to set a delay before charging or report to a smart phone app the charging status, but I didn't need any of that. The car itself allows me to set charging times, and I can use the Ford mobile app to check the charging status. There isn't even a power switch on the Juicebox Classic. When you plug the J1772 into your car, charging starts. Unplug, charging stops.

Please note: I am not an electrician. I just did lots of research on the Internet, spoke with a couple electricians, and managed to put something together that passed the city inspection. (And most importantly not burn down my house.) Don't trust anything I say or did!

Thankfully the distribution panel in my garage has plenty of room.


There are 2 hot leads coming into the panel: the thick red/black wire connected to the top left of the panel and the thick black wire connected to the top right of the panel. Each lead is 120V AC. The 2 leads combined supply 240V.

The white wires along the right side of my panel are the neutral. A 120V outlet will use one hot (either one) and the neutral. The Juicebox doesn't need a neutral wire as it doesn't require a 120V source. However I'll be including the neutral in my 14-50R outlet as who knows what it will be used for in the future.

The bare copper wires along the left side of my panel are the ground.

You can see each one of the hot leads is connected to a bus bar with blades that criss-cross down the panel for the circuit breakers to snap into. A 120V circuit breaker will only snap into one of those blades. A 240V circuit breaker will snap into both blades.

Very educational site / video on this: http://www.askmediy.com/install-220-volt-outlet-4-wire-dryer-outlet/

Shopping list:
 - NEMA 14-50 Outlet
 - Outlet Enclosure
 - 2x Romex Connectors
 - 6/3 gauge Romex cable (6 gauge for the 2 hots and neutral; ground slightly thinner)
 - Cover plate
 - 40 Amp 240V Breaker (Although my plug and wire are rated up to 50 Amp, my charger should only be pulling 30 amps.)
Total about $40

I cut away a section of drywall below my panel to install the new outlet.


AFTER SHUTTING OFF POWER TO THE DISTRIBUTION PANEL....
 - Mounted the outlet enclosure into the side of the stud below the panel.
 - Punched out a hole in the bottom of the panel.
 - Fastened the two connectors (one at the new hole in the bottom of the panel and one at the top of the outlet enclosure).
 - Pulled the Romex cable through the enclosure and into the electrical panel.
 - Tightened the two connectors onto the cable.
 - Connected the wires to the 14-50R outlet. It was very easy - there were labels for the two hots, the neutral, and the ground. Just strip back a little bit of insulation and tighten down on the terminal with a screwdriver. Honestly the hardest part was jamming the outlet with those fat 6 gauge wires into the enclosure.
 - Triple checked everything.
 - Connected the wires to the distribution panel. I stripped back the insulation from the wires as necessary to connect the 2 hot wires directly to the circuit breaker and the last 6 gauge wire to the white neutral bar. The bare copper wire connected to the ground bar. (You'll notice I tried to keep things tidy with the wires going along the outside of the panel.)
 - Triple checked everything again.
 - Reattached the drywall.
 - Turned on power to the panel.
 - Triple checked everything yet again. (You've got a multimeter, right?)
 - I was done! (Well I had to wait for the inspector the next day, but I was basically done.)


JuiceBox installation was stupid easy. You attach the mount to your wall (on studs of course) then the box just slides in from above. Plug in the 14-50 cable into your new outlet and it's powered.


Charging from empty is now only 3.5 hours! Multiple long trips in the car in one day are no longer a problem.

Again, I'm not an electrician so don't trust a word I'm saying. Consider me nothing more than another data point on your quest to get a 240V charger installed. :-)

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.

Saturday, April 11, 2015

Arduino Retro Computer: Managing Program Memory (FRAM)

Arduinos have very limited onboard RAM, so for my retro computer I'll be using the FRAM I added in the last post to store the BASIC programs. Managing FRAM isn't difficult, but it's a bit more complex than just defining a big array to manipulate. Below I create a handful of functions to manage the quirks of reading from and writing to FRAM.

The format and functions below are designed for a line number based program memory that you need to traverse forward and back through. However, it could easily be adapted for other types of storage. Please see the Alternate Data Formats section for ideas on how to make a pseudo file system.


Data Format


Programs can have any number of lines and each line can have anywhere from 1 to 100 characters. I don't want to block off the memory in 100 character chunks, because then my maximum program size would only be 320 lines (32,000 / 100). To maximize program size, I include the length of the command alongside the command so I can traverse forward and back with no wasted space.

I formatted the data within the chip as follows:
[Length of Command, 1 Byte, UInt8][Line Number, 4 Bytes, Int32][Command, Up to 100 Bytes, Char Array][Length of Command, 1 Byte, UInt8] .. repeat until length of command = 0..

The [Length of Command] field is so I know how long the current command line is. If the length is 0, then I know this is the end of the program. It's necessary to have the length before and after the data so I can traverse forward and backwards through the memory. An example reason to navigate backwards is to jump to a previous line for a "Goto" command without having to read from the beginning.

The [Line Number] field is an integer of the program's line number. This is purely for performance so that I can quickly parse what the line number is and jump forward or back as necessary. Converting text to an integer then comparing it to another integer is much slower than just comparing two integers.

The [Command] field is the complete command line the user had entered.

An example set of data:
10 PRINT "HELLO WORLD"
20 PRINT "HOW ARE YOU?"

Would be stored in memory as:
[22][10][10 PRINT "HELLO WORLD"][22][23][20][20 PRINT "HOW ARE YOU?"][23][0]


Alternate Data Formats


If you don't need to traverse back through the program memory, then you can leave out the 2nd length byte. That would save one byte per data entry and simplify things a bit.

Even more interesting, if you wanted to use this as a file system you could replace the 4-byte integer with a short string of characters (how about an 8 byte filename + 3 byte extension!).

[Length of File, 2 Bytes, UInt16][File Name + Extension, 11 Bytes, Char Array][File Data, Up to 65535 Bytes (or size of memory)] .. repeat until length of file = 0..

On the plus side, the file system would never be fragmented (the code below doesn't allow wasted space between data, it always condenses it upon each delete or insert). On the downside, it does a lot of extra read/writes to keep the data compressed / defragged. Which is ok for FRAM (fast, lots of writes are ok), but that could be bad for other types of memory.

Setup / Constants


I use the same FRAM chip for all the screens of the Arduino. So at each screen's instantiation, I assign a block of bytes of the FRAM chip for that screen's program memory.

Screen 0 will have memory address 0 through 15999.
Screen 1 will have memory address 16000 through 31999.

const int programMemorySizePerScreen_bytes = 16000;

class ScreenModel
{
  ...
  int programMemoryAddressStart_bytes;
  int programMemoryAddressEnd_bytes;

  bool init(byte newIndex)
  {
    screenIndex = newIndex;

    programMemoryAddressStart_bytes = screenIndex * programMemorySizePerScreen_bytes;
    programMemoryAddressEnd_bytes = programMemoryAddressStart_bytes + programMemorySizePerScreen_bytes - 1;

    ...
  }
  ...
}

Clearing the Program


The Program Memory is stored from programMemoryAddressStart_bytes to programMemoryAddressEnd_bytes. As soon as a command length returns a 0, I consider that the end of the program. If I want to clear a program, it's very easy - just set the first memory address to 0 length.

  bool commandNew()
  {
    programMemoryNew();
    addOutputLine("New program created.");

    return true;
  }

  bool programMemoryNew()
  {
    // Set first memory address to null. It's not necessary to individually
    // clear every line - we won't traverse past a null line.
    fram.write(programMemoryAddressStart_bytes, 0);
    
    return true;
  }


Reading / Writing Line Numbers


I store the line number as a 4-byte integer in the FRAM. Since the FRAM library only writes and reads individual bytes, I have to use bitwise operations to handle the number.

  long getProgramMemoryLineNumber(int memoryAddress_bytes)
  {
    // Get the 4 byte line number from 4 single bytes of the FRAM memory.
    // Take each single byte and offset its position into the corresponding
    // location of the 32 bit integer, then do an OR bitwise operation to
    // combine them into a single number.
    // Please note, Arduino int is only 2 bytes, using long for a 4 byte integer.
    return ((long)fram.read8(memoryAddress_bytes + 4) << 0) |
           ((long)fram.read8(memoryAddress_bytes + 3) << 8) |
           ((long)fram.read8(memoryAddress_bytes + 2) << 16) |
           ((long)fram.read8(memoryAddress_bytes + 1) << 24);
  }
  
  bool setLineNumberToMemory(char *memoryAddress_bytes, long writeLineNumber)
  {
    // Take an 8 bit mask of the 32 bit line number, shift it to the right
    // most 8 bits, then store it in an 8 bit char. Increment the pointer
    // by one byte and repeat until all 4 bytes of the line number have
    // been stored.
    // Please note, Arduino int is only 2 bytes, using long for a 4 byte integer.
    *memoryAddress_bytes = (char)((writeLineNumber & 0xFF000000) >> 24);
    memoryAddress_bytes ++;
    *memoryAddress_bytes = (char)((writeLineNumber & 0xFF0000) >> 16);
    memoryAddress_bytes ++;
    *memoryAddress_bytes = (char)((writeLineNumber & 0xFF00) >> 8);
    memoryAddress_bytes ++;
    *memoryAddress_bytes = (char)((writeLineNumber & 0xFF) >> 0);
    return true;
  }


Memory Manipulation Functions


The following are a variety of supporting function for reading and writing to a memory address.

  int getLastProgramMemoryAddress(int startingAddress_bytes)
  {
    // Find the last program memory address.
    int loopAddress_bytes = startingAddress_bytes;
    while (loopAddress_bytes <= programMemoryAddressEnd_bytes)
    {
      byte loopCommandLength_bytes = fram.read8(loopAddress_bytes);
      
      if (loopCommandLength_bytes == 0)
      {
        // Found the last program memory address.
        break;
      }
      else
      {
        // Increment forward past the command length and supporting variables.
        // 1 Byte, UInt8, Length of Command
        // 4 Bytes, Int32, Line Number
        // # Bytes, Char[], Data
        // 1 Byte, UInt8, Length of Command
        loopAddress_bytes += 1 + 4 + loopCommandLength_bytes + 1;
      }
    }
    return loopAddress_bytes;
  }

  bool programMemoryDeleteData(int deleteAddress_bytes)
  {
    // Get the length of the command.
    int lengthToDelete_bytes = 1 + 4 + fram.read8(deleteAddress_bytes) + 1;

    // Get the last address so we know how much data to shift.
    int lastAddress_bytes = getLastProgramMemoryAddress(deleteAddress_bytes);
    int newLastAddress_bytes = lastAddress_bytes - lengthToDelete_bytes;
    
    // This function shifts memory left (to delete data).
    // Start at the memory address to delete.
    int loopAddress_bytes = deleteAddress_bytes;

    // Loop to the last memory address.
    while (loopAddress_bytes <= newLastAddress_bytes)
    {
      // Copy the data byte then increment to the next address.
      byte dataToCopy = fram.read8(loopAddress_bytes + lengthToDelete_bytes);
      fram.write8(loopAddress_bytes, dataToCopy);
      loopAddress_bytes++;
    }
   
    return true;
  }

  bool programMemoryInsertData(int insertAddress_bytes, char writeData[sizeOfCommandArray+6], int writeDataLength_bytes)
  {
    // This function shifts memory right (for inserting data).
    
    // Start at the last program memory address.
    int lastAddress_bytes = getLastProgramMemoryAddress(insertAddress_bytes);

    for (int loopAddress_bytes = lastAddress_bytes;
             loopAddress_bytes >= insertAddress_bytes;
             loopAddress_bytes --)
    {
      fram.write8((loopAddress_bytes + writeDataLength_bytes), fram.read8(loopAddress_bytes));
    }
    
    // Insert the new data.
    for (int copyOffset_bytes = 0;
             copyOffset_bytes < writeDataLength_bytes;
             copyOffset_bytes ++)
    {
      fram.write8((insertAddress_bytes + copyOffset_bytes), writeData[copyOffset_bytes]);
    }
    
    // Terminate the program.
    fram.write8(lastAddress_bytes + writeDataLength_bytes, 0);

    return true;
  }

  int getProgramMemoryCommandString(int memoryAddress_bytes, char *memoryString)
  {
      byte commandLength_bytes = fram.read8(memoryAddress_bytes);
      memoryAddress_bytes += 1 + 4; // Skip over command length and line number field.
      for (int loopAddress_bytes = memoryAddress_bytes; loopAddress_bytes < memoryAddress_bytes + commandLength_bytes; loopAddress_bytes++)
      {
        // Copy the memory at this address into the memoryString.
        *memoryString = (char)fram.read8(loopAddress_bytes);
        // Then increment our memoryString pointer to the next memory block.
        memoryString++;
      }
      return commandLength_bytes;
  }

Editing a Program Line


If the user enters only a line number, then I interpret that as deleting the line. If the user enters any data after the line number, then I'll create a new line with that data. If the line already exists, I'll replace the existing line with the new one.

  bool commandEditProgramLine(char *commandString)
  {
    int lineNumber = atoi(commandString);
    if (lineNumber > 0)
    {
      // Get the string representation of that number.
      char lineNumberString[11];
      itoa(lineNumber, lineNumberString, 10);
      
      // If the commandString only contains the line number, then let's delete that line.
      if (strcmp(lineNumberString, commandString) == 0)
      {
        // Deleting a program line.
        if (programMemoryDeleteLine(lineNumber))
        {
          addOutputLine("Successfully deleted program line.");
        }
        else
        {
          addOutputLine("Error deleting program line.");
        }
      }
      else 
      {
        // Writing / editing a program line.
        if (programMemoryWriteLine(lineNumber, commandString, strlen(commandString)))
        {
          addOutputLine("Successfully wrote program line.");
        }
        else
        {
          addOutputLine("Error writing program line.");
        }
      }
    }
    else
    {
      addOutputLine("Invalid line number.");
    }
    
    return true;
  }

  bool programMemoryWriteLine(int writeLineNumber, char commandData[sizeOfCommandArray], int commandDataLength_bytes)
  {
    // 1 Byte, UInt8, Length of Command
    // 4 Bytes, Int32, Line Number
    // # Bytes, Char[], Data
    // 1 Byte, UInt8, Length of Command

    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, write data here.
        break;
      }

      // Check if we're replacing or inserting at this line.
      int loopLineNumber = getProgramMemoryLineNumber(loopAddress_bytes);
      
      if (writeLineNumber == loopLineNumber)
      {
        // Overwrite the current line.
        programMemoryDeleteData(loopAddress_bytes);
        break;
      }
      else if (writeLineNumber < loopLineNumber)
      {
        // Insert write line here.
        break;
      }
      
      // Increment to the next data line.
      loopAddress_bytes += 1 + 4 + loopCommandLength_bytes + 1;
    }
    
    // Valid address to write to, insert now.
    char writeData[sizeOfCommandArray+6];
    writeData[0] = commandDataLength_bytes;
    // writeData[1] through writeData[4] will have the line number.
    setLineNumberToMemory(&writeData[1], writeLineNumber);
    memcpy(&writeData[5], commandData, commandDataLength_bytes);
    writeData[5 + commandDataLength_bytes] = commandDataLength_bytes;
    int writeDataLength_bytes = commandDataLength_bytes + 6;
    programMemoryInsertData(loopAddress_bytes, writeData, writeDataLength_bytes);
    
    return true;
  }

  bool programMemoryDeleteLine(int deleteLineNumber)
  {
    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.
        break;
      }

      // Check if we're deleting this line.
      int lineNumber = getProgramMemoryLineNumber(loopAddress_bytes);
      if (deleteLineNumber == lineNumber)
      {
        // Delete this line.
        programMemoryDeleteData(loopAddress_bytes);
        break;
      }

      // Increment to the next data line.
      loopAddress_bytes += 1 + 4 + loopCommandLength_bytes + 1;
    } // loopAddress_bytes <= lastAddress_bytes
    return true;
  }


Listing the Contents of a Program


The commandList() function traverses the entire program to print each line of code to the screen. Ultimately I'll also have it so users can give a range of line numbers to display.

  bool commandList()
  {
    addOutputLine("== Start of Program ==");
    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.
        break;
      }

      char loopCommandString[sizeOfCommandArray];
      memset(loopCommandString, 0, sizeOfCommandArray);
      getProgramMemoryCommandString(loopAddress_bytes, loopCommandString);
      
      // The command array is up to 100 characters (2 lines x 50 characters per line).
      // We have to split up the output into two separate output lines if the user exceeds
      // the size of one line (50 characters). 
      for (int cursorIndex = 0; cursorIndex < loopCommandLength_bytes; cursorIndex += numberOfScreenColumns)
      {
        char commandLineOutput[sizeOfOutputColumnArray];
        memset(commandLineOutput, 0, sizeOfOutputColumnArray); // Ensure null terminated.
        // On the first loop through, copy the first 50 characters (numberOfScreenColumns).
        // If our cursor position exceeds 50 characters (numberOfScreenColumns), then
        // we'll loop through a second time to copy the remaining 50 characeters.
        memcpy(commandLineOutput, &loopCommandString[cursorIndex], numberOfScreenColumns);
        addOutputLine(commandLineOutput);
      } // cursorIndex
      
      // Increment to the next data line.
      loopAddress_bytes += 1 + 4 + loopCommandLength_bytes + 1;
    } // loopAddress_bytes <= lastAddress_bytes

    addOutputLine("==  End of Program  ==");
    return true;
  }

Commands Added to OS


The following if conditions were added to the submitCommand() function to parse the commands:

    else if (strcmp(commandFormatted, "new") == 0)
    {
      commandNew();
    }
    else if (strcmp(commandFormatted, "list") == 0)
    {
      commandList();
    }
    else if (isdigit(commandFormatted[0]))
    {
      commandEditProgramLine(commandFormatted);
    }

If the first character of the command entered begins with a number, then I interpret that as a user editing that program line.

We're approaching a fairly useful computer. Two must-have items remaining: a BASIC interpreter and case to protect everything.


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.