POV Reloaded

My persistence of vision sketch got quite a lot of attention. To be more precise its pictures got a lot of attention. So I think this sketch deserves a little bit of what is known as developer gold plating. That is the script is quite fine but I want to improve its implementation beyond what is really necessary.

So what is wrong with it in the first place? Well, I hate redundant comments. The comments for the picture data repeat the data. This was to make it more readable. However it also means that any change to the data needs a change to the comments. I justified this for myself by the idea that all this stuff will be generated from pictures anyway. Still I did not like it. Here comes the gold plated solution.

#include <MsTimer2.h>
#include <avr/io.h>
#include <avr/pgmspace.h>

#define PARSE(pattern) ((pattern>>14) & 0b111111), ((pattern>>8) & 0b111111), (pattern & 0b11111111)

uint8_t const pov_pattern[] PROGMEM = {
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00001111110000000000 ),
    PARSE( 0b00111111111100000000 ),
    PARSE( 0b01110000001110000000 ),
    PARSE( 0b11100000000111000000 ),
    PARSE( 0b11000000000011000000 ),
    PARSE( 0b11000000000011000000 ),
    PARSE( 0b11000000000011000000 ),
    PARSE( 0b11000000000011000000 ),
    PARSE( 0b11100000000111000000 ),
    PARSE( 0b01110000001110000000 ),
    PARSE( 0b00111111111100000000 ),
    PARSE( 0b00001111110000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b11111111111111111111 ),
    PARSE( 0b11111111111111111111 ),
    PARSE( 0b00000011000000000000 ),
    PARSE( 0b00000011100000000000 ),
    PARSE( 0b00000111110000000000 ),
    PARSE( 0b00001110011000000000 ),
    PARSE( 0b00011100001100000000 ),
    PARSE( 0b01110000000111000000 ),
    PARSE( 0b11100000000011000000 ),
    PARSE( 0b11000000000001000000 ),
    PARSE( 0b10000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b11100111111111111110 ),
    PARSE( 0b11100111111111111110 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
    PARSE( 0b00000000000000000000 ),
};

void blink() {
	static uint16_t index = 0;

	PORTC = pgm_read_byte(pov_pattern+(index++));
	PORTB = pgm_read_byte(pov_pattern+(index++));
	PORTD = pgm_read_byte(pov_pattern+(index++));

	if (index >= sizeof(pov_pattern)) { index = 0; }
}

void setup() {
	DDRD = 0b11111111; // set digital  0- 7 to output
	DDRB = 0b00111111; // set digital  8-13 to output
	DDRC = 0b00111111; // set digital 14-19 to output (coincidences with analog 0-5)

	MsTimer2::set(2, blink);
	MsTimer2::start();
}

void loop() { }

As you can see the comments are not needed anymore to visualize the pictures content. The magic has two ingredients. First there is the “PARSE” macro. It will take any integer and split it into 3 bytes as needed for the three output ports. The other part are the compiler capabilities. The compiler already knows how to parse binary constants and it can resolve constant expressions at compile time. Thus the output of the PARSE macro are not 3 expressions but 3 constants.

You might wonder why I did not get this when I wrote my first POV script. There were two issues that mentally prevented me from doing this. (1) I had seen the file binary.h in the Arduino distribution that defines binary constants. This brought me somehow into the direction to believe that only byte sized constants would be feasible. (2) I just did not get it how far I could push the macro processor. As you can see in the removing flicker experiments I learned a lot since then.

Of course this calls for a slightly improved POV generator as well.

#!/usr/bin/python
# -*- coding: utf-8 -*-

#
#	www.blinkenlight.net
#
#	Copyright 2011 Udo Klein
#
#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program. If not, see http://www.gnu.org/licenses/


import sys
from PIL import Image

# double check that there is at least one argument (the file name)
if len(sys.argv) != 2:
    print "Please specify a source filename"
    sys.exit(-1)

# determine source file name
sourcefilename = sys.argv[1]
# strip extension
name = sourcefilename.split(".")[0]

# let PIL determine the proper graphics file type
image = Image.open(sourcefilename)

# convert image to black and white
image = image.convert("1")

# get hold of image size
(xsize, ysize) = image.size
# ensure height is 20 pixels (=number of LEDs)
if ysize != 20:
	print "Image height must be 20 pixels but is {0} pixels".format(ysize)

# output common start of program
print """#include <MsTimer2.h>
#include <avr/io.h>
#include <avr/pgmspace.h>

#define PARSE(pattern) ((pattern>>14) & 0b111111), ((pattern>>8) & 0b111111), (pattern & 0b11111111)

uint8_t pov_pattern[] PROGMEM = {"""

# generate the bit pattern for the LEDs
for x in range(0, xsize):
	line = "    PARSE( 0b"
	for y in range(0, ysize):
		line = line + ('0' if image.getpixel((x, ysize-1-y)) != 0 else '1')
	line = line + " ),"

	print line

# output common end of program
print """};

void blink() {
	static uint16_t index = 0;

	PORTC = pgm_read_byte(pov_pattern+(index++));
	PORTB = pgm_read_byte(pov_pattern+(index++));
	PORTD = pgm_read_byte(pov_pattern+(index++));

	if (index >= sizeof(pov_pattern)) { index = 0; }
}

void setup() {
	DDRD = 0b11111111; // set digital  0- 7 to output
	DDRB = 0b00111111; // set digital  8-13 to output
	DDRC = 0b00111111; // set digital 14-19 to output (coincidences with analog 0-5)

	MsTimer2::set(2, blink);
	MsTimer2::start();
}

void loop() { }
"""

As you can see the new POV generator includes the PARSE macro into the generated code. Accordingly the data lines are now “self commenting” as desired.

Christmas is getting closer so my first application for this was a small picture I created with the GIMP. I just spelled out “Merry Christmas” and generated the corresponding sketch. The picture was again taken from a tripod. It is slightly out of focus in order to blur the LEDs a little bit.

Merry Christmas Blinkenlights

If you have fun playing around with this sketch post put your results into the comments. I am eager for feedback.

2 Responses to POV Reloaded

  1. Karel Lootens says:

    Hi,

    Im adapting this sketch for the Leonardo which has a slightly more complicated port layout.
    I won’t bother with rearranging the bits as I can do that on hardware side, but I was wondering: why do you need the 0&b111111 in the PARSE macro?

    I would suppose that as all bits in after the & operator are always 1 it would not make a difference. it does allow for “inverting the sign” but you did not mention this so i’m not sure.

    Thanks,

    Karel

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.