AndroidRGB Amp

Arduino Sketch

Click the above link to download the completed Arduino sketch or follow along below to create your own!

Overview

There are many uses for this amplification but our intended target is to control a premade strip of LEDs.

The current controller solution is a combination IR reciver and 3 channel amp to allow for simple colour adjustment and lighting control. The intent of this project is to add functionality to the controller by replacing it with an arduino to recieve IR commands and control lighting voltages. While this may seem to reinvent the wheel it will allow the user to upload custom lighting cycles to use with the default controller at low cost compared to commercial solutions.

Most types of lighting will not be at the arduino voltage and will require more current than the arduino can provide. Using third-party boards we can boost 5v to a usable 12v. Below are the materials I will be using in this project.

Materials

Arduino Board - Dealer's choice but if you plan to expand the code to more than one light you might want to pick a MEGA.

RGB Amplifier - Kind of specific but you can find them sourced here, if you are confident in your breadboard you could attempt to buy a TLP521-3 and 3 FDD6692's to create the required circuit. I do not provide a schematic for this as my specialty is software not microprocessor destruction.

LED Strip - Very common device with many, many iterations. Select one that fits your budjet and proceed. I use this as it comes with a remote and IR module to canabalize.

IR Detector - If your LED strip came with a remote it should have this or find it at a hobbyshop / digikey.

Remote - The default strip one is nice but you can use any similar remote that our library supports.

Connections

Connect the IR Detector using the 5V supply, a digital pin (11) and a ground.

Connect 2,3,4 on the arduino to the input of the RGB amplifier. Connect the V+ to the ground to use a 0V reference.

Finally connect the LED strip to the output of the amp.

To power the amp from the arduino's supply connect the VIN and GRD to the amps supply.

If you are using a different arduino board you should change any code you use to reflect your new pin arrangement.

Required Libraries

The best way to ensure smooth colour changing is a good timer. I use the TimerOne library provided by Arduino here.

We will be using another library to interperet the IR commands. I use PRJC's Library available here. You will need to extract the folders from both libraries into the /%arduino root%/libraries/ folder so they can be included during compilation.

If you are haveing trouble with the IR commands working you can run the following program on your arduino to check what signals are coming in (if any), they will be dumped to the serial monitor so make sure you are watching. Use the connection diagram above to reduce the need for code modification. Double check your remote but viewing it through a digital camera while emitting. If there is no light check the batteries.

#include "IRremote.h"

// IR Variables
int RECV_PIN = 8;
IRrecv irrecv(RECV_PIN);
decode_results results;

void setup()
{
  Serial.begin(115200);		// You may want to slow this down, make sure your serial monitor is set 
  							// accordingly
  irrecv.enableIRIn();		// Start the receiver
}

void loop()
{
  if (irrecv.decode(&results)) {
    Serial.println(results.value);
    irrecv.resume(); // Receive the next value
  }
}		

Arduino Code

.txt .pde

The entierety of the source code can be downloaded above.

The first step in our code is to declare all constants and variables. This include declaring libraries, if you get an error with the icnludes make sure that you can find examples from the libraries in your Arduino IDE, this usually means everything is installed correctly.

The pin values can be changed as required to fit your connections.

The state enumeration is important, should you wish to add additional lighting modes you will need to add them to the possible states.

//=================================================
// RGB Controller
// Daryl Dippel - 2012
// >>>
//		Accept IR commands from old remote to control light strip 

//=================================================

#include "IRremote.h"
#include "TimerOne.h"

//-------------------------------------------------
// Constants
//-------------------------------------------------
// IR
int irpin = 8;										// IR Signal Pin
// LED
int ledhb = 53;										// HB to show if board is crash, 
													// optional and not in conneciton diagram
int ledr = 2;										// Pins carrying RGB output
int ledg = 3;
int ledb = 4;
//-------------------------------------------------
// Variables
//-------------------------------------------------
// IR
IRrecv irrecv(irpin);								// Attach pin to software reciever
decode_results results;								// store results	
int IREvent = 0;									// signal an IR event has occured
// LED
float currentLEDR = 0;								// current RGB values on lights
float currentLEDG = 0;
float currentLEDB = 0;
float targetLEDR = 0;								// target RGB
float targetLEDG = 0;
float targetLEDB = 0;
int rampRDirection = 0;								// Direction of lighting change, 
													// -1 for dec, +1 for inc, 0 = not moving
float rampRModification = 0;						// For checking if we even need to adjust levels
int rampGDirection = 0;
float rampGModification = 0;
int rampBDirection = 0;
float rampBModification = 0;

float dimLevel = 1;									// Allows for darkening set lighting colours

int fadeLength = 500;								// How long we take to move to a colour
int rates[] = {25, 50, 100, 200, 300, 500, 750, 1000}; // Possiblities for Rate Selector
int rateSel = 5;
//
char currentRBG[] = "000000";						// Use HEX RGB convention to pass around colours
int ramp0Done = 0;									// be aware of what our ramp is doing so we can 
													// start new colours after one finishes
int M_Rainbow_Step = 0;								// all sequences need a step counter
int M_Cust0_Step = 0;	
int M_Cust1_Step = 0;	
// Timer
int ms = 0;											// count the ms in our timer, for hb
//
enum State {										// possible states of machine
		On,
		Colour,
		Off,
		Rainbow,
		Cust0,
		Cust1
   };

State mode = On;	

Next we declare the two most important fuctions in our program. The configureRamp function sets up all values needed by updateRamp based on the current RGB values, the target and the time allotted for transfer. The updateRamp function is called periodically to allow for gradual colour change.

//-------------------------------------------------
// Function Declaration
//-------------------------------------------------
void ConfigureRamp(char target[], int msDuration);	// Will set up increment ammounts for lighting changes
void UpdateRamp();									// called repeatedly to adjust colour	

Following this we initialize our program. Below is the init and loop functions indicative of arduino development. We set up our pins and libraries as well as start a Timer and initial mode.

The loop is very simple, first we check if a IR command has come in to modify the machines state. Failing to find a new command we proceed into our program mode. The current modes are Colour which is used for the magority of IR commands. The machine should sit here any time it is just staticly displaying a colour.

There is also an Off mode that will force a quick shutoff and modes that allow for the cycling of colours. It is simple to include a new mode should extra be required. Adding of IR modules will allow you to have a controller of customer colours etc.

       
//-------------------------------------------------
// Init
//-------------------------------------------------

void setup()
{
	Serial.begin(115200);		// You may want to slow this down, make sure your serial monitor is set accordingly
  	Serial.println("*******RGB Controller*******");
	Serial.println("***Arduino Based Solution***");	
	Serial.println("*****Daryl Dippel (2012)****");
	Serial.println("____________________________");
	Serial.println("____________LETS_DO_THIS____");

	//DDR						// set out pins to OUTPUT
	pinMode(ledhb, OUTPUT); 
	pinMode(ledr, OUTPUT); 
    pinMode(ledg, OUTPUT); 
    pinMode(ledb, OUTPUT); 
	
	//start IR reciever
	irrecv.enableIRIn();

	//timer
	Timer1.initialize(1000); // set a timer of length 1ms
    Timer1.attachInterrupt( timerIsr ); // attach the service routine here

	//set initial mode
	mode = Off;
}

//=================================================
// MAIN() - loop
//=================================================

void loop()
{

  if (irrecv.decode(&results)) {
	R_IR();				// generate IR Report in Console
	processIR(results.value);
    irrecv.resume();	// Receive the next value
  }
  
  switch(mode)
	  {
		case Off:
			ConfigureRamp("000000", 20);	// immediatly set the lights to off
			break;
		case Colour:
			if (IREvent == 1)				// Only set a target after a new command from IR
			{ConfigureRamp(currentRBG, fadeLength);	
			IREvent = 0;
			}
			break;
		case Rainbow:
			M_Rainbow();					// Sit in Rainbow mode
		  break;
		case Cust0:
			M_Cust0();
		  break;
		case Cust1:
			M_Cust1();
		  break;
		default:
			break;
	  }
}

The next part is our interrupt. This is called every 1ms reguardless of what the program is doing. If we are processing an IR command we WILL have an error. The best option is try again. If you needed reliable communication the remote would have to understand what's going on, and it just can't do that. The best we can do is decide if we want accurate timing, or accurate IR. I choose timing but if you feel that you are missing IR commands you can move the IR check to a faster and higher interrupt. Have fun!

  
/*
 * Timer interupt, hooked to 1ms interupt
 */
void timerIsr()
{
	ms++;   //tick first to ALWAYS TICK
	//upper counter control and toggle hb every 1s
	if (ms > 999)
	{
			digitalWrite( 53, digitalRead( 53 ) ^ 1 );
			ms = 0;
	}
	
	//Update ramp 40times / s, this can not change without changing the math in UpdateRamp
	if (ms % 25 == 0)
		{
			UpdateRamp();
		}
}
   
       

The IR portion of the program is capable of outputting any understood IR signals even if the do not match a command. It is simple to read the serial monitor to find what code should be added to the processIR function. As long as it does not interfere with a current code there is no need to remove old codes, you are merely adding functionality.

In the processIR function you will notice when we want to signal a new colour we write it to a buffer and flag that a new command is ready. One could directly call the ConfigureRamp from the IR command but you might want to do some extra work in the future such as output the HEX values. It is easier just to edit the mode and not all of the colour buttons.

     
// IR
/*
 * Reports the recived IR signal to consol for debugging
 */
void R_IR()
{
	Serial.println("---Found IR Response(s)---");
	Serial.print("Value: ");
	if (results.value == 0)
	{Serial.print("xGarbagex");}
	else
	{
	Serial.println(results.value);}
	return;
}



/*
 * Process the IR commands. We use codes specific to our remote, it is possible to add your
 * own cases for additional remotes by adding more values that do not conflict
 * Check serial output for any found compatible IR commands.
 */

void processIR(long IRCode)
{

	switch(IRCode)
	  {
		case 0:						// bad command return
			IREvent = 0;
			break;

		case 16187647:					//0,0  - Brighten
			brightenLED();	
			break;
		case 16220287:					//0,1  - Darken
			darkenLED();	
			break;
		case 16203967:					//0,2  - OFF
			mode = Off;
			break;
		case 16236607:					//0,3  - Rainbow Mode!
			mode = Rainbow;
			break;
		case 16195807:					//1,0  - R
			IREvent = 1;
			sprintf(currentRBG, "FF0000");
			mode = Colour;
			break;
		case 16228447:					//1,1  - G
			IREvent = 1;
			sprintf(currentRBG, "00FF00");
			mode = Colour;
			break;
		case 16212127:					//1,2  - B
			IREvent = 1;
			sprintf(currentRBG, "0000FF");
			mode = Colour;
			break;
		case 16244767:					//1,3  - W
			IREvent = 1;
			sprintf(currentRBG, "FFFFFF");
			mode = Colour;
			break;
		case 16191727:					//2,0  - 
			IREvent = 1;
			sprintf(currentRBG, "FF4400");
			mode = Colour;
			break;
		case 16224367:					//2,1  - 
			IREvent = 1;
			sprintf(currentRBG, "00FF44");
			mode = Colour;
			break;
		case 16208047:					//2,2  - 
			IREvent = 1;
			sprintf(currentRBG, "4400FF");
			mode = Colour;
			break;
		case 16240687:					//2,3  -  Increase Change Rate
			incRate();
			break;
		case 16199887:					//3,0  - 
			IREvent = 1;
			sprintf(currentRBG, "FF8800");
			mode = Colour;
			break;
		case 16232527:					//3,1  - 
			IREvent = 1;
			sprintf(currentRBG, "00FF88");
			mode = Colour;
			break;
		case 16216207:					//3,2  - 
			IREvent = 1;
			sprintf(currentRBG, "8800FF");
			mode = Colour;
			break;
		case 16248847:					//3,3  -  Decrease Change Rate
			decRate();
			break;
		case 16189687:					//4,0  - 
			IREvent = 1;
			sprintf(currentRBG, "FFBB00");
			mode = Colour;
			break;
		case 16222327:					//4,1  - 
			IREvent = 1;
			sprintf(currentRBG, "00FFBB");
			mode = Colour;
			break;
		case 16206007:					//4,2  - 
			IREvent = 1;
			sprintf(currentRBG, "BB00FF");
			mode = Colour;
			break;
		case 16238647:					//4,3  -  Custom Sequence 0
			mode = Cust0; 

			break;
		case 16197847:					//5,0  - 
			IREvent = 1;
			sprintf(currentRBG, "FFFF00");
			mode = Colour;
			break;
		case 16230487:					//5,1  - 
			IREvent = 1;
			sprintf(currentRBG, "00FFFF");
			mode = Colour;
			break;
		case 16214167:					//5,2  - 
			IREvent = 1;
			sprintf(currentRBG, "FF00FF");
			mode = Colour;
			break;
		case 16246807:					//5,3  -  Custom Sequence 1
			mode = Cust1;
			break;

		case 4294967295:  // This signal is sent out if a button is held down / repeated quickly
					
		  break;


		default:
			

			break;
	  }

	  return;
	  }

       

One of the program's features is to cycle through a colour sequence. The following functions are called in a sequence mode. This sequence sets an RGB target and waits for it to return a done signal before starting the next. There is not really a limit beyong the int limits to how many could be sequenced. Get creative!

     
//-------------------------------------------------
//Lighting Modes
//-------------------------------------------------

/*
 * Rainbow Transition Pattern
 */
void M_Rainbow()
{
	switch(M_Rainbow_Step)
	  {
		case 0:
			if (ramp0Done == 1)
			{ConfigureRamp("FF0000", fadeLength);
			M_Rainbow_Step++;}
		  break;
		case 1:
			if (ramp0Done == 1)
			{ConfigureRamp("FFFF00", fadeLength);
			M_Rainbow_Step++;}
		  break;
		case 2:
			if (ramp0Done == 1)
			{ConfigureRamp("00FF00", fadeLength);
			M_Rainbow_Step++;}
		  break;
		case 3:
			if (ramp0Done == 1)
			{ConfigureRamp("00FFFF", fadeLength);
			M_Rainbow_Step++;}
		  break;
		case 4:
			if (ramp0Done == 1)
			{ConfigureRamp("0000FF", fadeLength);
			M_Rainbow_Step++;}
		  break;
		case 5:
			if (ramp0Done == 1)
			{ConfigureRamp("FF00FF", fadeLength);
			M_Rainbow_Step = 0;}
		  break;

		default:
			break;
	  }
	return;
}


/*
 * Custom Transition Pattern 0
 */
void M_Cust0()
{
	switch(M_Cust0_Step)
	  {
		case 0:
			if (ramp0Done == 1)
			{ConfigureRamp("6600CC", fadeLength);
			M_Cust0_Step++;}
		  break;
		case 1:
			if (ramp0Done == 1)
			{ConfigureRamp("FF00FF", fadeLength);
			M_Cust0_Step++;}
		  break;
		case 2:
			if (ramp0Done == 1)
			{ConfigureRamp("0033FF", fadeLength);
			M_Cust0_Step = 0;}
		  break;

		default:
			break;
	  }
	return;
}

/*
 * Custom Transition Pattern 1
 */
void M_Cust1()
{
	switch(M_Cust1_Step)
	  {
		case 0:
			if (ramp0Done == 1)
			{ConfigureRamp("FFFF00", fadeLength);
			M_Cust1_Step++;}
		  break;
		case 1:
			if (ramp0Done == 1)
			{ConfigureRamp("00FF00", fadeLength);
			M_Cust1_Step++;}
		  break;
		case 2:
			if (ramp0Done == 1)
			{ConfigureRamp("00FFFF", fadeLength);
			M_Cust1_Step++;}
		  break;
		case 3:
			if (ramp0Done == 1)
			{ConfigureRamp("330066", fadeLength);
			M_Cust1_Step = 0;}
		  break;

		default:
			break;
	  }
	return;
}

       

The following functions are used to darken and brighten a dimness variable. The variable ranged from 0 to 1 and its multiplied against the final colour target to produce brightness levels.

    
//-------------------------------------------------
//Lighting Functions
//-------------------------------------------------
/*
 * Brighten Leds
 */
void brightenLED()
{
	if (dimLevel + 0.05 < 1)
	{dimLevel = dimLevel + 0.05;}
	else
	{dimLevel = 1;}
	
	return;

}
/*
 * Darken the leds
 */
void darkenLED()
{
	if (dimLevel - 0.05 > 0)
	{dimLevel = dimLevel - 0.05;}
	else
	{dimLevel = 0;}
	
	return;

} 
       

These functions select the desired rate of transition from an array of possiblities. Since the array only has 8 values we bound our options accordingly. If you adjust the possiblilities also adjust how you get to them here.

     
/*
 * Speed up the transition rate
 */
void incRate()
{
	if (rateSel < 7)
	{rateSel++;}
	else 
	{rateSel = 0;}

	fadeLength = rates[rateSel];
	
	return;

}

/*
 * Slow the transition rate
 */
void decRate()
{
	if (rateSel > 0)
	{rateSel--;}
	else 
	{rateSel = 7;}

	fadeLength = rates[rateSel];
	
	return;

}

       

In order to be able to make sense of our RGB strings we need to be able to return an integer. While there are built in options with sprint we would like to open up the possiblity of returning null values such that we can pass changes to one channel in the form of "XXXXFF".

Another small caviet is upper and lower case hexidecimal. We use our own custom conversion function intfromchar to produce desired results.

     
/*
 * return an integer from 0-255 from 2 hex characters, if sent XX return 404
 */
int getbright(char cmd2, char cmd3)
{
  if (cmd2 != 'X')
  {
  if (cmd3 != 'X')
  {
  int c1 = intfromchar(cmd2);
  int c2 = intfromchar(cmd3);
      
    if (c1<0 || c2<0) {
	  return -1;  // an invalid hex character was encountered
    } else {
	  return abs((c1*16) + c2);
    }
  }
  }
  return 404;
	
}

//=================================================
// Utility Function
//=================================================

/*
 * return the numerical value from the hex character
 */
int intfromchar (char cmd)
{
  switch(cmd)
  {
    case '0':
      return 0;
      break;
    case '1':
      return 1;
      break;
    case '2':
      return 2;
      break;
    case '3':
      return 3;
      break;      
    case '4':
      return 4;
      break;
    case '5':
      return 5;
      break;
    case '6':
      return 6;
      break;
    case '7':
      return 7;
      break; 
    case '8':
      return 8;
      break;
    case '9':
      return 9;
      break;
    case 'A':
      return 10;
      break;
    case 'B':
      return 11;
      break;      
    case 'C':
      return 12;
      break;
    case 'D':
      return 13;
      break;
    case 'E':
      return 14;
      break;
    case 'F':
      return 15;
      break;    
	case 'a':
      return 10;
      break;
    case 'b':
      return 11;
      break;      
    case 'c':
      return 12;
      break;
    case 'd':
      return 13;
      break;
    case 'e':
      return 14;
      break;
    case 'f':
      return 15;
      break;
	default:
		return 0;
  }
}

       

Finally this is where the magic happens. We take our target and a duration and set the ammount one would increment each light every update to produce the desired time. This is a bit of a sneaky solution but it works very well. If you modify how often the update is called you will of course have to modify the calculations. You can also see in the code that there is the possiblity to not modify a channel if it is given the ominous "missing" value.

UpdateRamp is in turn called periodically (40 times per second in our example). Here we make the actually modifications to the lighting values to arrive at our target. A channel is either ramping positivly or negativly or not at all and this is checked to determin what action should be taken with the modification value. Should we reach target the ramp is set to 0, the static state. When all three channels align we signal target reached.

I hope you have enoyed this short "tutorial" on creating an alternative lighting controller for RGB strips. If you have any questions, comments or additions feel free to contact me!

  
/*
 * Modification of Lighting UPDATE values occurs here
 * Set the ramp to incomplete and calculate the ammount changed per update
 * Note that the 25 is the mod value seen in the interrupt, if you change the frequency
 * of updateing you must change this to have the correct ammount modified
 */

void ConfigureRamp(char target[], int msDuration)
{
	ramp0Done = 0;
	if (getbright(target[0],target[1]) != 404)
	{
	targetLEDR = getbright(target[0],target[1]);

	if ((targetLEDR - currentLEDR) < 0)
	{rampRDirection = -1;}
	else if ((targetLEDR - currentLEDR) > 0)
	{rampRDirection = 1;}
	else
	{rampRDirection = 0;}

	rampRModification = ((targetLEDR - currentLEDR) / (msDuration/25));
	}

	if (getbright(target[2],target[3]) != 404)
	{
	targetLEDG = getbright(target[2],target[3]);

	if ((targetLEDG - currentLEDG) < 0)
	{rampGDirection = -1;}
	else if ((targetLEDG - currentLEDG) > 0)
	{rampGDirection = 1;}
	else
	{rampGDirection = 0;}

	rampGModification = ((targetLEDG - currentLEDG) / (msDuration/25));
	}

	if (getbright(target[4],target[5]) != 404)
	{
	targetLEDB = getbright(target[4],target[5]);

	if ((targetLEDB - currentLEDB) < 0)
	{rampBDirection = -1;}
	else if ((targetLEDB - currentLEDB) > 0)
	{rampBDirection = 1;}
	else
	{rampBDirection = 0;}

	rampBModification = ((targetLEDB - currentLEDB) / (msDuration/25));
	}

	return;
}

/*
 * Modification of Lighting values occurs here
 * First we check the direction variable and if we are not yet at target we "step" towards it in an increment
 * these increments are set by configure ramp by taking the current RGB and target RGB into consideration
 * if we are at the target for that channel, set the direction to 0.
 *  when all three channels are complete signal ramp done so a new one can begin
 */
void UpdateRamp()
{
	

	if (rampRDirection == 1) 
	{
		if ((currentLEDR + rampRModification) < targetLEDR)
		{
			currentLEDR = currentLEDR + rampRModification;
		}
		else
		{rampRDirection = 0;
		currentLEDR = targetLEDR;}
	}

	if (rampRDirection == -1) 
	{
		if ((currentLEDR + rampRModification) > targetLEDR)
		{
			currentLEDR = currentLEDR + rampRModification;
		}
		else
		{rampRDirection = 0;
		currentLEDR = targetLEDR;}
	}
	
	if (rampGDirection == 1) 
	{
		if ((currentLEDG + rampGModification) < targetLEDG)
		{
			currentLEDG = currentLEDG + rampGModification;
			
		}
		else
		{rampGDirection = 0;
		currentLEDG = targetLEDG;}
	}

	if (rampGDirection == -1) 
	{
		if ((currentLEDG + rampGModification)  > targetLEDG)
		{
			currentLEDG = currentLEDG + rampGModification;
		}
		else
		{rampGDirection = 0;
		currentLEDG = targetLEDG;}
	}

	if (rampBDirection == 1) 
	{
		if ((currentLEDB + rampBModification) < targetLEDB)
		{
			currentLEDB = currentLEDB + rampBModification;
		}
		else
		{rampBDirection = 0;
		currentLEDB = targetLEDB;}
	}

	if (rampBDirection == -1) 
	{
		if ((currentLEDB + rampBModification) > targetLEDB)
		{
			currentLEDB = currentLEDB + rampBModification;
		}
		else
		{rampBDirection = 0;
		currentLEDB = targetLEDB;}
	}
	
	// set the arduino pins to output the current RGB
	{analogWrite(ledr, (int)ceil(currentLEDR * dimLevel));}
	{analogWrite(ledg, (int)ceil(currentLEDG * dimLevel));}
	{analogWrite(ledb, (int)ceil(currentLEDB * dimLevel));}

	// check to see if the ramp is completed
	if (rampRDirection == 0)
	{if (rampGDirection == 0)
	{if (rampBDirection == 0)
	{ ramp0Done = 1;
	}}}
	return;
}