Need Help with 5x5 RGB LED Matrix Game

Hello All,

First time posting to the Arduino forum. I am a electrical and computer engineering student seeking advice on a winter project. As a coding project in my last term I made a battleship game that runs a 5x5 matrix that takes user input to the console. I think it would be very cool to turn this code into a handheld game using an LED matrix. I have done a lot of research on LED matrices and understand how they themselves are wired. The problem is I want to build an RGB 5x5 matrix but I can only find documentation on 8x8 matrices. All wired up my matrix will have 20 pins (5 green, 5 blue, 5 red, 5 anode) and each LED will have 3 current limiting resistors (1 for each color). The concept that I am having trouble grasping is how to control the matrix. From what I understand, you refresh each LED fast enough that you’re eye can’t see it turn off and this also helps to keep current draw low. I believe this is done through PWM. I already have a Teensy 2.0 that I would like to use as the main controller and will need additional LED drivers to branch out more pins. I would also like blue to appear to always be on (for ocean) and when there is a “hit” have the blue change to red. It seems like there is 1000 ways to wire this thing but main goal is to keep things small (handheld). I have seen people use MOSFET’s, LED drivers, shift registers, etc. What would be the best approach for my application?

Thanks,
Alex

I believe this is done through PWM.

That can control brightness.

From what I understand, you refresh each LED fast enough that you're eye can't see it turn off

This the multiplexing that implements persistence of vision so they can all look on.

Arrange as 5 row, each row connecting the 5 common anode. Arrange as 15 vertical columns, each column a R, G, or B. Then multiplex - drive the anodes, sink one column of cathode. Cathode off, drive anodes, next column cathode on.

Drive Anodes with cd74AC164 for 20mA of source current. Sink cathodes with TPIC6B595 to sink the 100mA from a column.

1 cd74AC164, 2 TPIC6B595, 5 resistors on the anodes (or 15 on the cathodes). Can use PWM on TPIC6B595 output enable to control brightness between colors. Leave each column on for 2mS for 33 Hz refresh rate. Use SPI.transfer to update the shift register. cd74AC164, use 2nd DS pin as active high chip select. Doesn't have a 2nd stage output, so turn off cathode drive before shifting data in. If not, LEDs may appear to flicker a little as you switch between columns (or with SPI.transfer(), maybe not - its sending data out at 4 MHz using default settings.

I just made a schematic. Is the wiring correct? I’m not 100% sure on the pin connections. The program I am using doesn’t support IC’s with pins above 18. So I had to overlap two smaller ones for illustration.

You’re pretty close.
The LEDs need to be like this. '164 driving the anode in rows, the tpic6b595’s sinking 1 cathode column at a time.
I didn’t show the control signals.

Get some better software - free hobbyist version of eagle from cadsoftusa.com, or free expressSCH from expresspcb.com

Awesome! I didn't know there was a free version of Eagle.

Thanks for the schematic. Are the resistor values the same? Since only one LED is on at a time, there is only one LED's worth of current draw? This way its one resistor per cathode row instead of three resistors per LED. Also, I am a bit confused on which pins on the TPIC6B595's and the cd74AC164 connect to the Teensy and where.

As size/compactness is important, here are a couple of alternatives to think about.

Max7219 can drive up to 64 leds with minimal extra components. But you have 75 leds. However, you don't need individual control of the blues, so perhaps that's not a problem.

Charlieplexing. I must sound like a broken record on this forum sometimes. 10 lines can be used to drive up to 90 leds directly by the mpu with some ordinary transistors and current limiting resistors.

Paul

The '164 will drive 1,2,3,4,5 LEDs high. The '595 will pull 1 common cathode low at a time, so up to 100mA.
Cycle thru all the columns quick enough, it will be flicker free and the eye will see all that are periodically high as on at the same time.
Resistors all the same. Select to limit to 20mA for LED color with lowest voltage drop (Red).
(5V - 2.2V)/20mA = resistor.

I use SPI.transfer( ) to send data to shift registers.

These are RGB LEDs, so MAX7219 not really appropriate.
Also complicates charlieplexing.

Okay cool. Thanks for clearing that up for me. One last thing, I noticed there are two non polarized capacitors in your schematic each bridging VCC and GND. Is that needed? If so, what values?

Yes, you need a 0.1uF ceramic cap on the Vcc pin of Every IC. If there are several Vcc pins, use several 0.1uF caps. Every time.

Great! Thanks for all your help!

Finally finished building the matrix, but I cannot get lights to turn on. I can’t find any sample code that runs a matrix with shift registers. I am able to manually test each row with a battery and it works fine. But I would like to have all of the lights turn on Blue, then all Green, and then all Red using the shift registers (sort of like a system test). Help with this code would be greatly appreciated. I have also attached a schematic of my current wiring.

Your LEDs are wired backwards. You need the '164s driving the anodes, and the '595s sinking the cathodes.

The 164 is connected to the common anodes and the 595's are connected to each color cathode. Just had it backwards on the schematic. It's fixed now.

I would do it like this: test to see if its time to change to the next column, then: turn off the cathodes output the anode data for the next column turn on the cathode for the next column

This should get you started. Add in with your code, declare all the variables, do the pinMode stuff, etc.

// set up a couple of arrays:
byte display[] = {  // '1' at a location results in an On LED, will send 1 column at a time
0x1f, // column 0, Red, upper 3 bits bit connected to anything: also 0b00011111
0x00, // column 0, Green
0x00, // column 0, Blue

0x1f, // column 1
0x00,
0x00,

0x1f, // column 2
0x00,
0x00,

0x1f, // column 3
0x00,
0x00,

0x1f, //column 4
0x00,
0x00,
};

// these may need swapping end for end, depends on how shift registers are wired
byte cathodes[] = { will send in pairs, column with a 1 will be turned on
0x80, 0x00, // column 0
0x40, 0x00, // 1
0x20, 0x00,  // 2
0x10, 0x00,  // 3
0x08, 0x00, // 4
0x04, 0x00, // 5
0x02, 0x00,  // 6
0x01, 0x00, // 7
0x00, 0x80, // 8
0x00, 0x40, // 9
0x00, 0x20, // 10
0x00, 0x10, // 11
0x00, 0x08, // 12
0x00, 0x04, // 13
0x00, 0x02, // 14
// 0x00, 0x01 not used
};

// then create the code in loop that will update the shift registers every 2200 microseconds (2.2mS):
void loop(){
// all time related elements are unsigned long
currentMicros = micros();
elapsedMicros1 = currentMicros - previousMicros1;
if (elapsedMicros1 >= columnTime){ // columnTime = 2200UL (1/(15 x 2.2mS) = 30 Hz
previousMicros1 = previousMicros1 + columnTime; 
// turn off cathodes
digitalWrite (cathodeSS, LOW); // I would use direct port  manipulation vs digitalWrite, way faster
SPI.transfer(0);
SPI.transfer(0);
digitalWrite (cathodeSS, HIGH);
// shift out next anode data
columnCount = columnCount +1;
if (columnCount == 15){columnCount = 0;} // reset after column 14
digitalWrite (anodeSS, HIGH); // think '164  needs High on DS to allow 2nd DS to be used
SPI.transfer(display[columnCount]);
digitalWrite (anodeSS, LOW); 
// now turn on next cathode
digitalWrite (cathodeSS, LOW);
SPI.transfer(cathodes[(columnCount *2)+0]);
SPI.transfer(cathodes[(columnCount *2)+1]);
digitalWrite (cathodeSS, HIGH);
} // end time check

// time to change the display?
elapsedMicros2 = currentMicros - previousMicros2;
if (elapsedMicros2 >= displayTime){ // displayTime = 1000000UL, 1 second ?
previousMicros2 = previousMicros2 + displayTime;
// write the code to change display[] for the next color to be on
} // end time check
} // end loop

I believe I wrote all the code correctly. All of the LED’s are dimly lit in each color except for the second row (row 1). Any idea?

Here is my code:

#include <SPI.h>  
#include <toneAC.h> 

#define NOTE_B0  31
#define NOTE_C1  33
#define NOTE_CS1 35
#define NOTE_D1  37
#define NOTE_DS1 39
#define NOTE_E1  41
#define NOTE_F1  44
#define NOTE_FS1 46
#define NOTE_G1  49
#define NOTE_GS1 52
#define NOTE_A1  55
#define NOTE_AS1 58
#define NOTE_B1  62
#define NOTE_C2  65
#define NOTE_CS2 69
#define NOTE_D2  73
#define NOTE_DS2 78
#define NOTE_E2  82
#define NOTE_F2  87
#define NOTE_FS2 93
#define NOTE_G2  98
#define NOTE_GS2 104
#define NOTE_A2  110
#define NOTE_AS2 117
#define NOTE_B2  123
#define NOTE_C3  131
#define NOTE_CS3 139
#define NOTE_D3  147
#define NOTE_DS3 156
#define NOTE_E3  165
#define NOTE_F3  175
#define NOTE_FS3 185
#define NOTE_G3  196
#define NOTE_GS3 208
#define NOTE_A3  220
#define NOTE_AS3 233
#define NOTE_B3  247
#define NOTE_C4  262
#define NOTE_CS4 277
#define NOTE_D4  294
#define NOTE_DS4 311
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_FS4 370
#define NOTE_G4  392
#define NOTE_GS4 415
#define NOTE_A4  440
#define NOTE_AS4 466
#define NOTE_B4  494
#define NOTE_C5  523
#define NOTE_CS5 554
#define NOTE_D5  587
#define NOTE_DS5 622
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_FS5 740
#define NOTE_G5  784
#define NOTE_GS5 831
#define NOTE_A5  880
#define NOTE_AS5 932
#define NOTE_B5  988
#define NOTE_C6  1047
#define NOTE_CS6 1109
#define NOTE_D6  1175
#define NOTE_DS6 1245
#define NOTE_E6  1319
#define NOTE_F6  1397
#define NOTE_FS6 1480
#define NOTE_G6  1568
#define NOTE_GS6 1661
#define NOTE_A6  1760
#define NOTE_AS6 1865
#define NOTE_B6  1976
#define NOTE_C7  2093
#define NOTE_CS7 2217
#define NOTE_D7  2349
#define NOTE_DS7 2489
#define NOTE_E7  2637
#define NOTE_F7  2794
#define NOTE_FS7 2960
#define NOTE_G7  3136
#define NOTE_GS7 3322
#define NOTE_A7  3520
#define NOTE_AS7 3729
#define NOTE_B7  3951
#define NOTE_C8  4186
#define NOTE_CS8 4435
#define NOTE_D8  4699
#define NOTE_DS8 4978

const int led = LED_BUILTIN;    // Teensy LED
int ledState = LOW;             // ledState used to set the LED
long previousMillis = 0;        // will store last time LED was updated
long LEDontime = 0;             // interval at which to blink (milliseconds)

int melody[] = {
  NOTE_G5, NOTE_D5, NOTE_G5 };  // notes in the melody
int noteDurations[] = {
  8, 8, 8};                     // note durations: 4 = quarter note, 8 = eighth note, etc.

int sensor1Pin = 19;            // Analog input pin to pot1
int sensor1Value = 0;           // variable to store pot1 value
int ledPin = 9;                 // LED pin
int sensor2Pin = 18;            // Analog input pin to pot2
int sensor2Value = 0;           // variable to store pot2 value

int cathodeSS = 0;
int anodeSS = 0;
int columnCount = 5;

long previousMicros1 = 0;       
long columnTime = 2200;
long previousMicros2 = 0;
long displayTime = 1000000;



void setup() {
  pinMode(led, OUTPUT);
  pinMode(cathodeSS,OUTPUT);
  pinMode(anodeSS,OUTPUT);
  digitalWrite(cathodeSS,HIGH);
  digitalWrite(anodeSS,HIGH);
  
  //Speaker Control:
  sensor2Value = analogRead(sensor2Pin);  // read the value from the sensor
  sensor2Value /=104;                      // converts 0-1023 to 0-10
  for (int thisNote = 0; thisNote < 3; thisNote++) {
    int noteDuration = 1000/noteDurations[thisNote];
    toneAC(melody[thisNote], sensor2Value, noteDuration, true); // Play thisNote at full volume for noteDuration in the background.
    delay(noteDuration * 4 / 3); // Wait while the tone plays in the background, plus another 33% delay between notes.
  }
  
  Serial.begin(9600);
  SPI.begin();
}


// set up a couple of arrays:
byte display[] = {  // '1' at a location results in an On LED, will send 1 column at a time
  0x1f, // column 0, Red, upper 3 bits bit connected to anything: also 0b00011111
  0x00, // column 0, Green
  0x00, // column 0, Blue

  0x1f, // column 1
  0x00,
  0x00,

  0x1f, // column 2
  0x00,
  0x00,

  0x1f, // column 3
  0x00,
  0x00,

  0x1f, //column 4
  0x00,
  0x00,
};

// these may need swapping end for end, depends on how shift registers are wired
byte cathodes[] = { 
  //will send in pairs, column with a 1 will be turned on
  0x80, 0x00,  // column 0
  0x40, 0x00,  // 1
  0x20, 0x00,  // 2
  0x10, 0x00,  // 3
  0x08, 0x00,  // 4
  0x04, 0x00,  // 5
  0x02, 0x00,  // 6
  0x01, 0x00,  // 7
  0x00, 0x80,  // 8
  0x00, 0x40,  // 9
  0x00, 0x20,  // 10
  0x00, 0x10,  // 11
  0x00, 0x08,  // 12
  0x00, 0x04,  // 13
  0x00, 0x02,  // 14
  // 0x00, 0x01 not used
};

// then create the code in loop that will update the shift registers every 2200 microseconds (2.2mS):
void loop(){
  //Teensy LED Control:
  unsigned long currentMillis = millis();
  if(currentMillis - previousMillis > LEDontime) {
    previousMillis = currentMillis;     // save the last time you blinked the LED 
    if (ledState == LOW)                // if the LED is off turn it on and vice-versa
      ledState = HIGH;
    else
      ledState = LOW;
    digitalWrite(led, ledState);     // set the LED with the ledState of the variable
  }

  //Matrix Brightness Control:
  sensor1Value = analogRead(sensor1Pin);  // read the value from the sensor1
  sensor1Value /=4;                      // converts 0-1023 to 0-255
  analogWrite(ledPin, sensor1Value);     // outputs PWM signal to LED

  //Matrix Control:
  unsigned long currentMicros = micros();
  int elapsedMicros1 = currentMicros - previousMicros1;
  if (elapsedMicros1 >= columnTime){ // columnTime = 2200UL (1/(15 x 2.2mS) = 30 Hz
    previousMicros1 = previousMicros1 + columnTime; 
    // turn off cathodes
    digitalWrite (cathodeSS, LOW); // I would use direct port manipulation vs digitalWrite, way faster
    SPI.transfer(0);
    SPI.transfer(0);
    digitalWrite (cathodeSS, HIGH);
    // shift out next anode data
    columnCount = columnCount +1;
    if (columnCount == 15){
      columnCount = 0;
    } // reset after column 14
    digitalWrite (anodeSS, HIGH); // think '164  needs High on DS to allow 2nd DS to be used
    SPI.transfer(display[columnCount]);
    digitalWrite (anodeSS, LOW); 
    // now turn on next cathode
    digitalWrite (cathodeSS, LOW);
    SPI.transfer(cathodes[(columnCount *2)+0]);
    SPI.transfer(cathodes[(columnCount *2)+1]);
    digitalWrite (cathodeSS, HIGH);
  } // end time check
  // time to change the display?:
  int elapsedMicros2 = currentMicros - previousMicros2;
  if (elapsedMicros2 >= displayTime){ // displayTime = 1000000UL, 1 second ?
    previousMicros2 = previousMicros2 + displayTime;
    // write the code to change display[] for the next color to be on
  } // end time check
} // end loop

This part will wreck the multipexing:

  for (int thisNote = 0; thisNote < 3; thisNote++) {
    int noteDuration = 1000/noteDurations[thisNote];
    toneAC(melody[thisNote], sensor2Value, noteDuration, true); // Play thisNote at full volume for noteDuration in the background.
    delay(noteDuration * 4 / 3); // Wait while the tone plays in the background, plus another 33% delay between notes.
  }

Removed everything but the code to run the matrix, but now either all of the LED’s are off or they flicker randomly. All while the second row remains off.

#include <SPI.h>  

int cathodeSS = 0;
int anodeSS = 0;
int columnCount = 5;

long previousMicros1 = 0;       
long columnTime = 2200;
long previousMicros2 = 0;
long displayTime = 1000000;

void setup() {
  pinMode(cathodeSS,OUTPUT);
  pinMode(anodeSS,OUTPUT);
  digitalWrite(cathodeSS,HIGH);
  digitalWrite(anodeSS,HIGH);

  Serial.begin(9600);
  SPI.begin();
}


// set up a couple of arrays:
byte display[] = {  // '1' at a location results in an On LED, will send 1 column at a time
  0x1f, // column 0, Red, upper 3 bits bit connected to anything: also 0b00011111
  0x00, // column 0, Green
  0x00, // column 0, Blue

  0x1f, // column 1
  0x00,
  0x00,

  0x1f, // column 2
  0x00,
  0x00,

  0x1f, // column 3
  0x00,
  0x00,

  0x1f, //column 4
  0x00,
  0x00,
};

// these may need swapping end for end, depends on how shift registers are wired
byte cathodes[] = { 
  //will send in pairs, column with a 1 will be turned on
  0x80, 0x00,  // column 0
  0x40, 0x00,  // 1
  0x20, 0x00,  // 2
  0x10, 0x00,  // 3
  0x08, 0x00,  // 4
  0x04, 0x00,  // 5
  0x02, 0x00,  // 6
  0x01, 0x00,  // 7
  0x00, 0x80,  // 8
  0x00, 0x40,  // 9
  0x00, 0x20,  // 10
  0x00, 0x10,  // 11
  0x00, 0x08,  // 12
  0x00, 0x04,  // 13
  0x00, 0x02,  // 14
  // 0x00, 0x01 not used
};

// then create the code in loop that will update the shift registers every 2200 microseconds (2.2mS):
void loop(){
  //Matrix Control:
  unsigned long currentMicros = micros();
  int elapsedMicros1 = currentMicros - previousMicros1;
  if (elapsedMicros1 >= columnTime){ // columnTime = 2200UL (1/(15 x 2.2mS) = 30 Hz
    previousMicros1 = previousMicros1 + columnTime; 
    // turn off cathodes
    digitalWrite (cathodeSS, LOW); // I would use direct port manipulation vs digitalWrite, way faster
    SPI.transfer(0);
    SPI.transfer(0);
    digitalWrite (cathodeSS, HIGH);
    // shift out next anode data
    columnCount = columnCount +1;
    if (columnCount == 15){
      columnCount = 0;
    } // reset after column 14
    digitalWrite (anodeSS, HIGH); // think '164  needs High on DS to allow 2nd DS to be used
    SPI.transfer(display[columnCount]);
    digitalWrite (anodeSS, LOW); 
    // now turn on next cathode
    digitalWrite (cathodeSS, LOW);
    SPI.transfer(cathodes[(columnCount *2)+0]);
    SPI.transfer(cathodes[(columnCount *2)+1]);
    digitalWrite (cathodeSS, HIGH);
  } // end time check
  // time to change the display?:
  int elapsedMicros2 = currentMicros - previousMicros2;
  if (elapsedMicros2 >= displayTime){ // displayTime = 1000000UL, 1 second ?
    previousMicros2 = previousMicros2 + displayTime;
    // write the code to change display[] for the next color to be on
  } // end time check
} // end loop

These need to be unsigned long, and add UL as noted:

long previousMicros1 = 0; long columnTime = 2200UL; long previousMicros2 = 0; long displayTime = 1000000UL;

add these to the pre-setup area: unsigned long elapsedMicros1; unsigned long elapsedMicros2; unsigned long currentMicros;

take unsigned long and int off these:

unsigned long currentMicros = micros(); int elapsedMicros1 = currentMicros - previousMicros1;

int elapsedMicros2 = currentMicros - previousMicros2;

Alright now all the lights are turning on. But its still random dim flashing.

Wow, this is pretty complicated. I would have done this with a string of WS2812 RGB addressable LEDs, arranged in a matrix shape. That would eliminate the complexity of lighting the LEDs yourself and allow you to focus on the game-play.

Will this game also have buttons associated with each LED? This is a great idea!