Arduino Uno Controlled 48x16 Color LED Array With Text and Animation
by electric_piano_5k in Circuits > LEDs
1690 Views, 23 Favorites, 0 Comments
Arduino Uno Controlled 48x16 Color LED Array With Text and Animation
I previously made an Arduino controlled scrolling text display with a single 16x16 color LED array. This Instructable improves on that with a 48x16 array made up of three 16x16 LED panels, controlled with Arduino Uno. Controlling 3 LED panels really maxes out the RAM and program memory and computation ability of the Arduino Uno, I had to use some programming tricks to make it work, but as you can see in the video, the result is pretty good! Read on if you are planning to make your own multi-panel LED array, even if you are planning to use a more powerful processor, maybe you can make use of some of the programming tricks and animation algorithms that I used.
Supplies
You need:
Three 16x16 LED arrays (WS2812b type). Before you get too ambitious and try to use more panels, read the discussion below on memory, power, and refresh rate.
An Arduino. I used Uno, but any Arduino should work. I really wanted to use an Arduino with ESP32, but I wasn't able to because my laptop is too old to run the latest Arduino IDE. Anyway I was able to do what I wanted with just the Uno.
Two 5V power supplies, one able to supply 30 or 40 amps to power the LED arrays and one to power the Arduino. I used an ATX computer power supply to provide both, using the main 5V power for the LED's and the 5V "standby" power for the Arduino. Read the discussion below about power requirements to be sure you are using the right kind of power supply.
A piece of thin plywood (I used chipboard because that's what I had) about 8 inches by 21 inches, to mount the LED panels onto.
Double-sided tape. I used "Alien Tape" which is very sticky, but other types of double-sided tape would work.
Wires, connector pins, solder, and other tools and supplies that you normally need when doing anything with Arduino.
Memory, Power, and Refresh Rate
Before doing anything, it is important to consider the memory and computation speed limitations of the Arduino, and the power requirements of the LED arrays. If you are exactly duplicating what I did, you can skip this section, but if you want to consider other array sizes or other types of animations, please read this section carefully.
REFRESH RATE: I was initially planning a larger display with 6 LED panels, but when I did some calculations of the expected refresh rate, I decided on a smaller display with 3 panels. The WS2812b LED's have a serial data interface with 800 kHz data rate. Each LED requires 1 byte (8 bits) of data for each of red, green and blue intensities (24 bits total). Three 16x16 LED panels have a total of 768 LEDs. That's a total of 18,432 bits of data to refresh the display. At 800 kHz data rate, the maximum refresh rate is then 43 frames per second, allowing zero time for Arduino computation between frames. With 6 panels the maximum refresh rate would be 22 frames per second, which is getting rather slow (allowing for some computation time), and this is why I decided to go with a smaller display. The final project with 3 panels achieves about 15-20 frames per second depending on the animation being displayed, so the Arduino processing is consuming about half the refresh time and the data transfer the other half. With 6 panels, the real refresh rate would likely be 7 to 10 frames per second which would look slow and choppy. I think with 3 panels I hit just the right balance between display size and refresh rate with the Arduino Uno. With a faster processor like ESP32, and using an LED control library like WLED which can control multiple arrays on different data pins simultaneously, you could make a larger display.
MEMORY: The Neopixel library uses 3 bytes of RAM memory to store the data which is to be written to each LED in the array. So, for 768 LEDs, 2304 bytes of RAM are required. A basic Arduino like Uno has only 2k bytes of RAM, so here is an apparent problem. The solution to this is to tell the Neopixel library your array has 256 LEDs (one panel only) and connect each panel to a different data pin. Then you can select the first data pin using Neopixel's setPin() command, write the data to those 256 LEDs, select the second pin, write the data for the next 256 LEDs, then select the third data pin and write the data to the last 256 LEDs. In this way you can drive all 768 LED's using only the memory needed for 256 LEDs (768 bytes). An Arduino with ESP32 would not require this trick, it has plenty of RAM and a faster processor, but unfortunately the computer I am using for Arduino programming is old (Windows 7) so I can't run Arduino IDE versions that support ESP32. You also need to store images that you want to display. I stored these in program memory (flash memory). The Arduino Uno has only 32k bytes of flash memory so I have to be careful to use animations that don't require too much stored data. The animations I used mostly consist of moving fixed images across the screen, so I needed to store the image only once, I didn't need to store each frame separately.
POWER: Each LED uses about 50 mA of current if set to full white intensity (red, green, blue) = (255, 255, 255). So, for 768 LED's, the total current could be as high as 38 amps if all LED's are set to white. So you need a power supply with at least this current capacity, or, you need to be sure to use lower brightness to avoid drawing too much current from the power supply. I used an old ATX desktop computer power supply rated for 40 amps at 5V. Be sure to read the specifications of the power supply you are using, often there is a minimum current as well as a maximum current. If your power supply has a minimum current, you will need to add a power resistor to draw the minimum current from the power supply in case all LEDs are turned off. Use the formula R = V/I to calculate what resistance value you need. For example, if your power supply requires a minimum of 2A, you need a 2.5 ohm resistor. The power dissipated by the resistor will be VI, so in this case 5V*2A = 10W, so the resistor must be rated for at least 10 watts. A resistive electrical device such as a light bulb (a car headlight perhaps) or a heater element would be a good dummy load. Fortunately, my power supply had a minimum current of 0 amps, so I did not need to add a resistor.
I needed to use a separate 5V power supply for the Arduino. If I connected the LED array and the Arduino to the same power supply, the Arduino would crash when a lot of LED's were lit, due to fluctuations on the 5V power when many LED's were being turned on. It is always good practice to power the Arduino separately if you have high current devices in your project. In my case I used the 5V "standby" supply of the ATX power supply, which normally powers only the power switch circuitry when the computer is off, but has plenty of current capability to power an Arduino.
Assemble the Display
Measure the panels and decide how much border you want around the panels. My panels were 6-1/4" square, and I wanted about 1" border, so I cut a piece of chipboard to a size of 20-3/4" x 8-1/4". I measured where the wires were going to be on the back of the LED panels and drilled a 1-1/4" hole at each wire location. NOTE: Being a newbie at making multi-panel LED displays, I oriented my panels in the orientation that the writing on the panels was right side up. This is WRONG! The panels should be oriented so that the Data Out (DOUT) connector on one panel is as close as possible to the Data In (DIN) connector on the next panel. If you do this, you will have 9 holes in a row near the top of the display, instead of three columns of holes as in my board. If you orient the panels in the correct way, and if you string your panels together to control them as one array, programming will be easier because the same serpentine pattern will continue into the next panel. I was still able to make it work, and in the end because I had to control each panel individually because of the Uno's memory limitation, it really didn't make a difference for me. If you want to use my program, of course you will have to orient the panels the same way I did, but I would encourage you to do it the proper way so you will have an easier time writing your own program.
I painted the board with primer, and the midnight blue was the darkest color I had in my leftover paint cans so I used that. The paint does make it look better than an unpainted board, but also helps the double-sided tape stick to the wooden board.
I put masking tape on the board to indicate where the edge of the panels should be, this makes it easier to get the panels installed in the correct positions. Apply double sided tape to the board, then stick the panels in place, being careful to put the wires through the holes in the board.
Connect the +5V and GND wires on the back of each display to the 5V power supply. On a computer power supply (and most other power supplies), +5V is a red wire and GND is a black wire. It is highly recommended to use the separate 5V and GND wires on each LED panel for this, do not rely on the connectors to transfer power from one panel to another, the connectors may overheat. Protect all of your connections with heat shrink tubing or electrical tape. NOTE: ATX power supplies require the green and black wires to be connected together in order for the main power to turn on.
I connected the Data In wire of the first LED panel to pin 5 of the Arduino. Data In of the second panel goes to pin 6, and Data In of the third panel goes to pin 7. If you have an Arduino with more than 2k RAM and want to control all the panels as one array, connect the Data Out of the first panel to the Data In of the second panel, and the Data Out of the second panel to the Data In of the third panel. You don't need the connector on the Data Out of the third LED panel, you can remove this one and use it for connecting Data In of the first panel to the Arduino.
Finally, connect Vin and GND on the Arduino to a separate 5V power supply. In my case I used the 5V "standby" power of the ATX power supply, this is the purple wire (pin 9). The ground of the Arduino and the ground of the LED panels must be connected together.
I attached the Arduino to the back of the board using two screws.
I taped white paper over the front of the LED panels to act as a diffuser, this is optional.
The Program
Install the Adafruit Neopixel library.
If you want to run my program, download the sketch and the .h files attached and place them in an Arduino sketch directory.
I would encourage you to try writing your own program. Here are some descriptions of what I did and how I made these animations work with minimal memory use and the fastest execution time.
My program cycles through 4 animations:
- Scrolling rainbow text.
- Fire effect
- Blinking eyes.
- Rising full moon with bat or witch flying past (alternates on each cycle)
The scrolling text and fire effect are part of the same program section, the fire effect only appears after the text has passed. I did it this way to make a nice transition between the text and fire, the fire appears at a certain point on the display as soon as the last letter has passed that point. The scrolling text is just a stored image (stored in program memory) which is moved across the display pixel by pixel. The text image was produced by creating the text in WordPad, copying to Paint, resizing to 16 pixels high, saving as a .png file, converting to {r,g,b} text compaitble with Arduino code, and copying to Notepad to save the {r,g,b} text as a .h file. Detailed instructions for this are given in my previous Instructable: https://www.instructables.com/Color-Scrolling-LED-Text-and-Images-With-Arduino/
The fire effect algorithm is a combination of algorithms from two sources. I took the color palette from this site:
https://www.hanshq.net/fire.html
I made a few modifications to the first elements of the color table, making the first elements dimmer and less blue. The main algorithm is mostly the one given here, which conveniently was written in c code:
https://lodev.org/cgtutor/fire.html
I had to make some modifications because the original code was made for a display with 128 rows and I have only 16 rows.
The blinking eyes are straightforward, just display the eyes image and turn the screen black for a short time to make the eyes "blink".
The rising moon is again just a stored image (48x48 pixels) which is scrolled upward across the screen. I move the moon image up by half a pixel each program loop, and I interpolate between adjacent pixels to get the value to display when the position is at a half pixel position. I accomplished this by having two pixel counters, on even loop numbers the two counters are the same and the "average" is just the same pixel value twice. On odd loop numbers the two counters differ by one and the value displayed is the average of two adjacent image values.
The bat or witch flying across the moon was done by multiplying the bat/witch {r,g,b} values and the moon {r,g,b} values, then dividing by 256. The bat and witch images are white {255, 255, 255} in the background and black {0,0,0} in the silhouette. The edges of the shapes contain some gray levels also, this is a result of the smoothing of the shape edges by the image processing software (Paint). Multiplying the moon color values by white and dividing by 256 does not change the values, but multiplying by black changes the value to zero. In this way, the bat or witch silhouette (complete with smoothed edges) is transferred onto the moon image. I made the bat and witch images have the same pixel dimensions to simplify the programming. There are two bat images, with wings up and wings down, the display alternates between the two to give the effect of flapping wings.
The rainbow text was done by a method similar to the bat and witch. The text was stored as white with a black background. As with the bat and witch silhouettes, the edges of the text contain gray levels to smooth the edges. I multiplied the {r,g,b} values of each pixel in the image by rainbow {r,g,b} color values depending on which row the pixel is in. This turned all the white or gray pixels in that row to the corresponding color, keeping the brightness of the original gray level. By changing the colors each loop cycle, the text color changes as the text scrolls.
To make the program execute as quickly as possible, I replaced any multiplication or division by factors of 2 (2, 4, 8, 128, 256) by bit shift operations >> (shift right, equivalent to divide) and << (shift left, equivalent to multiply). Multiply and divide operations require many clock cycles to execute, but a bit shift operation requires only one clock cycle, so this significantly speeds up the program execution. The bat and witch display algorithm required division by 255, I changed this to division by 256 so that I could use bit shift, the difference is not noticeable. The original fire algorithm made use of % (remainder) operations, but this requires a divide operation to determine the remainder, so I modified the program to remove all but one remainder operation. These changes made a noticeable improvement in the frame rate of the display.
Update: Grayscale Images for Reduced Memory Usage
In this version of the program, I converted the "Happy Halloween" image file (white text) and the bat and witch image files (black silhouettes with white background) to grayscale (one byte per pixel) instead of {r,g,b} (three bytes per pixel) to save on program memory. The way the animations work, these images are fundamentally grayscale anyway. I did not convert the moon image to grayscale, even though it is basically a grayscale image, because for future modifications (for other holiday seasons perhaps) one might substitute a color image instead of the moon. With these changes, the program uses only 64% of the available program memory instead of the previous version's 88%. So for example, one could have a much longer text display, or different text displays on alternate program cycles. The program uploaded here produces exactly the same display as the previous one, it just uses less program memory.
To convert the {r,g,b} image files to grayscale, I used an Arduino program "sketch_rgb_to_grayscale.ino" to write the new files on an SD card. I am sure there are easier ways to do the conversion, but I had the SD card reader and wanted to try it out. The conversion program simply saves one number per pixel, with a value that is the average of the red, green, and blue values for that pixel in the original image file. Some things to note when doing this:
- The Arduino SD library uses 8 letter file names. If you try to assign a longer file name, it will create an error.
- The SD library writes the file name in all capitals. The Arduino IDE will not recognize an include file with capital ".H" extension, it must be lowercase ".h", so the files had to be renamed with lowercase extension. This is annoyingly hard to do in Windows, what I did was open the file in Notepad and use Save As (select All files, not Text files) to create a new file with extension ".h" (that's why the "2" in each file name, I wanted to give it a name that was distinct from the original).
- The program will write the data to the end of the file, if it already exists. If you run the program multiple times, each run will append new data to the end of the previously written file. So, if you run the program and have to re-run due to some error, you need to first delete the previously written files from the SD card.