← All projects
Lighting Beginner

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.

Difficulty
Beginner
Time
30 min
Category
Lighting
Updated

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: the L suffix does the maths in long, so i × 65536 does not overflow on boards with a 16-bit int.
  • setBrightness() scales every colour and saves current. Call it once in setup(), not to animate brightness: that degrades colour accuracy.
  • NEO_GRB is the colour order of a WS2812B. If red and green are swapped, try NEO_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.