At the time of writing, when I did a search for "POV" on Hackster, it gave me 63 hits. But most of them require very special parts and tools and a lot of time to build. Some other come with only five LEDs. O.k., in principle it will work, but it looks very poor. Long time ago, there was an Arduino shield called Blinkenlight (see Credits), equipped with 20 LEDs, but it is not for sale any longer. As a replacement, I found this one at a well-known Chinese market-place:
You can mount three of them on a standard breadboard next to each other as shown in the picture on top and connect them with eighteen pins of an ATmega328 in DIP-28 package. (If you get an Arduino UNO in DIP version it is easy to program the chip and then remove it from the UNO board and insert it into the breadboard. Take care to keep the orientation accordingly.) In fact, you can arrange the parts in order to connect pins A0 to A5 directly with one of the modules saving you 6 wires. I must admit there will be two gaps between the modules because of the GND pins of the modules, but this is the price for using what is available on the market.
As you probably know, the pins of the ATmega328 are grouped in three ports, so for performance and easiness of programming, connect the LEDs of each module to the pins of the same port. As shown in the cover image, connect port C to the right-most LEDs, port B to the middle ones, and port D to the left ones. As you also will know, it is best to never make use of pins 0 and 1 on Arduinos as they as used to program the chip.
Having done this, you only need some data to present and a short program sending the data to the LEDs as fast as possible to induce the POV effect. Do not use digitalWrite().
How to create the data using spreadsheet software?The author of "Blinkenlight" did it this way:
#define PARSE(pattern) ((pattern>>14) & 0b111111), ((pattern>>8) & 0b111111), (pattern & 0b11111111)
uint8_t pov_pattern[] PROGMEM = {
PARSE( 0b00000000000000000000 ),
PARSE( 0b00001111110000000000 ),
PARSE( 0b00111111111100000000 ),
...
which is very clever but you have to transpose the pixels you want to show. For some reason, I prefer the method shown below. Most probably, you own an editor to implement the bitmap you are going to present: it could be Microsoft Excel or any compatible software.
In column A, you find the values representing the port bits of the pins used. In the next columns, you can enter the pattern you want to show.
In line 23, the values in A2 to A7 will be added, given a "1" is in that line. Cell C23 contains this formula:
=sumproduct($A2:$A7,C2:C7)
In line 24, the values in A9 to A14 are added, and in line 25 the values in A16 to A21 are added. Line 27 combines those three figures, separated by commas:
=C23&","&C24&","&C25&","
Those formulas can be copied in the right direction as far as your pattern extends. Now, just copy the contents of line 27 into your Arduino sketch. The line might begin like this:
0,0,3, 0,0,62, 0,7,32, 0,62,0, 124,4,0, 224,4,0, 124,4,0, 0,62,0, 0,7,32,
Actually, it is better to have them in separate files (in my case, named Albert.h and Einstein.h) to be included as shown below:
byte sel __attribute__ ((section(".noinit")));
const byte albert[] PROGMEM = {
#include "Albert.h"
};
const byte einstein[] PROGMEM = {
#include "Einstein.h"
};
void setup() {
if (sel == 0) sel = 1; else sel = 0;
DDRB = 255;
DDRC = 255;
DDRD = 255;
}
int x = 0;
void loop() {
if (sel == 0) {
PORTD = pgm_read_byte(albert + (x++)) << 2;
PORTB = pgm_read_byte(albert + (x++));
PORTC = pgm_read_byte(albert + (x++));
if (x > sizeof albert) x = 0;
}
else {
PORTD = pgm_read_byte(einstein + (x++)) << 2;
PORTB = pgm_read_byte(einstein + (x++));
PORTC = pgm_read_byte(einstein + (x++));
if (x > sizeof einstein) x = 0;
}
delay(1);
}
This program does even more: whenever you press the RESET key, it switches between the two patterns. Having uploaded this sketch, use a battery to power the arduino and wave the device back and forth as fast as you can. To get better results, do it in a dark room. When waving back and your pattern shows any text the letters will be shown mirrored.
How to create the data without using an external program?If you are not happy extending your toolchain it can be done like this:
For each of the ports you need six bits. When you take a look at the ASCII code there are the two characters '?' and '@' next to each other, one having bits 0 to 5 set and the other one having them cleared. So it is not too difficult to enter your data in a semi-graphical manner like this. Make sure the width of your editor window fits the line length. Avoid any line breaks.
const byte b0 = B00100000;
const byte b1 = B00010000;
const byte b2 = B00001000;
const byte b3 = B00000100;
const byte b4 = B00000010;
const byte b5 = B00000001;
const char dummy[] PROGMEM = {
"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
}; // serves only to help when editing the mask
const byte s = strlen(dummy);
// use OVERWRITE mode (Insert/Einfg)+ CapsLock-? to edit
// enter question marks for all pixels to be shown
// question mark: bits 0 to 5 are set
// at-sign: all bits 0 to 5 are zero
// don't forget to disable Insert & CapsLock when finished.
const char D[][s] PROGMEM = {
"@@@@@@?@@@@@@@@?@@@@@@@@@@@@??????@@@@@@@??????????@@??????@@@@@???????????@@@@@@@@@@@@@@@",
"@@@@@???@@@@@@@?@@@@@@@@@@@@?@@@@???@@@@@?@@@@@@@@@@@?@@@@???@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@@@@???@@@@@@@?@@@@@@@@@@@@?@@@@@@??@@@@?@@@@@@@@@@@?@@@@@@??@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@@@@?@?@@@@@@@?@@@@@@@@@@@@?@@@@@@@@??@@?@@@@@@@@@@@?@@@@@@@??@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@@@@?@?@@@@@@@?@@@@@@@@@@@@?@@@@@@@@@?@@?@@@@@@@@@@@?@@@@@@@@?@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@@@??@??@@@@@@?@@@@@@@@@@@@?@@@@@@@@@?@@?@@@@@@@@@@@?@@@@@@@??@@@@@@?@@@@@@@@@@@@@@@@@@@@",
}; const char B[][s] PROGMEM = {
"@@@@?@@@?@@@@@@?@@@@@@@@@@@@?@@@@@@@@??@@?@@@@@@@@@@@?@@@@???@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@@@?@@@?@@@@@@?@@@@@@@@@@@@?@@@@@@@??@@@?@@@@@@@@@@@?@@@???@@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@@??@@@??@@@@@?@@@@@@@@@@@@?@@@@???@@@@@????????@@@@??????@@@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@@???????@@@@@?@@@@@@@@@@@@??????@@@@@@@?@@@@@@@@@@@??@@@@@@@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@??@@@@@??@@@@?@@@@@@@@@@@@?@@@@???@@@@@?@@@@@@@@@@@?@?@@@@@@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@?@@@@@@@?@@@@?@@@@@@@@@@@@?@@@@@@??@@@@?@@@@@@@@@@@?@@?@@@@@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
}; const char C[][s] PROGMEM = {
"@@?@@@@@@@?@@@@?@@@@@@@@@@@@?@@@@@@@??@@@?@@@@@@@@@@@?@@@@?@@@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@@?@@@@@@@?@@@@?@@@@@@@@@@@@?@@@@@@@@??@@?@@@@@@@@@@@?@@@@@?@@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@??@@@@@@@??@@@*@@@@@@@@@@@@?@@@@@@@@??@@?@@@@@@@@@@@?@@@@@@?@@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@?@@@@@@@@@?@@@?@@@@@@@@@@@@?@@@@@@@??@@@?@@@@@@@@@@@?@@@@@@@?@@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"@?@@@@@@@@@?@@@?@@@@@@@@@@@@?@@@@???@@@@@?@@@@@@@@@@@?@@@@@@@@?@@@@@@?@@@@@@@@@@@@@@@@@@@@",
"?@@@@@@@@@@@?@@???????????@@??????@@@@@@@??????????@@?@@@@@@@@@?@@@@@?@@@@@@@@@@@@@@@@@@@@",
};
void setup() {
DDRB = 255;
DDRC = 255;
DDRD = 255;
}
byte x = 0;
void loop() {
PORTD = (pgm_read_byte(D[0] + x) & b0 |
pgm_read_byte(D[1] + x) & b1 |
pgm_read_byte(D[2] + x) & b2 |
pgm_read_byte(D[3] + x) & b3 |
pgm_read_byte(D[4] + x) & b4 |
pgm_read_byte(D[5] + x) & b5) << 2;
PORTB = pgm_read_byte(B[0] + x) & b0 |
pgm_read_byte(B[1] + x) & b1 |
pgm_read_byte(B[2] + x) & b2 |
pgm_read_byte(B[3] + x) & b3 |
pgm_read_byte(B[4] + x) & b4 |
pgm_read_byte(B[5] + x) & b5;
PORTC = pgm_read_byte(C[0] + x) & b0 |
pgm_read_byte(C[1] + x) & b1 |
pgm_read_byte(C[2] + x) & b2 |
pgm_read_byte(C[3] + x) & b3 |
pgm_read_byte(C[4] + x) & b4 |
pgm_read_byte(C[5] + x) & b5;
if (++x > s) x = 0;
delay(1);
}
That's all!
Comments