Working With An 8x8 LED Matrix

Working With An 8x8 LED Matrix

Having nearly completed the Sparkfun Inventor's Kit guidebook, I went on a purchasing frenzy, acquiring loads of various electronic components online.

Amongst these was a Dual Colour 8x8 LED Matrix. I chose to work with this first because it looked like something I could tackle with difficulty, which I believe is conducive to Flow. The fact that this was the first item to arrive in the mail also played a part in my decision.

I set out with a goal to make a scrolling message panel, albeit a small one. I'm pleased to say that I was able to achieve this, and I learned a lot along the way. I hope that by recording my projects in this blog that I'll further solidify my understanding of electrical theory.

The end result is this:

Step 1: What Are All These Pins For?

The Datasheet for my LED matrix wasn't exactly clear to me the first time I read it. The pinout leaves a lot to be desired as far as clarity goes:

LED matrix wiring diagram

So the pinout tells us a few things:

  1. There are 24 pins in total (8 red anodes, 8 green anodes, 8 common cathodes)
  2. The pins are in a crazy order.

What the pinout doesn't tell us, is the physical location of the pins on the matrix. The matrix is perfectly square, with no indicator of which pin is pin 1. So, I had to deduce that through trial and error. I wired up all of the pins, then tested pins until I found the anode and cathode for a corner LED:

The LED matrix connected to a breadboard

So, we now know that two consecutive pins can be used to power a corner LED. Given the assumption that the pins are ordered sequentially on the LED matrix, those pins can only be the following:

  • Pins 2 and 3
  • Pins 10 and 11
  • Pins 14 and 15
  • Pins 22 and 23

By process of elimination, it turns out that none of the options are possible. The most correct option on that list is that the circuit is using pins 22 and 23. The only incorrect part is that the pinout states that pin 23 is connected to the anode for a green LED. It turns out that the datasheet is incorrect, the black anodes actually depict red LEDs (lesson learned: never take a datasheet for gospel).

Armed with this knowledge, it was possible to figure out what all of the other pins are for. The image below is using the same orientation as the image previous image:

Pin diagram for the LED matrix

Step 2: Wiring Up

Now that we know what on earth the pins are for, this step is pretty easy, if not a bit tedious. Since I'm using the Sparkfun Redboard (equivalent to the Arduino Uno), I only have 13 digital out pins. Obviously, this isn't enough to accommodate all of the pins on the matrix.

I decided to compromise and only use the red LEDs, which means I still need 16 pins. Fortunately, the analog in pins can also be used as digital output pins, so the final wiring looks like this:

As a convention, I used red and orange cables to connect to the red LED anodes, and green and blue cables to connect to the common cathodes. I placed 330Ω resistors in series with each anode connection (more on that later). The connections are as follows:

Arduino Pin Matrix Pin Pin Type
2 11 Red Anode
3 8 Red Anode
4 5 Red Anode
5 2 Red Anode
6 14 Red Anode
7 17 Red Anode
8 20 Red Anode
9 23 Red Anode
11 13 Common Cathode
12 10 Common Cathode
A0 6 Common Cathode
A1 3 Common Cathode
A2 13 Common Cathode
A3 16 Common Cathode
A4 19 Common Cathode
A5 22 Common Cathode

Step 3: Making It Do Stuff

One hard truth I was hit with pretty early on is that making a legible picture show up on this thing wasn't going to be as simple as I had hoped.

There are 16 pins wired up, each with two possible states: On or Off. This equates to 216 possible voltage states that the pins can be in. However, there are 64 LEDs wired up (disregarding the green LEDs), which equates to 264 possible LED states. This means that it's impossible to make any pattern of LEDs light up at a given time (it's a violation of the Pigeonhole Principle).

My plan instead, then, was to only ever display one pixel at a time. That might sound like a pretty silly way to make an image scroller, but this actually isn't too dissimilar to the way old-fashioned CRT monitors work.

By turning single LEDs on for fractions of a second and repeating, we can trick the human eye into perceiving a still image. By feeding multiple images into this process, we can make it look like an image is moving.

The other advantage of this approach is that current will only be drawn for 0 or 1 LED at any given instance. This means that standard resistors can be used. If we were lighting up multiple pixels in a given column at the same time, a more sophisticated method would have to be used (and with my current skillset, I don't know what that would be).

Step 4: The Code

I've included comments in the code below to act as an explanation:

// The number of LEDs in the matrix in one dimension.
#define LED_MATRIX_SIZE 8

// The width of each letter when displayed on the matrix, including one column for
// spacing between letters.
#define LETTER_WIDTH 6
#define REPAINT_COUNT 50

// Red pins, in left-to-right order.
// Physical pin attachments to LED matrix: 11, 8, 5, 2, 14, 17, 20, 23.
const byte RED_PINS[LED_MATRIX_SIZE] = {2, 3, 4, 5, 6, 7, 8, 9};

// Ground pins, in top-to-bottom order (skip pin 13 to avoid using the integrated LED).
// Physical pin attachments to LED matrix: 22, 19, 16, 13, 3, 6, 9, 12.
const byte GROUND_PINS[LED_MATRIX_SIZE] = {A5, A4, A3, A2, A1, A0, 12, 11};

/* Binary representation of each letter, where 1 indicates an "On" pixel and 0 indicates
 * "Off", e.g. the letter "A" is defined as 0b0111010001111111000110001. Split into 5
 * rows and columns, that binary value looks like:
 * 0 1 1 1 0
 * 1 0 0 0 1
 * 1 1 1 1 1
 * 1 0 0 0 1
 * 1 0 0 0 1
 * 
 * Notice that the "1" pixels form a pixel representation of the letter "A". The same holds
 * true for the other letters defined below.
 */
const long LETTERS[] = {
  0b0111010001111111000110001, // 'A'
  0b1111010001111101000111110, // 'B'
  0b0111010001100001000101110, // 'C'
  0b1111010001100011000111110, // 'D'
  0b1111110000111101000011111, // 'E'
  0b1111110000111101000010000, // 'F'
  0b0111010000101111001001110, // 'G'
  0b1000110001111111000110001, // 'H'
  0b1111100100001000010011111, // 'I'
  0b0011100010000101001001100, // 'J'
  0b1000110010111001001010001, // 'K'
  0b1000010000100001000011111, // 'L'
  0b0101010101101011000110001, // 'M'
  0b1000111001101011001110001, // 'N'
  0b0111010001100011000101110, // 'O'
  0b1111010001111101000010000, // 'P'
  0b0110010010101101001001101, // 'Q'
  0b1111010001111101001010001, // 'R'
  0b0111110000011100000111110, // 'S'
  0b1111100100001000010000100, // 'T'
  0b1000110001100011000101110, // 'U'
  0b1000110001100010101000100, // 'V'
  0b1000110001101011101110001, // 'W'
  0b1000101010001000101010001, // 'X'
  0b1000101010001000010000100, // 'Y'
  0b1111100010001000100011111  // 'Z'
};

// Set all pins to output, so we can change the potential difference between
// positive and negative pins. We also default the cathode pins to HIGH, which creates
// a negative potential difference that results in no LEDs turning on.
void setup() {
  for (byte i = 0; i < LED_MATRIX_SIZE; ++i) {
    byte anodePin = RED_PINS[i];
    pinMode(anodePin, OUTPUT);
    
    byte groundPin = GROUND_PINS[i];
    pinMode(groundPin, OUTPUT);

    digitalWrite(GROUND_PINS[i], HIGH);
  }
}

void loop() {
  renderMessage();
}

void renderMessage() {
  // The message to display on the screen, with some space for padding
  // when displaying the message.
  char message[] = " I AM ALIVE ";

  int messageIndex = 0;
  char messageChar = message[messageIndex];

  while (messageChar != '\0') {
    char nextChar = message[messageIndex + 1];
    if (nextChar == '\0') {
      nextChar = ' ';
    }

    renderFrame(messageChar, nextChar);

    ++messageIndex;
    messageChar = message[messageIndex]; 
  }
}

// 2 characters are always present on the screen. This function handles the drawing of those two
// characters in such a way that they scroll off the screen to the left. As soon as the first
// character is invisible, the function exits and is called again with a new secondChar,
// while the previous secondChar is passed in as the new firstChar.
void renderFrame(char firstChar, char secondChar) {
  for (byte i = 0; i < LETTER_WIDTH; ++i) {
    for (byte j = 0; j < REPAINT_COUNT; ++j) {
      boolean frame[LED_MATRIX_SIZE][LED_MATRIX_SIZE] = {};
      
      renderCharacter(frame, firstChar, -i);
      renderCharacter(frame, secondChar, 6 - i);

      renderFrame(frame);
      delay(1);
    }
  }
}

// Draws a pixel representation of a single letter to the frame buffer.
void renderCharacter(boolean frame[LED_MATRIX_SIZE][LED_MATRIX_SIZE], char character, int offset) {
  for (int row = 0; row < 5; ++row) {
    for (int col = 0; col < 5; ++col) {
      int bitCount = (row * 5) + col;

      long displayCharacter;
      if (character == ' ') {
        displayCharacter = 0; // All spaces.
      } else {
        displayCharacter = LETTERS[character - 'A'];
      }

      // This is where the magic happens. The left shift and binary AND operations are used
      // together to effectively iterate over each bit of the character as defined in the
      // LETTERS array. This results in a 1 or 0, which is used to dictate whether a pixel
      // should be turned on or off for a given point.
      boolean isOn = displayCharacter & (1L << (24 - bitCount));

      // Only render the visible parts of the letter (part of the letter is likely to have
      // scrolled off the screen.
      int offsetCol = col + offset;
      if (offsetCol >= 0 && offsetCol < LED_MATRIX_SIZE) {
        frame[row][offsetCol] = isOn;
      }
    }
  }
}

// Converts the in-memory frame buffer into an actual image. This is achieved by rapidly
// turning on single LEDs that correspond to "true" values in the frame buffer, then turning
// them off after a short delay.
// A single pixel is turned on by bringing the anode pin HIGH and the corresponding cathode pin
// LOW. Remember that all cathodes were brought HIGH earlier in the setup() function.
void renderFrame(boolean frame[LED_MATRIX_SIZE][LED_MATRIX_SIZE]) {
  for (byte row = 0; row < LED_MATRIX_SIZE; ++row) {
    for (byte col = 0; col < LED_MATRIX_SIZE; ++col) {
        // Turn pixel on or off as required.
        digitalWrite(RED_PINS[row], frame[row][col]);
        digitalWrite(GROUND_PINS[col], !frame[row][col]);

        // Turn pixel back off.
        digitalWrite(RED_PINS[row], LOW);
        digitalWrite(GROUND_PINS[col], HIGH);
    }
  }
}

That's All There Is To It

If you've made it to the end of this article, thanks for your patience. This is my first blog post of substance, and I'd love to hear any feedback or constructive criticism in the comments section below. Cheers!