Recently I got a 8x8 RGB LED matrix from Jaycar and thought I'd see how I could drive it using an Arduino Uno (ATmega 328 P). This involves providing 32 outputs from a chip which only typically has 20 as well as using a technique called pulse-width-modulation (PWM) to manage not only on/off but brightness of the LEDs as well.
The LED matrix
The model I got is from Jaycar and you can see it here. It has common anodes on the rows and common cathodes for each colour in each column, so that makes 8 * 4 pins or 32 pins.
32 outputs
How do we get 32 outputs on a chip with only 20? A simple way to do this is using a serial-in, parallel-out shift register like the 74HC595. This is not an ideal chip to use because it is not designed for anything above low current applications, but I had a few lying around so I decided to use them.
A serial-in, parallel-out shift register allows you to send in a stream of 0s and 1s which are then used to set the pins of the chip high or low. In the case of the 595 series this allows you to control 8 pins using only 3 on your Arduino. They can also be daisy-chained and therefore you can control many more pins using only 3.
In my circuit I have 4 such shift-registers chained together, with the first one attached to the 8 common anodes and the remaining 3 connected to the blue, red and green cathodes. These 595 chips can source and sink current which is useful as we need a source for the anodes and a sink for the cathodes.
Remember that you need current limiting resistors to protect the LEDs and these should be on the cathodes, because only one LED on the common cathode can be lit at once. If you put the resistors on the anodes then as you light more than one LED in the row it will cause them to dim. If your LED matrix is swapped then you'd put the resistors on the anodes.
For higher power applications (like driving lots of LEDs) you can use the TPIC6C595 which works the same way.
Persistence of vision
Lighting an individual LED means connecting a single anode and a single cathode to the +v and 0v at the same time. You cannot control just one of them leaving the other constantly connected as this would lead to whole rows or columns of LEDs lighting up.
Therefore a trick called persistence of vision is used. This refers to the property of vision which causes you to see a light as on constantly when in fact it is being turned on and off very quickly. We can use that trick to light the LEDs one row at a time, cycling through the rows so fast that to the human eye they look to be constantly lit.
Typically the refresh rate to avoid flicker needs to be 100 to 200Hz (cycles per second). When I measured how often each row was being refreshed in my implementation it was close to 2.7kHz (2700 cycles per second), which leaves us lots of room to do other interesting things.
Mixing colours
Obviously it is easy to display the basic colours of red, green and blue as you can light those LEDs and leave the others dark.
Creating other colours is a case of additive mixing e.g. mixing red and green makes yellow.
So if our granularity is limited to LEDs being on and off we can create the following colours
Red | Green | Blue | Resulting Colour |
---|---|---|---|
On | Off | Off | Red |
Off | On | Off | Green |
Off | Off | On | Blue |
On | On | Off | Yellow |
On | Off | On | Purple |
Off | On | On | Cyan |
On | On | On | White |
Other colours
We can make other colours as well, by varying the brightness of the LEDs, this gives a greater possibility of colour mixing.
Varying brightness can be achieved using a technique called pulse-width-modulation or PWM. This allows you to vary the average power being delivered to a transducer by turning it on and off again very quickly and varying the duty-cycle of the wave form being fed into the transducer. For example a 100% duty-cycle means the current/voltage constantly flows, but a 50% duty-cycle means getting only half that.
Using this approach allows you to mix a colour like orange using 100% red and 25% green.
Implementing this on Arduino
Most Arduinos run on a 16MHz clock, so refreshing the matrix 100-200 times per second should be fairly easy.
How to use a shift register
There are three important pins when using a shift register
- Latch
- Clock
- Data
The latch pin is used to stop changes to the pins from happening until the new state is fully sent to the chip. You set the pin high when you start sending new data to the register and low when you've finished and you want the pins on the register to change.
The clock pin is a regular beat used to indicate the rhythm of sending data to the register.
The data pin is used to send each bit to the register at each clock beat.
Putting this together in some pseudo-code:
void send() {
setLatchHigh();
sendData();
setLatchLow();
}
Fortunately there is an existing function built in to Ardunio to 'shift out bits' called shiftOut
and there's a tutorial on how to use it here.
High performance
There's a downside to using in-built functions like shiftOut and digitalWrite. They are written to be friendly, not to be performant. IF you want to refresh large LED matrices and have enough CPU cycles left over to do something interesting then you will need to use some lower level constructs.
SPI
You can use the hardware SPI built into the ATmega328 in place of shiftout. It works the same way as shiftOut sends data to the shift register, but as it is implemented in hardware it is much faster.
Here's a basic example:
#include <SPI.h>
int latchPin = 8; // Pin connected to ST_CP of 74HC595
int clockPin = 13; // Pin connected to SH_CP of 74HC595
int dataPin = 11; // Pin connected to DS of 74HC595
void setup() {
// setup the pins
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
// setup SPI
SPI.setClockDivider(SPI_CLOCK_DIV2);
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
SPI.begin();
}
void loop() {
// set the latch pin high
digitalWrite(latchPin, HIGH);
// send bits to shift register
SPI.transfer(data);
// set the latch pin low
digitalWrite(latchPin, LOW);
}
Port registers
Instead of using digitalWrite to set the latch pin high/low, you can write directly to the port register for the pin you are using. This is normally not recommended as it makes the code harder to read and maintain, but if you are seeking top performance it is a good thing to do.
Here's another example:
// port register maniuplation macros
#define sbi(port, bit) (port) |= (1 << (bit))
#define cbi(port, bit) (port) &= ~(1 << (bit))
void setup() {
// do your setup things e.g. set pin modes and enable SPI
}
void loop() {
// set your latch pin high
// assumes your latch is pin 8
cbi(PORTB, 0);
// send data to shift register
// set your latch pin low
sbi(PORTB, 0);
}
Implementing PWM
There are 3 hardware PWM pins built into the ATmega328, but because we are using a shift-register, we have to implement this behaviour for ourselves. As I said earlier, in PWM, the duty-cycle is varied to change the average power being delivered to a transducer. We can do that by maintaining a counter and using this to decide if the pin should be on or off.
For example, by using a counting to 8, you can create 8 levels of brightness. Here's an example (using digitalWrite, but the same principle applies with port registers/shift-registers).
// this should set the led to 50% brightness
int pwmcounter = 1;
int brightness = 4;
int ledpin = PIN;
void setup() {
// do your setup things e.g. set pin modes and enable SPI
}
void loop() {
// reset the pwm counter if we need to
if(pwmcounter > 8) {
pwmcounter = 1;
}
// turn on pin if we should
if(pwmcounter <= brightness) {
digitalWrite(ledpin, HIGH);
} else {
digitalWrite(ledpin, LOW);
}
// increment the pwm counter
pwmcounter++;
}
Putting it all together
My code which goes along with the circuit digram above is listed on this Gist it uses hardware SPI, port registers and software PWM to create an 8 colour scrolling rainbow.
Here's a video of it working:
-- Richard, May 2020