arduino-display-pov-persistencia-vision

How to Create a POV Display with Arduino

  • 8 min

Have you ever tried writing your name in the air with a sparkler or a flashlight in the dark? Even though the light is at a single point at any given time, your brain perceives a complete line.

This phenomenon is called Persistence of Vision (POV), and it involves merging rapid images into a continuous sensation. The human retina retains an image for approximately 1/10 to 1/25 of a second. If something moves faster than that, our brain merges the discrete images.

In this project, we are going to build a POV Display. We will use a single row of LEDs (one dimension) and, by moving it quickly, we will create a virtual two-dimensional screen in the air.

This project is a masterclass in execution timing. Here, delay(100) is an eternity. We will be working in the order of microseconds.

Trading Space for Time

On a normal screen (like your phone’s), you have a grid of pixels that all light up at once. On a POV display, we don’t have the X-axis. We only have the Y-axis (our row of LEDs).

To generate the X-axis, we take advantage of movement.

  • At time t = 0, we light up the first column of the image.
  • We move our hand (or a motor rotates).
  • At time t = Δt, we light up the second column.

If we do it fast enough, the brain reconstructs the complete matrix.

This means we must “refresh” the complete image at least 25 times per second to avoid excessive flickering.

Components

  • [1x|blue] Arduino Nano | Or Uno (Nano better for size)
  • [8x|green] LEDs | Red or blue, high brightness
  • [8x|gray] 220Ω or 330Ω Resistor | Limit LED current
  • [1x|red] 5V Powerbank or battery with regulator | Portable power supply
  • [Various|gray] Small prototyping board | Or directly soldered

For this build, we will aim for compactness. An Arduino Uno is a bit large to shake by hand, so if you have an Arduino Nano, now is the time to use it. If not, the Uno will work just as well.

Connection Diagram

We are going to connect the 8 LEDs to 8 consecutive digital pins. This is crucial to simplify programming (and to optimize it if we wanted to use direct port manipulation).

We will use pins D2 to D9.

LED Row

We connect the anodes to the pins via resistors and join all the cathodes to GND.

[ { “from”: “LED 1 (Top) + resistor”, “to”: “Pin 9”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “LED 2 + resistor”, “to”: “Pin 8”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “LED 3 + resistor”, “to”: “Pin 7”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “LED 4 + resistor”, “to”: “Pin 6”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “LED 5 + resistor”, “to”: “Pin 5”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “LED 6 + resistor”, “to”: “Pin 4”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “LED 7 + resistor”, “to”: “Pin 3”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “LED 8 (Bottom) + resistor”, “to”: “Pin 2”, “color”: “red”, “note”: “Cathode to GND” }, { “from”: “Common GND (all LEDs)”, “to”: “Arduino GND”, “color”: “black” } ]

The order matters. If you connect them backwards, your text will appear upside down or mirrored.

Drawing with Ones and Zeros

This is where programming becomes visual. To define which LEDs light up in each column, the most convenient method is to use binary notation.

Imagine you want to draw the letter “I”.

  • Column 1: Base (e.g., 0b10000001)
  • Column 2: Base (e.g., 0b10000001)
  • Column 3: Full vertical bar (e.g., 0b11111111)
  • Column 4: Base (e.g., 0b10000001)
  • Column 5: Base (e.g., 0b10000001)

By using 0b, we can “see” the drawing directly in the code. A 1 means LED on, a 0 means off.

Defining the Image

Let’s draw a Heart, which is easier to recognize than text if the speed isn’t constant.

// LED pins (from D2 to D9)
// Note: The order in the array defines which is the most significant bit
const int ledPins[] = {2, 3, 4, 5, 6, 7, 8, 9};

// --- THE DRAWING ---
// Each byte represents a vertical COLUMN of the image.
// The array is read from left to right.
// 1 = LED On, 0 = LED Off.

byte image[] = {
  0b00000000, // Empty space
  0b00000000, 
  0b00011000, //   **
  0b00111100, //  ****
  0b01111110, // ******
  0b11111111, // ******** (Center of the heart)
  0b11111111, // ********
  0b01111110, // ******
  0b00111100, //  ****
  0b00011000, //   **
  0b00000000, 
  0b00000000
};

int imageLength = sizeof(image);
int timePerColumn = 2; // Milliseconds each column stays on
Copied!

setup

We configure the pins as outputs.

void setup() {
  for (int i = 0; i < 8; i++) {
    pinMode(ledPins[i], OUTPUT);
    digitalWrite(ledPins[i], LOW); // Start off
  }
}
Copied!

Paint Function

We need a function that takes a byte (e.g., 10010001) and lights up the corresponding LEDs.

void showColumn(byte data) {
  for (int i = 0; i < 8; i++) {
    // Read the 'i' bit from our data byte
    // bitRead is a very useful Arduino function for this
    int state = bitRead(data, i);
    
    digitalWrite(ledPins[i], state);
  }
}
Copied!

Main Loop

The loop simply iterates through the image array repeatedly.

void loop() {
  // Iterate through each column of our image
  for (int i = 0; i < imageLength; i++) {
    showColumn(image[i]);
    
    // This pause defines the WIDTH of the pixel.
    // If you move fast, you need a small delay.
    // If you move slowly, you need a larger delay.
    delay(timePerColumn);
  }
  
  // Turn everything off between repetitions so the end doesn't blend with the start
  showColumn(0);
  delay(10); // Black space between "frames"
}
Copied!

Testing the Setup

  1. Upload the code.
  2. Disconnect the USB and connect a 5V portable power supply, ensuring the assembly is already insulated and secured.
  3. Move the assembly side to side, keeping it away from people, objects, and cables.
  4. You should see a red heart floating in the air.

If the heart looks “stretched”, reduce timePerColumn. If it looks “compressed” or very narrow, increase timePerColumn or shake your hand more slowly.

Direct Port Manipulation

If you try to write long texts or complex images, you will notice that eight calls to digitalWrite() introduce a temporal separation between LEDs. At high speeds, this can slightly deform each column.

To solve this, we can use Port Registers. On the Arduino Uno/Nano, pins D0 to D7 belong to PORTD.

A port write significantly reduces this latency, although it depends on the microcontroller and requires careful handling of all bits in the register:

void showColumnFast(byte data) {
  // Write directly to the Port D register
  // WARNING: Pins 0 and 1 are RX/TX. Writing there can interrupt Serial communication.
  // Ideally, use PORTB (Pins 8-13) or be careful with the bitmask.
  
  PORTD = data; 
}
Copied!

This snippet does not work with the D2-D9 wiring from the example: you would need to redesign the connections and preserve the bits of the pins that are not part of the display. On an Uno, writing to the entire PORTD also affects D0 and D1, which are used by the serial port.

Project Evolution

The POV wand you shake by hand is level 1. How do we turn it into an engineering project?

  1. Rotary POV: This must only be built with a balanced rotor, a sturdy enclosure, and a calculated mounting for the rotation speed. A detached board or battery can cause serious injury.
  2. Synchronization (Hall Sensor): When spinning with a motor, the image will rotate and move on its own. You need a fixed reference point. Place a magnet on the base and a Hall effect sensor on the rotating part. Each time the sensor detects the magnet, the Arduino knows it has completed one revolution and restarts the drawing at the exact same point. This makes the image appear “still” in the air.
  3. Dynamic Text: Add a Bluetooth module. You could send “HELLO” from your phone, and the Arduino would convert those letters into the corresponding bitmap to display it.