Introduction: How to Make Proper Rainbow and Random Colors With the RGB Color Model

It would seem that such simple things as a rainbow effect or smoothly changing random colors on an RGB LED are pretty trivial. However, time and again I come upon projects using strange approaches to this matter. The most common strangeness is the use of the HSV color model for RGB LED programming. Really, people! An RGB LED consists of three LEDs: red, green and blue. What’s better suited to control such a thing than the red, green and blue color model? Why complicate matters by additional algorithms converting data from a very different set of rules to the native one? Still, HSV persists; it’s even present in an excellent ShiftPWM library...

In any case, one of the readers asked for a code in my previous instructable dealing with RGB LED strips connection to an Arduino, and I promised an article. Here it goes, hopefully it’s not too late for some of the readers to make some quick Christmas lights. Here you’ll learn of different possible approaches to an RGB rainbow, random colors and their transitions, as well as some bits on the usefulness of a sine wave and lookup tables.

You’ll need just an Arduino and an RGB LED to run the sketches present.

But first, let’s deal with the HSV. If you want to get to the sketches ASAP, you may skip to the second step now.

Step 1: To HSV or Not to HSV

The most important HSV achievement is bringing together two color worlds: the light-emitting one (RGB) and the light-reflecting one (CMYK). These worlds are very different: for example, mixing red and green on the screen results in pure yellow, but try mixing them on paper and you’ll get a dirty blot. Thus HSV is pretty useful for graphic designers —they can do everything in one model and be reasonably sure that the result will look the same on the screen and on paper. However, as with all the other universal solutions, some constraints are imposed, thus, people who work exclusively with printed designs have to rely on CMYK, and people dealing with LEDs are better off with the RGB model. It’s just way better suited for light-emitting sources!

Let me dig a bit deeper into this. HSV stands for Hue, Saturation and Value (or Brightness). Hue is a circle, it has values from 0 to 360. Saturation and Value are measured in percents (0 to 100). Not exactly suited for a world of bytes... And what’s more, two of these values are almost useless when dealing with LEDs, because they are made for complex hi-res designs, not single pixels (aka RGB LEDs).

Saturation can be used to produce soft, pastel colors, without the complex calculations needed with RGB model. But with LEDs you don’t want pastel colors (they look just like different shades of white, in fact): you need your colors as bright and clear as possible, so Saturation is at 100% almost all the time (excluding the animation/transformation moments, but they are done with algorithms; more on that later).

With Brightness you can achieve some dark colors on screen. Lower brightness in red (H=0) and you get burgundy, lower brightness in orange (H=40) and get brown. Do the same with LEDs and you’ll get the same red and yellow colors, just a bit dimmer; there’s no such thing as a ‘brown LED’. You see deep dark red on the screen because there are different colors present there; try to fill it with dark red entirely, turn off all the lights and you’ll see a red rectangle; you won’t be able to tell whether it’s ‘burgundy’ or ‘scarlet’ due to absence of any reference. Same with LEDs: even if you have a bunch of them, they are still separate. Thus the use of Value in HSV model with RGB LEDs is limited to setting global brightness, fadeouts and transitions; all this can be easily done in RGB by simple simultaneous division applied to each channel.

Another thing that makes HSV Value/Brightness useless with LEDs is the fact that diodes, unlike computer monitors, are not calibrated, and are slow; their brightness is not linear. At 50% they look almost the same as at 100%, which, in fact, is very good, because it helps control power consumption.

Which leaves us with Hue: a nice circle containing all the colors of a rainbow in a neat 0-360° sequence. Seems like producing a rainbow with it is the easiest thing imaginable: the simple for (int k=0; k<360; k++) cycle will do the trick. What can be wrong with that?

See the picture with normal HSV graph above. As the model was made to contain every possible color, it considers yellow (HSV = 60, 100, 100) to have both red and green of the RGB model at maximum (255, 255, 0). With RGB LED it means that both red and green diodes are fully on. Which means that every composite color (yellow, cyan, magenta) consumes two times more current than a base one (red, green, blue). Not good, especially if you’re dealing with long LED strips or a matrix of RGB LEDs powered by USB. The second graph (‘Power-conscious HSV’) looks better, but it’s not the pure Hue of HSV, and is easier implemented with RGB model.

The third graph shows a sine wave rainbow. In my opinion it’s the best one, as it produces deeper base colors and is devoid of spikes. And it can be implemented only in the RGB model. On to the next step.

Step 2: Rainbow Time!

The video above shows same five LEDs running rainbow in three modes: regular HSV (top), ‘power-conscious’ HSV (middle) and sine wave (bottom). Filming LEDs is not an exactly rewarding experience, but hopefully you can see the difference between different modes. There’s a B&W footage at the end, it clearly shows the spikes of HSV modes. In any case, here is the code that’ll let you repeat the experience yourself:


// uint8_t is the same as byte
// uint16_t is unsigned int // I just noticed that I mixed these in this sketch, sorry

const uint8_t lights[360]={ 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 15, 17, 18, 20, 22, 24, 26, 28, 30, 32, 35, 37, 39, 42, 44, 47, 49, 52, 55, 58, 60, 63, 66, 69, 72, 75, 78, 81, 85, 88, 91, 94, 97, 101, 104, 107, 111, 114, 117, 121, 124, 127, 131, 134, 137, 141, 144, 147, 150, 154, 157, 160, 163, 167, 170, 173, 176, 179, 182, 185, 188, 191, 194, 197, 200, 202, 205, 208, 210, 213, 215, 217, 220, 222, 224, 226, 229, 231, 232, 234, 236, 238, 239, 241, 242, 244, 245, 246, 248, 249, 250, 251, 251, 252, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 253, 253, 252, 251, 251, 250, 249, 248, 246, 245, 244, 242, 241, 239, 238, 236, 234, 232, 231, 229, 226, 224, 222, 220, 217, 215, 213, 210, 208, 205, 202, 200, 197, 194, 191, 188, 185, 182, 179, 176, 173, 170, 167, 163, 160, 157, 154, 150, 147, 144, 141, 137, 134, 131, 127, 124, 121, 117, 114, 111, 107, 104, 101, 97, 94, 91, 88, 85, 81, 78, 75, 72, 69, 66, 63, 60, 58, 55, 52, 49, 47, 44, 42, 39, 37, 35, 32, 30, 28, 26, 24, 22, 20, 18, 17, 15, 13, 12, 11, 9, 8, 7, 6, 5, 4, 3, 2, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

const uint8_t HSVlights[61] = {0, 4, 8, 13, 17, 21, 25, 30, 34, 38, 42, 47, 51, 55, 59, 64, 68, 72, 76, 81, 85, 89, 93, 98, 102, 106, 110, 115, 119, 123, 127, 132, 136, 140, 144, 149, 153, 157, 161, 166, 170, 174, 178, 183, 187, 191, 195, 200, 204, 208, 212, 217, 221, 225, 229, 234, 238, 242, 246, 251, 255};

const uint8_t HSVpower[121] = {0, 2, 4, 6, 8, 11, 13, 15, 17, 19, 21, 23, 25, 28, 30, 32, 34, 36, 38, 40, 42, 45, 47, 49, 51, 53, 55, 57, 59, 62, 64, 66, 68, 70, 72, 74, 76, 79, 81, 83, 85, 87, 89, 91, 93, 96, 98, 100, 102, 104, 106, 108, 110, 113, 115, 117, 119, 121, 123, 125, 127, 130, 132, 134, 136, 138, 140, 142, 144, 147, 149, 151, 153, 155, 157, 159, 161, 164, 166, 168, 170, 172, 174, 176, 178, 181, 183, 185, 187, 189, 191, 193, 195, 198, 200, 202, 204, 206, 208, 210, 212, 215, 217, 219, 221, 223, 225, 227, 229, 232, 234, 236, 238, 240, 242, 244, 246, 249, 251, 253, 255};

uint8_t outputPins[6] = {3, 5, 6, 9, 10, 11}; // PWM pins

// setRGBpoint (0, ...) for pins 3, 5, 6; setRGBpoint (1, ...) for pins 9, 10, 11. // See array above void setRGBpoint(byte LED, uint8_t red, uint8_t green, uint8_t blue) { // this code is for common anode LEDs. If you use common cathode ones, // remove the '255-' bits. analogWrite(outputPins[LED*3], 255-red); analogWrite(outputPins[LED*3+1], 255-green); analogWrite(outputPins[LED*3+2], 255-blue); }

// the real HSV rainbow void trueHSV(byte LED, int angle) { byte red, green, blue;

if (angle<60) {red = 255; green = HSVlights[angle]; blue = 0;} else if (angle<120) {red = HSVlights[120-angle]; green = 255; blue = 0;} else if (angle<180) {red = 0, green = 255; blue = HSVlights[angle-120];} else if (angle<240) {red = 0, green = HSVlights[240-angle]; blue = 255;} else if (angle<300) {red = HSVlights[angle-240], green = 0; blue = 255;} else {red = 255, green = 0; blue = HSVlights[360-angle];} setRGBpoint(LED, red, green, blue); }

// the 'power-conscious' HSV rainbow void powerHSV(byte LED, int angle) { byte red, green, blue; if (angle<120) {red = HSVpower[120-angle]; green = HSVpower[angle]; blue = 0;} else if (angle<240) {red = 0; green = HSVpower[240-angle]; blue = HSVpower[angle-120];} else {red = HSVpower[angle-240]; green = 0; blue = HSVpower[360-angle];} setRGBpoint(LED, red, green, blue); }

// sine wave rainbow void sineLED(byte LED, int angle) { setRGBpoint(LED, lights[(angle+120)%360], lights[angle], lights[(angle+240)%360]); }

void setup() { }

void loop() { for (int k=0; k<360; k++) {

// uncomment the mode (or modes) you need below. // with all six PWM outputs connected you may use 2 modes, change one 0 to 1.

trueHSV(0, k);

//powerHSV(0, k);

//sineLED(0, k);

delay(30); } }

Just connect one or two RGB LEDs to your Arduino and uncomment the needed routines in the loop(). Note that this code was written for common anode LEDs, if you have common cathode ones remove all three ‘255-‘ from the setRGBpoint() function.

The setRGBpoint() function itself is included for easier transition between PWM outputs and LED drivers. Change it accordingly if you’re using the latter. Note that you can still use one byte values, just multiply them in the function with ‘<<4’ for 12-bit output and ‘<<8’ for 16-bit one.

This code is enough to get you started, but if you want it explained a bit, read on (and yes, the random colors will follow shortly).

Step 3: Lookup Tables

A large first part of the code in previous step consists of big constant arrays. These are lookup tables storing pre-calculated values for sine wave and HSV functions. Of course, it is possible instead to calculate the exact value of each LED the time it is turned on, for example, for normal HSV:

byte red, green, blue;

if (angle<60) {red = 255; green = round(angle*4.25-0.01); blue = 0;} else if (angle<120) {red = round((120-angle)*4.25-0.01); green = 255; blue = 0;} else if (angle<180) {red = 0, green = 255; blue = round((angle-120)*4.25-0.01);} else if (angle<240) {red = 0, green = round((240-angle)*4.25-0.01); blue = 255;} else if (angle<300) {red = round((angle-240)*4.25-0.01), green = 0; blue = 255;} else {red = 255, green = 0; blue = round((360-angle)*4.25-0.01);} setRGBpoint(LED, red, green, blue);

These calculations will free some dynamic memory, but at the cost of program memory and processing time. While it is more or less OK with simple multiplication of HSV, doing sine waves in realtime requires operations with floating point and most certainly should be avoided, hence the lookup tables.

Note that the sine wave lookup table even contains an array of zeroes at the end – that’s because I use these tables a lot and found that a hundred more filled bytes of dynamic memory is a good trade-off for a cleaner code.

You can calculate these tables during setup() instead of inserting them in the code – again, as I use them a lot, I prefer to do them once and then just copy-paste the arrays.

You may want to make your own table of different size or with different values, so here’s the sketch. It will print all three arrays in the Serial Monitor window, it’s easy to copy-paste them from there.

uint8_t lights[360];
uint16_t kkk;

void setup() {

Serial.begin(9600);

//sine wave Serial.println("const uint8_t lights[360]={"); for (float k=PI; k<(3*PI); k=k+PI/120) { lights[kkk]=int((cos(k)+1)*127.7); // I use cosinus if (lights[kkk]<10) Serial.print(" "); // I like to keep the table clean if (lights[kkk]<100) Serial.print(" "); // told you I use them a lot! Serial.print(lights[kkk]); if (kkk<255) Serial.print(", "); //'if' portion is useful if you're not filling the // rest of the table with zeroes. It makes sure there's // no extra comma after the final value of array. // I keep it here just for that situation if (kkk%8==7) Serial.println(); // new line for cleaner table! kkk++; }

for (; kkk<360; kkk++) // fill the rest with zeroes { Serial.print(" 0"); if (kkk<359) Serial.print(", "); if (kkk%8==7) Serial.println(); } Serial.println("};");

// HSV // note that unlike the previous one these two just print out the numbers without storing them // change the Serial.print to lights[k]= if you need to store the values in lights[] array // for later use in the loop() Serial.println("const uint8_t HSVlights[61] = {"); for (int k=0; k<61; k++) { Serial.print(round(k*4.25-0.01)); if (k<60) Serial.print(", "); } Serial.println("};"); //power-conscious HSV Serial.println("const uint8_t HSVpower[121] = {"); for (int k=0; k<121; k++) { Serial.print(round(k*2.125-0.01)); if (k<120) Serial.print(", "); } Serial.println("};"); }

void loop() { }

It is a good idea to keep the lookup-table-generating code in the setup() portion of your sketch until you are perfectly satisfied with their results, then copy-paste the const table and remove the code.

Step 4: Random Colors

Ok, rainbow done, let’s do some nice random colors. What can be easier? Just

byte r, g, b;
r = random(255); g = random(255); b = random(255); setRGBpoint(LED, r, g, b);

will do, right? I guess you already see that this will result in uncontrollable power fluctuations. What’s more, this thing mostly looks like just slightly tinted shades of white, as it fills all three channels with some garbage (in HSV model that will be an average of 50% Saturation – but we want 100%!).

Look at this:

r = random(256);
g = random(256-r); b = (255-r-g); setRGBpoint(LED, r, g, b);

Here there is no problem with power consumption, but you can see that this particular algorithm favors the first channel (red): it gets 50% of random values, and the other half is shared between the two remaining ones. This is not as bad as it looks and you should not dismiss this algorithm. Human eye is not very good in dealing with blue and green colors (in fact, I’ve read somewhere that we learned to distinguish those colors not that long ago; for ancient Egyptians they were the same). But we are pretty OK with reds. We can easily tell apart scarlet, orange, ochre, yellow and lemon – these colors are between red and green in the RGB model. Same is true for violet, purple, fuchsia, magenta, rose and pink. But can you remember the same amount of cyan tints? Emerald? Err… sky-blue?.. So, cheating a bit with red in your random algorithm can be perfectly ok if you know what you’re doing.

Let’s try a real random that doesn’t favor any single color channel. For this I prefer to use a small array of bytes and a counter that’s incremented each time the function is run:

byte color[3];
byte count, a0, a1, a2; color[count]=random(256); a0=count+random(1)+1; color[a0%3]=random(256-color[count]); color[(a0+1)%3]=255-color[a0%3]-color[count]; setRGBpoint(LED, color[0], color[1], color[2]); count+=random(15); // to avoid repeating patterns count%=3;

It’s ok, but these three randoms are not saturated enough because we fill three channels with some values. If we want nice deep colors we should fill only two of them, leaving the last one at zero. This algorithm will produce such colors (in fact, it totally corresponds to the ‘power-conscious’ HSV model giving us clear Hues at 100% Saturation):

color[count]=random(256);
a0=random(1); a1=((!a0)+count+1)%3; a0=(count+a0+1)%3; color[a0]=255-color[count]; color[a1]=0; setRGBpoint(LED, color[0], color[1], color[2]); count+=random(15); // to avoid repeating patterns count%=3;

What about the sine-wave lookup table we did earlier? It can also be used, and it produces even deeper and clearer colors, shifted a bit more towards base red, green and blue:

a0=random(240);
color[count]=lights[a0]; a1=random(1); a2=((!a1)+count+1)%3; a1=(count+a1+1)%3; color[a1]=lights[(a0+100)%240]; color[a2]=0; setRGBpoint(4, color[0], color[1], color[2]); count++; count%=3;

These are just a few examples, test them out to find which one suits you best. There are tons of ways to do random colors, and it’s a good idea to mix them for better results.

Step 5: Color Shifting

Now that you’re doing your favorite run of random colors let’s make them change seamlessly. Again the HSV model is of no help: it will make us go through the neighboring spectrum colors. The RGB model provides a shortcut: you have three color channels and you just need to move each of them from their starting points to their targets. We’re avoiding floating point operations, so need to change the types of some variables:

uint16_t color[3], nextColor[3];
long colorStep[3]; byte count, a0, a1, a2;

void setNextColor()
{ nextColor[count]=random(256)<<8; a0=random(1); a1=((!a0)+count+1)%3; a0=(count+a0+1)%3; nextColor[a0]=(255-nextColor[count])<<8; nextColor[a1]=0; }

void loop() {
setNextColor(); for (byte k=0; k<3; k++) colorStep[k]=((long)nextColor[k] - color[k])/100; for (byte k=0; k<100;k++) { for (byte i=0; i<3; i++) color[i]+=colorStep[i]; setRGBpoint(0, color[0]>>8, color[1]>>8, color[2]>>8); delay(10); } for (byte k=0; k<3; k++) color[k] = nextColor[k]; setRGBpoint(0, color[0]>>8, color[1]>>8, color[2]>>8); delay(500); count+=random(15); count%=3; }

And what about using the lookup table of the sine waves? Here we go, this one is even better as it doesn’t need any stops to show the exact color we’re moving to (it gets enough showtime from the sine wave):

void setNextColorSine()
{ a0=random(240); nextColor[count]=lights[a0]<<8; a1=random(1); a2=((!a1)+count+1)%3; a1=(count+a1+1)%3; nextColor[a1]=lights[(a0+100)%240]<<8; nextColor[a2]=0; }

void loop() {
setNextColorSine(); for (byte k=0; k<3; k++) colorStep[k]=((long)nextColor[k] - color[k])/255; for (byte k=0; k<120;k++) { setRGBpoint(0, (color[0]+colorStep[0]*lights[k])>>8, (color[1]+colorStep[1]*lights[k])>>8, (color[2]+colorStep[2]*lights[k])>>8); delay(10); } for (byte k=0; k<3; k++) color[k] = nextColor[k]; setRGBpoint(0, color[0]>>8, color[1]>>8, color[2]>>8); delay(10); // no need for extra delay, the sine wave will keep the color long enough count++; count%=3; }

Hopefully by this time you get the idea how to do fade-outs and fade-ins…

And that’s it for today! Next time I hope to finally get to my favorite topic of OnePixel information displays (yes, the RGB model is vital there too)...

Until then — comments and questions welcome.