Phase 3: GPIO & Hardware 12 min read

Digital Outputs on ESP32: Control LEDs, Relays, and More

Learn how to control digital outputs on ESP32 with digitalWrite(), manage output current, drive LEDs and relays, and use safe GPIO output pins.

Updated June 19, 2026

Introduction to Digital Outputs on ESP32

A digital output pin actively drives a voltage: either 3.3 V (HIGH, logic 1) or 0 V (LOW, logic 0). Through digital outputs you turn LEDs on and off, trigger relay coils, control motor drivers, signal other microcontrollers, and create timing signals. The ESP32 provides up to 30 usable output-capable GPIOs with a maximum of 40 mA per pin — powerful enough for most direct-drive applications.

Output Current Capabilities

Staying within safe current limits is critical to protect your ESP32 from damage:

Limit Value Meaning
Max per GPIO pin 40 mA Absolute maximum — do not exceed
Recommended per pin 12 mA Safe for continuous operation
Total across all GPIOs 1200 mA Chip-wide limit
3.3 V rail (VDD) 600 mA Combined board regulator output

Standard 5 mm LEDs draw ~20 mA, which is fine. Relay coils (60–100 mA), buzzers, and DC motors far exceed GPIO limits — use a transistor or dedicated driver IC for those.

Safe Output Pins on ESP32

Pin Group Use As Output? Notes
GPIO4, GPIO13, GPIO14 Safe ✓ General purpose, no boot concerns
GPIO16, GPIO17, GPIO18, GPIO19 Safe ✓ Excellent general-purpose outputs
GPIO21, GPIO22, GPIO23 Safe ✓ Also used for I2C/SPI — share carefully
GPIO25, GPIO26, GPIO27 Safe ✓ Also DAC-capable (GPIO25, GPIO26)
GPIO32, GPIO33 Safe ✓ Also good ADC channels
GPIO0, GPIO2, GPIO5, GPIO12 Careful ⚠ Affect boot mode or flash voltage
GPIO6–GPIO11 Avoid ✗ Internal flash — do not use
GPIO34–GPIO39 Never ✗ Input only — no output capability

Basic Digital Output: The Blink Sketch

Arduino (C++)
#define LED_PIN 2   // Built-in LED on ESP32 DevKit

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);
  Serial.println("Blink demo running...");
}

void loop() {
  digitalWrite(LED_PIN, HIGH);   // LED on  (3.3 V on pin)
  Serial.println("LED ON");
  delay(1000);

  digitalWrite(LED_PIN, LOW);    // LED off (0 V on pin)
  Serial.println("LED OFF");
  delay(1000);
}

Controlling Multiple Outputs

Arduino (C++)
const int LEDS[]  = {16, 17, 18, 19};
const int NUM_LED = 4;

void setup() {
  for (int i = 0; i < NUM_LED; i++)
    pinMode(LEDS[i], OUTPUT);
}

void loop() {
  // Knight Rider scan
  for (int i = 0; i < NUM_LED; i++) {
    digitalWrite(LEDS[i], HIGH);
    delay(100);
    digitalWrite(LEDS[i], LOW);
  }
  for (int i = NUM_LED - 2; i > 0; i--) {
    digitalWrite(LEDS[i], HIGH);
    delay(100);
    digitalWrite(LEDS[i], LOW);
  }
}

Non-Blocking Output Timing with millis()

Using delay() blocks the entire processor. For projects that must do other things while toggling outputs, use millis():

Arduino (C++)
#define LED_PIN    2
#define INTERVAL 500   // ms between toggles

unsigned long lastToggle = 0;
bool ledState = false;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);
}

void loop() {
  unsigned long now = millis();
  if (now - lastToggle >= INTERVAL) {
    lastToggle = now;
    ledState   = !ledState;
    digitalWrite(LED_PIN, ledState);
    Serial.printf("LED is now %sn", ledState ? "ON" : "OFF");
  }
  // Other code here runs continuously without blocking
}

Driving an LED with Current Limiting

Never connect an LED directly between GPIO and GND — it will draw unlimited current and burn out the LED or damage the GPIO. Always use a current-limiting resistor:

Resistor Calculation
R = (Vsupply - Vf) / If

Standard red LED:   Vf = 2.0V, If = 20mA
R = (3.3 - 2.0) / 0.020 = 65Ω → use 68Ω or 100Ω

Blue/white LED:     Vf = 3.2V, If = 20mA
R = (3.3 - 3.2) / 0.020 = 5Ω → too low! Limit to 5–10mA instead
R = (3.3 - 3.2) / 0.010 = 10Ω
Component Connection
LED anode (+) Via 100 Ω resistor to ESP32 GPIO
LED cathode (–) ESP32 GND

Driving High-Current Loads via Transistor

When you need to switch loads above 40 mA (buzzers, motors, relay coils), a small NPN transistor like the 2N2222 or BC547 lets the GPIO control the load safely:

NPN Transistor Switch Wiring
3.3V or 5V ──────────── Relay coil (+)
                               |
                        Relay coil (–)
                               |
                          Collector (NPN)
                           Emitter ──── GND

ESP32 GPIO ── 1kΩ ── Base (NPN)

Add flyback diode across relay coil: anode to GND, cathode to VCC.

GPIO State at Boot

Several ESP32 GPIOs are sampled at boot to determine the boot mode. Placing an output device on a strapping pin can cause boot failures:

  • GPIO0: LOW at boot → enters flash download mode (avoid for general output)
  • GPIO2: Must be LOW or floating at boot for normal operation
  • GPIO5: Outputs 1 kHz PWM signal during boot
  • GPIO12: HIGH at boot selects 1.8 V flash voltage (very dangerous)

For reliable output control, stick to GPIO16, GPIO17, GPIO18, GPIO19, GPIO21, GPIO22, GPIO23, GPIO25, GPIO26, GPIO27, GPIO32, GPIO33.

Summary

Digital outputs on ESP32 are simple but require awareness of three constraints: current limits (40 mA max per pin, 12 mA recommended), boot-strapping conflicts (avoid GPIO0, GPIO2, GPIO5, GPIO12), and input-only pins (GPIO34–GPIO39 cannot be outputs). Use millis() instead of delay() for non-blocking timing, a transistor for loads above 40 mA, and always include current-limiting resistors for LEDs.

Frequently Asked Questions

Each ESP32 GPIO can source (output HIGH) or sink (output LOW) a maximum of 40 mA, but the recommended safe limit is 12 mA per pin. The total current across all GPIOs should not exceed 1200 mA. For loads above 40 mA use a transistor or MOSFET.
Call pinMode(pin, OUTPUT) in setup() then use digitalWrite(pin, HIGH) to drive the pin to 3.3V or digitalWrite(pin, LOW) to drive it to 0V (GND).
Avoid GPIO0 (boot mode), GPIO2 (connected to built-in LED on some boards, affects boot), GPIO5 (outputs PWM at boot), GPIO12 (affects flash voltage), and GPIO6–GPIO11 (connected to internal flash). GPIO34–GPIO39 cannot be outputs at all.
When set HIGH, an ESP32 GPIO pin outputs approximately 3.3 V. It is not 5 V. If you need 5 V output to drive 5 V peripherals, use a transistor or level-shifting circuit.
Not directly. A relay coil typically draws 60–100 mA which far exceeds the 40 mA GPIO maximum. Use a relay module with a built-in transistor driver, or add your own NPN transistor (e.g. 2N2222) between the GPIO and the relay coil.
In OUTPUT mode the GPIO actively drives the pin to either 3.3V (HIGH) or GND (LOW). In INPUT mode the pin is high-impedance and reads the external voltage. Setting a pin to OUTPUT while something external also tries to drive it can short-circuit and damage the GPIO.
Yes. For maximum speed use direct register writes: GPIO.out_w1ts.val = (1 << pin) for HIGH and GPIO.out_w1tc.val = (1 << pin) for LOW. This can toggle pins in the 10–80 MHz range. For most projects, digitalWrite() at ~1 µs per call is plenty fast.
If one pin is HIGH and the other LOW, you create a short circuit through the GPIO drivers, potentially damaging both pins and the chip. Never connect two OUTPUT pins directly together. Use OUTPUT only when you own the signal.
No. GPIO output states are not preserved through deep sleep by default. Pins return to their boot-state configuration when the ESP32 wakes up. You can hold output states during light sleep, but deep sleep resets GPIO.
For PWM use ledcSetup(), ledcAttachPin(), and ledcWrite() which drive the hardware LEDC peripheral. For simple on/off patterns, digitalWrite() with delay() or millis() timing is sufficient. True digital output has no intermediate speeds.

Projects to Build

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