Introduction: Arduino Controlled RGB LED Dot Matrix Board

This project is a relatively straightforward and simple DIY music board.
The Dot Matrix Board will allow any aged person to create their own music and beats.
All you need to have is an understanding of Arduino and circuitry.



Step 1: Gather Parts Needed

Here is a general parts list that is required:

1. 12" X 24" Plywood board
2. 12" X 24" X 1/16" Plexiglass sheet of plastic
3. 12" X 96" Sheet of aluminum foil
4.  6 Different colored wire, including braided and non-braided
5. 18 Common Anode RGB LED's
6. 18 3" X 3" sheets of 1/4" foam
7. 2 Arduino Uno boards
8. 4 TLC5940 Microcontroller chips
9. 1 Blank PCB
10. 6 10+KΩ resistors
11. 4 8.2 KΩ


Step 2: Assemble the Board

To begin building the board on which to be stepped on, first you need to assemble the buttons. We chose aluminum foil due to its simple conductivity and ease of use. I had to solder on braided wire onto two sheets of 3"  X 3" Aluminum foil 36 times in the corner. These sheets are simply hot glued together so they will not fall apart. Please note that solder will not attach to the aluminum foil. To bypass this, just hold the braided wire on the foil and heat with your soldering iron. When you put your solder on, just leave the braided wire connected to the foil, and the whole system will cool, allowing a solid connection.

After all of the sheets are assembled, we need to complete the plywood board. Mark out 18 equal squares on the board. The squares are roughly 4" X 4". There are a few holes that need to be drilled using a 3/8" drill bit. The circles in red are meant for the wires coming from the buttons. In the center of each of the squares, drill another hole. This will be used for the LED's when we get to installing them.

Once your board has 24 holes drilled into it, we can begin the installation of the buttons. Take 18 of the foil sheets and cut a 1" X 1" hole into the sheet. This allows the LED to show through. All that I needed to do was shove the wire through a red hole (on the diagram). Keep track of which wire goes to which number button. This will ease the process of programming later. There is no specific order of where the wires need to go into the hole, but they do need to be kept track of where the wires are from.

Next I flipped over the board and began to wire everything together. On the previous step we looked at a diagram showing where everything goes onto the Arduino board. Make sure you use a resistor with a value of at least 10 KΩ. This helps clear up the signal from the button. Each row is connected. Make sure this is done following the diagram correctly.


The next step is super easy and super quick. After the aluminum sheets have been glued to the plywood board, I needed to assemble the foam. The foam makes the button return to its original state. I cut four 3/8" square pieces into the foam in a square formation. This allows the foil to make contact together. After cutting these four holes, make one big one in the center. This will let the LED show through everything and onto the Plexiglass.

After I completed the foam, I glued it onto the foil sheets. Remember those foil sheets with the wires soldered onto them? Well I did the same process again. All you need to do instead of making each row connected, is to make each column connected. On one side of the common wire, solder one of the resistors to that wire. On the non-soldered side of the resistor, make a common ground. This ground will go to a ground port on an Arduino. I glued the top foil sheet to the foam to make the connection.

The plexiglass comes next. Cut the plexiglass to fit each of the squares. The squares need to be as close together as possible. Glue these to the foil on top.

The RGB LEDs come next. I used simple telephone wire for my LEDs because it is stranded together, making the behind the scenes a lot easier and neater. Because we used RGB LEDs, there are four connections that have to be made. Negative, Red, Green and Blue. These I just soldered the corresponding color to the correct lead of the LED.

We will get to soldering these to the board later.

Step 3: Mutiplexing the RGB LEDs and Buttons

To achieve the look we wanted, we planned for a 6 by 3 sized RGB LED board. With 3 pins to control each LED, and 18 LEDs total, we needed to be able to independently control 54 analog pins. The Arduino Uno we employed to actually control all of these has a total of 6 pins that even output PWM analog at all, so multiplexing was a requirement.

TLC5940

The TLC5940 has 16 output pins that each pulse 4095 times each duty cycle, as opposed to the Arduino's 255 per cycle. This allows for much finer dimming and control for time-sensitive projects - perfect for LEDs. Multiple of them can also be linked together to create larger chains of multiplexing control. The video below is an excellent example of how the TLC driver works, and the basics to transfer LED data to the driver. The code used in this project to communicate with the TLC drivers is credited to this video as well. It also explains all the minor adjustments that need to be made to allow it to work properly, such as the Amperage Reference pin:

http://www.youtube.com/watch?v=FehBLNHMlfo

The TLC's wiring is the same for all additional drivers, with the exception of the signal (SIN and SOUT) pins, which are instead wired in series between each driver. (See Diagram1.)

TLC Data sheet: http://www.ti.com/lit/ds/symlink/tlc5940.pdf

Button Mutliplexing

To wire 18 buttons to the Arduino, creative wiring had to be used. Basically, the grid of buttons connects a power source (one of the Arduino ports) to an analog port (another set of ports). (Diagram2.) Even though multiple buttons are connected to each analog input, only one has power at a time. Its a coordinate system for button recognition. The following link shows this principle, just with LEDs instead of buttons: https://www.instructables.com/file/FMMWU6BH2TJ4FPQ

Arduino Comunication

Due to the fact that the TLCs require precise timing and speed, we dedicated one Arduino for data transfer only. We used an additional one to actually calculate which LEDs need to be which colors. Its basically the same interaction between the Video Card and the CPU - one comes up with what needs to be displayed, and the other transfers that data to the LEDs as fast as possible. This does a great job of reducing flicker, and avoids issues with timing on the program's part due to the fact that the LED driver Arduino has to readjust its internal clock, which can interfere with the regular tones and delays.

The communication is actually facilitated by the serial Tx and Rx (1 and 0) ports. The Tx port from the control Arduino is connected to the Rx port from the LED Driver Arduino. A library called Easy Transfer is used to make this process painless. Here is the link to the instruction website we used: http://www.billporter.info/2011/05/30/easytransfer-arduino-library/. Just make sure you download the libabrary files and SAVE them to the "C:\Program Files (x86)\arduino\libraries" folder.

A speaker should also be attached if sound is wanted.





Step 4: Programming

Like we said in step 3, we HIGHLY recommend watching this video: http://www.youtube.com/watch?v=FehBLNHMlfo. It shows how to actually connect the Arduino to the TLCs in the first place, so its a great place to start. As far as programming, we split the work among two Arduinos and got them to communicate. The following programs are simply framework for a final product. The LED Driver Arduino is completed, while the Controller is simply a shell for whatever you want to program the board to do:

LED Driver Arduino:

//Texas Instruments TLC 5940 with Arduino
//With help from www.kevindarrah.com

#include <SPI.h>//Serial Peripheral Interface Library

//Include the Easy Tranfer Lib
#include <EasyTransfer.h>
EasyTransfer ET;

//This must be EXACTLY the same on the other Arduino
struct RECEIVE_DATA_STRUCTURE{
  int LEDGrid[18];
};

int LEDData[18];

RECEIVE_DATA_STRUCTURE matrix;

byte ch=0, chbit=0, spibit=0, spibyte=0;// variables used by tlc sub routine
int SINData;//variable used to shift data to the TLC
byte transferbyte[48];// bytes that are sent out to the tlc5940 via SPI
// 48 because 16 channels @ 12bits gives 384bits, 384/8 = 48 bytes, 12 bit to 8 bit conversion
byte DCvalue[32];//0-63, 6 bit DOT Correction Bytes
int i, j, k, l,m,n; //misc variables
int count = 0;
//*******************************************************************************************
//*******************************************************************************************
void setup(){//   MAIN SETUP   MAIN SETUP   MAIN SETUP   MAIN SETUP   MAIN SETUP

  ET.begin(details(matrix), &Serial);

  pinMode(7, OUTPUT);//XLAT
  pinMode(3, OUTPUT);//OSC2B GSCLK
  pinMode(4, OUTPUT);//VPRG
  pinMode(11, OUTPUT);//MOSI DATA
  pinMode(13, OUTPUT);//SPI Clock
  //Pin 5 is the blank

  //Set up the SPI
  SPI.setBitOrder(MSBFIRST);//Most Significant Bit First
  SPI.setDataMode(SPI_MODE0);// Mode 0 Rising edge of data, keep clock low
  SPI.setClockDivider(SPI_CLOCK_DIV4);//Run the data in at 16MHz/4 - 4MHz
  for(i=0; i<48; i++)//clear out Gray Scale Data
    transferbyte[i]=0;
  for(i=0; i<32; i++)//set Dot Correction data to max (63 decimal for 6 bit)
    DCvalue[i]=63; 

  Serial.begin(9600);//debugging?

  //set up DOT Correction
  DotCorrection();// sub routine helps

  noInterrupts();// set up the counters, so don't go into interrupts
  TCCR2A=B00010010;//Timer 2 set to Compare Mode Toggling pin 5 @ 8MHz, Arduino Digital 3
  // TIMER 2 IS GSCLCK
  //Timer 2 prescaler set to 1, 16/1=16 MHz, but toggles pin 5 every other cycle, 8MHz
  TCCR2B=B00000001;

  TCCR1A=B00000000;//Timer 1 doesn't toggle anything, used for counting
  //Timer 1 prescaler set to Fclk/256
  //Why? We need to count 4096 pulses out of Timer 2 - pin 5
  //8 MHz = 1 pulse every 125ns - - 4096 pulses would need 512us
  //Timer 1 runs at 16MHz/256=62.5kHz, we need a match at every 512us
  //Basically, I can get an interrupt to get called every 512us, so...
  // I need to run Timer 2 @ 8MHz for 512us to get 4096 pulses
  // I can't count those pulses directy (too fast) , so
  // I'll count using Timer 1, which makes a count every 16us
  // The counter starts at 0, so we'll set it to 31 to get an interrupt after 512us
  TCCR1B=B00001100;//Mode=CTC with OSCR1A = TOP and 256 as the prescaler
  // Mask set up, will call ISR (Inerrupt Service Routine) for Compare match on A
  TIMSK1=B00000010;
  //These are the match values for the counters
  // 0 here means it will match on one cycle of the clock/prescaler
  OCR1A= 31;//to get our 512us Interrupt
  interrupts();// kick off the timers!
  //attachInterrupt(0, update, RISING);
  //48
  for(i=0; i<48; i++)//wipe out the data in tlc
    tlc(i, 0);// This is how you update the LEDs, tlc is a subroutine with two inputs
  // tlc(Channel, Value)  Channel in this case is 0-32 and value is 0-4095 duty cycle
  //4095 is 100% ON
  pinMode(5, OUTPUT);//BLANK  We set this pin up here, so it remains in a high impedance
  // state throughout the setup, otherwise the LEDs go crazy!  even if you write this HIGH
  for(int i=0;i<22;i++){
    tlc((i*3),4095); 
  }
  delay(100);
  for(int i=0;i<22;i++){
    tlc((i*3),0); 
  }
  for(int i=0;i<22;i++){
    tlc((i*3)+1,4095); 
  }
  delay(100);
  for(int i=0;i<22;i++){
    tlc((i*3)+1,0); 
  }
  for(int i=0;i<22;i++){
    tlc((i*3)+2,4095); 
  }
  delay(100);
  for(int i=0;i<22;i++){
    tlc((i*3)+2,0); 
  }
}


void loop(){//   MAIN LOOP   MAIN LOOP   MAIN LOOP   MAIN LOOP   MAIN LOOP   MAIN LOOP
  updateLED();
  //This would be a good place to put animations due to the fact that you can use all
  //of the color combos, not just 10 or so.
}


ISR(TIMER1_OVF_vect){
}// Over Limit Flag Interrupt  you need this even if you don't use it
ISR(TIMER1_COMPB_vect){
}// Compare B - Not Used
ISR(TIMER1_COMPA_vect){ // Interrupt to count 4096 Pulses on GSLCK
  PORTD |= 1<<5;// write blank HIGH to reset the 4096 counter in the TLC
  PORTD |= 1<<7;// write XLAT HIGH to latch in data from the last data stream
  PORTD &= ~(1<<7);//XLAT can go low now
  PORTD &= ~(1<<5);//Blank goes LOW to start the next cycle
  SPI.end();//end the SPI so we can write to the clock pin
  PORTB |= 1<<5;// SPI Clock pin to give it the extra count
  PORTB &= ~(1<<5);// The data sheet says you need this for some reason?
  SPI.begin();// start the SPI back up
  for(SINData=95; SINData>=0; SINData--){// send the data out!
    SPI.transfer(transferbyte[SINData]);// The SPI port only understands bytes-8 bits wide
    // The TLC needs 12 bits for each channel, so 12bits times 64 channels gives 768 bits
    // 768/8=96 bytes, 0-95
  }
  count++;
}


void updateLED(){
  ET.receiveData();
  for(int i=0;i<18;i++){

    //Print what is going on
    Serial.print(matrix.LEDGrid[i]);
    Serial.print(",");

    //Switch that receives the 0-10 data and converts that to colors.
    switch (matrix.LEDGrid[i]) {
    case 0://OFF
      tlc(((3*i)),0);
      tlc(((3*i)+1),0);
      tlc(((3*i)+2),0);
      break;
    case 1://RED
      tlc(((3*i)),4095);
      tlc(((3*i)+1),0);
      tlc(((3*i)+2),0);
      break;
    case 2://GREEN
      tlc(((3*i)),0);
      tlc(((3*i)+1),0);
      tlc(((3*i)+2),4095);
      break;
    case 3://BLUE
      tlc(((3*i)),0);
      tlc(((3*i)+1),4095);
      tlc(((3*i)+2),0);
      break;
    case 4://PURPLE
      tlc(((3*i)),2000);
      tlc(((3*i)+1),4095);
      tlc(((3*i)+2),0);
      break;
    case 5://TEAL
      tlc(((3*i)),0);
      tlc(((3*i)+1),4095);
      tlc(((3*i)+2),4095);
      break;
    case 6://YELLOW
      tlc(((3*i)),3000);
      tlc(((3*i)+1),0);
      tlc(((3*i)+2),4095);
      break;
    case 7://PINK
      tlc(((3*i)),4095);
      tlc(((3*i)+1),3500);
      tlc(((3*i)+2),3500);
      break;
    case 8://ORANGE
      tlc(((3*i)),2232);
      tlc(((3*i)+1),0);
      tlc(((3*i)+2),1108);
      break;
    case 9://LIGHT BLUE
      tlc(((3*i)),1000);
      tlc(((3*i)+1),4095);
      tlc(((3*i)+2),2000);
      break;
    case 10://WHITE
      tlc(((3*i)),1500);
      tlc(((3*i)+1),4095);
      tlc(((3*i)+2),4095);
      break;
    default:
      tlc(((3*i)),100);
      tlc(((3*i)+1),200);
      tlc(((3*i)+2),200);
    }
  }
  Serial.println("");
}

void tlc(int channel, int value){// TLC to UPDATE TLC to UPDATE TLC to UPDATE TLC to UPDATE
  // This routine needs to happen as fast as possible!!!
  if(value>4095)
    value=4095;
  if(value<0)
    value=0;
  // We need to convert the 12 bit value into an 8 bit BYTE, the SPI can't write 12bits
  //We figure out where in all of the bytes to write to, so we don't have to waste time
  // updating everything
  //12 bits into bytes, a start of 12 bits will either at 0 or 4 in a byte
  spibit=0;
  if(bitRead(channel, 0))//if the read of the value is ODD, the start is at a 4
    spibit=4;
  //This is a simplification of channel * 12 bits / 8 bits
  spibyte = int(channel*3/2);//this assignes which byte the 12 bit value starts in
  for(chbit=0; chbit<12; chbit++, spibit++){// start right at where the update will go
    if(spibit==8){//during the 12 bit cycle, the limit of byte will be reached
      spibyte++;//roll into the next byte
      spibit=0;//reset the bit count in the byte
    }
    if(bitRead(value, chbit))//check the value for 1's and 0's
      bitSet(transferbyte[spibyte], spibit);//transferbyte is what is written to the TLC
    else
      bitClear(transferbyte[spibyte], spibit);
  }//0-12 bit loop
}
void DotCorrection(){
  PORTD |= 1<<4;//VPRG to DC Mode HIGH
  spibyte=0;//reset our variables
  spibit=0;
  for(ch=0; ch<32; ch++){// 6 bit a piece x 32 Outputs
    for(chbit=0; chbit<6; chbit++){
      if(spibit==8){
        spibyte++;
        spibit=0;
      }
      if(bitRead(DCvalue[ch], chbit))//all 6 bits
        bitSet(transferbyte[spibyte], spibit);//setting bit 7 of transfer byte
      else
        bitClear(transferbyte[spibyte], spibit);
      spibit++;
    }
  }
  SPI.begin();
  for(j=spibyte; j>=0; j--){
    SPI.transfer(transferbyte[j]);
  }
  PORTD |= 1<<7;
  PORTD &= ~(1<<7);
  PORTD &= ~(1<<4);//VPRG is good to go into normal mode LOW
}


Controller Arduino:

/*
Data read:
0:  OFF        REALLY NOW
1:  RED IDK
2:  GREEN PLAYING
3:  BLUE NOTE
4:  PURPLE     BAR
5:  TEAL       -
6:  YELLOW     -
7:  PINK       -
8:  ORANGE     -
9:  L-BLUE     -
10: WHITE      -
DEF:TEST       -
*/

//The above data set corresponds to the color data defined in the LED Driver Arduino

//Include the Easy Tranfer Lib
#include <EasyTransfer.h>
EasyTransfer ET;

//This must be EXACTLY the same on the other Arduino
struct SEND_DATA_STRUCTURE{
  int LEDGrid[18];
};

SEND_DATA_STRUCTURE matrix;

//----------------------------Variables-------------------------

int buttonRaw[6][3];
int LEDData[6][3];

//--------------------------------------------------------------

void setup(){
  //Begin Serial on 9600 - debugging
  Serial.begin(9600);

  //EasyTransfer begins
  ET.begin(details(matrix), &Serial);

  //Pins 7-9 are used for the button wires
  pinMode (7, OUTPUT);
  pinMode (8, OUTPUT);
  pinMode (9, OUTPUT);

  //Define speaker port - not actually used in this shell program
  pinMode (2, OUTPUT);

  buttonCheck();

}

//------------------------------Setup---------------------------

void loop(){
  buttonCheck();
  update();

  //Add actually program in this space. Read the button values, set the
  //lights, and play sounds. Its all up to you. This is the framework
  //for whatever you want.

}

//-----------------------------Programs--------------------------

//Updater: add actual light sending mechanics. Simply set LEDData[x][y]
//array coordinate that corresponds with the appropriate light to a number
//from 0 to 10 as defined at the very top of this program.

void update(){
  for(int y = 0; y < 3; y++){
    for(int x = 0; x < 6; x++){
      matrix.LEDGrid[gridCount] = LEDData[x][y];
      gridCount++;
    }
    delay(5);
  }
  gridCount = 0;
  ET.sendData();
}

//-------------------

//Button Check: checks button values and saves that RAW data to an array.
//The order goes from (0,0)-(6,0), then (0,1)-(6,1), etc. This data can
//be used to toggle values, etc.

void buttonCheck(){
  for(int y=0;y<3;y++){
    digitalWrite(7+y,HIGH);
    for(int x=0;x<6;x++){
      buttonRaw[x][y] = analogRead (x);
    }
    digitalWrite(7+y,LOW);
  }
}

Arduino Contest

Participated in the
Arduino Contest