Lesson: Exploring Color with an Arduino and an RGB LED

By Robert Walsh

Engage

Have you ever wondered how we see color? Or how our TVs, phones, tablets, and computers can display such vivid and detailed pictures? In this lesson, we will use an Arduino to control red, green, and blue light emitting diodes (LEDs) to create a whole spectrum of colors, and then we will see how the concepts are applied in LED displays. To complete this activity, you will need:

  • An Arduino Mega 2560 (other models will also work, but the pin numbers will be different)
  • One red, one blue, and one green LED
  • One common cathode RGB LED
  • Three 220 Ohm or 330 Ohm resistors
  • Three push buttons
  • Three 5K Ohm or 10K Ohm resistors
  • A breadboard
  • Several male-to-male jumper wires
  • A computer and USB cable to program the Arduino

Note: All of the components needed for this lesson (except the computer) are available in the ELEGOO Mega 2560 Starter Kit available from Amazon.

Explore

The finished product will have three buttons, each of which will gradually increase the brightness of one of the colored LEDs as well as the corresponding component in the RGB LED. This will let us observe how combining differing amounts of red, green, and blue results in light that our eyes perceive as various shades of color.

We are going to build the circuit and write the sketch for this lesson in several small parts. There is essentially one small circuit that will be repeated for each of the three colors.

Controlling a Red LED

The first circuit will control the red LED. Here is the wiring diagram:

Wiring diagram for the portion of the circuit that will control the red LED

For this circuit, we have connected the common ground rail on the breadboard to a GND (ground) pin on the Arduino, and we have connected the common power rail to a 5V pin. The cathode (negative leg) of the red LED is connected to ground. The anode (positive leg) is connected to a 220 Ohm (or 330 Ohm) resistor. The other end of the resistor is connected to both a signal pin on the Arduino (I used pin 12) and to the red leg of the RGB LED. With the RGB LED held so that the longest leg (typically the common cathode) is second from the left, the red leg should be the first leg from the left. The common cathode of the RGB LED is connected to ground.

The button is inserted into the breadboard across the center channel. One leg is connected to the common power rail, while the other connects both to a signal pin on the Arduino (I used pin 4) and to a 5K Ohm (or 10K Ohm) resistor that is connected to ground. This is called a pull down resistor.

Which signal pin you choose for the LED is not important except that it must be capable of pulse width modulation (PWM) output. This is how we will eventually control the brightness of the LED.

Here is the Arduino sketch for this circuit:

const int RED_LED = 12; /* the signal pin to which the red LED anode is connected */
const int RED_BTN = 4; /* the signal pin to which the button for red is connected */

const int OFF = 0; /* the value for the LED when it is off */
const int ON = 1; /* the value for the LED when it is on */

boolean redPressed = false; /* indicates whether the button for red is currently pressed */
int redState = OFF; /* indicates the current state of the red LED */

void setup()
{
    pinMode(RED_LED, OUTPUT); /* set the red LED pin to output */
    pinMode(RED_BTN, INPUT); /* set the pin for the button to control the red LED to input */

    digitalWrite(RED_LED, OFF); /* ensure the red LED is off */
}

boolean debounce(int whichButton, boolean lastState)
{
    /* 
     * The debounce function ensures the button state has 
     * had time to "settle" before returning whether 
     * it is pressed 
     */

    boolean currentState = digitalRead(whichButton); /* get the state of the button */

    if (currentState != lastState) /* if the new state is different from the last */
    {
        delay(5); /* wait 5 milliseconds and read the state again */
        currentState = digitalRead(whichButton);
    }
    return currentState;
}

int nextState(int currentState)
{
    /*
     * The nextState function determines the new state
     * for the LED based on its current state
     */

    if (currentState == OFF) /* if the LED is currently off */
    {
        return ON; /* its next state should be on */
    }

    return OFF; /* otherwise, it is on and should be off */
}

void loop()
{
    boolean isPressed = debounce(RED_BTN, redPressed); /* check to see if the button for the red LED is pressed */
    if (isPressed && !redPressed) /* if the button is pressed now but was not pressed before */
    {
        redState = nextState(redState); /* get the new state for the red LED */
    }
    redPressed = isPressed; /* save the pressed state of the button for the red LED */

    digitalWrite(RED_LED, redState);
}


Compile the sketch and upload it to the Arduino. The red LED and the RGB LED should be off. Pressing the button should turn on the red LED and cause the RGB LED to light up red. Pressing the button again should turn both LEDs off.

Gradually Increasing the Brightness

So far, we are only turning the LEDs on and off. It would be more interesting if we could gradually increase the brightness so that the lights behave like a 3-way light bulb. To do that, we need to make some changes to the sketch. Specifically, we need to add more states than just off and on, and we will need to use pulse width modulation (PWM) to vary the voltage going to the circuit. Here is the new sketch with the changes highlighted:

const int RED_LED = 12; /* the signal pin to which the red LED anode is connected */
const int RED_BTN = 4; /* the signal pin to which the button for red is connected */

const int OFF = 0; /* the value for the LED when it is off */
const int DIM = 256 / 4; /* the value for the LED when it is 1/4 bright */
const int HALF = 256 / 2; /* the value for the LED when it is 1/2 bright */
const int SEMI = (256 / 4) * 3; /* the value for the LED when it is 3/4 bright */
const int FULL = 255; /* the value for the LED when it is fully lit */

boolean redPressed = false; /* indicates whether the button for red is currently pressed */
int redState = OFF; /* indicates the current state of the red LED */

void setup()
{
    pinMode(RED_LED, OUTPUT); /* set the red LED pin to output */
    pinMode(RED_BTN, INPUT); /* set the pin for the button to control the red LED to input */

    digitalWrite(RED_LED, OFF); /* ensure the red LED is off */
}

boolean debounce(int whichButton, boolean lastState)
{
    /* 
     * The debounce function ensures the button state has 
     * had time to "settle" before returning whether 
     * it is pressed 
     */

    boolean currentState = digitalRead(whichButton); /* get the state of the button */

    if (currentState != lastState) /* if the new state is different from the last */
    {
        delay(5); /* wait 5 milliseconds and read the state again */
        currentState = digitalRead(whichButton);
    }
    return currentState;
}

int nextState(int currentState)
{
    /*
     * The nextState function determines the new state
     * for the LED based on its current state
     */

    if (currentState == OFF) /* if the LED is currently off */
    {
        return DIM; /* its next state should be on */
    }

    if (currentState == DIM) /* if the LED is currently dim */
    {
        return HALF; /* its next state should be half */
    }

    if (currentState == HALF) */ if the LED is currently half */
    {
        return SEMI; /* its next state should be 3/4 */
    }

    if (currentState == SEMI) /* if the LED is currently 3/4 */
    {
        return FULL; /* its next state should be full */
    }

    return OFF; /* otherwise, it is already fully lit and should next be off */
}

void loop()
{
    boolean isPressed = debounce(RED_BTN, redPressed); /* check to see if the button for the red LED is pressed */
    if (isPressed && !redPressed) /* if the button is pressed now but was not pressed before */
    {
        redState = nextState(redState); /* get the new state for the red LED */
    }
    redPressed = isPressed; /* save the pressed state of the button for the red LED */

    analogWrite(RED_LED, redState);
}


Once this sketch is compiled and uploaded, pressing the button should cycle the red LED and the RGB LED through five different states, each with a different brightness.

Controlling a Green LED

Now that we can control the red LED, let’s add a green one. We are going to replicate the existing circuit by adding a green LED and button to control it. Here is the new wiring diagram:

Wiring diagram for the portion of the circuit that will control the red and green LEDs

The cathode of the green LED is connected to ground, and the anode is connected to a 220 Ohm or 330 Ohm resistor. The other end of the resistor connects both to the green leg of the RGB LED and to a signal pin on the Arduino (I used pin 11). Again, which signal pin is not important so long as it is capable of PWM. The green leg of the LED is typically to the right of the common cathode. The button is the same as for the red LED except that it is connected to a different signal pin on the Arduino (I used pin 3).

We also need to make some changes to the sketch:

const int RED_LED = 12; /* the signal pin to which the red LED anode is connected */
const int RED_BTN = 4; /* the signal pin to which the button for red is connected */

const int GREEN_LED = 11; /* the signal pin to which the green LED anode is connected */
const int GREEN_BTN = 3; /* the signal pin to which the button for green is connected */

const int OFF = 0; /* the value for the LED when it is off */
const int DIM = 256 / 4; /* the value for the LED when it is 1/4 bright */
const int HALF = 256 / 2; /* the value for the LED when it is 1/2 bright */
const int SEMI = (256 / 4) * 3; /* the value for the LED when it is 3/4 bright */
const int FULL = 255; /* the value for the LED when it is fully lit */

boolean redPressed = false; /* indicates whether the button for red is currently pressed */
int redState = OFF; /* indicates the current state of the red LED */

boolean greenPressed = false; /* indicates whether the button for green is currently pressed */
int greenState = OFF; /* indicates the current state of the green LED */

void setup()
{
    pinMode(RED_LED, OUTPUT); /* set the red LED pin to output */
    pinMode(RED_BTN, INPUT); /* set the pin for the button to control the red LED to input */

    pinMode(GREEN_LED, OUTPUT); /* set the green LED pin to output */
    pinMode(GREEN_BTN, INPUT); /* set the pin for the button to control the green LED to input */

    digitalWrite(RED_LED, OFF); /* ensure the red LED is off */
    digitalWrite(GREEN_LED, OFF); /* ensure the green LED is off */
}

boolean debounce(int whichButton, boolean lastState)
{
    /* 
     * The debounce function ensures the button state has 
     * had time to "settle" before returning whether 
     * it is pressed 
     */

    boolean currentState = digitalRead(whichButton); /* get the state of the button */

    if (currentState != lastState) /* if the new state is different from the last */
    {
        delay(5); /* wait 5 milliseconds and read the state again */
        currentState = digitalRead(whichButton);
    }
    return currentState;
}

int nextState(int currentState)
{
    /*
     * The nextState function determines the new state
     * for the LED based on its current state
     */

    if (currentState == OFF) /* if the LED is currently off */
    {
        return DIM; /* its next state should be on */
    }

    if (currentState == DIM) /* if the LED is currently dim */
    {
        return HALF; /* its next state should be half */
    }

    if (currentState == HALF) */ if the LED is currently half */
    {
        return SEMI; /* its next state should be 3/4 */
    }

    if (currentState == SEMI) /* if the LED is currently 3/4 */
    {
        return FULL; /* its next state should be full */
    }

    return OFF; /* otherwise, it is already fully lit and should next be off */
}

void loop()
{
    boolean isPressed = debounce(RED_BTN, redPressed); /* check to see if the button for the red LED is pressed */
    if (isPressed && !redPressed) /* if the button is pressed now but was not pressed before */
    {
        redState = nextState(redState); /* get the new state for the red LED */
    }
    redPressed = isPressed; /* save the pressed state of the button for the red LED */

    isPressed = debounce(GREEN_BTN, greenPressed); /* check to see if the button for the green LED is pressed */
    if (isPressed && !greenPressed) /* if the button is pressed now but was not pressed before */
    {
        greenState = nextState(greenState); /* get the new state for the green LED */
    }
    greenPressed = isPressed; /* save the pressed state of the button for the green LED */

    analogWrite(RED_LED, redState);
    analogWrite(GREEN_LED, greenState);
}


We simply added constants and variables to represent the state of the green LED and the button that controls it. Then we replicated the behavior that controls the red LED so that it works for the green one, too. Once the code is compiled and uploaded, pressing the button for red will increase the brightness on the red LED, and pressing the button for green will control the green LED. The interesting part, though, is that when both the red and green LEDs are lit, the RGB LED is no longer either red or green but a combination of both!

Controlling a Blue LED

The final step is to replicate the circuit once more so that we can control a blue LED. Here is the wiring diagram:

Wiring diagram for the completed circuit that will control the all three LEDs

The breadboard may be starting to get a little bit crowded, but the new components should be familiar. The cathode of the blue LED is connected to ground, and the anode is connected to a 220 Ohm or 330 Ohm resistor. The other end of the resistor connects to the blue leg of the RGB LED and to a signal pin (capable of PWM) on the Arduino (I used pin 10). The button is the same as for the buttons for red and green, but it should be connected to a different signal pin (I used pin 2).

The new sketch is here:

const int RED_LED = 12; /* the signal pin to which the red LED anode is connected */
const int RED_BTN = 4; /* the signal pin to which the button for red is connected */

const int GREEN_LED = 11; /* the signal pin to which the green LED anode is connected */
const int GREEN_BTN = 3; /* the signal pin to which the button for green is connected */

const int BLUE_LED = 10; /* the signal pin to which the blue LED anode is connected */
const int BLUE_BTN = 2; /* the signal pin to which the button for blue is connected */

const int OFF = 0; /* the value for the LED when it is off */
const int DIM = 256 / 4; /* the value for the LED when it is 1/4 bright */
const int HALF = 256 / 2; /* the value for the LED when it is 1/2 bright */
const int SEMI = (256 / 4) * 3; /* the value for the LED when it is 3/4 bright */
const int FULL = 255; /* the value for the LED when it is fully lit */

boolean redPressed = false; /* indicates whether the button for red is currently pressed */
int redState = OFF; /* indicates the current state of the red LED */

boolean greenPressed = false; /* indicates whether the button for green is currently pressed */
int greenState = OFF; /* indicates the current state of the green LED */

boolean bluePressed = false; /* indicates whether the button for blue is currently pressed */
int blueState = OFF; /* indicates the current state of the blue LED */

void setup()
{
    pinMode(RED_LED, OUTPUT); /* set the red LED pin to output */
    pinMode(RED_BTN, INPUT); /* set the pin for the button to control the red LED to input */

    pinMode(GREEN_LED, OUTPUT); /* set the green LED pin to output */
    pinMode(GREEN_BTN, INPUT); /* set the pin for the button to control the green LED to input */

    pinMode(BLUE_LED, OUTPUT); /* set the blue LED pin to output */
    pinMode(BLUE_BTN, INPUT); /* set the pin for the button to control the blue LED to input */

    digitalWrite(RED_LED, OFF); /* ensure the red LED is off */
    digitalWrite(GREEN_LED, OFF); /* ensure the green LED is off */
    digitalWrite(BLUE_LED, OFF); /* ensure the blue LED is off */
}

boolean debounce(int whichButton, boolean lastState)
{
    /* 
     * The debounce function ensures the button state has 
     * had time to "settle" before returning whether 
     * it is pressed 
     */

    boolean currentState = digitalRead(whichButton); /* get the state of the button */

    if (currentState != lastState) /* if the new state is different from the last */
    {
        delay(5); /* wait 5 milliseconds and read the state again */
        currentState = digitalRead(whichButton);
    }
    return currentState;
}

int nextState(int currentState)
{
    /*
     * The nextState function determines the new state
     * for the LED based on its current state
     */

    if (currentState == OFF) /* if the LED is currently off */
    {
        return DIM; /* its next state should be on */
    }

    if (currentState == DIM) /* if the LED is currently dim */
    {
        return HALF; /* its next state should be half */
    }

    if (currentState == HALF) */ if the LED is currently half */
    {
        return SEMI; /* its next state should be 3/4 */
    }

    if (currentState == SEMI) /* if the LED is currently 3/4 */
    {
        return FULL; /* its next state should be full */
    }

    return OFF; /* otherwise, it is already fully lit and should next be off */
}

void loop()
{
    boolean isPressed = debounce(RED_BTN, redPressed); /* check to see if the button for the red LED is pressed */
    if (isPressed && !redPressed) /* if the button is pressed now but was not pressed before */
    {
        redState = nextState(redState); /* get the new state for the red LED */
    }
    redPressed = isPressed; /* save the pressed state of the button for the red LED */

    isPressed = debounce(GREEN_BTN, greenPressed); /* check to see if the button for the green LED is pressed */
    if (isPressed && !greenPressed) /* if the button is pressed now but was not pressed before */
    {
        greenState = nextState(greenState); /* get the new state for the green LED */
    }
    greenPressed = isPressed; /* save the pressed state of the button for the green LED */

    isPressed = debounce(BLUE_BTN, bluePressed); /* check to see if the button for the blue LED is pressed */
    if (isPressed && !bluePressed) /* if the button is pressed now but was not pressed before */
    {
        blueState = nextState(blueState); /* get the new state for the blue LED */
    }
    bluePressed = isPressed; /* save the pressed state of the button for the blue LED */

    analogWrite(RED_LED, redState);
    analogWrite(GREEN_LED, greenState);
    analogWrite(BLUE_LED, blueState);
}


Again, we simply replicated the code that handles the red and the green buttons and LEDs for blue. After compiling and uploading, you should be able to control all three LEDs independently, and the color of the RGB LED should depend on the values of each of the individual color LEDs.

Explain

Perceiving Color

What we perceive as color is light at different wavelengths1. Receptors in the eye stimulated by light at varying wavelengths and convert these stimuli into signals that the brain perceives as colors2. Combining different amounts of red, green, and blue light can produce the full spectrum2.

LED computer displays and televisions use this same technique to produce high-resolution images3. These displays are composed of many (over 8 million for a 4K TV) RGB LEDs that can be lit individually. By controlling the red, green, and blue values of these picture elements (called pixels), the display can represent images. The more pixels that are available, typically referred to as the resolution of the display, the better the quality of the resulting image.

Pulse Width Modulation (PWM)

This lesson leverages a concept called pulse width modulation4 or PWM. In order to control the brightness of the LEDs, we need to somehow vary the amount of voltage sent to them. However, the Arduino is capable of setting the value of output pins to either high (5 volts) or low (0 volts). By controller how long the signal is high, though, we can fool the circuit into behaving as if it is getting less voltage. The effect is similar to flicking a light switch on and off very quickly. The light can only be on or off; there are no other possible values. If, though, the switch is flipped fast enough, it will look like it is dim. The ratio of the time spent on and the time spent off dictates how bright the light will appear. Similarly, if the Arduino toggles the state of the pin between high and low very quickly, the LED’s brightness can be controlled. Just as with the light switch, setting the pin high longer makes the LED appear brighter. The time spent on is called the duty cycle. In this lesson, we defined DIM to be 25% of the maximum value. When we use analogWrite to send that value to the signal pin, we are telling the Arduino to use a 25% duty cycle. In other words, the pin will be high 25% of the time and low 75% of the time. Only some of the Arduino’s pins are capable of PWM.

Pull Down Resistors

You probably noticed that we connected a high-value resistor to ground with each of the buttons. This is called a pull down resistor5. The reason for this is to ensure that there is a complete circuit even when the button is open (not pressed). One side of the button is connected to the common power rail on the breadboard. When the button is pressed, the switch is closed, and the signal pin reads high. This is because power is able to flow through the button. Without the resistor, when the button is not pressed, the signal pin would be connected to an open circuit, and the Arduino would not be able to accurately determine its state. By adding the resistor, even when the button is not pressed, there is a closed circuit from the signal bin to ground. Also, since current always follows the path of least resistance, when the button is pressed, there is less resistance through the switch to the power rail than through the resistor to the ground rail. Therefore, the Arduino will detect that the pin is high. In short, when the button is not pressed (and the switch is open), there is only one path: through the resistor to ground. When the button is pressed (and the switch is closed), there are two paths, so current will travel the path with less resistance which goes through the button to the power rail.

Debounce

When a button is initially pressed or released, its state may briefly vary between high and low before settling into the correct final state6. To deal with this, we use a technique called debouncing. That’s a fancy word to mean that we read the button’s state, and if we think it is changing, we wait a very short amount of time and read it again. In our code, the debounce function first reads the state of the button. Then, if the current state is different from the previous state, we wait (delay) for 5 milliseconds and read the state again. This gives us a reliable value for the button’s current state.

Extend

Improving the Code

The final sketch for this project is pretty clean. It uses constants to represent the pins and the various states for the LEDs rather than having “magic numbers” scattered throughout the code. It uses functions for determining whether a button is pressed and for calculating the next state of the LED. The functions are written so that they can be used for any of the buttons or LEDs rather than having to replicate the copy for each button and LED.

There are, though, some improvements that could be made. First, the nextState function could be rewritten to use a switch statement rather than a series of if statements. Switch is better when there are multiple outcomes that are mutually exclusive. Alternatively, an enum could be used to represent that states so that the next state could be determined by simply iterating over the set of possible values.

There is some duplication in the loop function for determining whether a button is pressed and then calculating the next state. This could be extracted into a function that returns a struct containing the next state and whether the button is pressed.

Improving the User Experience

It is great that we can set each of the colored LEDs to one of five possible values. This give us 125 possible colors for the RGB LED (5 red * 5 green * 5 blue). However, analogWrite can accept up to 256 values (where 0 is off and 255 is fully lit)! We could give our user more control over the lights if we added new brightness levels. If the user could set each individual LED to any value between 0 and 255, over 16 million colors would be possible! At some point, though, it would become difficult to get a specific color by having to cycle through more and more values with individual button presses. Changing the circuit to use potentiometers instead of buttons would provide a better mapping between the value of the control mechanism and the shade of the RGB LED. With buttons, we are using digital sensors to create analog values, but with potentiometers, we would be reading analog values directly from the controls. We could even use a single two-axis joystick to control the colors, but we would need to find a way to map two-dimensional coordinate values into a set of three possible color values between 0 and 255. A color wheel might help.

Color Blindness

Color blindness is an inability to see the full spectrum of color. It is caused when some of the color receptors in the eye (called the cones) do not work properly7. About 8% of men and 1% of women are affected by some form of color blindness1. Is there anything that you learned about color in this lesson that might help people who suffer from color blindness?

Evaluate

What we learned in this lesson:

  • How red, green, and blue can be combined to create the full spectrum of color
  • How to use pulse width modulation to vary the brightness of an LED
  • How to use a pull down resistor to ensure that a button’s state can be determined reliable when it is not pressed
  • How to use the debounce technique to eliminate spurious and potentially inaccurate readings of a button’s state while it is being pressed or released

How did you do with this lesson?

  • What parts were easy and what parts were confusing?
  • Were any parts a review of things you already knew?
  • What would you like to know more about?

References

1Science Learning Hub. (n.d.). Colours of light. https://www.sciencelearn.org.nz/resources/47-colours-of-light

2Pantone. (n.d.). How do we see color? An introduction to color an the human eye. https://www.pantone.com/articles/color-fundamentals/how-do-we-see-color

3Eizo. (n.d.). EIZO 4K monitors: High definition and large screen sizes. https://www.eizo.com/library/basics/eizo_4k_monitors/

4Heath, J. (2017, April 4). PWM: Pulse width modulation: What is it and how does it work? Analog IC Tips. https://www.analogictips.com/pulse-width-modulation-pwm/

5EE Power. (n.d.) Pull up resistor / pull down resistor. https://eepower.com/resistor-guide/resistor-applications/pull-up-resistor-pull-down-resistor

6Arduino. (n.d.). Debounce. https://www.arduino.cc/en/Tutorial/BuiltInExamples/Debounce

7Turbert, D. (2019, September 6). What is color blindness? American Academy of Ophthalmology. https://www.aao.org/eye-health/diseases/what-is-color-blindness

Leave a Reply

Your email address will not be published. Required fields are marked *