Instructables
Picture of Arduino Persistence of Vision Display
pov.jpg
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..
janandh11 months ago
sir can i get clear coding???
cyassine1 year ago
hi
the code is givien me errors while compiling
!!!
cool
imitic1 year ago
hello,
i am working on a POV propeller clock project.
I would like to know if it is possible to use your source code and amend its input and display a clock as an output
Mr.What (author)  imitic1 year ago
certainly. I don't even ask for credit (LGPL type stuff), although I would appreciate it.
Bongmaster2 years ago
got schematic?
Mr.What (author)  Bongmaster2 years ago
The schematic would be fairly simple:
          Arduino                    |\ | 
D3,4,5,6,7,8, -----/\/\/\/\/\/\------| \|------- Gnd
9,10,11,12                           | /| 
                                     |/ | 
Do this 10 times. One LED/resistor pair on each arduino output.
Mr.What (author)  Mr.What1 year ago
Likes are ascii art of what the display would look like. See the example above... those are the lines I types in. It says "Quelab" in tall, thin, squished letters.
dots and spaces are ignored. non-dots, are displayed as light-on. Spin the display fast enough to "fatten" the letters to the desired width.
nathan_kia1 year ago
Thanks for your great code,
can you explain a little bit more how I can input the text that I want to be displayed .
you code seems so great but it need a little more of explaination.
yawstick1 year ago
For something that is more of a beginner example and better demonstrates how the individual letters are made have a look here http://www.youtube.com/watch?v=kCvmqZe8Ikg
I did not understand what these lines mean.I am totally new to Arduino.I have just blinked an LED.

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.

Can you tell me the straight forward procedure and how do I edit the text to be displayed ?