Skip to main content

7.4 RGB LED Controller

This project makes an RGB LED change colour with the light in the room. In darkness it glows red; as the light rises the colour slides through green and blue, and under a bright light it turns white.

It brings together two things you have already built. The photoresistor circuit is exactly the one from 2.3 Photoresistor Analog Read, and the idea of setting a brightness instead of just switching a pin on and off comes from 3.2 Distance Fade LED. What is new is that one sensor reading now drives three outputs at the same time.

In this documentation you will learn:

  • How to find the four legs of an RGB LED and which one is shared.
  • How one analog reading can be turned into three separate brightnesses with map().
  • How PWM mixes red, green and blue into any colour in between.
  • Why the colour fades smoothly instead of jumping.

Hardware required:

  • 1x Soldered NULA MINI board
  • 1x Breadboard
  • 1x Photoresistor (LDR)
  • 1x RGB LED
  • 1x 10kΩ resistor
  • 3x 330 Ω resistors
  • 8x Jumper wires
  • 1x USB-C cable
ℹ️
This example uses two different kinds of resistor, doing two completely different jobs. The 10kΩ is not protecting anything. It is half of the measuring circuit, exactly as in 2.3. The three 330 Ω are current limiters, one per colour channel. An RGB LED is three LEDs in one package, so it needs three of them.
⚠️
Check the bands before you plug anything in. 10kΩ reads brown-black-orange and 330 Ω reads orange-orange-brown. They are easy to mix up, and swapping them will not damage anything but will stop the circuit working.

Putting the components together

Follow the twelve steps below. Each photo is taken from the same position, so you can compare it with the previous one and see exactly what changed.

ℹ️
Everything in this example lives on the f–j side of the breadboard, because every pin it needs is on that edge of the board: IO2 in row 25, IO3 in row 26, IO4 in row 27, IO5 in row 28, 3V3 in row 29 and GND in row 30. The board covers most of those rows, so the outermost holes (j25 through j30) are the ones you can reach.

1. Insert the NULA MINI board on the breadboard

ℹ️
This step assumes you know how a breadboard is wired inside and what its power rails are. For an introduction, see Breadboard Fundamentals documentation page.

Push the board into one end of the breadboard so that its two rows of pins sit on either side of the centre channel, with the chip facing down. It should occupy rows 25 to 30.

NULA MINI board seated on the breadboard
Step 1: the board seated on the breadboard

2. Bring ground out to the rail

One jumper from j30 (GND) across to the blue rail. That rail becomes the shared ground for the whole circuit, and both halves connect back to it.

Jumper from j30 to the blue negative rail
Step 2: GND brought out to the blue − rail

3. Bring 3.3 V out to row 14

A second jumper, from j29 (3V3) up to row 14. This is the top of the measuring circuit.

Jumper from j29 to row 14
Step 3: 3V3 brought out to row 14

4. Add the photoresistor

The photoresistor bridges row 14, where 3.3 V now arrives, and row 11.

Photoresistor bridging rows 14 and 11
Step 4: the photoresistor bridging rows 14 and 11
ℹ️
A photoresistor has no polarity: either leg can go either way round. It is just a resistor whose value changes with light.

5. Add the 10kΩ resistor

The resistor continues the chain downwards, from row 11 to row 9.

10k resistor bridging rows 11 and 9
Step 5: the 10kΩ resistor bridging rows 11 and 9

6. Connect the resistor to ground

One jumper from row 9 across to the blue rail.

Jumper connecting row 9 to the negative rail
Step 6: the bottom of the 10kΩ resistor wired to the − rail

The chain is now complete: 3.3 V → photoresistor → 10kΩ → GND. Current flows through both parts in series.

7. Tap the middle with IO5

Now the measurement itself. Run a jumper from j28 (IO5) to row 11, the row shared by the photoresistor's lower leg and the resistor's upper lead.

IO5 connected to the junction between the photoresistor and the resistor
Step 7: IO5 tapping the junction at row 11
ℹ️
Row 11 is the whole point of this half of the circuit. It is the midpoint between the two resistances, and its voltage moves as the light changes. That is what IO5 measures. If the wire lands on row 14 or row 9 instead, you will still get a reading. It simply will not change when the light does.

8. Add the RGB LED

An RGB LED is three LEDs in one package sharing a single leg. Hold it with the legs pointing down and find the longest one. That is the shared leg, the common cathode. On this LED it sits third along, with one leg on one side of it and two on the other.

Seat the LED so its four legs land in rows 1, 2, 3 and 4, with the long leg in row 3:

LegRow
Red, outermost, furthest from the long leg1
Green, between Red and the long leg2
Common cathode, the longest leg3
Blue, alone on the other side of the long leg4
RGB LED seated with its four legs in rows 1 to 4
Step 8: the RGB LED with its four legs in rows 1, 2, 3 and 4
⚠️
This is not the leg order most RGB LEDs use, and it is not the order you will find in most tutorials, which usually assume Red–common–Green–Blue with the shared leg second. The LED in this kit really does run Red, Green, common, Blue. Go by the length of the legs rather than by a diagram, and if the colours come out wrong at the end, this is the first thing to check.

9. Ground the common cathode

One jumper from row 3 to the blue rail. All three colour channels return to ground through this single wire.

Jumper connecting the common cathode row to the negative rail
Step 9: the common cathode wired to the blue − rail

10. Add the three 330 Ω resistors

Each colour channel needs its own current limiter. Bridge each of the three colour legs out to a free row further along the board:

  • from row 1 (Red) to a free row
  • from row 2 (Green) to another free row
  • from row 4 (Blue) to a third free row
Three 330 ohm resistors added, one per colour channel
Step 10: one 330 Ω resistor for each colour channel
ℹ️
Which rows you land in does not matter, as long as each one is empty and no two resistors share a row. Spread them across different columns so the bodies do not sit on top of each other. Keep row 3 clear, since that is the common cathode, and row 9 as well, since that already belongs to the photoresistor's ground connection.
⚠️
A resistor with both legs in the same row does nothing at all. Every hole in a row is one connection, so a resistor bridging two holes in the same row is simply short-circuited. The channel still lights, often brighter than the other two, but it is running with no current limiting whatsoever. Each resistor must span two different rows.

11. Wire the three colour channels to the board

Three more jumpers, from the far end of each resistor back to the matching pin. Use three different colours of wire, because they run side by side and you will want to tell them apart later.

ChannelFromTo
Redthe Red resistor's free rowj25 (IO2)
Greenthe Green resistor's free rowj26 (IO3)
Bluethe Blue resistor's free rowj27 (IO4)
Three coloured jumpers connecting the resistors to IO2, IO3 and IO4
Step 11: the three colour channels wired to IO2, IO3 and IO4
⚠️
Rows 25, 26 and 27 are one hole apart and tucked under the edge of the board, which makes them easy to miscount. Count in from j30 at the very end (GND, 3V3, IO5, IO4, IO3, IO2) rather than guessing from the middle.

12. Connect the board to your computer

The finished circuit powered over USB-C
Step 12: the finished circuit, powered over USB-C

How one sensor becomes three colours

Reading the light

The board cannot measure resistance, only voltage. A photoresistor changes its resistance with light, so on its own it gives the board nothing to read. Pairing it with the fixed 10kΩ makes a voltage divider, and the voltage at the point between the two depends on the ratio of their resistances:

  • In bright light the photoresistor's resistance drops, it keeps less of the 3.3 V for itself, and the reading at IO5 rises.
  • In darkness its resistance climbs, it keeps more of the voltage, and the reading falls.

analogRead() turns that voltage into a number from 0 to 4095, because the NULA MINI has a 12-bit ADC.

Setting the colour

analogWrite() does the opposite. Instead of only switching a pin fully on or fully off, it switches it on and off very quickly, using PWM, or Pulse Width Modulation, and the longer the pin stays on during each cycle, the brighter that channel looks. Writing all three at once is what mixes a colour.

Each channel takes a value from 0 to 255 here, which is the board's default output range. The three together are just an ordinary RGB colour: 255, 0, 0 is red, 0, 255, 0 is green, and 255, 255, 255 is white.

Joining them up

The light range is split into three equal parts. FIRST_THIRD is 1365 and SECOND_THIRD is 2730, simply 4095 divided into thirds, and each part gets its own transition:

LDR valueWhat the code doesResult
0 → 1365red fades down while green fades upred → green
1366 → 2730green fades down while blue fades upgreen → blue
2731 → 4095blue stays full while red and green riseblue → white

In every part, one channel is mapped upwards while another is mapped downwards. That is what makes one colour slide into the next instead of jumping. And because each part begins exactly where the previous one ended, the boundaries are invisible: at 1365 the colour is pure green whichever side you approach it from.


Code

/**
**************************************************
*
* @file 7.4_RGB_LED_Controller.ino
* @brief Project that uses a photoresistor to set the colour of an RGB LED. As the light in the room changes,
* the colour slides from red in darkness, through green and blue, all the way to white in bright light.
* It builds on the photoresistor from section 2.3 and the LED brightness control from section 3.2, and
* shows how one sensor reading can drive three outputs at the same time.
* For details, connection diagram and more, check out the example documentation at: <link placeholder>
* @author Soldered
***************************************************
*/

/*
This is a variable to which we pass the number of pin that we had connected the photoresistor's output to. Because we
need to read a whole range of values here and not only HIGH or LOW, this has to be a pin that supports analog input.
The NULA board has a pin naming logic as follows: IO5, where 5 is the number that we give to the variable.

This example also needs a 10k resistor. A photoresistor changes its resistance with light, but the board can only
measure a voltage, so we pair the two in what is called a voltage divider: the fixed resistor turns the changing
resistance into a changing voltage that the board can read.
*/
const int LDR_PIN = 5;

/*
These are the variables to which we pass the numbers of pins that we had connected the three colour channels of the
RGB LED to. An RGB LED is really three LEDs in one package, one red, one green and one blue, and by lighting them at
different strengths we can mix any colour we like. All three pins have to support PWM, which is what lets us set a
brightness instead of only on or off.

Remember that each of the three colour channels needs its own 330 Ohm resistor in series with it. An RGB LED counts as
three LEDs, so it takes three resistors, and without them the channels draw more current than either they or the pins
are built for.
*/
const int RED_PIN = 2;
const int GREEN_PIN = 3;
const int BLUE_PIN = 4;

/*
This variable will store the raw analog value read from the photoresistor. Since the NULA board uses a 12-bit ADC, the
value will range from 0 in complete darkness to 4095 in bright light.
*/
int ldrValue = 0;

/*
These three variables hold the brightness of each colour channel, from 0 for off to 255 for fully on.
*/
int r = 0, g = 0, b = 0;

/*
These two variables split the light range into three equal parts, which is what gives us our three colour transitions.
4095 divided by three is 1365, so the first part ends there and the second one ends at twice that. Feel free to
experiment with these values to move the colour changes to different light levels.
*/
const int FIRST_THIRD = 1365;
const int SECOND_THIRD = 2730;

void setup() {

/*
Serial.begin() establishes serial communication between your board and your computer via a USB cable. We use it here
to watch both the light level and the mixed colour, which makes it much easier to understand what the code is doing.
*/
Serial.begin(115200);

/*
pinMode() is a function that configures the specified pin to behave either as an input or as an output.
The photoresistor is something we read, so it goes into INPUT mode, while the three colour channels are things we
write to, so they go into OUTPUT mode.
*/
pinMode(LDR_PIN, INPUT);

/*
analogReadResolution() defines how many bits are used for ADC readings. The NULA MINI supports 12-bit resolution,
which means analogRead() returns values from 0 to 4095. That is the range the colour ranges below are built around,
so we state it here rather than relying on the default.
*/
analogReadResolution(12);

pinMode(RED_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(BLUE_PIN, OUTPUT);

//Print out the initial message so we know that the program started successfully.
Serial.println("RGB LED Controller with full-spectrum color mapping started");
}

void loop() {

/*
analogRead() reads the voltage at the given analog pin and converts it into a number. The more light falls on the
photoresistor, the lower its resistance and the higher this number becomes.
*/
ldrValue = analogRead(LDR_PIN);
Serial.print("LDR value: ");
Serial.println(ldrValue);

/*
This is where the colour is decided. We split the light range into three parts and give each one its own transition,
so that the colour never jumps: it always slides from wherever it was into the next colour.
map() is a function that takes a number from one range and rescales it into another range, and we use it here to turn
a light level into a brightness. Notice how in each part one channel is being mapped upwards while another is mapped
downwards, which is exactly what makes one colour fade into the next.
*/
if (ldrValue <= FIRST_THIRD) {

//Darkest third: fade from red (255, 0, 0) to green (0, 255, 0).
r = map(ldrValue, 0, FIRST_THIRD, 255, 0);
g = map(ldrValue, 0, FIRST_THIRD, 0, 255);
b = 0;
}
else if (ldrValue <= SECOND_THIRD) {

//Middle third: fade from green (0, 255, 0) to blue (0, 0, 255).
r = 0;
g = map(ldrValue, FIRST_THIRD + 1, SECOND_THIRD, 255, 0);
b = map(ldrValue, FIRST_THIRD + 1, SECOND_THIRD, 0, 255);
}
else {

/*
Brightest third: fade from blue (0, 0, 255) to white (255, 255, 255). White is simply all three channels on at
once, which is why red and green rise here while blue stays at full brightness.
*/
r = map(ldrValue, SECOND_THIRD + 1, 4095, 0, 255);
g = map(ldrValue, SECOND_THIRD + 1, 4095, 0, 255);
b = 255;
}

/*
analogWrite() is a function that writes an "in between" value to a pin instead of only HIGH or LOW. It does this
using PWM, which stands for Pulse Width Modulation: the pin is switched on and off very quickly, and the longer it
stays on during each cycle, the brighter that colour appears. Writing all three at once is what mixes the colour.
*/
analogWrite(RED_PIN, r);
analogWrite(GREEN_PIN, g);
analogWrite(BLUE_PIN, b);

//Print the mixed colour too, so we can compare it against the light level above.
Serial.print("RGB: ");
Serial.print(r); Serial.print(", ");
Serial.print(g); Serial.print(", ");
Serial.println(b);

/*
A short pause between readings. Keeping it small makes the colour changes look smooth.
*/
delay(100);
}

What you should see

Upload the sketch, then open Tools → Serial Monitor and set the baud rate to 115200. Two lines appear ten times a second: the raw light reading, and the three brightnesses the code worked out from it.

In an ordinary lit room the reading sits high, in the top third of the range, so the LED settles on a strong blue.

The RGB LED glowing blue on the finished circuit
Ordinary room light: the LED glows blue
Serial Monitor showing an LDR value of about 2923 and RGB values of 35, 35, 255
The matching Serial output: 2923 becomes 35, 35, 255

Now put a finger over the photoresistor. The reading drops into the middle band and the colour swings all the way to green.

A finger shading the photoresistor while the RGB LED glows green
Shading the sensor with a finger: the LED turns green
Serial Monitor showing an LDR value of about 1266 and RGB values of 19, 236, 0
The matching Serial output: 1266 becomes 19, 236, 0

Those two captures are worth reading side by side, because you can check the code's arithmetic by hand. At 1266 the reading is in the first band, so red is mapped down from 255 and green up from 0: 255 − 1266 × 255 ÷ 1365 is 18.5, which the board rounds to the 19 it printed. The number on the screen and the colour in front of you are the same fact, twice.

The whole range

What you doRoughly what you will readRGBColour
Leave it in a lit room~2900–330035, 35, 255blue
Shade it with a finger~127019, 236, 0green
Seal your palm over itunder ~300199, 56, 0orange-red
Shine a phone torch on itclose to 4095255, 255, 255white
ℹ️
Red is harder to reach than you would expect. A cupped hand only gets the reading down to around 1300, which is still green. Light leaks in around your wrist and between your fingers. To see red you need genuine darkness: turn the room lights off, or drop an opaque cup or box over the sensor. Watch the number rather than guessing, since anything under about 300 is unmistakably red.
ℹ️
Your numbers will not match these exactly. They depend on the light in your room and on the particular photoresistor. What matters is that the value moves when the light changes. If it sits at a fixed number no matter what you do, go back to step 7 and check that IO5 really is tapping row 11.

If the colours come out wrong

If the LED lights but shows the wrong colour (green where you expect red, or one channel that never comes on), the circuit is fine and the mapping is not. Work through it in this order:

  1. Check the long leg is in row 3 and wired to the rail. If the shared leg is somewhere else, the channels fight each other and mostly stay dark.
  2. Check each resistor spans two rows. A shorted one makes its channel far brighter than the other two, which reads as that colour taking over.
  3. Swap the jumpers at the board end. If red and blue are exchanged, swap the wires in j25 and j27. The Serial Monitor tells you what colour the board intended: if it prints 255, 0, 0 and you see blue, the two channels are crossed.

That last one is the useful habit: the RGB line is the board's own statement of what it is trying to display, so any disagreement between it and the LED in front of you is a wiring fault, not a code fault.


Full example

Check out the full example code on the link below:

7.4_RGB_LED_Controller.ino

Project that uses a photoresistor to control an RGB LED, smoothly changing colour from red to white based on ambient light intensity.