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.

Friday, March 13, 2015

2013 Ford Focus Electric

As you could tell from my very first post, I'm really into electric cars. I enjoy doing basic mechanical work on my own cars (oil changes, spark plugs, etc), but it's even better if the car doesn't ever need it!

The Spitfire is a fun good-weather car to cruise with the top down to the local store. It's however not practical for a commute. I've been looking at production electric vehicles for years, but they're either too costly for me or too difficult to find. The Nissan Leaf is probably the best notable exception, and my wife and I did take one for a test drive. I don't want to convince anyone to not buy that car, but for us it lacked a little too much in space, power, and looks appeal. In terms of electric cars it is a solid option if you can't afford a Model S.

I searched for the Ford Focus Electric on and off for years but could never find one for sale anywhere in the state of Texas. That is, until this past week. A Ford dealership in Austin had a 2013 Focus Electric for sale, so the wife and I tried it out. It looks good, has just enough room for our monster stroller in the back, and feels good on power. We bought it.


I tried to find detailed numbers on range (and how it varies based on A/C, driving conditions, etc), but I didn't have any luck. So I'll do it here! I've only had the car for a week, but I'll update the numbers here as I get more experience with the car.

From what I can tell, the car's reported maximum range takes into account driving history (aggressiveness) and climate control. Ford advertises a range of 76 miles. When I picked the car up from the dealership the driving history showed quite a few lead foots had been test driving it. That's understandable as test drivers want to see what the car can do (including myself). At the dealership, even fully charged, the car only reported 63 miles. It wasn't until I drove the car for an hour to clear off that aggressive driving history that the range finally reported >70.

Range Statistics


All of the following data points are from my very initial experience with the car. I'll refine / expand them more as I get more time with it.

The car reports the driving history on a scale of 0 to 6 (Wh/mi x 100):
Coasting - 0
Level 65 MPH - 3
Heavy acceleration - 6

Rules of thumb from full charge:
- Climate control reduces range 12 miles
- Fairly aggressive driving reduces range 12 miles

Since Ford says 76 miles, that's about a 15% reduction on range for either condition. Those are additive - turn on your A/C and drive aggressively and there goes more than a quarter of your range. I haven't figured out the climate control yet. Having the A/C on doesn't automatically cut the range; it's heavily dependent upon the temperature you set inside the car. (Also I've had the A/C blowing cold air without any drop in range, but other times I've seen significant drops.) More experimentation will have to be done.

So some data points to help others interested in the car... (I bet these numbers would apply well to the Leaf / similar electric vehicles.)

Outside temperature cool (50 F to 60 F).
Interstate Highway Driving.
Light on the pedals.
Power consumption history averaging about 3.
No climate control.
Range: 63 to 74 miles

Outside temperature cool (60 F).
Country Highways.
Fairly aggressive driving.
Power consumption history averaging between 3 and 6.
With climate control set to 67 F.
Range: 57 to 59 miles

Outside temperature cool (60 F).
Mostly Interstate Highway, some lower speeds.
Light on the pedals.
Power consumption history averaging just under 3.
No climate control.
Range: 72 to 81 miles

Outside temperature warm (75 F).
Mostly Interstate Highway.
Light on the pedals.
Climate control on (A/C blowing cold air).
Range: 80 miles
(While driving, the range did make a sudden drop of several miles. I assume that was caused by the climate control.)

Outside temperature cold (32 F).
Country Highways / Interstate.
Climate control off.
Range: 60 to 65 miles

Outside temperature cold (32 F).
Country Highways / Interstate.
Heater on.
Range: 42 to 46 miles

Highest reported range: 83 miles
!! UPDATE in August 2016 !!
Highest reported range: 95 miles (Prior trip involved a lot of stop and go driving in heavy traffic.)
!! UPDATE in January 2017 !!
Lowest reported range: 42 miles (Freezing temperatures and heater on.)

I feel the car has a reliable 60 miles range - that's the maximum distance I would plan a trip for that didn't have charging along the way. That 60 miles includes highway driving with gentle hills, easy on the accelerator, and limited climate control.

How accurate is the range?


I've seen the car's estimated range from a full charge to be anywhere from 57 to 81 miles. So how accurate was it?

I've noticed the initial / full-charge range it gives is heavily dependent upon the driving history (maybe the last hour of history).

I've had a full charge report 64 miles, made a long trip, recharged, then had it report 74 miles.

Similarly, I've had a full charge report 74 miles, made a long trip, recharged, then had it report 64 miles.

So is it accurate? It seems to be if you're driving similar conditions to your previous trip.

What about when you're almost out of power? Does it suddenly drop from 5 miles range to 0? When we test drove the car, it only had 6 miles range on it. We made a 5-6 mile test drive and pulled back into the dealership with either 0 or 1 mile range remaining. So I'd say the range is accurate even at very lower values.

When you're driving, the car reports 3 distances: Range, Budget, and Status

At the start of the trip, both range and budget are identical; status is 0. The budget miles decrement based on actual distance traveled. The range miles vary based on your battery charge remaining, climate control settings, and driving history (Wh/mi). The status is just the difference between the budget and range.

Positive status mean you're doing better than your budget (good job!). Negative status mean you're doing worse than your budget (uh oh!). If you had planned a trip that really pushed to the limit of your range and you start seeing negative status, then you know you need to make adjustments (slower driving, turn off climate control, etc).

If your current drive is exactly like your previous drive (accelerations/braking, hills, cruising speeds, etc all the same), then budget generally equals range (status stays near 0). Taking climate control out of this, I've seen the status on drives easily range from +8 to -8!

Cost of Electricity


My electric costs $0.0962/kWh. The car's battery holds 23 kWh of charge. Rough calculation of $0.0962 x 23 = $2.21 to "fill up".

Two big assumptions with that cost:
- As least for LiFePO4 batteries, you don't want to discharge them beyond 80%. I wouldn't be surprised if Lithium-Ion can do better, but I still doubt Ford allows the batteries to discharge to 0%. (Meaning even if the car reports 0% battery, I really only need to charge 18.4 kWh, not 23 kWh, to reach 100%.)
- There are going to be losses with the charger; I just don't know how bad they are.

So if we assume a typical daily drive of 40 miles; that is a rough cost of $2. It will be interesting to see the next electric bill - will it only be up $60?

!!! UPDATE !!!
I got my electric bill! I purchased the car within a day of my billing cycle so the difference between the bills is a good indication of the effect of the car.
March bill: $163.83
April bill: $129.08
So even with doing the majority of driving with the electric car, the bill went down! Apparently electricity for the car is minor compared to the air conditioner / heater.

Summary


At this point we are VERY happy with the car. It's not for everyone, but it could be for a lot of people. I'd say it's contingent on the following conditions:
#1 Have a second gas car to drive for longer trips.
#2 Majority of trips are under 60 miles.
#3 Don't have frequent detours that could push you over 60 miles.
#4 Have a place to charge at home.



Sunday, March 8, 2015

Arduino Retro Computer: Program Memory (FRAM)

Arduinos have a very limited amount of internal RAM. Between the code for my video output, sound output, joystick input, keyboard input, and BASIC interpreter, I am running out of memory for the user's programs. FRAM is this very nice memory I found on Adafruit's website. It is very fast, non-volatile (does not lose memory when unpowered), and comes in large enough sizes for decent sized BASIC programs.

Adafruit has 2 types of interfaces for their FRAMs: I2C and SPI. I went with I2C to make it easy to expand the computer with additional FRAM chips if I want to in the future.

https://learn.adafruit.com/adafruit-i2c-fram-breakout?view=all

I soldered leads onto the FRAM chip.


I'm connecting pins as follows:
FRAM SDA to Arduino SDA (Digital Pin 20)
FRAM SCL to Arduino SCL (Digital Pin 21)
FRAM VCC to Arduino 5V
FRAM GND to Arudino GND
Uses I2C address 0x50 - Default

FRAM A0/A1/A2 control the I2C address so you can have multiples connected at once.



(I know my little computer is turning into a mess of wires; I'll work on a case for it soon.)

The FRAM library is available via the Adafruit website, I included it in my OS sketch:

// FRAM Library
#include <Wire.h>
#include <Adafruit_FRAM_I2C.h>

However at compile time it threw an error of not being able to find Wire.


It turns out my version of the Arduino IDE (1.0.5) is so old it didn't contain new standard libraries that the FRAM uses. I downloaded / installed the new IDE (1.6.0), but then I got a different error:
'prog_ucar' has not been declared


A type definition that Gameduino was using was deprecated and removed in the latest IDE.

To solve this problem, I modified my Gameduino library's GD.h file; at the top I added:

typedef const unsigned char prog_uchar;


Now that the upgrade errors have been resolved, I was able to create a global FRAM object:

Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();

Initialize the object in setup():

void setup()
{
  ...
  fram.begin();
  ...
}

And finally make a simple test to make sure it was reading and writing.

  fram.write8(0, 'a');
  fram.write8(1, 'b');
  fram.write8(2, 'c');
  char test0 = (char)fram.readu(0);
  char test1 = (char)fram.readu(1);
  char test2 = (char)fram.readu(2);
  Serial.print(test1);
  Serial.print(test0);
  Serial.print(test2);

The output was "bac", perfect!

That's all for now. Up next for the computer will either be a case or the start of the BASIC interpreter. The interpreter will store the user's BASIC code into the FRAM so I can keep the Arduino's SRAM available for operating system expansion.

Thursday, February 26, 2015

Arduino Retro Computer: Video Output

A little overdue, but I'm now going to cover how I handle the output to a monitor. I'm piggybacking off the ScreenModel described in the "Command Input" post; I recommend reviewing that code to get a more complete picture.

Quick recap - the ScreenModel class contains a display buffer, command buffer, and functions to interact between the two. Multiple screens are defined that the user can switch between by using PageUp and PageDown. To do this, I have a character array defined in the ScreenModel that holds the contents of the entire screen. When you switch between screens if flips between which buffer is drawn.

Output Window

Window Divider

Command Window


I'm using a Gameduino and its GD library for the interface to an external monitor. It should be fairly easy to port this code to another display interface by just replacing a handful of Gameduino specific library calls (and possibly adjusting a few of the constants for number of rows/columns).


Initializing the Gameduino



Pretty simple initialization for the Gameduino, include the library headers and a few commands in the setup() function.

// Gameduinio Library
#include <SPI.h>
#include <GD.h>

void setup()
{
  GD.begin();
  GD.ascii();
  GD.fill(0, ' ', 4096);

  ...
}


Define Constants for the Screen



These constants work well with the Gameduino, but they may require adjusting for different graphics displays.

const int numberOfScreenRows = 37;
const int numberOfScreenColumns = 50;
const int numberOfCommandRows = 2;
const int numberOfOutputRows = numberOfScreenRows - numberOfCommandRows - 1;
const int sizeOfOutputColumnArray = numberOfOutputColumns + 1; // Include null terminator.


ScreenModel Class



Similar to the command input, the bulk of the logic resides in the ScreenModel class. Again rather than pasting it all as one big block, I'm going to look at functions individually. All of the remaining code belongs in the ScreenModel class defined here:

class ScreenModel
{
  public:

  char outputArray[numberOfOutputRows][sizeOfOutputColumnArray];
  char blankLine[sizeOfOutputColumnArray];
  byte screenIndex;

  .. A whole bunch of additional functions described below ..
};


ScreenModel Init



The ScreenModel init assigns the screen index and clears the input and output buffers so they'll be ready for use. I'm also defining a blankLine that is used as a template for empty output (I must explicitly draw the spaces so it clears them off the screen).

So the user isn't completely thrown to the wolves, I give a small hint to type "help"! The addOutputLine function is the go-to function for me to write to the screen and let the ScreenModel handle updating the screen buffers and redrawing the screen.

Between the output and input is a divider line of "="s with the current screen index.

  bool init(byte newIndex)
  {
    screenIndex = newIndex;

    // Define the blank line.
    memset(blankLine, ' ', sizeOfOutputColumnArray - 1);
    blankLine[sizeOfOutputColumnArray - 1] = 0;
    
    clearOutput();
    clearCommand();
    
    addOutputLine("System Ready");
    addOutputLine("Type 'help' for commands.");

    drawWindowDivider();
    redrawCommandWindow();

  .. A whole bunch of other initializing function calls ..
  }


Clearing Output



The clearOutput() function handles everything necessary to clear the output of a given screen. It's currently only called on init(), but I see adding a new a "CLS" or similar command that will also clear the output.

  bool clearOutput()
  {
    // Assign the blank line to every output line.
    for (int rowLoop = 0; rowLoop < numberOfOutputRows; rowLoop++)
    {
      strcpy(outputArray[rowLoop], blankLine);
    }

    // Force a window refresh.
    redrawOutputWindow();
    
    return true;
  }


Switching Screens



Pressing the PageUp or PageDown key switches the active screen. After the activeScreen pointer is switched, a call is made to activeScreen->switchToScreen(). This function forces the output and input windows to be redrawn. The command divider must also be redrawn so it shows the correct screen index.

  void switchToScreen()
  {
    // need to redraw command window when switching screens
    redrawOutputWindow();
    drawWindowDivider();
    redrawCommandWindow();
    return;
  }
  
  bool redrawOutputWindow()
  {
    for (int rowLoop = 0; rowLoop < numberOfOutputRows; rowLoop++)
    {
      GD.putstr(0, rowLoop, outputArray[rowLoop]);
    }

    return true;
  }
  
  bool drawWindowDivider()
  {
    char indexString[12];
    itoa(screenIndex, indexString, 10);

    // Draw a divider line.
    char dividerLine[sizeOfOutputColumnArray];
    memset(dividerLine, '=', sizeOfOutputColumnArray - 1);
    
    // Null terminate.
    dividerLine[sizeOfOutputColumnArray - 1] = 0;
    
    // Add screen index to near the beginning.
    memcpy(&dividerLine[1], indexString, strlen(indexString));
    
    GD.putstr(0, 34, dividerLine);

    return true;
  }


  bool redrawCommandWindow()

  {
    int startRowCommandWindow = numberOfScreenRows - numberOfCommandRows;
    for (int rowLoop = 0; rowLoop < numberOfCommandRows; rowLoop++)
    {
      char drawString[sizeOfOutputColumnArray];
      memcpy(drawString, &commandArray[rowLoop * numberOfScreenColumns], numberOfScreenColumns);
      drawString[sizeOfOutputColumnArray - 1] = 0;
      GD.putstr(0, startRowCommandWindow + rowLoop, drawString);
    }


    return true;
  }


Displaying Command Line Input



As the user types keys for a command, the following code takes those key presses and displays them in the command window section. The drawCommandCharacter function is called whenever a command key is pressed (addCharacter), a command character is removed (removeCharacter), or the entire command line is cleared (clearCommand). 

What makes the code a bit more complicated is that it supports a multiline command. Also rather than forcing the the entire command window to be redrawn with each key press, I'm only drawing the character that has changed.

  bool drawCommandCharacter(int charPosition, char commandChar)
  {
    int drawPositionRow = numberOfScreenRows - numberOfCommandRows;
    int drawPositionColumn = charPosition;

    while (drawPositionColumn >= numberOfScreenColumns)
    {
      // Command array wraps to next line.
      drawPositionColumn -= numberOfScreenColumns;
      drawPositionRow ++;
    }

    char commandString[2];
    commandString[0] = commandChar;
    commandString[1] = 0;
    GD.putstr(drawPositionColumn, drawPositionRow, commandString);
    return true;
  }


Displaying Program Output



Last but not least is the code to actually output to the screen.

  bool addOutputLine(char *outputLineString)
  {
    // Move all existing lines up one.
    for (int rowLoop = 1; rowLoop < numberOfOutputRows; rowLoop++)
    {
      memcpy(outputArray[rowLoop-1], outputArray[rowLoop], sizeOfOutputColumnArray);
    }

    // Get the output length (limited to the width of the screen).
    int outputLength = strlen(outputLineString);
    if (outputLength >= sizeOfOutputColumnArray)
    {
      outputLength = sizeOfOutputColumnArray - 1;
    }
    
    // Take a blank line with null termination. Copy the output string to
    // the blank line, but don't include the output line's null termination!
    // That way we maintain the trailing empty spaces of the blank line.
    char outputLine[sizeOfOutputColumnArray];
    strcpy(outputLine, blankLine);
    memcpy(outputLine, outputLineString, outputLength);
    
    // Add new line to the bottom.
    memcpy(outputArray[numberOfOutputRows-1], outputLine, sizeOfOutputColumnArray);
    
    // Force a window refresh.
    redrawOutputWindow();
    
    return true;
  }


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, February 17, 2015

Arduino Retro Computer: Calculator

The most basic function of a computer is to be... a calculator. (For an interesting story, learn about the history of VisiCalc and the Apple 2.)

I have to do calculations for work frequently throughout the day, and I usually have several Windows Calculators up at one time. This retro computer can be perfect for that role.


For my initial implementation, I'm supporting the basic math operators: +, -, *, and /. The user will be able to set the initial value and/or perform those math operations. Each screen of the OS will keep track of its last calculated math operation. That way users can continuously operate on running results. (And if they switch to a different screen, they can have a different running calculation going.)

The following code is a follow-up to the previous post "Arduino Retro Computer: Command Input". I highly recommend reading that post to understand this one better.

The basic premise is if the command string parsed by submitCommand() starts with the character "=", then the command string gets sent to commandMath() for processing.

The following commands are available:
=###   (Set the previousMathResult to the given ###.)
=+###   (Increment the previousMathResult by the given ###.)
=-###   (Decrement the previousMathResult by the given ###.)
=*### (Multiply the previousMathResult by the given ###.)
=/### (Divide the previousMathResult by the given ###.)
=###+### (Add two numbers, store the result in previousMathResult.)
=###-### (Subtract two numbers, store the result in previousMathResult.)
=###*### (Multiply two numbers, store the result in previousMathResult.)
=###/### (Divide two numbers, store the result in previousMathResult.)

Math commands can also be compounded:
=###+###-### (Perform addition followed by subtraction.)


What's Bad / Future Improvements


  • The calculator does not adhere to proper "Operator Precedence". http://en.wikipedia.org/wiki/Order_of_operations Calculations are always performed from the left to the right, regardless of the operation. (And no ability to use parenthesis to denote the order.)
  • More advanced functions such as sin/cos/tan, power, and sqrt aren't (yet) supported.
  • The calculator does not (yet) report hex and binary values. This is important to my work so I'll eventually get that added.
  • Doing =-### subtracts the previous result by ### rather than sets the current value to -###. (It thinks the user wants to perform a subtraction operation rather than set a negative value.)

(I have a strong feeling a "Calculator Part 2" will be posted in the near future.)


Screen Shot



At the start you can see I set the previousMathResult to 5. I then perform several additions on the running value.

I am also able to perform a calculation on 2 numbers regardless of the last result. (2+2=4)


The Code


class ScreenModel
{
  public:
  ..
  double previousMathResult;

  bool init(byte newIndex)
  {
    ..
    previousMathResult = 0.0;
    ..
  }

  bool submitCommand()
  {
    ..
    // Command Formatted is what the user entered with a null terminator.
    // The commandFormatted is incremented by one when it is passed into
    // commandMath so it doesn't have to reparse the leading "=".
    else if (strncmp(commandFormatted, "=", 1) == 0)
    {
      commandMath(commandFormatted+1);
    }
    ..
  }

  double commandMath(char *commandString)
  {
    // Determine the starting number and store in the result variable.
    // If an operator, we'll begin with the result of the previous math calculation.
    double result;
    switch (*commandString)
    {
      case 0:
      case '+':
      case '-':
      case '*':
      case '/':
        // First character is terminator or operator.
        result = previousMathResult;
        break;
      default:
        // Read the starting number. Also advance the commandString pointer
        // to the end of the number / location of the operator.
        result = strtod(commandString, &commandString);
        break;
    }
    
    // Ensure the operator and the next digit are not a terminator.
    while ((*commandString != 0) && (*(commandString+1) != 0))
    {
      // Next character should be the operatot.
      char *operatorChar = commandString;
      // Increment the commandString pointer passed the operator and read the number;
      double number = strtod(++commandString, &commandString);
      switch (*operatorChar)
      {
        case '+':
          result += number;
          break;
        case '-':
          result -= number;
          break;
        case '*':
          result *= number;
          break;
        case '/':
          // Divide by 0 protection.
          if (number != 0.0)
          {
            result /= number;
          }
          break;
        default:
          // Invalid operator - possibly a space. Continue processing to the next
          // character. Even if we have multiple spaces between the numbers and the
          // operators, the while loop will eventually increment the commandString
          // pointer to the next valid operation.
          break;
      }
    }

    // Completed processing the string. Format the result and display!
    char mathString[sizeOfOutputColumnArray];
    // Can't use snprintf - Arduino does not support floats in sprintf.
    //snprintf(mathString, sizeOfOutputColumnArray, "%lf", result);
    dtostrf(result, sizeOfOutputColumnArray-1, 8, mathString);
    addOutputLine(mathString);
    

    previousMathResult = result;
    
    return result;
  }

};


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.

Sunday, February 8, 2015

Arduino Retro Computer: Command Input

I've created a ScreenModel class that contains a display buffer, command buffer, and functions to interact between the two. My current plan is to have 2 operating modes: OS Mode and Program Mode. If a BASIC program is actively running, then the screen is in Program Mode, otherwise it's in OS Mode. The mode determines where keyboard inputs are processed.

I can define multiple screens and switch between them using the PageUp and PageDown keys. (Similar to how command line Linux works with the function keys.) That way a BASIC program may be running on screen #1, but the user can switch to screen #2 and enter system commands and/or execute a second program.

Please note: All of this is very subject to change. Also, the following code just highlights the command input; we'll cover the video output at a later time.

If anyone is curious my Arduino Sketch size is now 16,856 bytes (of a 258,048 byte maximum). I've got plenty of room for additional development!

Reminder of what the OS looks like (with the output window above and command window below):



Initializing the Keyboard


We covered this code earlier in the keyboard posts, but here's a quick recap in how we initialize the keyboard for input. We can poll the keyboard input with keyboard.available() and keyboard.read() - which we'll do in the primary Arduino loop() (covered below).

#include <PS2Keyboard.h>

const int KeyboardDataPin = 30;
const int KeyboardIRQpin =  2;

PS2Keyboard keyboard;

void setup()
{
  ...
  keyboard.begin(KeyboardDataPin, KeyboardIRQpin);

  // Initialize the serial input with the computer (for debugging).
  Serial.begin(9600);
  ...
}


Initializing the Screens


At this time, I'm defining only 2 screens. It's enough to test the Arduino functions without swallowing up even more of the limited RAM. I may keep it at 2 or I may increase it. The plan for this operating system is to be multitasking, not multithreading. Multiple user programs will be able to be executing at the same time, but it's all on one thread. The OS will have to alternate between the screens to process the BASIC commands.

I keep a pointer to the active screen. Pointers are defined by including a * after the variable type / before the variable name. That keeps the code a bit cleaner. Rather than keeping track of the active screen index and doing something such as:
screen[activeScreen].inputKeyboard(inputKey);
I can do:
activeScreen->inputKeyboard(inputKey);

There are many more uses of pointers throughout the code - especially when handling strings. If you don't know what they are, then I highly recommend you learn about them. (So far I've used them in very simple situations in this project; they can actually be quite powerful.) Unfortunately pointers have a bad reputation, but be cautious of anyone that tells you to "never use" a certain coding feature.

The code to initialize the screen:

const byte numberOfScreens = 2;

// Define the screens. (Each can operate in OS mode or run a program.)
ScreenModel screen[numberOfScreens];
// Keep track of which screen is active to know where to send inputs to.
ScreenModel *activeScreen;

void setup()
{
  ...
  // Initialize each of the screens (pass in the index of the screen to the constructor).
  screen[1].init((byte)1);
  screen[0].init((byte)0);

  // Set the default screen to index 0 and trigger a screen change.
  activeScreen = &screen[0];
  activeScreen->switchToScreen();
  ...
}


Main Program Loop


The main program loop monitors for input from the keyboard. If it's a Page Up or Page Down, then it switches the active screen. Otherwise, it sends the keyboard input to the active screen for processing.

void loop()
{
  // Loop through each screen and process pending actions in running programs. (Not done yet.)
  // Then check keyboard input for the current screen.
  // If the current screen is in OS mode, process inputs as OS commands.
  // If the current screen is in Program mode, send inputs to the Program.
  
  if (keyboard.available())
  {
    // read the next key
    char inputKey = keyboard.read();
    
    // Check if need to swap between OS screens.
    if (inputKey == PS2_PAGEDOWN)
    {
      // Switch to the next screen.
      Serial.print("[PgDn]");
      byte newScreenIndex = activeScreen->screenIndex + 1;
      if (newScreenIndex >= numberOfScreens)
      {
        newScreenIndex = 0;
      }
      activeScreen = &screen[newScreenIndex];
      activeScreen->switchToScreen();
    }
    else if (inputKey == PS2_PAGEUP)
    {
      // Switch to the previous screen.
      Serial.print("[PgUp]");
      byte newScreenIndex;
      if (activeScreen->screenIndex == 0)
      {
        newScreenIndex = numberOfScreens - 1;
      }
      else
      {
        newScreenIndex = activeScreen->screenIndex - 1;
      }
      activeScreen = &screen[newScreenIndex];
      activeScreen->switchToScreen();
    }
    else
    {
      // Send the command to the screen.
      activeScreen->inputKeyboard(inputKey);
    }

  }
  
  return;
}


ScreenModel Class


The bulk of the logic resides in the ScreenModel class. Rather than paste it all as one big block (then try to explain it out of the block), I'm going to look at functions individually. All of the remaining code belongs in the ScreenModel class defined here:

class ScreenModel
{

  public:

  byte screenIndex;

  bool init(byte newIndex)
  {
    screenIndex = newIndex;
    
    operatingMode = operatingModeOS;

    clearCommand();

    .. More init stuff ..

    return true;
  }

  .. A whole bunch of additional variables and functions described below ..

};


ScreenModel Mode Variables


Each screen needs to know which operating mode it is in to know how to process commands (and know if it's actively executing a program!). I've defined a simple enumeration of the operating mode and a byte to store the state.


  enum operatingModeEnum
  {
    operatingModeOS = 0,
    operatingModeProgram = 1
  };
  byte operatingMode;


ScreenModel Keyboard Input


Keyboard presses that are received in the main loop() are passed into the ScreenModel function inputKeyboard(inputKey).

inputKeyboard(inputKey) will relay that input to either  inputKeyboardOS(inputKey) or inputKeyboardProgram(inputKey) depending on the operating mode.

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


ScreenModel Keyboard Program Input


inputKeyboardProgram(inputKey) hasn't been developed yet. At this point I just have a place holder such that if ESC is pressed it exits out of program mode.

  // Keyboard input intended to be processed by the currently running program.
  void inputKeyboardProgram(char inputKey)
  {
    if (inputKey == PS2_ESC)
    {
      operatingMode = operatingModeProgram;
    }
    // -- Add code to send to active program. --
    return;
  }


ScreenModel Keyboard OS Input


inputKeyboardOS(inputKey) will submitCommand() if enter is pressed, clearCommand() if ESC is pressed, removeCharacter() if delete is pressed, or addCharacter(inputKey) if a character / digit is pressed.

  // Keyboard input intended to be processed by the OS.
  void inputKeyboardOS(char inputKey)
  {
    // check for some of the special keys
    if (inputKey == PS2_ENTER)
    {
      // Submit the current command.
      Serial.println();
      submitCommand();
    }
    else if (inputKey == PS2_TAB)
    {
      // Autofill commands.
      Serial.print("[Tab]");
    }
    else if (inputKey == PS2_ESC)
    {
      // Clear the current command.
      clearCommand();
      Serial.print("[ESC]");
    }
    else if (inputKey == PS2_LEFTARROW)
    {
      // Navigate left through the command array.
      Serial.print("[Left]");
    }
    else if (inputKey == PS2_RIGHTARROW)
    {
      // Navigate right through the command array.
      Serial.print("[Right]");
    }
    else if (inputKey == PS2_UPARROW)
    {
      // Load previous command.
      Serial.print("[Up]");
    }
    else if (inputKey == PS2_DOWNARROW)
    {
      // Load next command.
      Serial.print("[Down]");
    }
    else if (inputKey == PS2_DELETE)
    {
      // Delete the command array character.
      Serial.print("[Del]");
      removeCharacter();
    }
    else if (inputKey >= 32 && inputKey <= 126)
    {
      // Type commands into the command array.
      Serial.print(inputKey);
      addCharacter(inputKey);
    }

    return;    
  }


ScreenModel Command Array


The lower portion of the screen is the command window. It's two lines, 50 characters per line, supporting 100 characters of input total. Each screen has its own command array.

We want the video output to specifically draw empty characters (to clear out what was previously drawn). Thus nowhere in the 100 characters will there ever be a null terminator (0), it will be an empty space instead. You will see many calls to drawCommandCharacter to specifically clear a character from the output. Again, video output will be described in a later post.

For better or worse, the entire command array can be filled; meaning there is no null terminator (not even at the last index of the array). When we have to do additional processing on the command array, we'll add in the null terminator as necessary. I'm not super excited about this approach so it's very likely to change, but it is how I wrote it a while ago (this is some of the first code I wrote for the Retro Computer, before I started the blog).

const int sizeOfCommandArray = 100;

  char commandArray[sizeOfCommandArray];
  int cursorPosition;

  // Character or digit was typed, add it to the command array.
  int addCharacter(char newChar)
  {
    commandArray[cursorPosition] = newChar;
    drawCommandCharacter(cursorPosition, newChar);
    
    return incrementCursorPosition();
  }
  
  // Delete key was pressed, delete from the command array.
  int removeCharacter()
  {
    int newPosition;
    if (cursorPosition == (sizeOfCommandArray - 1) &&
        commandArray[cursorPosition] != ' ')
    {
      // Special case if we're on the last character.
      // Delete the last character but don't decrement back.
      newPosition = cursorPosition;
      commandArray[cursorPosition] = ' ';
      drawCommandCharacter(cursorPosition, ' ');
    }
    else
    {
      // Standard case.
      // Delete the previous character and decrement back.
      newPosition = decrementCursorPosition();
      commandArray[cursorPosition] = ' ';
      drawCommandCharacter(cursorPosition, ' ');
    }
    return newPosition;
  }

  int incrementCursorPosition()
  {
    if (cursorPosition < (sizeOfCommandArray - 1))
    {
      cursorPosition ++;
    }
    return cursorPosition;
  }
  
  bool decrementCursorPosition()
  {
    if (cursorPosition > 0)
    {
      cursorPosition --;
    }
    return cursorPosition;
  }

  bool clearCommand()
  {
    for (int cursorLoop = 0; cursorLoop < sizeOfCommandArray; cursorLoop++)
    {
      if (commandArray[cursorLoop] != ' ')
      {
        drawCommandCharacter(cursorLoop, ' ');
      }
      commandArray[cursorLoop] = ' ';
      
    }
    cursorPosition = 0;

    return true;
  }


ScreenModel Submitting Commands


When an operating system command is submitted (by pressing enter), I want to display it in the output window, execute the command, and clear the input. Ultimately I also want to store the command in a history so I can jump between previously entered commands.

We're straying a little bit into how the video output works, but to be able to understand the submit code at all I need to define a couple constants:
const int numberOfScreenColumns = 50;
const int numberOfOutputColumns = numberOfScreenColumns;
const int sizeOfOutputColumnArray = numberOfOutputColumns + 1;  // Include null terminator.

The following is the start of the submit command. As more operating system commands are created, this list will grow. (And as it grows, I'll investigate alternatives to a bunch of if/elseif strcmp - a slow and CPU intensive operation.)

My next post will dive into the details of a few of these initial commands. I then may address the video output.

  bool submitCommand()
  {
    // Display the command entered in the output window.
    if (cursorPosition == 0)
    {
      // Empty command, just add a space to the output window.
      addOutputLine("");
    }
    else
    {
      // 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 < cursorPosition; cursorIndex += numberOfOutputColumns)
      {
        char commandLineOutput[sizeOfOutputColumnArray];
        memset(commandLineOutput, 0, sizeOfOutputColumnArray); // Ensure null terminated.
        // On the first loop through, copy the first 50 characters (numberOfOutputColumns).
        // If our cursor position exceeds 50 characters (numberOfOutputColumns), then
        // we'll loop through a second time to copy the remaining 50 characeters.
        memcpy(commandLineOutput, &commandArray[cursorIndex], numberOfOutputColumns);
        addOutputLine(commandLineOutput);
      }
    }
    
    // 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;
    }
    
    if (strcmp(commandFormatted, "help") == 0)
    {
      commandHelp();
    }
    else if (strcmp(commandFormatted, "memory") == 0)
    {
      commandMemory();
    }
    else if (strcmp(commandFormatted, "screen") == 0)
    {
      commandScreen();
    }
    else if (strncmp(commandFormatted, "=", 1) == 0)
    {
      commandMath(commandFormatted+1);
    }
    
    // Clear Command (And eventually save it into history.)
    clearCommand();
    
    return true;
  }


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, January 24, 2015

Orbital Aero Model: Vector Model

** Update 21 Jan 2017: This Vector class has been deprecated, new version posted here. **

I created a Vector class that is used extensively in the code. Absolute locations, relative locations, velocities, accelerations, and forces all use this same class to hold values in 3 dimensional space. My vector is a simple 1 dimensional array of 3 values. Using an array makes things easier when dealing with matrix math (which will be introduced later). Also keeping it generic means those values could be considered an x/y/z or phi/theta/psi (orientation).

Operator Overloading


Operator overloading allows the programmer to use standard math operators on structures or classes that the compiler would normally not know what to do with. I use them extensively with vectors in the Orbital Aero Model; it helps to both condense the code and make it more readable.

For example, I'm trying to add vectors b and c together to store them in a.

Instead of writing code such as:
a.x = b.x + c.x;
a.y = b.y + c.y;
a.z = b.z + c.z;

I could write:
a = b + c;

The overload for that case would be:
Vector Vector::operator+(const Vector &vectorToAdd)
{
return Vector(this->value[0] + vectorToAdd.value[0], this->value[1] + vectorToAdd.value[1], this->value[2] + vectorToAdd.value[2]);
}

The addition operation is being performed on vector b (this) and vector c (vectorToAdd). It is returning a new vector of the sum that is stored in a.

A similar example is to add the value of b into a.

Instead of writing code such as:
a.x += b.x;
a.y += b.y;
a.z += b.z;

I could write:
a += b;

The overload for that case would be:
Vector &Vector::operator+=(const Vector &vectorToAdd)
{
this->value[0] += vectorToAdd.value[0];
this->value[1] += vectorToAdd.value[1];
this->value[2] += vectorToAdd.value[2];
return *this;
}

The addition operation is being performed on vector a (this) and vector b (vectorToAdd). The summed value is stored directly in a.

Since so much of the code uses my vector class that has operator overloading, I figured it would be best to share that class first. As I extend the functionality of my vector math, I'll update this post to include the latest additions.

More Resources


For more information on vector math, I recommend learning the following concepts:
Vector
Dot Product
Cross Product

If you've never seen a Dot Product or Cross Product before, you may be questioning what they're for. They're actually both VERY important concepts when doing 3-dimensional math. You'll see examples of them in action in the code that follows.

VectorModel.h


#pragma once

class Vector
{
private:

public:
double value[3];

Vector();

Vector(double value0, double value1, double value2);

Vector &Vector::operator=(double newValue);

Vector &Vector::operator=(const Vector &thatVector);

Vector Vector::operator-();

Vector Vector::operator+(const Vector &vectorToAdd);

Vector Vector::operator-(const Vector &vectorToSubtract);

Vector Vector::operator*(double valueToMultiply);

Vector Vector::operator*(const Vector &vectorToMultiply);

Vector Vector::operator/(double valueToDivide);

Vector &Vector::operator+=(const Vector &vectorToAdd);

Vector &Vector::operator-=(const Vector &vectorToSubtract);

Vector &Vector::operator*=(double valueToMultiply);

Vector &Vector::operator/=(double valueToDivide);

double magnitude();

Vector unit();

Vector sign();

static double dotProduct(Vector vector1, Vector vector2);
};


VectorModel.c


#include "math.h"
#include "VectorModel.h"

Vector::Vector()
{
this->value[0] = 0.0;
this->value[1] = 0.0;
this->value[2] = 0.0;
return;
}

Vector::Vector(double value0, double value1, double value2)
{
this->value[0] = value0;
this->value[1] = value1;
this->value[2] = value2;
return;
}

Vector &Vector::operator=(double newValue)
{
this->value[0] = newValue;
this->value[1] = newValue;
this->value[2] = newValue;
return *this;
}

Vector &Vector::operator=(const Vector &thatVector)
{
// Protect against self-assignment. (Otherwise bad things happen when it's reading from memory it has cleared.)
if (this != &thatVector)
{
this->value[0] = thatVector.value[0];
this->value[1] = thatVector.value[1];
this->value[2] = thatVector.value[2];
}
return *this;
}

Vector Vector::operator-()
{
return Vector(-this->value[0], -this->value[1], -this->value[2]);
}

Vector Vector::operator+(const Vector &vectorToAdd)
{
return Vector(this->value[0] + vectorToAdd.value[0], this->value[1] + vectorToAdd.value[1], this->value[2] + vectorToAdd.value[2]);
}

Vector Vector::operator-(const Vector &vectorToSubtract)
{
return Vector(this->value[0] - vectorToSubtract.value[0], this->value[1] - vectorToSubtract.value[1], this->value[2] - vectorToSubtract.value[2]);
}

Vector Vector::operator*(double valueToMultiply)
{
return Vector(this->value[0] * valueToMultiply, this->value[1] * valueToMultiply, this->value[2] * valueToMultiply);
}

Vector Vector::operator*(const Vector &vectorToMultiply)
{
return Vector(this->value[0] * vectorToMultiply.value[0], this->value[1] * vectorToMultiply.value[1], this->value[2] * vectorToMultiply.value[2]);
}

Vector Vector::operator/(double valueToDivide)
{
return Vector(this->value[0] / valueToDivide, this->value[1] / valueToDivide, this->value[2] / valueToDivide);
}

Vector &Vector::operator+=(const Vector &vectorToAdd)
{
this->value[0] += vectorToAdd.value[0];
this->value[1] += vectorToAdd.value[1];
this->value[2] += vectorToAdd.value[2];
return *this;
}

Vector &Vector::operator-=(const Vector &vectorToSubtract)
{
this->value[0] -= vectorToSubtract.value[0];
this->value[1] -= vectorToSubtract.value[1];
this->value[2] -= vectorToSubtract.value[2];
return *this;
}

Vector &Vector::operator*=(double valueToMultiply)
{
this->value[0] *= valueToMultiply;
this->value[1] *= valueToMultiply;
this->value[2] *= valueToMultiply;
return *this;
}

Vector &Vector::operator/=(double valueToDivide)
{
this->value[0] /= valueToDivide;
this->value[1] /= valueToDivide;
this->value[2] /= valueToDivide;
return *this;
}

double Vector::magnitude()
{
return sqrt( (this->value[0] * this->value[0]) + (this->value[1] * this->value[1]) + (this->value[2] * this->value[2]) );
}

Vector Vector::unit()
{
double mag = magnitude();
Vector unitVector;
unitVector.value[0] = this->value[0] / mag;
unitVector.value[1] = this->value[1] / mag;
unitVector.value[2] = this->value[2] / mag;
return unitVector;
}

Vector Vector::sign()
{
Vector signVector;
signVector.value[0] = sgn(this->value[0]);
signVector.value[1] = sgn(this->value[1]);
signVector.value[2] = sgn(this->value[2]);
return signVector;
}

double Vector::dotProduct(Vector vector1, Vector vector2)
{
return ((vector1.value[0] * vector2.value[0]) + (vector1.value[1] * vector2.value[1]) + (vector1.value[2] * vector2.value[2]));
}


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.