Reverse engineering an 8×8 LED grid for Arduino

Recently I’ve been doing a lot of work with LEDs. For an upcoming class called DevFest I’ll be leading a group of ~60 coders in their first steps with Arduino and robotics. This course includes a getting started grab bag of fun electronics, including an LED panel. None of the parts came with any kind of documentation or API. I had to do some reverse engineering to figure out how they worked. This LED panel was both really frustrating and really fun to figure out.

Documentation hunt

The first thing I did is look for documentation. On one side of the grid was a smudged model number that said 1588Asomething. Searching online for LED 8×8 pins, LED 1588, and so on produced a lot of pictures that made this more confusing, not less.

Image courtesy of Arduino.cc
Image courtesy of Arduino.cc

The various documents all agreed on only one thing: 8 pins would be anode inputs, 8 would be cathode outputs. The anodes and cathodes are shared. By controlling the flow of electricity through the right pair of pins I could make a single light of my choice turn on. A bit like calling out coordinates in a game of Battleship – anode X and cathode Y would turn on light (X,Y). There were only two challenges left:

  1. The two rows of pins were a random mix of anodes and cathodes.
  2. There was no marking to tell which pin was 1 and which was 16.

Next, I tried the Display > RowColumnScanning example that comes with Arduino software.  Random glitchy effects that meant nothing to me. I doubled checked I had the right Arduino pins, but no go.

A hero appears

For the next three hours I tried different software methods to turn on just one row of lights. I figured if I turned on all the cathodes and then one anode I should get a whole row to light up. Problem #1 meant that I was taking random shots in the dark and getting nowhere slowly.

After dinner I took my setup to the local hackspace, VHS. Fellow member Simon took one look at the problem and said “Did you try a multimeter? it will have enough current to light the LEDs.”

LEDs lit

At this point a guy named Graham joined in. First we agreed on a number for each pin. Facing the LEDs, I put the side with the serial number facing south. The pin order we decided was pin 1 south-west, pin 16 north-west. Problem #2 solved!

I touched the multimeter red wire to a pin, then dragged the black pin across the rest while calling out the pin number, and Graham wrote down which ones caused a light to turn on. The first few produced no light, which meant we either had two anodes or two cathodes. Eventually, we found one anode (red wire) and one cathodes (black wire). Then we moved only the black wire to find all eight cathodes (pins 3,4,6,10,11,13,15,16) and, by inference, all eight anodes (pins 1,2,5,7,8,9,12,14). Problem #1 solved!

Sorting the pin order

The next step was to figure out which anode was for column 0, 1, and so on. So I wrote a sketch that would turn on all the cathodes and then power one anode at a time. That would light a whole column at once. By tracking the order of the columns lighting up we could unscramble the pins and nice left-to-right ordering.

[code lang=”c”]
// these pin numbers start at 1, because I was stupid.
// a list of cathode pins, unsorted
const int cathode[8] = { 1,2,5,7,8,9,12,14 };
// a list of anode pins, unsorted
const int anode[8] = { 3,4,6,10,11,13,15,16 };

// translate the pins on the LED panel to pins on the Arduino UNO. 16-19 are analog pins A2-A5
const int arduino_to_grid[16] = { 13,12,11,10, 16,17,18,19, 2,3,4,5, 6,7,8,9 };

// I want to turn on the column N from the left.
// this figures out which pin on the LED that is,
// then figures out which pin on the arduino matches that LED pin.
// two translations!
int out(int x) {
return arduino_to_grid[anode[x]-1];
}

// I want to turn on the row N from the top.
// this figures out which pin on the LED that is,
// then figures out which pin on the arduino matches that LED pin.
// two translations!
int in(int y) {
return arduino_to_grid[cathode[y]-1];
}

// test the sequence of cathodes
// should light each row top to bottom
void one_cathode_at_a_time() {
int i,j;

// 8s could be defined as NUM_INPUTS and NUM_OUTPUTS
// allow electricity to flow from all anodes
for(j=0;j<8;++j) {
digitalWrite(out(j),HIGH);
}
// change one cathode at a time
for(i=0;i<8;++i) {
digitalWrite(in(i),LOW);
delay(SCROLL_DELAY);
digitalWrite(in(i),HIGH);
}
// turn off all anodes
for(j=0;j<8;++j) {
digitalWrite(out(j),LOW);
}
}

// test the sequence of anodes
// should light each column left to right
void one_anode_at_a_time() {
int i,j;

// 8s could be defined as NUM_INPUTS and NUM_OUTPUTS
// allow electricity to flow into all cathodes
for(j=0;j<8;++j) {
digitalWrite(in(j),LOW);
}
// change one anode at a time
for(i=0;i<8;++i) {
digitalWrite(out(i),HIGH);
delay(SCROLL_DELAY);
digitalWrite(out(i),LOW);
}
// block all cathodes
for(j=0;j<8;++j) {
digitalWrite(in(j),HIGH);
}
}

void setup() {
int i;

// set all the pins to output.
// 16 could be defined as NUM_INPUTS+NUM_OUTPUTS
for(i=0;i<16;++i) {
pinMode(arduino_to_grid[i],OUTPUT);
}

// 8 could be defined as NUM_OUTPUTS
// turn everything off.
for(i=0;i<8;++i) {
digitalWrite(out(i),LOW);
}
// 8 could be defined as NUM_INPUTS
for(i=0;i<8;++i) {
digitalWrite(in(i),HIGH);
}
}

void loop() {
one_anode_at_a_time();
}[/code]

We counted through the sequence several times to make sure we got it right.

i column that lit actual anode pin
0 6th 13
1 1st 3
2 2nd 4
3 4th 10
4 3rd 6
5 5th 11
6 7th 15
7 8th 16

So we rearranged the pins to match, confirmed it was right, and then repeated the process for the cathodes.

[code lang=”c”]// a list of anode pins, unsorted
//const int output[8] = { 3,4,6,10,11,13,15,16 };
// a list of anode pins, sorted by left to right
const int output[8] = { 13,3,4,10,6,11,15,16 };

// a list of cathode pins, unsorted
//const int input[8] = { 1,2,5,7,8,9,12,14 };
// a list of cathode pins, sorted by top to bottom
const int input[8] = { 9,14,8,12,1,7,2,5 };[/code]

Now we had nice sweeping lines like waves on a beach. So how do light just one pixel?

Total pixel control

[code lang=”c”]#define GRID_W (8)
#define GRID_H (8)
#define SCROLL_DELAY (30) // milliseconds. 1000/30 = 33 frames per second

// I want to turn on point P(x,y), which is X from the left and Y from the top.
// I might also want to hold it on for ms milliseconds.
int p(int x,int y,int ms) {
digitalWrite(out(x),HIGH);
digitalWrite(in(y),LOW);
delay(ms);
digitalWrite(in(y),HIGH);
digitalWrite(out(x),LOW);
}

// method to test p(x,y)
// should light one pixel at a time, top to bottom, left to right.
void test_p() {
int x,y;

for(x=0;x<GRID_W;++x) {
for(y=0;y<GRID_H;++y) {
p(x,y,SCROLL_DELAY);
}
}
}

void loop() {
one_cathode_at_a_time();
one_anode_at_a_time();
test_p();
}[/code]

Waves on a beach, then one dot at a time.

Scrolling text

For the final demonstration of total control, I made a short message ticker.

[code lang=”c”]#define LETTER_W (6)
#define MESSAGE_LEN (4)
#define MESSAGE_W (MESSAGE_LEN * LETTER_W)
#define LETTER_H (8)

// 1 means on
// 0 means off
// message says “V H S ”
const int message[] = {
1,0,0,0,1,0, 1,0,0,0,1,0, 0,1,1,1,1,0, 0,0,0,0,0,0,
1,0,0,0,1,0, 1,0,0,0,1,0, 1,0,0,0,0,0, 0,0,0,0,0,0,
1,0,0,0,1,0, 1,0,0,0,1,0, 1,0,0,0,0,0, 0,0,0,0,0,0,
1,0,0,0,1,0, 1,1,1,1,1,0, 0,1,1,1,0,0, 0,0,0,0,0,0,
1,0,0,0,1,0, 1,0,0,0,1,0, 0,0,0,0,1,0, 0,0,0,0,0,0,
0,1,0,1,0,0, 1,0,0,0,1,0, 0,0,0,0,1,0, 0,0,0,0,0,0,
0,1,0,1,0,0, 1,0,0,0,1,0, 0,0,0,0,1,0, 0,0,0,0,0,0,
0,0,1,0,0,0, 1,0,0,0,1,0, 1,1,1,1,0,0, 0,0,0,0,0,0,
};

// Animated message ticker.
// scroll_step controls ticker position, clock controls ticker speed.
void vhs_message() {
// for animations
long clock=millis();
int scroll_step=0;
int x,y,nx,ny;

do {
// for every dot on the LED grid
for(x=0;x<GRID_W;++x) {
for(y=0;y<GRID_H;++y) {
// find the 1 or 0 in the message to display at this dot
ny = MESSAGE_W * y;
nx = ( scroll_step + x ) % MESSAGE_W;
// if it’s a 1, turn on the light for a fraction of a second.
// persistence of vision will create the illusion thath it’s solid.
if( message[ ny + nx ] == 1 ) {
p(x,y,0);
}
}
}

// animation time – adjust scroll_step, which will push the message to the left.
if( millis() – clock > SCROLL_DELAY*2) {
clock=millis();
++scroll_step;
}
// scroll the whole message 3 times.
} while(scroll_step < MESSAGE_W * 3);
}

void loop() {
one_cathode_at_a_time();
one_anode_at_a_time();
test_p();
vhs_message();
}[/code]

Download

You can get all the code for this post from the LED8x8 github repository.

Final thoughts

I realize only after the fact what the LED matrix scheme was trying to tell me. At the time it didn’t make any sense. Bad documentation! Bad!

These same techniques can be used with RGB LEDs, too.

How about adding two joysticks and playing a tiny game of Pong?

If you enjoyed this post and found it useful, please share it with your friends. If it helps you build something cool, tweet me a picture!

wordpress is blowing up > and < in my code. It’s fine in the github download.