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.

No comments:

Post a Comment