Tuesday, August 2, 2011

Persistence Of Vision (with full color graphics)

This is my Graphical Persistence of Vision display.

It uses 16 RGB LEDs to display full color 24-bit bitmap files created by the MS Paint tool. What is especially cool about this project is that unlike many common POV systems, this one displays full color graphics, not just text.  Above is a Youtube video showing the POV in action.

Initial Designs:
An early design with 8 RGB LEDs and 3 shift registers.
The TV remote was used to change program settings
while the device was spinning on the ceiling fan.
Initially, the POV was created using 8 red LEDs, a shift register and an Arduino.  This allowed for single color text based images.  Once I gained experience and learned the concepts the design moved on to use 8 RGB LEDs.  For this I used 8 common-cathode RGB LEDs, 3 shift registers (parallel out), and an Arduino.  Considering that each LED had 3 available colors in it I needed 3 times the number of I/O lines, which was 24.  By daisy-chaining 3 shift registers I was able to have the 24 I/O lines needed for my LED array.  In this version, each color was assigned a specific shift register.  If I wanted only blue lights then only that shift registers would receive 1's.  If I wanted PURPLE then I would send 1's to the RED and BLUE shift registers.  This allowed me to combine colors as well required complex methods to store color information in the graphics.  I did not have an efficient means at this point to use multiple colors in the same image so I would pretty much would just change the color of an entire image rather than have an image with multiple colors.  Plus, the color palette was limited so I was only able to display text and very simple single color graphics.  Each single-color image required a 36 byte array.

Sample Images of this POV version:

16 Color Bitmaps:
I wanted to develop an easier method of adding color to my images so I started to research bitmaps.  I first tried 16 color bit maps because they were small file size and stored each pixel as a color.  This Arduino code was the first to utilize MS Paint bitmap files and was much simpler than my previous attempts at adding multiple colors to an image.  My code would cycle through each byte in the bitmap, determine what color it represented, convert the 16 colors from the image to the limited available 8 color palette and then display it row by row.  The code was a radical upgrade from previous code that used online POV generators to create simple graphics and fonts for text.  However, the code was never utilized on any board.  I could only combine solid colors such as BLUE and RED to make PURPLE or all the colors to make WHITE.  I only had 8 colors available if you accept BLACK as a color.  Very shortly after this code was completed I studied the other MSPaint compatible bitmap formats instead and discovered how 24-bit bitmaps work. This was the last POV that used 8 RGBs LEDs with shift registers.  In this version, each pixel was 1 byte so a 16 by 16 pixel image would take use 256 bytes.

24-bit Bitmaps:
This Persistence of Vision project displays
full color graphics, not just text.
MSPaint 24-bit bitmaps store each individual pixel as a 3 bytes RGB value.  These values were not for just solid colors but for each and every value in between.  I was using common anode RGB LEDs with TLC5940 constant current PWM LED drivers.  By using the specific RGB data for each pixel I could actually display complete full color graphics by using the Pulse Width Modulation (PWM) abilities of the TLC5940 to vary the brightness of each color of the RGB LED.  Unfortunately, the size of these images for a 16 by 16 pixel image would now be 3 times larger because 3 bytes per pixel were required, for a total of 768 bytes per image, which introduced a new problem to solve.  The SRAM in the Arduino simpily could not hold many images this way so I needed a solution to better utilize memory.  It was suggested to me that I store the images in FLASH instead of the normally used SRAM.  PROGMEM is kinda complex and I wanted to design an easy way to use it.  It is difficult because of how pointers work with the architecture used by the Arduino.
This is a sample of the graphical ability of my
P.O.V. display.  I mounted it to my ceiling fan.

PROGMEM and Address Spaces:
The Arduino uses the Harvard Architecture which according to Wikipedia means "The Harvard architecture is a computer architecture with physically separate storage and signal pathways for instructions and data." (Wiki link is below.)  Each variable is stored at a specific memory address and with Harvard Architecture systems there are two separate address spaces for SRAM and for FLASH storage.  Normally, the program itself stays in FLASH and the active code and variables/string values reside in SRAM.  When you are accessing a variable value, you really are just accessing a type of pointer to a memory address that contains the value for that variable which normally would be in SRAM.  However, if you store that value in FLASH instead then the value actually resides in a completely different memory address space, even if it had the same numerical address.  Therefore, variables in PROGMEM (FLASH) must be stored and retrieved in a specific way which is rather complicated for the typical user.  Fortunately, Mikal Hart, who runs the Arduiniana website, created an easy to use library to facilitate the use of PROGMEM.  Suddenly, it was very simple to store large arrays in FLASH.  In fact, it was so easy that now I store each and every text string in flash as well for my programs which save me a considerable amount of SRAM.  Running out of SRAM in Arduino is not fun at all and is very confusing because your program just crashes and you don't know why.  Even the functions available on the Internet that determine how much SRAM is left are somewhat confusing as you  really probably aren't running out of SRAM but actually no longer have enough heap/stack space.  The heap and the stack are very low-level terms in machine language.  Basically, you have to have enough SRAM left to handle the heap/stack but that value may very from program to program.  Still, the function is very useful.  It is found in the SD.h library which is included in the Arduino IDE 0022.  The specific file in the library that has the function is "sdfatutil.h".  I am not exactly sure how this function works as it goes very low level, but it has been a reliable indicator of SRAM.

The FreeRam() function is below.  You can just copy and paste it to your program.  

static int FreeRam(void) {
  extern int  __bss_end;
  extern int* __brkval;
  int free_memory;
  if (reinterpret_cast<int>(__brkval) == 0) {
    // if no heap use from end of bss section
    free_memory = reinterpret_cast<int>(&free_memory)
                  - reinterpret_cast<int>(&__bss_end);
  } else {
    // use from top of stack to heap
    free_memory = reinterpret_cast<int>(&free_memory)
                  - reinterpret_cast<int>(__brkval);
  return free_memory;

Creating C "Prototypes"
Now that I had the flash library I encountered another problem very specific to the C language and how the Arduino software compiles code.  In C, each and every function needs what is called a "prototype."  This "prototype" defines the function name, what the datatype of each value passed to it will be, and what datatype the function returns if any.  I wanted to pass the array for my images to a drawing function but it wouldn't work.  It turns out that the Arduino software does not recognize the "objects" (or data types) defined by the flash library.  Normally, the Arduino would create the prototypes for you for all of your functions but it could not create prototypes for functions that have "objects" that it does not recognize.  The solution was simple.  I just had to manually enter the prototypes for each function that was passed a value that was located in FLASH.  This is definitely useful information to the beginner.

Here is the technical explanation from Mikal Hart:  
"   The FLASH_STRING macro creates and object of type _FLASH_STRING.
FLASH_ARRAY creates an object of type _FLASH_ARRAY.
FLASH_TABLE creates an object of type _FLASH_TABLE.   "

Again, these objects are not recognized by the Arduino compiler so you'll need to add the appropriate prototypes manually to the top section of your code for any function that will use these new objects.  Notice how in the following example that I just define the object types that will be passed to the particular function and the value that the function may return."FullDrawBitmap" accepts a "_FLASH_ARRAY," a "word", and a "boolean".  "DrawWith16RGB" accepts a "_FLASH_ARRAY", and a "word" value.  This is pretty much all that you need to do to handle this prototype problem.

C Prototype Example:  
void FullDrawBitmap    (_FLASH_ARRAY, word, byte, boolean);
void DrawWith16RGB (_FLASH_ARRAY, word);  

Direct Port Manipulation vs digitalWrite()
The code in this latest version reads through the bitmap data backwards due to the order that the data is stored in the bitmap.  Each row of the image has 16 pixels or LEDs, and each LED/pixel has 1 byte for Red, Green, and Blue.  Therefore, each row reads in 48 bytes of data per row.  The TLC is a 12-bit PWM device with 16 channels on each chip.  To handle the 48 lights I would need 48 channels or 3 chips.  In total, this is 48 * 12 == 576 bits per column (or 72 bytes per column.)  All in all, that is 48 bytes for each row of the image and another 72 bytes to actually send it for a grand total of 120 bytes (or 960 bits) for each and every row of the supplied image.  Yikes!  Plus, considering the RPM of my ceiling fan, I needed to keep the LEDs on for very short periods of time, between 200 - 800 micro seconds (not milli, micro.)  Every 200 microseconds, the SPI bus that I use to control the TLC5940 chips must be clocked 576 times!  That's pretty fast.  So fast in fact that you actually can not do it fast enough with normal digitalWrite() commands.  I was using a library (link below) to facilitate the communications to the TLC5940 chips and it used direct port manipulation.  Researching on the internet, I found that digitalWrite actually is much slower and in my situation, without direct port manipulation I would have been unable to complete the project.

What's Next?
I like the design and believe that it is solid in its current form but there is definitely room for improvement.
  • For starters, since the new design I have been unable to use my TV remote to control the project whilst in motion because the TLC5940 library uses both timer1 and timer2.  The IR Remote library needs to have timer1 and when I put them both together they could not cooperate.  I have been using a Bluetooth adapter connected to the Arduino's serial port but I believe that this is an overly complicated solution.  
  • Using the TLC5947 will solve the timer problem because it has its own built-in GSclock and operates much like a shift register and not requiring both of my timers to use it.  The TLC5947 is a surface mount IC so I will need to create a PCB design and etch it.  The TLC5947 is a 24 channel driver so I'll actually need one less chip.  
  • Since I will be using the physically smaller TLC5947 with 24 channels each I would like to use 32 surface mount RGB LEDs for greater resolution.  Cost will be a concern so I'll need to be frugal.
If you are developing a POV device, or working with PROGMEM/FLASH then hopefully this post was helpful to you.  Definitely take a peek at some of the references/links below as they were very helpful to me in creating this project.

References / Links:

POV Message Generator

Simplified Windows BMP Bitmap File Format Specification

Write your own 24-bit BMP

Texas Instruments TLC5940 - Arduino Playground

An Arduino Library for the TI TLC5940 16-Channel PWM Chip

Read, Set, Oscillate!  The Fastest Way to Change Arduino Pins

PROGMEM - Arduino

Flash - Arduiniana - Arduino wisdom and gems by Mikal Hart

Ken Shirriff's blog- A Multi-Protocol Infrared Remote Library for the Arduino

Harvard Architecture - Wikipedia

Propeller Clock Timing Theory

A FAT16/FAT32 Arduino library for SD/SDHC cards


1 comment:

Keep it clean. :)