RGB laboratory: mix your own colour

Build an RGB mixer where a button selects the red, green or blue channel and one potentiometer adjusts its intensity. Learn common-cathode and common-anode wiring.

Getting started with ESP32 and ArduinoBeginner90 min

What you will learn

  • Distinguish common-cathode and common-anode RGB LEDs.
  • Connect R, G and B to separate PWM outputs through three individual resistors.
  • Use a debounced button to select a channel.
  • Store three intensities in an array and adjust only the selected channel.
  • Explain immediate application of the potentiometer position when changing channels.
  • Create and check magenta and cyan combinations.

Before you start

Complete courses 02, 03 and 05: LED and resistor, INPUT_PULLUP and debouncing, analogue reading and PWM.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

Build a colour laboratory with an RGB LED, potentiometer and button. The button selects red, green or blue; the potentiometer adjusts that channel. The other values remain in working memory until selected again.

Learning objectives

  • Distinguish common-cathode and common-anode RGB LEDs.
  • Connect three LED channels with separate resistors.
  • Control three values using one potentiometer.
  • Use a button to cycle through R, G and B.
  • Distinguish requested brightness from the electrical PWM output.

Prerequisites

Complete courses 02, 03 and 05. You should understand LED polarity, the series resistor, INPUT_PULLUP, button debouncing and analogue scaling. Use a classic ESP32/ESP32-WROOM-32 board and its GPIO labels. C3, S2 and S3 boards require a separately adapted wiring profile.

Equipment

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
Four-lead RGB LED1R, G, B and COM. Separate common-cathode and common-anode circuits are supplied; physical lead order is not assumed.
220 Ω resistor3One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
10 kΩ potentiometer1Two end terminals and a wiper W; identify their physical positions on the actual component.
Momentary pushbutton1Use two terminals connected only when pressed; two legs on the same side may already be joined.
Jumper wires10Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity; select M-M/F-M leads to suit the board.

Use a four-lead RGB LED with one common and three colour leads. An addressable RGB module does not fit this circuit. Prepare three 220 Ω resistors, one per channel.

How colours combine

The RGB package contains red, green and blue light-emitting elements. We request each intensity with a value from 0 to 255. Red and green together produce yellowish light; red and blue produce magenta; green and blue produce cyan. The exact appearance depends on the LED and the relative output of its channels.

For a common-cathode LED, the common lead connects to GND, and more PWM on-time produces more light. For a common-anode LED, the common lead connects to 3V3: a channel lights when its output sinks current towards GND. The sketch therefore calculates its output as 255 minus the requested intensity for that variant.

Wiring and choosing the variant

RGB — common cathode

Default circuit

Connect COM to GND. Keep COMMON_ANODE false. R/G/B/COM are functional labels, not a physical lead order.

Required sketch setting: #define COMMON_ANODE false

RGB — common cathode — electrical connections listed in the table below
RGB — common cathodeEnlargeSVGPNG
Connections · RGB — common cathode
FromToConnection
ESP32.GPIO25R1.1Red-channel PWM.
R1.2RGB1.R220 Ω resistor for the red channel.
ESP32.GPIO26R2.1Green-channel PWM.
R2.2RGB1.G220 Ω resistor for the green channel.
ESP32.GPIO33R3.1Blue-channel PWM.
R3.2RGB1.B220 Ω resistor for the blue channel.
RGB1.COMESP32.GNDCommon cathode; leave COMMON_ANODE set to false.
ESP32.3V3RV1.END_AOne end of the 10 kΩ potentiometer.
ESP32.GNDRV1.END_BThe other potentiometer end.
RV1.WESP32.GPIO34Wiper to the ADC1 input.
ESP32.GPIO27SW1.CONTACT_ADigital input configured as INPUT_PULLUP.
SW1.CONTACT_BESP32.GNDThe selected contact pair closes only while pressed.

RGB — common anode

Alternative circuit

Connect COM to 3V3. Set COMMON_ANODE true in the sketch. Each channel still needs its own 220 Ω resistor.

Required sketch setting: #define COMMON_ANODE true

RGB — common anode — electrical connections listed in the table below
RGB — common anodeEnlargeSVGPNG
Connections · RGB — common anode
FromToConnection
ESP32.GPIO25R1.1Red-channel PWM.
R1.2RGB1.R220 Ω resistor for the red channel.
ESP32.GPIO26R2.1Green-channel PWM.
R2.2RGB1.G220 Ω resistor for the green channel.
ESP32.GPIO33R3.1Blue-channel PWM.
R3.2RGB1.B220 Ω resistor for the blue channel.
RGB1.COMESP32.3V3Common anode; set COMMON_ANODE true.
ESP32.3V3RV1.END_AOne end of the 10 kΩ potentiometer.
ESP32.GNDRV1.END_BThe other potentiometer end.
RV1.WESP32.GPIO34Wiper to the ADC1 input.
ESP32.GPIO27SW1.CONTACT_ADigital input configured as INPUT_PULLUP.
SW1.CONTACT_BESP32.GNDThe selected contact pair closes only while pressed.

The diagrams show electrical functions, not physical lead order. Identify R, G, B, the common lead and LED type from the actual part's documentation, markings or a verified diode test. A photograph and lead length alone cannot identify every colour reliably.

  1. Disconnect USB. Connect logical R through 220 Ω to GPIO25, G through another 220 Ω to GPIO26, and B through the third 220 Ω to GPIO33.
  2. For common cathode, connect the common lead to GND and keep #define COMMON_ANODE false.
  3. For common anode, use the alternative diagram: connect the common lead to 3V3 and change the setting to #define COMMON_ANODE true. Use the variant matching your LED.
  4. Connect the potentiometer ends to 3V3 and GND, with its wiper on GPIO34.
  5. Connect the button between GPIO27 and GND. On a four-leg button, choose contacts joined only when pressed; some pairs are already internally connected.
  6. Check separate breadboard groups, all three resistors and the common ground before reconnecting USB.

Arduino setup

Use Arduino IDE 2.x, esp32 by Espressif Systems 3.3.12, and ESP32 Dev Module. Download the Arduino example (.zip) and extract it. Select your port, open rgb_mixer.ino from its rgb_mixer folder and check COMMON_ANODE. Select Verify, then Upload. Set the Serial Monitor to 115200 baud. No additional libraries are needed. Package installation

Complete sketch

rgb_mixer.ino Arduino / C++
Download .ino

#include <Arduino.h>

// Set true ONLY for a common-anode RGB LED wired with its common lead to 3V3.
#ifndef COMMON_ANODE
#define COMMON_ANODE false
#endif

const uint8_t RGB_PINS[3] = {25, 26, 33};
const char* CHANNEL_NAMES[3] = {"R", "G", "B"};
constexpr uint8_t POT_PIN = 34;
constexpr uint8_t BUTTON_PIN = 27;
constexpr uint32_t PWM_FREQUENCY_HZ = 5000;
constexpr uint8_t PWM_RESOLUTION_BITS = 8;
constexpr uint32_t DEBOUNCE_MS = 30;
constexpr uint32_t SAMPLE_INTERVAL_MS = 20;
constexpr uint32_t REPORT_INTERVAL_MS = 200;

uint8_t brightness[3] = {0, 0, 0};
uint8_t selectedChannel = 0;
bool pwmReady = true;
int lastReading = HIGH;
int stableButtonState = HIGH;
uint32_t lastChangeMs = 0;
uint32_t lastSampleMs = 0;
uint32_t lastReportMs = 0;

void writeColors() {
  for (uint8_t channel = 0; channel < 3; channel++) {
    const uint8_t duty = COMMON_ANODE ? 255 - brightness[channel] : brightness[channel];
    ledcWrite(RGB_PINS[channel], duty);
  }
}

void sampleSelectedChannel() {
  const uint16_t rawValue = analogRead(POT_PIN);
  brightness[selectedChannel] = (static_cast<uint32_t>(rawValue) * 255U) / 4095U;
  writeColors();
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(POT_PIN, INPUT);
  analogReadResolution(12);
  analogSetPinAttenuation(POT_PIN, ADC_11db);

  for (uint8_t channel = 0; channel < 3; channel++) {
    pinMode(RGB_PINS[channel], OUTPUT);
    digitalWrite(RGB_PINS[channel], COMMON_ANODE ? HIGH : LOW);
    if (!ledcAttach(RGB_PINS[channel], PWM_FREQUENCY_HZ, PWM_RESOLUTION_BITS)) {
      pwmReady = false;
    }
  }

  if (!pwmReady) {
    // Leave every channel off if any PWM channel could not be configured.
    for (uint8_t channel = 0; channel < 3; channel++) {
      ledcDetach(RGB_PINS[channel]);
      pinMode(RGB_PINS[channel], OUTPUT);
      digitalWrite(RGB_PINS[channel], COMMON_ANODE ? HIGH : LOW);
    }
    Serial.println("ERROR: RGB PWM setup failed. Check the board and ESP32 core.");
    return;
  }

  writeColors();
  lastReading = digitalRead(BUTTON_PIN);
  stableButtonState = lastReading;
  lastChangeMs = millis();
  lastSampleMs = lastChangeMs;
  lastReportMs = lastChangeMs;
  Serial.println("WB Maker Bridge - Button selects R/G/B; potentiometer sets its value.");
  Serial.println("A newly selected channel immediately uses the current potentiometer position.");
}

void loop() {
  if (!pwmReady) {
    delay(100);
    return;
  }

  const uint32_t now = millis();
  const int reading = digitalRead(BUTTON_PIN);

  if (reading != lastReading) {
    lastReading = reading;
    lastChangeMs = now;
  }

  if (now - lastChangeMs >= DEBOUNCE_MS && reading != stableButtonState) {
    stableButtonState = reading;
    if (stableButtonState == LOW) {
      selectedChannel = (selectedChannel + 1) % 3;
      sampleSelectedChannel();
      lastSampleMs = now;
    }
  }

  if (now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
    lastSampleMs = now;
    sampleSelectedChannel();
  }

  if (now - lastReportMs >= REPORT_INTERVAL_MS) {
    lastReportMs = now;
    Serial.print("Selected: ");
    Serial.print(CHANNEL_NAMES[selectedChannel]);
    Serial.print(" | R: ");
    Serial.print(brightness[0]);
    Serial.print(" | G: ");
    Serial.print(brightness[1]);
    Serial.print(" | B: ");
    Serial.println(brightness[2]);
  }
}

The download matches this sketch. Confirm your LED's lead functions before testing.

Understanding the sketch

RGB_PINS associates colours with GPIO pins; brightness stores three intensities. selectedChannel chooses the element changed by the potentiometer. The expression (selectedChannel + 1) % 3 produces R → G → B → R.

With INPUT_PULLUP, the released button reads HIGH and the pressed button reads LOW. The sketch accepts a change after 30 ms of stability and advances only on a new press. Holding the button does not repeat the selection. Espressif: GPIO

The potentiometer is sampled approximately every 20 ms, scaling its 12-bit reading to 0–255. ADC clipping can occur at the endpoints, as in course 05. PWM uses 5000 Hz and 8 bits. ledcAttach() and ledcWrite() take GPIO pins in the 3.x API. Espressif: LEDC

After an accepted press, the newly selected channel immediately takes the current potentiometer position. Its previous value is replaced; the other two remain unchanged. The writeColors() function converts stored intensities into outputs, including inversion for common anode. Approximately every 200 ms, the report displays Selected, R, G and B.

Run and observe

Set the potentiometer to minimum before resetting. All initial intensities are zero and R is selected. The first sample, about 20 ms later, sets R from the potentiometer. If the control is above minimum, red will therefore appear immediately.

Increase R fully. Press once: G takes the same position. Lower G and observe that R remains stored. Press again for B, holding the button for two seconds: it should advance only once. Release before pressing again.

Target intensities R/G/BExpected light contribution
255 / 0 / 0Red
0 / 255 / 0Green
0 / 0 / 255Blue
255 / 255 / 0Yellowish
255 / 0 / 255Magenta
0 / 255 / 255Cyan

These are experimental targets; the potentiometer adjusts one selected channel at a time.

Troubleshooting

ProblemCheck and correction
Selecting R produces blueIdentify the colour leads again and correct the R/G/B connections.
Brightness is reversed or colours stay onMatch the LED type, common lead connection and COMMON_ANODE setting.
Colour jumps after a pressThis is intentional: the new channel immediately uses the current potentiometer position.
Button does not change channelsCheck its switched contact pair, GPIO27 and GND.
One colour is very faintCheck that resistor and connection; equal numbers need not produce equal channel brightness.
LEDC calls cause compilation errorsCheck the selected board and ESP32 package version 3.3.12.

Independent challenge

Starting from a reset, make magenta with R and B at maximum and G at zero. Record your sequence of presses and adjustments. Then make cyan and explain what happens when changing from G to B.

Check your understanding

  1. Why does our circuit use separate resistors rather than one in the common lead?
  2. What changes in the wiring and sketch for common anode?
  3. Does a channel retain its old value when you select it again?

Answer guidance and worked solution

Separate resistors limit each channel's current. For common anode, connect the common lead to 3V3 and set COMMON_ANODE to true; requested intensity 255 then becomes electrical duty 0. A newly selected channel immediately takes the potentiometer value.

For magenta, reset with the potentiometer at maximum: R becomes 255. Select G, then turn the potentiometer to zero. Select B and turn it to maximum. For cyan, reset at minimum so R remains zero. Select G and increase it; select B while the potentiometer remains at maximum. B immediately takes the high value while G keeps it. Confirm the values in the Serial Monitor.

Downloads

Download the example and matching diagram. Record compilation checks separately from physical board tests.

Primary references