Tuesday, January 18, 2011

Arduino notebook: an RGB LED and the color wheel (Part 1)

As I recently wrote about, I received a new Arduino experimental kit a couple weeks ago for Christmas. In a simple learning phase, last weekend I built one of the projects in the getting started book and then expanded on it to make it do a bit more.

The project was to control a RGB LED that came with the kit. A RGB LED basically has three LEDs in a single physical package. The LEDs have a common anode so the package has four leads coming out of it. One you connect to +5V and each of the others you connect to its own pin on the Arduino through a resistor. This configuration allows you to turn on each color individually by setting that color's pin off (to ground). If the pin is set on, the voltage is +5V and the LED will turn off since there is no current going through the LED. This is a bit backwards from normal intuition, but it makes sense if you understand what's happening with the voltage/current.

RGB LED color wheel_bbClick image to see larger. Here's a schematic view.

I started with a simple circuit as shown in CIRC-12 here. [Update: also shown above.] It simply consists of the LED connected to +5V on its anode and three resistors on the other legs. The other side of the resistors are connected to three pins on the controller. Pretty simple stuff. I then entered the following program from the manual and ran it.
int ledPins[] = { 9, 10, 11 };
int inputPin = 0;

const boolean ON = LOW;
const boolean OFF = HIGH;

const boolean RED[] = {ON, OFF, OFF};
const boolean GREEN[] = {OFF, ON, OFF};
const boolean BLUE[] = {OFF, OFF, ON};
const boolean YELLOW[] = {ON, ON, OFF};
const boolean CYAN[] = {OFF, ON, ON};
const boolean MAGENTA[] = {ON, OFF, ON};
const boolean WHITE[] = {ON, ON, ON};
const boolean BLACK[] = {OFF, OFF, OFF};
const boolean* COLORS[] = {RED, YELLOW, GREEN, CYAN, BLUE, MAGENTA, WHITE, BLACK};

void setup()
{
for(int i = 0; i < 3; i++)
pinMode(ledPins[i], OUTPUT);
}

void loop()
{
//setColor(ledPins, YELLOW);
randomColor();
}

void randomColor()
{
int rand = random(0, sizeof(COLORS) / sizeof(boolean*));
setColor(ledPins, COLORS[rand]);
delay(1000);
}

void setColor(int* led, const boolean* color)
{
for(int i = 0; i < 3; i++)
digitalWrite(led[i], color[i]);
}
This simple program (sketch in Arduino terminology), will either set the color to one of the predefined values if the first line in loop() is uncommented or will randomly select one of the colors each second if the second line is uncommented. One thing to note, because the LED has a common anode, to turn a color off, we have to give that pin a voltage. This is why the ON constant to indicate the color should be lit is defined as a LOW voltage and the OFF is defined as a HIGH voltage. This definition makes it easier to read the color definitions.

So far, so good, but a bit on the boring side. I decided in order to juice things up a bit I wanted it to cycle through the colors of the color wheel rather than randomly pick things out of an array. In addition to this, I wanted to fade from one color to the next rather than have abrupt jumps. At this point, one of the beauties of microcontrollers comes to the forefront: all this can be done through simply software changes.

The example above turns each LED either full on or full off. To make a smooth transition, I needed to display the LEDs in partially lit states. This is done with what's called pulse-width modulation, or PWM. The beauty of the Arduino is that it has this facility built-in. Instead of calling digitalWrite as in the code above, you call analogWrite with a value between 0 and 255 where 0 is full off, 255 is full on and values in-between represent a proportional state between full off and full on. So, the first step was to change the boolean types to bytes. This was a simple change of type and the definitions for ON and OFF. Finally, I changed setColor to use analogWrite instead of digitalWrite. This gave a functionally equivalent version that was now setup to use intermediate values.
int ledPins[] = { 9, 10, 11 };
int inputPin = 0;

const byte ON = 0;
const byte OFF = 255;

const byte RED[] = {ON, OFF, OFF};
const byte GREEN[] = {OFF, ON, OFF};
const byte BLUE[] = {OFF, OFF, ON};
const byte YELLOW[] = {ON, ON, OFF};
const byte CYAN[] = {OFF, ON, ON};
const byte MAGENTA[] = {ON, OFF, ON};
const byte WHITE[] = {ON, ON, ON};
const byte BLACK[] = {OFF, OFF, OFF};
const byte* COLORS[] = {RED, YELLOW, GREEN, CYAN, BLUE, MAGENTA, WHITE, BLACK};

void setup()
{
for(int i = 0; i < 3; i++)
pinMode(ledPins[i], OUTPUT);
}

void loop()
{
//setColor(ledPins, YELLOW);
randomColor();
}

void randomColor()
{
int rand = random(0, sizeof(COLORS) / sizeof(byte*));
setColor(ledPins, COLORS[rand]);
delay(1000);
}

void setColor(int* led, const byte* color)
{
for(int i = 0; i < 3; i++)
analogWrite(led[i], color[i]);
}

The next step is to change the code to display colors in the order of the color wheel rather than randomly. To start, I used the existing COLORS array. I changed the loop method to call a new method setHueValue with a parameter that computed the new hue value and added a delay. I also removed the randomColor function since I wasn't using it anymore.
int ledPins[] = { 9, 10, 11 };
int inputPin = 0;

const byte ON = 0;
const byte OFF = 255;

const byte RED[] = {ON, OFF, OFF};
const byte GREEN[] = {OFF, ON, OFF};
const byte BLUE[] = {OFF, OFF, ON};
const byte YELLOW[] = {ON, ON, OFF};
const byte CYAN[] = {OFF, ON, ON};
const byte MAGENTA[] = {ON, OFF, ON};
const byte WHITE[] = {ON, ON, ON};
const byte BLACK[] = {OFF, OFF, OFF};
const byte* COLORS[] = {RED, YELLOW, GREEN, CYAN, BLUE, MAGENTA, WHITE, BLACK};

void setup()
{
for(int i = 0; i < 3; i++)
pinMode(ledPins[i], OUTPUT);
}

void loop()
{
setHueValue(computeNextValue());
delay(1000);
}

int colorValue = -1;

int computeNextValue()
{
colorValue = (colorValue+1) % 6;
return colorValue;
}

void setHueValue(int hueValue)
{
setColor(ledPins, COLORS[hueValue]);
}

void setColor(int* led, const byte* color)
{
for(int i = 0; i < 3; i++)
analogWrite(led[i], color[i]);
}
This was great for five values, but I wanted it to cycle smoothly from one color to the next. To do this with a table lookup method was going to require a huge table. A different approach to the problem was needed.

RGB is a common way to model color with anything that projects light, such as computer monitors and LEDs. However, there are other ways of thinking about color. For example, things that reflect light such as painting and printing, use CMYK or Cyan-Magenta-Yellow-Black. If you ever mixed paint in art class, this is the color wheel you learned. There are another models that can be handy when doing various types of computations, one of which is Hue-Saturation-Value, or HSV. For what I wanted to do, this works well.

The first step in this stage was to find an easy to use function for converting HSV values to RGB values. A simple search of Google for "hsv to rgb converter" gave me an open source PHP (or perhaps it was Python) function. A quick conversion to C used by the Arduino and I had the hsvToRgb function below. This function takes a hue value in the range 0 to 360. This is because the HSV model maps the hue to a circle and so it uses a degree measurement to specify the hue value. The Saturation and Value portions of the model are a percentage from 0 to 100%, represented by fractional values from 0 to 1. For the purposes of this experiment, I just set them both to 1.

Once I had the color converter, I next changed computeNextValue to return a max value of 359 instead of 5. This will give me the proper input for the hue value. Then I removed all the color definitions at the top of the sketch since the color values are now computed and the constants won't be needed. I reduced the time delay to 10 milliseconds. With 360 steps, this will give about a 3.5 second cycle time. Next I changed the COLORS array reference in setColor to call the new function. Since the HSV code returns values in 0 to 255 range and the pins need things inverted for voltage reasons, in setColor, I subtract the color in the byte array from 255 rather than passing it in directly. The only real difference this makes from a user perspective is the starting color is at red instead of 180 degrees out of phase at cyan.

With all this, I accomplished my goals for this experiment. Here is the final sketch.
int ledPins[] = { 9, 10, 11 };
int inputPin = 0;
void setup()
{
for(int i = 0; i < 3; i++)
pinMode(ledPins[i], OUTPUT);
}

void loop()
{
setHueValue(computeNextValue());
delay(10);
}

int colorValue = 0;

int computeNextValue()
{
colorValue = (colorValue+1) % 360;
return colorValue;
}

void setHueValue(int hueValue)
{
setColor(ledPins, hsvToRgb(hueValue, 1, 1));
}

void setColor(int* led, const byte* color)
{
for(int i = 0; i < 3; i++)
analogWrite(led[i], 255-color[i]);
}

byte rgb[3];

byte* hsvToRgb(int h, double s, double v)
{
// Make sure our arguments stay in-range
h = max(0, min(360, h));
s = max(0, min(1.0, s));
v = max(0, min(1.0, v));
if(s == 0)
{
// Achromatic (grey)
rgb[0] = rgb[1] = rgb[2] = round(v * 255);
return rgb;
}
double hs = h / 60.0; // sector 0 to 5
int i = floor(hs);
double f = hs - i; // factorial part of h
double p = v * (1 - s);
double q = v * (1 - s * f);
double t = v * (1 - s * (1 - f));
double r, g, b;
switch(i)
{
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
default: // case 5:
r = v;
g = p;
b = q;
}
rgb[0] = round(r * 255.0);
rgb[1] = round(g * 255.0);
rgb[2] = round(b * 255.0);
return rgb;
}
In the next installment, I'll walk through adding some simple control mechanisms to this circuit along with various software changes to make it respond differently with the same hardware.
Post a Comment