My LED Matrix Needs a Little TLC

J.B. Langston
Feb 27, 2012 · 10 min read

I picked up a set of 10 Sure Electronics 8x8 red/green LED matrices for about $13 on eBay. I figured I could build an Arduino-powered 24x24 LED sign to show messages, run Conway’s Game of Life, and play simple games, and still have one more 8x8 matrix left over to experiment with. I did eventually get a working prototype, but I had no idea of the rabbit hole that it would take me down.

The easiest way to get a matrix running on an Arduino is to drive the rows and columns directly from its I/O pins. For a quick introduction on how this works, refer to the Arduino row-column scanning tutorial. Although it’s very straightforward, this approach has two major deal breakers:

  1. The LEDs in a typical matrix are rated at 20 mA, so if all the LEDs in a row are on at once, the pin attached to the row would have to supply 160 mA to light the LEDs at full brightness. The Arduino can only source 40 mA per pin, so this limits us to 5 mA per LED, resulting in a dim display. Since only one row is lit at a time, the display is even dimmer.
  2. It requires 16 I/O pins just to drive a single color 8x8 matrix, so forget about driving multiple colors or scaling up to multiple matrices using this approach.

Another approach I investigated was to use the Maxim MAX7219 or MAX7221 LED display driver chips. The chip was originally intended to drive an eight digit, seven segment display, but can also drive an 8x8 matrix. Both Beginning Arduino and Arduino Cookbook cover this technique in depth. Arduino inherited a library from Wiring to drive matrices using these chips, so getting it working was very easy. Since these chips are purpose built for driving LEDs, they also provide plenty of current to drive the matrix at full brightness. Unfortunately, they also have several drawbacks:

  1. They’re expensive — the cheapest I found the MAX7219 was for $5.25 at Digi-Key.
  2. The chips were originally designed to drive eight digits on a seven segment display, so scaling up to multiple matrices is awkward. Driving multicolor matrices likewise requires multiple chips and is not straightforward.
  3. Each LED is either on or off using this chip, so it’s not possible to have multiple shades of color.

This brings me approach I finally settled on. I’m using a Texas Instruments TLC5940 16-channel LED sink driver for the columns and a Micrel MIC5891 8-bit serial input latched source driver for the rows. The TLC5940 has a number of advantages that sold me on it:

  1. This chip is a current sink, meaning that it attaches to the LED’s cathode and lights the LED by pulling the cathode toward ground. Since it can drive 16 channels, it is a perfect fit for an 8x8 bi-color common anode matrix like the ones I am using.
  2. It supports greyscale control using PWM, so the brightness of the red and green LEDs can be finely tuned to produce red, green, or any shade in between.
  3. It’s not exactly cheap, but it only requires three TLC5940s ($4.20 each) and six MIC5891s ($2.50 each) to drive a 24x24 bi-color matrix versus eighteen MAX7221s ($5.25 each). Therefore, at the best prices I could find we’re looking at a total of $27.60 for the driver chips with the TLC5940/MIC5891 combo versus $94.50 for the MAX7221s.

The MIC5891 is basically a standard shift register (a la 74HC595) which has PNP Darlington transistors built into its output stages, allowing it to source up to 500 mA per output. A standard 74HC595 could be substituted, but each of the eight outputs would need to be attached to an external PNP Darlington or P-channel MOSFET in order to source enough current for the rows.

One TLC5940 and one MIC5891 can drive one 8x8 red/green matrix or two 8x8 single-color matrices side-by-side. It is possible to add an arbitrary number of columns and rows by chaining together additional TLC5940s and MIC5891s. Eventually, my plan is to scale up to a single 8x8 matrix to a 24x24 matrix composed of nine individual 8x8 tiles.

To build a 24x24 matrix out of nine individual 8x8 matrices, the common columns and rows of each matrix must be tied together. As you can see in the picture below, I’ve got a lot of soldering to do.

As the number of columns increases, current demands for the rows will increase proportionally, so multiple outputs of the MIC5891 must be tied together to provide sufficient current. Each row in a 24 column red-green matrix requires approximately 960 mA if all 48 LEDs are on at full brightness. Therefore three TLC5940s and six MIC5891s should be sufficient to drive it.

I started out driving the chips using a Solarbotics Ardweeny, which is basically just a bare ATmega328 chip with a backpack PCB containing a ceramic oscillator, an FTDI-compatible header, some reset circuitry, and an LED attached to pin 13. It sells for $9 directly from Solarbotics.

However, the ATmega328 will not have enough SRAM to hold a 12-bit 48x24 frame buffer when I scale my matrix up. I also got tired of manually wiring up my AVR programmer each time I wanted to program it since the Ardweeny doesn’t have a 6-pin header for it. When I burned up one of my Ardweenies by accidentally wiring up the wrong pins to an external power supply, this was the last straw.

My search for a better alternative lead me to the Sanguino which sells for $25 from MakerBot Industries. It is a breadboardable Arduino-compatible ATmega644 board with a built-in regulated power supply as well as ISP, JTAG, and FTDI headers. The ATmega644 has more I/O pins, an extra timer, two USARTs, twice the SRAM and 4x the Flash of the ATmega328 used on standard Arduinos.

For even more horsepower, I upgraded the Sanguino’s MCU from a stock 644 to a 1284 and replaced the 16MHz crystal with a 20MHz crystal. The 1284 is the most powerful non-SMT AVR chip that you can buy. Compared to the 644, it has 4x the SRAM at 16K and twice the flash at 128K. This should provide enough room for the frame buffer with plenty of space left over to write interesting programs to use it. The 1284 is a drop-in replacement for the 644, so using it in the Sanguino did not require any special considerations.

As it turns out, finding the right hardware was the easy part. Driving the TLC5940 is not exactly simple. I first came across the TLC5940 Arduino Library by Alex Leone, but this library is not intended to drive a multiplexed matrix. There is an alpha-quality version of the library that does multiplexing, but it’s poorly documented and the example code uses a 74LS138 3:8 line decoder to control the rows instead of the shift register I had decided on. I tried to read the library’s code but it left me mystified.

Luckily, Matt Pandina has written a mini-book called — appropriately enough — Demystifying the TLC5940. Matt takes an iterative approach to developing a library for the TLC5940. First he implements a verbatim translation of the TLC5940 programming flow chart. Then he gradually improves the library, adding features such as hardware-driven clock output, timer-based interrupts, and SPI communication. This was exactly what I needed to understand the hardware and the code well enough to make the necessary changes to get my matrix working. I based my initial code on the final library from Chapter 7 of his book and got a prototype 8x8 matrix working.

Matt has done some really interesting work on his library since writing the tutorial. He’s improved the performance of the library by 1.75x using the AVR’s built-in USART and implemented multiplexing for use with an RGB POV toy that he’s blogging about on Google+. He hasn’t gotten around to updating the book to cover his latest changes, but he has released the new source code on the book’s website. It is in the ch9 subdirectory of src.zip.

The new code is multiplexing the anodes of his RGB LEDs using some P-channel MOSFETs attached directly to the pins of the AVR, while the cathodes are attached to the TLC5940. This is fine for his purposes since he’s only multiplexing three anodes, but it won’t work for me since my matrix will eventually have 24 common-anode rows. Therefore, I needed to adapt his code to use a shift register to drive the rows instead.

When I e-mailed Matt about the new code, he pointed me towards the following line in the ISR in tlc5940.c which directly toggles the pins on the AVR that are attached to the rows:

MULTIPLEX_PIN = toggleRows[row]; // toggle two pins at once

I’ve changed that line to the following block of code, which will drive the rows using the shift register instead of directly:

#if (TLC5940_USE_ROW_SHIFT_REGISTER)if (row == 0)
setHigh(ROW_SIN_PORT, ROW_SIN_PIN);
else
setLow(ROW_SIN_PORT, ROW_SIN_PIN);
pulse(ROW_SCLK_PORT, ROW_SCLK_PIN);
#else
MULTIPLEX_PIN = toggleRows[row]; // toggle two pins at once
#endif

Since the TLC5940 is handling the latching and blanking on the columns, I simplified the operation of the shift register by tying the latch and blank pins high and low, respectively. That means that all my code has to do is toggle the shift register’s serial data and clock pins in such a way that the shift register lights one row at a time. When the ISR is handling row 0, I set the shift register’s serial data pin high so that it will turn on the first row when the clock pin is pulsed. On all the subsequent rows I set the serial data pin low and send out a clock pulse, causing the lighted row to shift down one. When the ISR cycles back around to row 0, I repeat the process.

Next, I added the following block to TLC5940_Init which sets up the output pins for the shift register and shifts out any garbage the shift register may contain when it is powered up:

#if (TLC5940_USE_ROW_SHIFT_REGISTER)
setOutput(ROW_SCLK_DDR, ROW_SCLK_PIN);
setOutput(ROW_SIN_DDR, ROW_SIN_PIN);

// Clear row shift registers
setLow(ROW_SIN_PORT, ROW_SIN_PIN);
for (int i = 0; i < TLC5940_MULTIPLEX_N; i++)
pulse(ROW_SCLK_PORT, ROW_SCLK_PIN);
#endif

I also added #if (!TLC5940_USE_ROW_SHIFT_REGISTER)to the declaration of toggleRows in tlc5940.c and tlc5940.h, so the variable doesn’t take up memory if it’s not needed.

Finally, when I moved to the ATmega1284, I discovered that the ports and pins used by SPI and USART are different than on the 328. Therefore, I have conditionalized the code in tlc5940.h that defines these pins so that they are defined appropriately depending on which chip the code is being compiled for.

I’ve conditionalized my code so that the use of the shift register is configurable. The TLC5940_USE_ROW_SHIFT_REGISTER define controls whether the library uses the shift register or directly toggles the anodes using the pins on the AVR. The ROW_SCLK_DDR, ROW_SCLK_PIN, ROW_SCLK_PORT, ROW_SIN_DDR, ROW_SIN_PIN, and ROW_SIN_PORT defines configure which pins are used to drive the shift register. All of these defines have been added to the Makefile.

These were the only changes necessary to get the library working with my matrix. I have committed the code that I derived Matt’s optimized ch9 library to GitHub.

Matt’s library was not written with Arduino in mind, so I have been using AVR Studio to program my matrix, and I have not tested any of my recent code with the Arduino IDE, bootloader, or libraries. Eventually, I would like to make the library as Arduino-compatible as possible, but this is not currently my top priority.

The library now optionally uses the AVR’s USART to drive the TLC5940 up to 1.75x faster. When this option is enabled, it is definitely not Arduino-compatible since Arduino needs control of the USART to load code via the bootloader. With the USART option disabled, the code should still be marginally compatible with Arduino, subject to the following caveats. I have not tested it, but if you are the adventurous type, feel free to give it a shot and let me know if it works for you. I’ll try to help with any issues you encounter.

The library requires the CKOUT fuse bit to be enabled so that the clock signal is output on digital pin 8 (pin 14 of the ATmega328). This requires re-burning the bootloader on your Arduino with a different set of fuse bits. You will need an ISP or a second Arduino programmed as an ISP to burn the bootloader.

Once you have your hardware wired up to reburn the bootloader, find the file hardware\arduino\boards.txt in your Arduino directory and edit it as follows:

  1. Find the section like the one below that corresponds to your Arduino and paste a copy of it at the top of the file (don’t change the original section).
  2. Add clko to the first part of each line. In this example, I have changed atmega328 to atmega328clko on each line.
  3. Change the name of the section to indicate CLKO is enabled as I have on the atmega328clko.name line below.
  4. Change the line containing bootloader.low_fuses from 0xFF to 0xBF as shown below.
atmega328clko.name=Arduino Duemilanove w/ ATmega328 (CLKO enabled)

atmega328clko.upload.protocol=arduino
atmega328clko.upload.maximum_size=30720
atmega328clko.upload.speed=57600

atmega328clko.bootloader.low_fuses=0xBF
atmega328clko.bootloader.high_fuses=0xDA
atmega328clko.bootloader.extended_fuses=0x05
atmega328clko.bootloader.path=atmega
atmega328clko.bootloader.file=ATmegaBOOT_168_atmega328.hex
atmega328clko.bootloader.unlock_bits=0x3F
atmega328clko.bootloader.lock_bits=0x0F

atmega328clko.build.mcu=atmega328p
atmega328clko.build.f_cpu=16000000L
atmega328clko.build.core=arduino
atmega328clko.build.variant=standard

Save the file. Now open Arduino, and under Tools>Board, select the entry that you just created (e.g., “Arduino Duemilanove w/ ATmega328 (CLKO enabled)”). Select Tools>Burn Bootloader and wait for the process to complete. The CKOUT fuse bit will now be set on your Arduino. After doing this, you will no longer have control over digital pin 8. If you want to return pin 8 to normal operation, you will need to select your original board from Tools>Board and burn the bootloader again.

Once you have finished reprogramming your Arduino’s fuse bits, make the following connections between the Arduino and the TLC5940/MIC5891:

Arduino        TLC5940     MIC5891
Digital 2* 2 SERIAL DATA IN
Digital 3* 3 CLOCK
Digital 8 18 GSCLK
Digital 9* 24 XLAT
Digital 10 23 BLANK
Digital 11 26 SIN
Digital 13 25 SCLK

Also, due to this library’s use of Timer 0, it will interfere with the millis(), micros(), delay(), and delayMicros() functions in Arduino. The library could probably be modified to use a different timer, but I have not tried yet.

Once I get my full 24x24 matrix working with the library in it’s current form, I will evaluate changes to further improve compatibility with Arduino.

Adventures in Electronics

Chronicles of my hobbyist electronics projects

J.B. Langston

Written by

Computer and Electronics Nerd

Adventures in Electronics

Chronicles of my hobbyist electronics projects