Phase 3: GPIO & Hardware 14 min read

Driving LEDs with ESP32: Resistors, PWM, RGB, and NeoPixels

Complete guide to driving LEDs with ESP32 — calculate resistor values, control brightness with PWM (LEDC), drive RGB LEDs, and use WS2812B NeoPixels.

Updated June 19, 2026

LED Basics and Why Resistors are Mandatory

An LED (Light Emitting Diode) has no internal current limiting. Without a resistor, Ohm’s law gives infinite current through a forward-biased diode until it burns out — typically in under a second. Every LED circuit must include a current-limiting resistor between the GPIO pin and the LED anode (positive leg).

LEDs have two important parameters: forward voltage (Vf) — the voltage drop across the LED when conducting — and forward current (If) — the current needed for the desired brightness.

LED Color Typical Vf Recommended If Resistor (3.3V supply)
Red 1.8–2.2 V 10–20 mA 68–150 Ω
Yellow / Orange 2.0–2.2 V 10–20 mA 56–130 Ω
Green 2.0–3.0 V 10–20 mA 15–130 Ω
Blue 3.0–3.4 V 5–10 mA 0–33 Ω (use 33 Ω min)
White 3.0–3.4 V 5–10 mA 0–33 Ω (use 33 Ω min)
Resistor Formula
R = (Vsupply - Vf) / If

Red LED on 3.3V at 15 mA:
R = (3.3 - 2.0) / 0.015 = 86.7 Ω → use 100 Ω

Blue LED on 3.3V at 8 mA:
R = (3.3 - 3.2) / 0.008 = 12.5 Ω → use 33 Ω (safer margin)

Simple LED Wiring

Component Connection
LED Anode (+) long leg 100 Ω resistor → ESP32 GPIO
LED Cathode (–) short leg ESP32 GND
Arduino (C++) — Basic LED blink
#define LED_PIN 16

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);   // LED on
  delay(500);
  digitalWrite(LED_PIN, LOW);    // LED off
  delay(500);
}

PWM Brightness Control with LEDC

ESP32 has a dedicated LEDC peripheral with 16 channels capable of high-frequency PWM. This is the correct way to control LED brightness:

Arduino (C++) — LEDC fade
#define LED_PIN  16
#define CHANNEL   0
#define FREQ   5000    // 5 kHz
#define BITS      8    // 8-bit → 0–255

void setup() {
  ledcSetup(CHANNEL, FREQ, BITS);   // Configure channel
  ledcAttachPin(LED_PIN, CHANNEL);  // Bind GPIO to channel
}

void loop() {
  // Fade in
  for (int duty = 0; duty <= 255; duty++) {
    ledcWrite(CHANNEL, duty);
    delay(8);   // 256 steps × 8ms ≈ 2 second fade
  }
  // Fade out
  for (int duty = 255; duty >= 0; duty--) {
    ledcWrite(CHANNEL, duty);
    delay(8);
  }
}

RGB LED Control

A common-cathode RGB LED has 4 pins: GND (common), R, G, B. Connect each color pin through a 100 Ω resistor to a separate GPIO. Use LEDC on all three channels for full color mixing:

Arduino (C++) — RGB LED full color
#define R_PIN 16
#define G_PIN 17
#define B_PIN 18

void setupLEDC(int pin, int ch) {
  ledcSetup(ch, 5000, 8);
  ledcAttachPin(pin, ch);
}

void setColor(int r, int g, int b) {
  ledcWrite(0, r);   // R channel
  ledcWrite(1, g);   // G channel
  ledcWrite(2, b);   // B channel
}

void setup() {
  setupLEDC(R_PIN, 0);
  setupLEDC(G_PIN, 1);
  setupLEDC(B_PIN, 2);
}

void loop() {
  setColor(255,   0,   0);  delay(1000);  // Red
  setColor(  0, 255,   0);  delay(1000);  // Green
  setColor(  0,   0, 255);  delay(1000);  // Blue
  setColor(255, 100,   0);  delay(1000);  // Orange
  setColor(128,   0, 128);  delay(1000);  // Purple
  setColor(255, 255, 255);  delay(1000);  // White
  setColor(  0,   0,   0);  delay(500);   // Off
}

WS2812B NeoPixel LEDs

NeoPixels are individually addressable RGB LEDs with a built-in driver chip. One GPIO pin can control a strip of 60, 100, or even 300 LEDs via a high-speed serial protocol. Install the Adafruit NeoPixel library from the Arduino Library Manager.

Arduino (C++) — NeoPixel rainbow
#include <Adafruit_NeoPixel.h>

#define LED_PIN    16
#define NUM_LEDS   12   // Number of pixels in strip

Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  strip.begin();
  strip.setBrightness(80);   // 0–255, limit for power safety
  strip.show();
}

void loop() {
  for (int i = 0; i < NUM_LEDS; i++) {
    strip.setPixelColor(i, strip.Color(255, 0, 0));  // Red
    strip.show();
    delay(50);
    strip.setPixelColor(i, strip.Color(0, 0, 0));    // Off
  }
}

Power note: Each WS2812B LED can draw up to 60 mA at full white. A strip of 30 LEDs = 1.8 A. Always power LED strips from a dedicated 5V supply, not from the ESP32 5V pin.

Driving High-Power LEDs via MOSFET

MOSFET LED Driver Circuit
12V LED strip (+) ──── 12V PSU (+)
12V LED strip (–) ──── MOSFET Drain (IRLZ44N)
MOSFET Source ──── GND (common with ESP32 GND)
ESP32 GPIO16 ──── 10kΩ ──── MOSFET Gate

Use PWM on GPIO16 to dim a 12V LED strip via IRLZ44N logic-level MOSFET.
IRLZ44N can handle 47A — vast overkill, but it runs cool at low currents.

Summary

Always use a current-limiting resistor with individual LEDs. Use the LEDC peripheral (ledcSetup / ledcAttachPin / ledcWrite) for smooth brightness control. Drive RGB LEDs with three LEDC channels for 16 million colors. For addressable strips, use the Adafruit NeoPixel or FastLED library. Power any high-current load from a separate supply, not the GPIO pin.

Frequently Asked Questions

Use R = (Vsupply - Vf) / If. For a red LED (Vf=2V, If=20mA) on 3.3V: R = (3.3 - 2.0) / 0.020 = 65Ω. Round up to 68Ω or 100Ω. For blue/white LEDs (Vf=3.2V), reduce current to 5–10mA to stay in the comfortable 100–200Ω range.
Use the LEDC (LED Controller) hardware peripheral. Call ledcSetup(channel, frequency, resolution) to configure the channel, ledcAttachPin(pin, channel) to bind the GPIO, then ledcWrite(channel, dutyCycle) where duty cycle ranges from 0 (off) to 2^resolution-1 (full brightness).
ESP32 with newer Arduino core (3.x) supports analogWrite(pin, value) where value is 0–255. On older cores (2.x) you must use the LEDC functions directly. The LEDC approach gives more control over frequency and resolution, so it is always preferred.
Each GPIO can source/sink 12 mA safely (40 mA max). A standard LED draws 5–20 mA. You could drive 20+ LEDs from individual GPIO pins within current limits, but total chip current across all GPIOs should stay under 1200 mA. For many LEDs, use a shift register or dedicated LED driver IC.
An RGB LED has four pins: one common (anode or cathode) and three color pins (R, G, B). Use three GPIO pins with LEDC PWM on each channel. ledcWrite(R_channel, 255) sets red full brightness. Mixing values creates any color in the 16-million-color 24-bit space.
WS2812B (NeoPixel) LEDs have a built-in controller chip. They are controlled via a single-wire serial protocol, and you can chain hundreds together on one GPIO. Each LED stores its own color value. Use the Adafruit NeoPixel or FastLED library with the ESP32 to drive them.
For LED dimming, 1 kHz is standard and well above the 50 Hz flicker threshold visible to the human eye. For motor or audio PWM you may need 25–50 kHz. Use 5000 Hz (5 kHz) as a safe default: ledcSetup(0, 5000, 8) gives 8-bit resolution at 5 kHz.
No — 5V LED strips run at 5V and typically draw much more current than a GPIO can provide. Power the strip from a 5V supply (USB or regulator), connect the strip GND to ESP32 GND, and control the strip via a logic-level MOSFET (e.g. IRLZ44N) driven by a PWM GPIO.
Common causes: PWM frequency too low (below 100 Hz is visible flicker — increase to 1 kHz+), duty cycle at 0 briefly during PWM reconfiguration, or power supply noise. Also, some phones cameras show flicker due to their rolling shutter interacting with the PWM frequency — this is camera artifact, not real flicker.
Use a for loop incrementing the LEDC duty cycle from 0 to the maximum value (e.g. 255 for 8-bit resolution) with a short delay between steps. For a 1-second fade: step every 4 ms (256 steps × 4 ms = ~1 second).

Projects to Build

Put this knowledge to work — try one of these hands-on projects.