A colour wheel
A walk through every hue, red to green to blue and back, in 360 small steps. At every step at most two channels are lit and they add to 255: one fading out while the next fades in. That keeps the walk from ever passing through white or going dark.
Two channels at a time
Press play and follow the three lines. Red starts at full. Over the first third of the lap it falls to zero while green rises to full. Over the second, green falls while blue rises. Over the last, blue falls while red comes back.
At every step at most two channels are lit, and their values add to 255. That is the whole trick. Because the third channel is always off, the walk never passes through white; because the two lit ones always sum to 255, it never goes dark in between. Every hue on the way is a pair.
The sketch
wheel(hue) is a few lines of integer arithmetic. hue / 120 picks the
sector. (hue % 120) * 255 / 120 is how far through the sector the hue is,
scaled to 0 to 252, and the falling channel gets 255 minus that. The largest
intermediate value is 119 × 255, which fits in an Uno's 16-bit int.
loop() moves the hue on by one degree every 20 ms without calling delay().
It checks millis(), and when 20 ms have passed it steps and returns. The rest
of the time loop() is free, so a button or a sensor can share the sketch.
Blinking without
stopping is the same
pattern with one LED.
Why it is not perfectly even
The wheel sends equal steps of number, not equal steps of light. Green gives the most light per step, so the green part of the lap looks brighter and seems to last longer, and on a 3.3 V board the red part does, for the reason in why 3.3 V turns it pink.
Scaling each channel by your trimmed white
numbers before
it reaches analogWrite narrows the difference. It does not change the shape
of the wheel, which is the part this article is about.
The code
wheel() turns a hue from 0 to 359 into three analogWrite values, and loop() steps the hue by one degree every 20 ms using millis(), so nothing waits in delay(). Change the three pin numbers to your PWM pins.
/*
RGB LED - a colour wheel TK02 / /p/tk02
Wiring. Count from the square pad on the TinkerBlock board, LED side
up, header at the bottom:
GND -> GND
NC -> nothing (both NC pins are unconnected on the board)
NC -> nothing
RED -> a PWM pin: D9 on an Uno, GPIO 25 on an ESP32,
GPIO 4 on an ESP32-S3, GP13 on a Raspberry Pi Pico
GREEN -> D10, GPIO 26, GPIO 5, GP14
BLUE -> D11, GPIO 27, GPIO 6, GP15
Arduino IDE
Tools > Board your board, e.g. ESP32S3 Dev Module
Tools > Port the one that appears when you plug in
Tools > USB CDC On Boot Enabled (ESP32-S3 only)
No library needed. ESP32 boards need board package 2.0 or later.
*/
// Uno: 9, 10, 11. ESP32: 25, 26, 27. ESP32-S3: 4, 5, 6. Pico: 13, 14, 15.
const int RED_PIN = 4;
const int GREEN_PIN = 5;
const int BLUE_PIN = 6;
const unsigned long STEP_MS = 20; // per degree: 7.2 s per lap
void setColour(int r, int g, int b) {
analogWrite(RED_PIN, r);
analogWrite(GREEN_PIN, g);
analogWrite(BLUE_PIN, b);
}
// hue 0 to 359: red at 0, green at 120, blue at 240.
void wheel(int hue) {
int sector = hue / 120;
int up = (hue % 120) * 255 / 120; // 0 to 252 across the sector
int down = 255 - up;
if (sector == 0) setColour(down, up, 0); // red to green
else if (sector == 1) setColour(0, down, up); // green to blue
else setColour(up, 0, down); // blue to red
}
int hue = 0;
unsigned long last = 0;
void setup() {
pinMode(RED_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(BLUE_PIN, OUTPUT);
}
void loop() {
if (millis() - last >= STEP_MS) {
last += STEP_MS;
wheel(hue);
hue = (hue + 1) % 360; // 359 wraps to 0, never 360
}
// anything else the sketch does goes here, and never waits
}The hue is split into three sectors of 120 degrees. In each, one channel counts down from 255 while the next counts up, and the third is 0. The same function is in the MicroPython version, line for line.
The same wheel in MicroPython. It uses time.ticks_ms and ticks_diff rather than sleep, so the loop is free for other work, as in the Arduino version.
"""
RGB LED - a colour wheel, MicroPython TK02 / /p/tk02
Wiring. Count from the square pad on the TinkerBlock board, LED side
up, header at the bottom:
GND -> GND
NC -> nothing (both NC pins are unconnected on the board)
NC -> nothing
RED -> GPIO 25 on an ESP32, GPIO 4 on an ESP32-S3,
GP13 on a Raspberry Pi Pico
GREEN -> GPIO 26, GPIO 5, GP14
BLUE -> GPIO 27, GPIO 6, GP15
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine and time are built in.
"""
from machine import Pin, PWM
import time
# ESP32: 25, 26, 27. ESP32-S3: 4, 5, 6. Pico: 13, 14, 15.
red = PWM(Pin(4), freq=1000)
green = PWM(Pin(5), freq=1000)
blue = PWM(Pin(6), freq=1000)
STEP_MS = 20 # per degree: 7.2 s per lap
def set_colour(r, g, b):
red.duty_u16(r * 257)
green.duty_u16(g * 257)
blue.duty_u16(b * 257)
def wheel(hue):
# hue 0 to 359: red at 0, green at 120, blue at 240.
sector = hue // 120
up = (hue % 120) * 255 // 120 # 0 to 252 across the sector
down = 255 - up
if sector == 0:
set_colour(down, up, 0) # red to green
elif sector == 1:
set_colour(0, down, up) # green to blue
else:
set_colour(up, 0, down) # blue to red
hue = 0
last = time.ticks_ms()
while True:
if time.ticks_diff(time.ticks_ms(), last) >= STEP_MS:
last = time.ticks_add(last, STEP_MS)
wheel(hue)
hue = (hue + 1) % 360 # 359 wraps to 0, never 360
# anything else the program does goes here, and never waitsticks_diff handles the millisecond counter wrapping round, which a plain subtraction does not. duty_u16 takes 0 to 65535, so set_colour multiplies each value by 257. Stop it with Ctrl-C in Thonny's shell.
When it does not work
Green gives more light per step than red or blue, so the green third of the wheel looks brighter and wider than the others. It is the same imbalance as the white article's. Scaling each channel by your trimmed white numbers narrows it.
Check the hue wraps from 359 to 0, not to 1 or 360. At 359 the sketch sends almost pure red, and at 0 exactly pure red, so a correct wrap is invisible. A hue of 360 lands in the last branch with nothing to scale, and the LED flashes blue for one step.
Change STEP_MS. At 20 ms per degree one lap takes 7.2 seconds; at 5 ms it takes under two. The PWM runs hundreds of times faster than either, so the LED follows any speed a sketch can manage.
Red is stronger than the others on a 3.3 V board, so the whole wheel leans red and the green and blue thirds are dim. It is expected, and the article on 3.3 V explains it. An Uno's 5 V pins give a more even wheel.
The short list of reasons one colour will not come on.
When a colour is missing →Edit this page — content/books/rgb-led/a-colour-wheel.mdx
Questions about this product
See what other owners have asked, and read their solutions.
RGB LED
Loading discussions…
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.