Radio Control Panel – LCD Text Scroller

To meet the deadline for the Instructables Microcontroller contest, I had to write up and submit the Instructable before I could put up my detailed write-up here.  There’s still a lot of write-up to do, but I wanted to spend some time discussing the scrolling mechanism I used for the LCD text.

Quick Introduction

The project in question is a standalone control panel for the WiFi internet radio I built (based on MightyOhm’s WiFi Radio project).  It is a modified wireless router that has a USB sound card, runs OpenWRT, and can play shoutcast stations, like my favorites at SomaFM (currently, Groove Salad).  I have also used Tinkernut’s instructions (also based on MightyOhm’s), which suggest a software-based control panel (in this case, Ario).  I wanted to avoid using my computer (as Tinkernut did), and also to avoid opening up my router and soldering on a serial port header (as MightyOhm did).

So, I built and Arduino-based ethernet-capable embedded system with a station selector knob, which uses the MPD (Music Player Daemon, the jukebox software running on the router) network protocol API to command it.  Part of the project is a character LCD that displays the current station, artist, and song.  However, the LCD is only 16×2 characters, so most song names and artists don’t fit.  So, I needed some way of scrolling the text so that it would all be displayed in an easy-to-read, appealing way.

For the display, the controller will store the station name in the first line, and an “Artist – Title” string in the second line.  The lines will scroll from right to left, with blank space on either side, with the line being completely blank for exactly one step in the scrolling sequence.  The lines will scroll independently, so that short station names won’t bit hidden while long song titles scroll by.

LCD Display Model

To control what I display on the LCD, I model the screen as an internal buffer, named scrollBuf, of N lines (in this case, 2, mapping directly to the lines on the display) and M characters (where M > 16, the maximum display on the buffer itself).  We also maintain an array of integers, named startPos, which maintains the start position into the scrollBuf string that we are printing to the LCD (for example, if we had scrolled the first 4 characters of a line off the screen, startPos would keep track of this).  This start position is offset by SCREEN_COLUMNS, because it includes the blank spaces that exist between the scrolled messages.  That will be explained in better detail later.

#define SCREEN_LINES 2
#define SCREEN_COLUMNS 16
#define SCROLL_BUF_LEN 64
char scrollBuf[SCREEN_LINES][SCROLL_BUF_LEN} = { 0 };
static uint8_t startPos[SCREEN_LINES] = { 0 };

In this case, I selected a maximum width of 64 characters (which, with the null terminator, of course, only holds 63 actual characters), trying to strike a balance between what most artist and song names were limited to, and how much RAM would be taken up by the scroll buffer (the ATmega328P on my Boarduino only has 2KB).

Scrolling Algorithm

When the station changes, the station name is put into scrollBuf[0] (naturally, length-checked and null terminated).  The controller periodically requests the current status of the radio, and parses out the current artist and song title.  The controller then periodically updates the LCD display, scrolling each line one character to the left, and wrapping as necessary.  Here’s how it works:

We loop through each of the lines on the screen buffer (any of which may have been populated with text elsewhere by the controller), and declare a variable to contain the “print buffer”, which will contain the string to be printed to the current line of the LCD.  The pBuf variable is SCREEN_COLUMNS wide, plus one character for the null terminator (since the LCD can display a full 16 characters).  We initialize pBuf to all blank spaces, and add a null terminator at the end.

for (uint8_t i = 0; i < SCREEN_LINES; i++) {
  char pBuf[SCREEN_COLUMNS+1] = { 0 };
  memset(pBuf, ' ', SCREEN_COLUMNS);
  pBuf[SCREEN_COLUMNS] = '';

Now is the good part.  Our aim is to figure out which part of the current line to put into the print buffer, and to figure out where in the print buffer to put it.  If the line is scrolling in from the left, there will be blank spaces at the front of it.  If it has filled the line and is scrolling across, there will be no blank spaces.  If the end has scrolled into view and is moving to the left, there will be spaces at the end.  Finally, once the entire line of the display has shown blank spaces for exactly one scrolling step, the line should start scrolling in from the right again.  To figure this out, we compute three values.

The first is the offset into the print buffer where the start of the displayed line is to be copied, in other words, how many blank spaces before the first character in the string.  It is found by the taking the greater of the number of columns (offset by the start position of the current line) and 0:

  uint8_t pBufOffset = max(SCREEN_COLUMNS - startPos[i], 0);

For example, if the start position is 3 (i.e., three blank spaces have been scrolled off the screen, 13 blank spaces remain, followed by the first three characters of the string), then pBufOffset will be 13.  If the start position is 20 (i.e., all 16 blank spaces, and four characters of the string have been scrolled off to the left), then pBufOffset will be 0.  We never want pBufOffset to be less than 0, that would be really bad when we use it as the index to copy into the buffer.

The next value we need to compute is the offset into the scroll buffer that we will be copying from, in other words, how many characters into the display string have been scrolled off the screen already.  It is found by taking the greater of the start position (offset by the number of columns on the screen) and 0:

  uint8_t scrollBufOffset = max(startPos[i] - SCREEN_COLUMNS, 0);

For example, if the start position is 3 (as above), then scrollBufOffset will be 0, since we want to copy the first characters of the display screen into the print buffer, after the 13 spaces.  Likewise, if the start position is 20 (also see above), then scrollBufOffset will be 4, since we want to skip over the 4 characters that have been scrolled off the screen to the left.

Finally, we need to compute the length of the string to copy into the print buffer.  This is found by taking the lesser of the number of screen columns (offset by the place we will be starting the string in the print buffer) and the length of the string to be scrolled.

  uint8_t len = min(SCREEN_COLUMNS - pBufOffset, strlen(scrollBuf[i]) - scrollBufOffset);

For example, if the start position is 3, then pBufOffset will be 13, and so at most 3 characters of the string can be copied in to the print buffer.  If the length of the string is (somehow) fewer than 3 characters, that will be used instead.  Likewise, if the start position is 20, then pBufOffset will be 0, and so at most 16 characters of the string can be copied in to the print buffer.  If the string is fewer than 16 characters left to display, then the entire remainder of the string will be copied in, followed by blank spaces.

Now we copy from the scroll buffer, at the specified offset, into the print buffer, at the specified offset, copying at most the computed number of characters, and add a null terminator.

  strncpy(pBuf + pBufOffset, scrollBuf[i] + scrollBufOffset, len);
  pBuf[pBufOffset + len] = '';

Finally, we must fill in any remaining, trailing spaces, so that the string sent to the display is exactly SCREEN_COLUMNS wide.

  if (strlen(pBuf) < SCREEN_COLUMNS) {
    memset(pBuf + strlen(pBuf), ' ', SCREEN_COLUMNS - strlen(pBuf));
  }
  pBuf[SCREEN_COLUMNS] = '';

Now we can send the print buffer to the display, and increment the start position counter (wrapping it as necessary).  We cycle startPos[i] up to the full length of the string plus the number of columns to account for the blank spaces between the scrolled text.

  if (startPos[i] < strlen(scrollBuf[i]) + SCREEN_COLUMNS) {
    startPos[i]++;
  } else {
    startPos[i] = 0;
  }

And that’s pretty much it.  It seems to work well.  Here you can see the entire code block in all its contextual glory, or you can find it in my github repository.


// how many lines are there on the screen?
#define SCREEN_LINES   2
// how many columns, i.e., how many characters can be displayed?
#define SCREEN_COLUMNS 16
// how many characters should I reserve for the scroll buffers?
// (note that null terminator limits the actual string length 
// to one less than this number)
#define SCROLL_BUF_LEN 64

// scrollBuf contains a buffer of text for each line,
// which will be scrolled from right to left
char scrollBuf[SCREEN_LINES][SCROLL_BUF_LEN] = { 0 };

// in milliseconds, how long to wait before scrolling display
// one character to the left.
#define SCROLL_DELAY 100

// render display maintains 
void renderDisplay(bool force = false) {
  // startPos maintains, for each line of the display, 
  // how many characters the display string has been scrolled.
  // it includes the blank spaces (SCREEN_COLUMNS of them, to be exact)
  // that exist between the scrolled messaged, so it must be offset
  // by SCREEN_COLUMNS to find the actual position in the display string
  // that is the first to be displayed (if the beginning of the string 
  // itself has been scrolled off the screen to the left)
  static uint8_t startPos[SCREEN_LINES] = { 0 };

  static uint32_t nextUpdate = 0;
  if ( (millis() > nextUpdate) || force) {
    nextUpdate = millis() + SCROLL_DELAY;

    // for each line of the display
    for (uint8_t i = 0; i < SCREEN_LINES; i++) {

      // declare a print buffer that will hold the line
      // as it is built up, to replace line i on the display
      // the size is the number of columns + 1, to allow room
      // for the null terminator
      char pBuf[SCREEN_COLUMNS+1] = { 0 }; // initialize array to null
      memset(pBuf, ' ', SCREEN_COLUMNS);   // set full line to blank spaces
      pBuf[SCREEN_COLUMNS] = '';         // sanity check for null terminator, could probably remove.

      // pBufOffset is the offset into the print buffer to begin the display string,
      uint8_t pBufOffset      = max(SCREEN_COLUMNS - startPos[i], 0);
      uint8_t scrollBufOffset = max(startPos[i] - SCREEN_COLUMNS, 0);
      uint8_t len = min((SCREEN_COLUMNS - pBufOffset), strlen(scrollBuf[i]) - scrollBufOffset);

      // we copy into the pBufOffset'th character into the pBuf string,
      // from the scrollBufOffset'th character in the scrollbuffer string,
      strncpy(pBuf + pBufOffset, scrollBuf[i] + scrollBufOffset, len);
      // add a null terminator (may not be necessary)
      pBuf[pBufOffset + len] = '';      

      // fill any remaining space after the contents pBuffer with spaces
      if (strlen(pBuf) < SCREEN_COLUMNS) {
        memset(pBuf + strlen(pBuf), ' ', SCREEN_COLUMNS - strlen(pBuf));
      }
      // add a null terminator
      pBuf[SCREEN_COLUMNS] = '';

      // set the cursor to the beginning of the appropriate line
      // and send the display buffer to be printed
      lcd.setCursor(0, i);
      lcd.print(pBuf);      

      // increment (or reset) the start position
      if (startPos[i] < strlen(scrollBuf[i]) + SCREEN_COLUMNS) {
        startPos[i]++; 
      } else {
        startPos[i] = 0;
      }
    }
  }
}

Downloads

Here is the original Instructable for the Standalone WiFi Radio Control Panel.

You can find the latest version of the source, along with the schematic and some photos, in my github repository.

Advertisements
This entry was posted in Projects, Radio Control Panel. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s