Introduction: AVR Dual RGB Matrix Driver
A friend of mine had a great idea for an awesome project. He wanted to create a pair of servo rigs that would mimic the movement of his hands using a leap motion. On top of the servos would sit some lasers so he could create some kind of laser-light show.
I talked him down from lasers to LEDs, and we eventually settled on using RGB LED matrices. He initially tried to use Rainbowduino to drive a single LED matrix, but it wasn't working out so well. I offered to design and build a custom driver board that could not only drive one matrix, but two, using a single ATmega328p - the same chip used on the Rainbowduino board.
Check out this Instructable where I teach you how to build the entire parent project!
So grab your soldering irons and order some parts. It's to do some real embedded systems design.
Step 1: Parts and Tools
As always, we need to gather a few materials before we dive into the project. There is a lot going on here, so be sure to double check the parts!
Parts and Components
- ATmega328p - 8 bit MCU
- 2 x RGB Matrices (These cheap ones on Amazon are what I used)
- 4 x 16 pin female header - standard spacing
- 1 x 3 pin female header - standard spacing
- 2 x 3 pin male header - standard spacing
- 6 x TLC5916 - 8 x Constant Current LED Sink
- HEF4094 - 8 bit SIPO latching register
- 8 x NTD2955 - P Channel MOSFET
- 8 x BS270 - N Channel MOSFET
- 9 x 0.1uF Ceramic Capacitor
- 2 x 22pF Ceramic Capacitor
- 16MHz Crystal
- 17 x 10k Resistor
- 4 x 4.7k Resistor
- 2 x 2.67k Resistor - You can use a 2.2k in series with 470 ohms
- 2 x 680 ohm Resistor
- Small normall-open momentary push button
- Perf Board(s) - To house soldered components
- 2 x 10uF Aluminum Capacitor - (Optional) Spaced out on boards to help with power line performance
Tools Used
- Computer - for programming and such
- AVR Programmer - This one from Sparkfun is fantastic.
- Soldering Iron and Solder
- Wire - some small gauge and some larger gauge to handle higher current levels
Step 2: Understanding Matrix Multiplexing
Multiplexing LEDs can be tricky, but we're working with RGB LEDs, so think of each RGB as three individual LEDs. For an 8 x 8 matrix, that is 192 total LEDs on a single matrix. Even though there are only 32 connection pins, it is still possible to individually control the color and brightness of every single LED.
Take a look at this diagram from the datasheet for the RGB Matrix I am using.
You should see that each "row" shares a common anode. Similarly, each column (of each color LED) shares a common cathode. To drive just a single LED, we will drive its shared anode HI and its cathode LO. To control the color of each individual LED, we will have to do some low level multiplexing.
There are eight rows of common anodes, so each row will only be on (powered) for 1/8th of the time. The trick is to switch between which row is on so fast that a human eye cannot detect it. In this way, the pins driving the LED cathodes will only control one row of LEDs at a time. As the active row is switched, new values for the cathode lines will have to be loaded.
Attachments
Step 3: Determining Matrix Pin 1
It was impossible to tell what pin was what just by looking on the matrices that I got. It is also unknown if the pin numbering wraps around the matrix in the same way that pins are numbered on an IC. The only real way to know is by applying voltages to a few pins until you see a pattern.
Using the schematic in the previous step, we can try to illuminate a single LED. If a positive voltage is applied to pin 17 with a negative applied to pin 1, the blue LED of row 1, column 1, should turn on.
DO NOT APPLY DIRECT BATTERY VOLTAGES TO THE PINS. Always use an inline current limiting resistor. A 10k resistor and 9V battery will work fine.
If the pin numbering is similar to that of an IC, pins 1 and 17 will be in opposite corners, if not, they will be directly across from each other.
Once you think you have found this single blue LED, test your findings by trying to turn on a few other random LEDs. Once you have the pin numbering down, be sure to mark pin 1!
I preferred to plug the matrix across two vertical breadboards for development; however, this meant my rows were actually columns. Because of this, I refer to the "rows" of the datasheet as "columns." I also start counting from 0 instead of 1, as in common in low-level control.
Step 4: Circuit Design
With a basic understanding of how to multiplex the LEDs, we can build a circuit to drive them. A single ATmega328p will be used with an external 16MHz crystal. This chip will rapidly switch control between the eight common anode columns (remember, these are "rows" on the datasheet). Since the current through all LEDs in a single row is far too much to be sourced by an MCU pin, I am using a HI-side MOSFET pair to source the LED current.
Remember a current source is on the positive side of a component. A current sink is on the negative side. You can think of it like this (in terms of common circuit design orientation, at least): a source pushes current, while a sink pulls it.
Originally, I was driving the transistors directly from the MCU, but I added an external 8bit shift register to do this in order to free IO lines for future capability expansion.
The current sinking is handled by a few 8 channel constant current sinks. For 2 matrices, 6 chips are required - one for each column of R, G, and B LEDs.
You should also notice that I have included an I2C connection. This will be used for external control of the LED colors and/or strobe patterns. In addition, the ISP lines are not shown on this schematic, but it is a good idea to include them in your circuit so you can actually program the chip! Lastly, there is a line called "servo control"
The only values that might change for you are the external resistors on the TLC5916 current sinks. These resistors set the current for the LEDs, and I will go over how to set these values in the next few steps.
I have included the circuit as a Cadsoft Eagle file as well. The servo control line is not necessary for this project.
Attachments
Step 5: RGB White Balancing Overview
On each LED current sink, there is an external LED connection R-EXT. This resistance sets the LED current; however, it will not be sharing that current, so we don't need to worry about power ratings.
According to the datasheet (which I attached for your viewing pleasure), the equation to determine the LED current is as follows: (1.25V / R-Ext) *15
The gain can actually be adjusted by a special control line sequence, but it's easiest just to find a resistance using the default gain and step size.
Now, there is a definitely science to properly white balancing LEDs; however, I was able to do it rather quickly just by simple tests and observations. In case you don't know, the individual colors created by an RGB LED are formed by combining various levels of red, green, and blue. If all three are fully on, a pure white light should be seen. Here are the basic combinations, with each LED being fully on or off.
- RED + GREEN + BLUE = WHITE
- RED + GREEN = YELLOW
- RED + BLUE = MAGENTA
- GREEN + BLUE = CYAN
If all three LEDs are drawing the same current, the colors will not look right. This is a result of the various intensities that different colored LEDs produce at different currents.
In the next step, we will determine the resistance values for each LED current sink.
Attachments
Step 6: RGB White Balancing
On my matrices, the blue LEDs were the most intense. The red seemed more intense than the green; however, I needed more red than green for a proper balance. With this in mind, the blue current is limited the most, followed by the green, with the red LEDs be driven the hardest.
- Start with a common resistance value such as 10k ohms.
- Assign this value to the blue LEDs.
- Use about half of this value for the green LEDs, such as 4.7k.
- Use about 1/10th of the blue resistance for the red LEDs, such as 1k.
- Enable all three current sinks and study the light, but be careful not to stare directly into the LED.
- Take note of the white balance... does it seem to be more of one color than another?
- Disable just one color (such as blue), then check the resulting mix.
- If the mix is too much of one color, increase that resistance.
- If the mix is too little of one color, decrease that resistance.
- Repeat from step 5 until you are happy with the mix.
- Get a second opinion from a fresh pair of eyes.
- Once happy, reduce all resistances equally to increase the overall current of the LEDs.
For my matrices, I settled on the following values:
- Red: 680 --- I_LED = 1.25V / 680 * 15 = 28mA
- Green: 2.67k --- I_LED = 1.25V / 2.67k * 15 = 7mA
- Blue: 4.7k --- I_LED = 1.25V / 4.7k * 15 = 4mA
The drivers can sink much higher levels of current, and the LEDs have higher ratings, especially only being on 1/8th of the time, but this was plenty bright for my purposes.
Step 7: Update the AVR Fuse Bytes
To drive this many LEDs, we need a lot of clock cycles - this means we will need to change the default AVR fuses which control the clock source.
The ATmega328p ships with the fuses set to run on the internal 8MHz oscillator. This clock is then stepped down to 1MHz internally. I decided to use an external 16MHz crystal. This is the same timing that many of the original Arduino boards used, and it also makes for nice, even numbers involved in the timing control.
Take note, programming the AVR fuses is very simple, but it is also the easiest way to brick a chip. You can easily disable programming all together or set the clock source to some strange frequency. Always double check your byte values before programming!
Load a terminal console, and issue the following command:
> avrdude
If this is an unknown command, then you will need to install some software - namely, the AVR-GCC compiler and the AVRDUDE programming tool. These are bundled in WinAVR or Atmel Studio, but can also be individually installed on any OS.
Using the amazing fuse calculator available here, we can see that to use an external 16MHz crystal without the clock divide by 8 option, we should change the low fuse to be 0xE7.
This is the basic command to change the low fuse:
> avrdude -p -c -P -U lfuse:w::m
If you are using my recommended AVR programmer, the following command will work, if not, you will need to replace some of the values with those matching your setup.
> avrdude -p atmega328p -c usbtiny -P usb -U lfuse:w:0xe7:m
If successful, you should see a success message. You can always verify the fuse bits with this command:
> avrdude -p atmega328p -c usbtiny -P usb -V
Step 8: Firmware - Part 1: Conceptual Overview
The provided library defines the following functions in pubnub/pubnub.h.
The complete source code is available in this GitHub repository.
Before I dive into the code, let's take a moment to go over all that is happening. Once the AVR is initialized, the main loop is responsible only for LED chase sequences. All actual control is handled in interrupt service routines. For this, the 8bit timer/counter 0 is used.
Conceptually speaking, the timer is used to indicate that it is either time to turn LEDs on, or time to turn LEDs off. What LEDs should be on or off is determined by a pair of multidimensional arrays:
Next, will talk about how the colors are actually being created by Timer 0.
// 1 bit for each RGB LED in each columnstatic void uint8_t leds[MATRICES][COLUMNS][COLORS]; // The actual color of each RGB (see color_8bit.h)static void uint8_t colors[MATRICES][COLUMNS][LEDS];
The leds array is 2 x 8 x 3 bytes - one byte for each color row (R, G, & B) in each column, in each array. Each bit in this byte represents one LED in that row. The colors array is 2 x 8 x 8 bytes - one byte to represent the desired color of every LED.
In order to have enough time to control both matrices, a resolution of 4 is being used. This may seem tiny compared to the resolution of 255 used in computer colors, but keep in mind that 4 x 4 x 4 = 64 unique colors. It is possible to increase this resolution some, but 64 unique colors seems like plenty for my purposes. In actuality, many of these colors were very similar, so I reduced this down to 44 in the modules/macros/color_8bit.h header file. This file also contains an array of the individual R, G, & B levels required to create every available color.
Next, will talk about how the colors are actually being created by Timer 0.
Step 9: Firmware - Part 2: Color Creation
The complete source code is available in this GitHub repository.
As previosly mentioned, one 8 bit timer is being used to control all of the LED colors. Every firing of the compare match A ISR signals a timing event. A resolution of 4 implies actual values of 0, 1, 2, and 3. A variable is used to count what ISR trigger time it currently is, and a switch-case statement is used to do a few different things at different times. When the count is equal to max resolution, nothing happens because all LEDs should stay on if they are already on, or stay off if they are already off. At 0, it is time to switch control to the next of 8 columns. All LEDs are then set to be on, but not actually turned on. The code then continues to to the default case which handles all other times.
In this final case, a loop iterates through every RGB LED in the active column for both matrices. If the current counter is equal to the off time for the R, G, or B LEDs for the desired COLOR, that LED is set to be off. Finally, the data is shifted out to the constant current drivers to actually turn the LEDs on or off. This shifting is a bit-bang version of actual SPI protocol, with each LED driver chip having a separate data line so they can all be loaded at once.
//----------------------------------------------- // LED Color Control Framework //-----------------------------------------------// Timer 0 Compare Match A Interrupt Service RoutineISR (TIMER0_COMPA_vect) { // Switch the Counter value from '0' to LED Max Resolutionswitch (OCR0A_cnt) { //-------------------------- // Max Resolution, don't do anything! // At this point, LEDs should stay on //--------------------------case MAX_COLOR_RESOLUTION: break;//-------------------------- // Start of LED Period //--------------------------case 0: // Start a positive pulse to shift through the control register// Turn all LEDs on// Continue to next case...//-------------------------- // All Other times // - Loop through all colors for all leds // - Turn individual LEDs off at the right time //--------------------------default: // Find LEDs to turn off...do { // Loop through every LED for both matrices// Update "leds" array with new values// Sample for the Red LED of the current row and column in matrix 0 this_color = colors[0][column][led]; if (OCR0A_cnt == COLOR_LEVELS[this_color][RED_LEVEL]) leds[0][column][RED_LEDS] &= ~(_BV(led)); } while ( ... ); // Shift control to next column// Transfer "leds" array data to LED driversdo { // Loop through 8 bits of a data byte (Bit Bang SPI)// Set data bits// Shift bit out to LED drivers } while ( ... ); // Latch data and enable LEDsbreak; } // Restart the countif (++OCR0A_cnt > MAX_COLOR_RESOLUTION) OCR0A_cnt = 0; }
Next, we'll talk about the default operation of the driver.
Step 10: Firmware - Part 3: Driver Operation
The complete source code is available in this GitHub repository.
Like I said before, this matrix driver is actually part of a bigger project. As such, the circuit is set up to act as a TWI (I2C) bus slave. The current code will run the default chase sequence (a pair of smiley faces), waiting for further instructions from the TWI bus. In terms of the larger project, a single data byte is sent with each bit in the byte representing a quandrant of the matrix to enable. Once data is received, the LOOP_QUADS sequence is enabled. This simply loops through all of the possible colors defined in modules/macros/color_8bit.h, displaying the colors on which ever matrix quadrants are enabled. If no data is received after so long (roughly 10 seconds), the matrices revert to the default chase sequence (a pair of smiley faces).
The entire TWI operation is handled in the TWI Interrrupt service routine. Although the ISR is only expecting one byte of data, it will be trivially easy to alter this code to handle more data bytes. Doing so would allow external control over all aspects of the matrix driver including setting individual LED colors, chase sequences, or even inactive display!
Step 11: Build the Circuit
I highly recommend you build the circuit on a breadboard and play around with the firmware before you build anything more permanent, but a soldered circuit is the end goal. You might find a bit of a different component configuration works better for your purposes, and it is always easier to switch parts on a breadboard than desolder something from a PCB.
I elected to hand solder everything to a pair of perf boards. Ideally, the entire circuit would be housed on a single board, but I wanted to lay the parts out nicely since this project will likely be on display somewhere. You could also design a custom PCB, but it wasn't necessary here.
Step 12: Wrapping Up
And that about does it! Like I said, these matrices are part of a much larger project, so the driver is set up to be controlled by some other circuit component.
Let me know if you have any questions, and be sure to check out my Instructable for the main project!