esp32 intermediate 30 min

ESP32: drive an addressable RGB LED strip over RMT, the right way

Wire a WS2812B addressable RGB LED strip to an ESP32, drive it with the RMT peripheral and the FastLED library, and avoid the 800-LED limit and the always-on power math.

Code available for: ESP32 ArduinoArduino C
Published Aug 26, 2026

Addressable RGB LED strips are why a lot of people buy an ESP32. The WS2812B is the common one: 5V power, one data line, one LED per “pixel,” daisy-chainable. You can buy a 5-meter strip of 60 LEDs per meter (300 LEDs) for about $25 and control all of them from a single GPIO pin.

The trick is the timing. WS2812Bs expect a very specific waveform (800 kHz, with high and low pulses measured in hundreds of nanoseconds). The ESP32 has a peripheral called RMT (Remote Control) that is designed for exactly this. Bit-banging works on a Raspberry Pi Pico or an Arduino, but the ESP32’s RMT is more reliable. Use it.

This tutorial covers the LEDs, the RMT, the FastLED library, the wiring (with the level-shifter caveat), the 5-meter power-injection pattern, the gamma correction pattern, and the always-on power math.

What you need

  • ESP32 dev board
  • WS2812B strip (5V, 30 or 60 LEDs per meter; the “B” in WS2812B is important; the older WS2811 has different timing)
  • 5V power supply rated for the strip (e.g. 5V 10A for a 5m strip of 60 LEDs per meter at full white)
  • Level shifter (3.3V to 5V) on the data line, e.g. SN74HCT125
  • Capacitor (470uF to 1000uF, 6.3V or higher) across the strip power pads
  • Resistor (330 to 470 ohm) on the data line, between the ESP32 and the strip
  • Three jumper wires

The LEDs: WS2812B, SK6812, APA102

There are three common addressable LED families:

  • WS2812B: the most common. 5V power, 800 kHz one-wire protocol, GRB color order, no clock line. Cheap and ubiquitous.
  • SK6812: similar to WS2812B but with better color consistency and optional white channel (RGBW). Pin-compatible with WS2812B in most cases.
  • APA102 (also called “DotStar”): 5V power, two-wire protocol (data + clock), higher refresh rate, more expensive. Used when the WS2812B timing is too unreliable (e.g. very long strips).

The FastLED library supports all three. The wiring for APA102 is different (it has a clock line in addition to data). For most projects, WS2812B is the right pick.

The RMT peripheral

The ESP32 has an RMT (Remote Control) peripheral that is designed to transmit and receive precisely-timed pulses. It is the right tool for WS2812Bs because the timing is critical and the CPU cannot be trusted to hit it under load (Wi-Fi, Bluetooth, other interrupts).

The older way was to bit-bang the protocol from the CPU. It worked on an ESP8266 and it sometimes worked on an ESP32, but under Wi-Fi load, the timing would slip and the LEDs would glitch.

The RMT peripheral handles the timing in hardware. You set up a channel, give it a buffer of pulse timings, and the peripheral transmits. The CPU is free to do other things.

The Arduino-ESP32 core has a built-in rmt library, but most people use the FastLED library (which uses the RMT under the hood on ESP32).

Install FastLED

Sketch >> Include Library >> Manage Libraries >> search FastLED >> install.

FastLED is the most popular library for addressable LEDs on Arduino- compatible boards. It supports the ESP32 RMT, has animation helpers, and has good documentation.

The wiring

ESP32 5V  ----+----- 5V+ on power supply
              |
              +----- 5V on WS2812B strip
ESP32 GND ----+----- GND- on power supply
              |
              +----- GND on WS2812B strip
ESP32 GPIO 5 --[330R]--[ SN74HCT125 ]--[330R]-- DIN on WS2812B strip

Three things in this diagram that are not optional:

  • The 330R resistor on the data line, right at the strip end. The WS2812B datasheet asks for it, and without it the first LED can flicker or, on a bad day, fry.
  • The level shifter (SN74HCT125 or similar) between the ESP32 and the strip. The WS2812B expects 0.7 * VCC = 3.5V for a logic high, and the ESP32 outputs 3.3V. That is close enough to work in many cases, but the level shifter is the safe move.
  • The bulk capacitor across the strip’s power pads. WS2812Bs draw current in spikes (each LED can pull 60 mA when you turn it on white), and without bulk capacitance you get brownouts that reset the ESP32.

The level shifter is the part most people skip. It works without it, until it does not. For a small strip on a breadboard, skip it. For a permanent install, add it.

The FastLED code

#include <FastLED.h>

#define LED_PIN     5
#define NUM_LEDS    60
#define BRIGHT      60   // 0-255, 60 is sane for indoor eye-candy

CRGB leds[NUM_LEDS];

void setup() {
  FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(BRIGHT);
  FastLED.clear();
  FastLED.show();
}

void loop() {
  fill_rainbow(leds, NUM_LEDS, 0, 7);
  FastLED.show();
  delay(20);
}

The addLeds<WS2812B, LED_PIN, GRB> line tells FastLED which LED type, which pin, and which color order. WS2812Bs use GRB (green-red- blue) in the wire protocol, not RGB. FastLED handles the byte swapping.

The BRIGHT constant is the global brightness cap. The FastLED library multiplies every color by BRIGHT/255 before sending, so BRIGHT=60 keeps the current to about 14 mA per LED instead of 60 mA per LED.

Gamma correction

The WS2812B is linear: CRGB(128,128,128) outputs half the brightness of CRGB(255,255,255). But the human eye is logarithmic: half the perceived brightness is more like CRGB(188,188,188). The result is that dim levels look “stepped” - the jump from 1 to 2 is invisible, the jump from 200 to 201 is huge.

The fix is gamma correction: a lookup table that maps 0-255 (linear) to 0-255 (perceived linear). FastLED has it built in:

#include <FastLED.h>

void setup() {
  // Turn on gamma correction
  FastLED.setCorrection(TypicalLEDStrip);
  // ... rest of setup
}

The TypicalLEDStrip is one of several built-in corrections. Pick the one that matches your LED type. For WS2812B, TypicalLEDStrip or TypicalSMD5050 are both good. The difference is small.

Without gamma correction, fades look like they have a “dead zone” at the bottom. With gamma correction, fades look smooth.

The 5-meter power injection pattern

A 5-meter strip of 60 LEDs per meter has 300 LEDs. At full white, that is 18 amps. The strip’s power wire is not rated for 18 amps (usually 22 AWG, rated for about 5 amps). The result is voltage drop: the LEDs at the end of the strip get dimmer and may glitch.

The fix is to inject power at both ends:

Power supply 5V ---+----- 5V pad on start of strip
                    |
                    +----- 5V pad on end of strip (separate wire)
Power supply GND --+----- GND pad on start of strip
                    |
                    +----- GND pad on end of strip

The data line still daisy-chains through the strip. The power comes from both ends.

For longer strips (10+ meters), inject power every 2-3 meters.

The 800-LED limit

The ESP32’s RMT buffer is fixed-size. Each LED takes about 24 bits in the buffer (8 bits per channel, 3 channels). 800 LEDs at 24 bits is 19,200 bits. The default RMT buffer on the ESP32 is 64 KB (8,192 32-bit words), so 800 LEDs is about the maximum.

You can push the limit by reducing the RMT buffer size and accepting slower refresh rates, but the practical limit is about 800 LEDs with FastLED on an ESP32. For more, use multiple ESP32s, use an external LED driver, or use a different protocol (e.g. APA102 with a hardware clock).

For 95% of projects, 800 LEDs is plenty. A 5m strip at 60 LEDs/m is 300 LEDs, well under the limit.

Animation patterns

FastLED has a few built-in helpers:

// Rainbow: cycles through the spectrum
fill_rainbow(leds, NUM_LEDS, hue, delta_hue);

// Theater chase: a single "lit" pixel moves down the strip
addGlitter(80, leds, NUM_LEDS);   // adds random sparkles
fill_solid(leds, NUM_LEDS, CRGB::Red);  // all red

// Custom: each LED is a different color
for (int i = 0; i < NUM_LEDS; i++) {
  leds[i] = CHSV(i * 10, 255, 255);   // hue, saturation, value
}
FastLED.show();

The CHSV (hue, saturation, value) color model is more useful for animations than CRGB (red, green, blue). The hue is a 0-255 number that wraps around the color wheel. Hue 0 is red, hue 85 is green, hue 170 is blue. Incrementing the hue gives a smooth rainbow.

The “always-on LED strip” power math

A WS2812B at full white draws about 60 mA. A 60-LED strip at full white draws 3.6 amps. A 300-LED strip (5m at 60 LEDs/m) at full white draws 18 amps.

This is the part people forget when they install a strip. A “5V 3A” phone charger can run about 50 LEDs at full white. A “5V 10A” laptop brick can run about 165 LEDs at full white. For a full 5m strip at full white, you need a real supply (the 5V 20A “LED power supply” sold on Amazon is the typical pick).

The setBrightness(60) cap keeps the current to about 14 mA per LED, which means a 300-LED strip draws about 4.2A. That is a manageable power supply.

The takeaway: a 5m strip is a 100W load. Treat it like a 100W load when you plan the power supply and the wiring.

What you learned

  • The RMT peripheral on the ESP32 is the right way to drive WS2812Bs. No bit-banging.
  • The data line needs a level shifter (3.3V to 5V) for a permanent install. The 330R resistor is not optional.
  • 5V power injection at both ends of a long strip prevents voltage drop.
  • Gamma correction makes fades look smooth. Without it, dim levels look stepped.
  • A 300-LED strip at full white draws 18 amps. Plan the power supply.

When something breaks

The first LED flickers or does not light up. Data signal is borderline. Add the level shifter, or shorten the data wire.

The strip is dim at the end. Voltage drop. Inject power at both ends.

The strip resets when you turn on white. Brownout. Add the bulk capacitor across the strip’s power pads.

FastLED compilation fails on ESP32-S3. The RMT API changed. Update FastLED to 3.6 or newer.

The colors are wrong. The color order is wrong. WS2812B is GRB; some cheap clones are RGB. Change the third parameter in addLeds<WS2812B, LED_PIN, GRB> to RGB.

CRGB(255,255,255) is not actually white. The LEDs are individually calibrated, but the strip has variation. Set FastLED.setCorrection(TypicalLEDStrip) and the variation becomes less obvious.

What to build next

  • A music-reactive strip with a MAX4466 microphone.
  • A Wi-Fi controlled strip with a web UI.
  • A “fire” effect with red/orange flickering.
  • A 16x16 matrix for text and animations.

The music-reactive version is the most fun. The matrix is the project that uses the most LEDs (256 in a 16x16 grid).