A Rainbow on a WS2812 Ring: HSV Colours and Gamma Correction
WS2812 (NeoPixel) addressable LEDs run on a single wire, each in its own colour. We run a moving rainbow on a 16-LED ring: the HSV colour wheel, gamma32 for natural colours, and powering and protecting the strip.
About the project
Every WS2812B LED contains a chip: it takes its 24 colour bits from the data stream and passes the rest down the chain. So a strip of hundreds of pixels runs on a single signal wire. We put a smoothly rotating rainbow on a 16-LED ring.
What you need
- ESP32 DevKit
- WS2812B 16-LED ring (or a piece of strip)
- A 330–470 Ω resistor on the data line and a 470–1000 µF capacitor on the supply (for strips)
- The Adafruit NeoPixel library
Wiring
| Ring | ESP32 |
|---|---|
| 5V (VCC) | 5V (VIN) |
| GND | GND |
| DIN (DI) | GPIO16 (through 330 Ω) |
- Current. One pixel at full white draws ~60 mA. For 16 pixels that is up to 1 A; for a metre of 60-pixel strip, 3.6 A. USB and the board's 5V pin are good for about 500 mA, so bigger strips get their own 5 V supply, with its ground tied to the ESP32.
- Levels. At 5 V a WS2812 formally wants ≥ 3.5 V on its input, and the ESP32 gives 3.3 V. With short wires it usually works. If the colours glitch, add a level shifter (74AHCT125), or run the first pixel from a slightly lower voltage.
HSV instead of RGB
A rainbow in RGB means "flowing" between channels by hand. In the HSV model a colour is a hue on a circle, a saturation and a value (brightness). Adafruit NeoPixel uses a hue of 0 to 65535 for the full circle. We spread the 16 pixels evenly around it and shift the starting point every frame:
colour(i) = ColorHSV(hue + i × 65536 / 16)
gamma32() corrects for the non-linear way we see brightness. Without it the rainbow looks washed out, with over-wide whitish transitions.
Code
// A rotating rainbow on a 16-LED WS2812 ring
#include <Adafruit_NeoPixel.h>
const int PIN = 16;
const int COUNT = 16;
Adafruit_NeoPixel ring(COUNT, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
ring.begin();
ring.setBrightness(80); // ~30 %: your eyes and your power supply will thank you
}
void loop() {
static uint16_t hue = 0;
for (int i = 0; i < COUNT; i++) {
ring.setPixelColor(i, ring.gamma32(ring.ColorHSV(hue + i * 65536L / COUNT)));
}
ring.show(); // send the colours to the ring
hue += 512; // hue shift = rotation speed
delay(20);
}
Good to know
65536L: theLsuffix does the maths inlong, soi × 65536does not overflow on boards with a 16-bitint.setBrightness()scales every colour and saves current. Call it once insetup(), not to animate brightness: that degrades colour accuracy.NEO_GRBis the colour order of a WS2812B. If red and green are swapped, tryNEO_RGB.- If the first pixel dies, the whole strip goes dark. The resistor on DIN and the capacitor on the supply protect against spikes when plugging in.
What next
Add a PIR sensor and a photoresistor, and the ring becomes a night light that switches on with motion in the dark. It has its own article on the site.