Introduction: Arduino Persistence of Vision Display

About: Degrees in EE, specializing in Digital Signal Processing. Working as a software engineer for 30+ years.
A persistence of vision (PoV) display is a row of LED's that flash out columns of a message.  When the array of LED's is moved, like when mounted on a bicycle wheel, the message can be read as if it were many LED's wide, instead of a single row.

The hardware setup for a PoV display is fairly straightforward, but this instructable contains code where the display can be controlled and programmed readily over the serial connection, and display settings can be saved so that they are automatically loaded and run when powered up from a battery.

You will want an Arduino AVR microcontroller board, such as an Uno, Nano, or mini.  Around 10 LED's, and 10 resistors from about 100 to 220 ohms.

For our example, we will assume a 10 LED display.  Wire the LED's in series with a resistor to digital I/O pins 3-10, and arrange them in a straight row.

Load the attached sketch.

From the serial monitor (or terminal emulator), type h to get the help menu.  This will show several commands.

My Serial reader class compresses whitespace, so you will want to use . to represent "off" in the line settings.  See the attached Quelab.dat sample line setting input file.  The lines of this file can be cut and pasted into a terminal emulator to set the PoV message.

Once your desired display has been loaded, use the s) command to save its settings to EEPROM to be used at next reset.


int dummy=0; // this is to force sketch to put arduino include here

#define MODE_UNKNOWN 0
#define MODE_PoV     1
#define MODE_RANDOM  2
#define MODE_CYLON   3

#define MAX_COLS 96
#define SERIAL_BUF_LEN (MAX_COLS+15)
#include 
SerialLineBuffer LineBuf;

struct {
  short nCols;  // no. columns in buffer
  short spaceCols; // no. columns "space" time before repeat or reverse
  short mode;  // MODE_ code from above
  short cylonCols;  // no. cols of time for each cylon flash
  int colTime;  // milliseconds/column
  int misc[3];  // reserved for future use
  short disp[MAX_COLS];  // display flags
} State;

// These are the DI/O pins used for the display
#define NPINS 10
int ledPins[NPINS] = {12,11,10,9,8,7,6,5,4,3};

#include 

void loadState()
{
  int n = sizeof(State);
  byte *bp = (byte *)(&State);
  for(int i=0; i < n; i++, bp++) *bp = EEPROM.read(i);
  if (!validState()) initState();
}
void saveState()
{
  int n = sizeof(State);
  byte *bp = (byte *)(&State);
  for(int i=0; i < n; i++, bp++) EEPROM.write(i,*bp);
}

// set state to a reasonable default
void initState()
{
  State.nCols = 2;
  State.spaceCols = 1;
  State.cylonCols = 10;
  State.mode = MODE_PoV;
  State.colTime = 10; // ms
  for (int i=0; i < MAX_COLS; i++) State.disp[i] = (i&1)?0x5555:0x2aaa;
  saveState();
}

void setup()
{
  int i;
  for (i=0;i
  {     pinMode(ledPins[i],OUTPUT);   }   pinMode(13,OUTPUT);  // use on-board LED   // restore state from EEPROM   loadState();   Serial.begin(9600); } void loop() {   checkCommand();   int i,dt,k;   dt = State.colTime;   switch (State.mode)     {     case MODE_CYLON:       dt *= State.cylonCols;       for (i=0; i < NPINS; i++)         {           digitalWrite(ledPins[i],HIGH);           delay(dt);           digitalWrite(ledPins[(i+NPINS-1)%NPINS],LOW);           delay(dt);         }       for (i=NPINS-2; i >= 0; i--)         {           digitalWrite(ledPins[i],HIGH);           delay(dt);           digitalWrite(ledPins[(i+1)%NPINS],LOW);           delay(dt);         }       break;     case MODE_PoV:       for (i=0; i < State.nCols; i++)         {           short mask=1;           for (k=0; k < NPINS; k++, mask <<= 1)             digitalWrite(ledPins[k],(mask & State.disp[i])?HIGH:LOW);         }       for (k=0; k < NPINS; k++) digitalWrite(ledPins[k],LOW);       delay(State.spaceCols*dt);       break;     default:  // random default       {         dt *= 10;         k = random(100);         int lvl = (k<50)?LOW:HIGH;         int j = random(NPINS);         digitalWrite(ledPins[j],lvl);         delay(dt);       }   }   digitalWrite(13,digitalRead(13)?LOW:HIGH);  // toggle heartbeat } // poll for commands from serial port void checkCommand() {   short mask;   if (!LineBuf.isComplete()) return;   char key = lowCase(*(LineBuf.get()));   switch(key)     {       //short mask;       //int k;       //char *b;     case 'h' :       Serial.println("  h)  help (print this message)");       Serial.println("  s)  save state");       Serial.println("  r)  random lights mode");       Serial.println("  c)  cylon mode");       Serial.println("  p)  PoV sign mode");       Serial.println("  n)  no. cols to display");       Serial.println("  t)  col time, ms");       Serial.println("  b)  blank cols between repeat");       Serial.println("  i)  re-Initialize state");       Serial.print(  " Lx)  Set pattern for line x, 0<=x<=");       Serial.println(NPINS);       break;     case 's' : saveState(); break;     case 'r' : State.mode = MODE_RANDOM; break;     case 'p' : State.mode = MODE_PoV; break;     case 'c' : State.mode = MODE_CYLON; break;     case 'i' : initState(); break;     case 'n' : State.nCols    =nextInt(LineBuf.get()+1); break;     case 't' : State.colTime  =nextInt(LineBuf.get()+1); break;     case 'b' : State.spaceCols=nextInt(LineBuf.get()+1); break;     case 'l' :     {       char *b = LineBuf.get()+1;       int k = ((int)(*b)) - ((int)'0');       if ((k<0) || (k > 15)) break;       b++;       short mask = (short)(1<
      for (int i=0; i < State.nCols; i++, b++)         {           if (isOn(*b)) State.disp[i] |=  mask;           else          State.disp[i] &= ~mask;         }       break;     }     default :       Serial.print("Unrecognized Command : <");       Serial.print(LineBuf.get());       Serial.println(">");       Serial.println("Send command h for help.");     }     printState();     printMsg(); } void printState() {   Serial.print(State.nCols);   Serial.print(" Columns   ");   Serial.print(State.spaceCols);   Serial.print("   ");   Serial.print(State.colTime);   Serial.println("ms/col");   Serial.flush(); } void printMsg() {   int i,k;   Serial.println();   for (i=0; i < State.nCols; i++) Serial.print("-");   short mask=1;   for(k=0; k < NPINS; k++, mask <= 1)     {       for (i=0; i < State.nCols; i++) Serial.print(State.disp[k]&mask?"X":" ");       Serial.println("|");     }   Serial.println();   for (i=0; i < State.nCols; i++) Serial.print("-");   Serial.println();   Serial.flush(); } // parse next int from a string int nextInt(const char *s) {   const char *c = s;   int val = 0;   for(;;)     {       int k = ((int)(*c)) - ((int)'0');       if ((k<0)||(k>9)) return val;       val *= 10;       val += k;       c++;     } } bool isOn(const char c) {   if ((c=='0') || (c=='.') || (c==' ') || (c==0)) return false;   //if ((c=='1')||(lowCase(c)=='x')) return true;   return true; } bool validState() {   // check for silly state, set to default if inconsistent   if ((State.mode <= 0) || (State.mode >  3) ||       (State.nCols<  1) || (State.nCols>MAX_COLS)) return false;   if ((State.spaceCols < 1) || (State.spaceCols > 10*MAX_COLS)) return false;   if ((State.colTime < 1)   || (State.colTime > 10000)) return false;   if ((State.cylonCols < 1) || (State.cylonCols > 10000)) return false;   return true; }


-------- ioUtil.h
class SerialLineBufferPrivates;
class SerialLineBuffer
{
public:
  SerialLineBuffer();
  //~SerialLineBuffer();
  bool isComplete();  // reads from serial, return true if 0 or EOLN
  void clear();
  void begin();
  int length() const;
  int maxLength() const;
  char *get();  // retrieve current buffer and clear
  char buf[SERIAL_BUF_LEN+1];
protected:
  int _maxLength, _len;
  bool _complete;
private:
  //class SerialLineBufferPrivates *Priv;
};

char lowCase(const char a);
int caseCmp(const char a0, const char b0);
char *extractKey(char *cmdStr, char **val);
bool keyMatch(const char *key, const char *key1);
-------------------------------   ioUtil.cpp
#include 

#define NULL 0

// don't want to depend on ctype.h, just for this!
bool isBlank(int c)
{
  if(c == 7) return(false); // bell
  return( (c <= ' ') || (c > '~') );
}

#if defined(ARDUINO) && ARDUINO >= 100
#include 
#warning ARDUINO
#else
#error ARDUINO not >= 100
#include 
#endif

void SerialLineBuffer::begin()
{
  _maxLength = SERIAL_BUF_LEN;  // AVR dynamic mem is tricky
  _len = 0;
  _complete = false;
}
SerialLineBuffer::SerialLineBuffer() { begin(); }
bool isTerminator(int c)
{
  if (c == 0) return(true);
  if (c == ';') return(true);  // sending \n to serial is tricky.  accept this too.
  //if ((c=='\n') || (c=='\r') || (c=='\m')) return(true);
  if ((c>=10) && (c<=13)) return(true);  // \r, \n, form feed, vert tab
  return(false);
}

/// read from serial, return true if 0 or EOLN
bool SerialLineBuffer::isComplete()
{
  if (_complete)
    return(true);  // don't read more until this line is consumed
  // add characters from serial
  while(Serial.available() > 0)
    {
      int nextByte = Serial.read();
      //Serial.print("Got ");Serial.println(nextByte);
      if ((nextByte < 0) || (nextByte >= 256))
        return(_complete);
      if (isTerminator(nextByte))
        {
   //Serial.print("terminator ");Serial.println(nextByte);
          buf[_len] = 0;
          _complete = (_len > 0);
          return(_complete);
        }

      if (isBlank(nextByte))
        {
   //Serial.print("blank ");Serial.println(nextByte);
          if (_len > 0) // ignore leading whitespace
            {
              if (buf[_len-1] != ' ')  // compact space to 1 space
  {
    buf[_len++] = ' '; // convert all space to ' '
  }
            }
        }
      else
{
   buf[_len++] = (char)nextByte;
}

      // don't allow overflow
      if (_len >= _maxLength)
{
   Serial.println("\nOverflow.  truncating command string");
   _complete = true;
}
    }
  return(_complete);
}

void SerialLineBuffer::clear()
{
  _len = 0;
  _complete = false;
}
int SerialLineBuffer::length() const { return(_len); }
int SerialLineBuffer::maxLength() const { return(_maxLength); }

/// retrieve current buffer and clear
char *SerialLineBuffer::get()
{
  buf[_len]=0;
  clear();
  return(buf);
}

//-----------------------------------------------------------
/// split a keyword-value pair string into a key string and value string
const char nullChar = 0;  // static is scary on AVR
char *extractKey(char *cmdStr, char **val)
{
  *val = (char *)&nullChar;
  if (cmdStr == NULL) return(NULL);
  char *key = cmdStr;
  while (*key)  // process comments
    {
      if (*key == '#') *key=0;  // comment
      else key++;
    }
  key = cmdStr;
  while(*key &&  isBlank(*key)) key++; // trim leading space
  *val = key;
  while(**val && !isBlank(**val)) *val += 1; // skip key
  **val = 0;
  *val += 1;
  while(**val &&  isBlank(**val)) *val += 1; // skip whitespace
  return(key);
}

char lowCase(const char a)
{
  if ((a >= 'A') && (a <= 'Z'))
    {
      int sft = ((int)'a') - ((int)'A');
      int b = (int)a + sft;
      return((char)b);
    }
  return(a);
}

int caseCmp(const char a0, const char b0)
{
  char a = lowCase(a0);
  char b = lowCase(b0);
  if (a < b) return(-1);
  return((a>b)?1:0);
}

bool keyMatch(const char *key0, const char *key1)
{
  //Serial.print("keyMatch(");Serial.print(key0);Serial.print(",");Serial.print(key1);Serial.print(")=");
  while(*key0 || *key1)
    {
      if (caseCmp(*key0, *key1))
      {
        //Serial.println("false");
        return(false);
      }
      if (*key0) key0++;
      if (*key1) key1++;
    }
  //Serial.println("true");
  return(true);
}
--------------------------------  Input data example
L0..Q.............l......b....
L1.QQQ............l......b....
L2Q...Q...........l......b....
L3Q...Q.u..u..ee..l..a.a.b.b..
L4Q...Q.u..u.e..e.l.a.aa.bb.b.
L5Q...Q.u..u.e..e.l.a..a.b..b.
L6Q.Q.Q.u..u.eeee.l.a..a.b..b.
L7Q..Q..u..u.e....l.a..a.b..b.
L8.Q.Q..u..u.e..e.l.a.aa.bb.b.
L9.QQ.Q..uu...ee..l..a.a.b.b..