If you just need the quick answer: the universally safe ESP32 pins for general-purpose digital outputs and inputs—without risking boot failures or peripheral conflicts—are GPIO 4, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, and 33. GPIO 34, 35, 36, and 39 are strictly input-only. Every other pin carries a specific hardware caveat, from strapping pin boot states to ADC2 Wi-Fi conflicts.

The ESP32-WROOM-32 is a powerhouse, but its pinout is a minefield for builders migrating from the simpler Arduino Uno. This guide breaks down the hardware constraints, provides a battle-tested project build using only safe pins, and gives you the exact debugging steps for the most common pin-related failures.

The ESP32 Pinout Minefield: What to Avoid

Before wiring up your next sensor array, you need to understand three major hardware constraints built into the ESP32 silicon.

1. Strapping Pins (Boot Failures)

Strapping pins dictate the boot mode of the ESP32. If these pins are pulled to the wrong logic level during power-on or reset, the chip will hang, boot into flash mode, or output debug noise instead of running your code.

  • GPIO 0: Must be HIGH or floating to boot normally. (Pulled LOW to enter flash mode).
  • GPIO 2: Must be LOW or floating to boot normally. (Do not attach a pull-up resistor here).
  • GPIO 12 (MTDI): Must be LOW. If pulled HIGH, it changes the flash voltage regulator to 1.8V, which will brownout a 3.3V flash chip and brick the boot sequence.
  • GPIO 15 (MTDO): Must be LOW or floating for normal boot log output.

2. Input-Only Pins

GPIO 34, 35, 36 (VP), and 39 (VN) have no internal pull-up/pull-down resistors and lack output drivers. They are strictly for reading analog voltages or digital inputs. Attempting to use digitalWrite() on these pins will silently fail.

3. The ADC2 vs. Wi-Fi Conflict

The ESP32 has two Analog-to-Digital Converters. ADC1 (GPIO 32-36) works perfectly alongside Wi-Fi. ADC2 (GPIO 0, 2, 4, 12-15, 25-27) shares hardware resources with the Wi-Fi radio. If your code initializes Wi-Fi, any attempt to read an ADC2 pin will fail and return -1. Always use ADC1 pins for analog sensors in IoT builds.

Project Build: Safe-Pin Environmental Relay Controller

This build demonstrates proper pin selection by wiring an I2C display, a digital temperature/humidity sensor, an analog light sensor, and a relay module using only conflict-free GPIOs.

Difficulty: Beginner-Intermediate | Time: 45 Minutes
Target Board Variant: ESP32-WROOM-32 DevKit v1 (30-pin layout)

Parts List

  • 1x ESP32-WROOM-32 DevKit v1 (30-pin variant, e.g., HiLetgo or NodeMCU-32S)
  • 1x 0.96" I2C OLED Display (SSD1306 driver, 4-pin I2C variant)
  • 1x DHT22 Temperature/Humidity Sensor (AM2302)
  • 1x GL5528 LDR (Light Dependent Resistor) + 10kΩ pulldown resistor
  • 1x 5V Opto-isolated Relay Module (Active LOW trigger)
  • Jumper wires, breadboard, 5V/2A USB power supply

Pin Mapping Table

Component ESP32 GPIO Why this pin?
OLED SDA (I2C) GPIO 21 Default hardware I2C SDA. Safe, no boot conflicts.
OLED SCL (I2C) GPIO 22 Default hardware I2C SCL. Safe, no boot conflicts.
DHT22 Data GPIO 4 Safe digital I/O. (Avoids strapping pins 0, 2, 12, 15).
LDR Analog Out GPIO 34 ADC1_CH6. Input-only, immune to Wi-Fi ADC2 conflicts.
Relay IN (Trigger) GPIO 26 Safe digital output. Capable of driving opto-isolator LED.

Complete Compilable Code (Arduino IDE 2.x / ESP32 Core 3.x)

This code requires the Adafruit SSD1306, Adafruit GFX, and DHT sensor library packages installed via the Arduino Library Manager. It includes explicit pin definitions and error handling for sensor timeouts.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

// --- PIN DEFINITIONS ---
#define PIN_OLED_SDA  21
#define PIN_OLED_SCL  22
#define PIN_DHT       4
#define PIN_LDR       34  // ADC1 pin (Safe with WiFi)
#define PIN_RELAY     26

// --- COMPONENT CONFIGS ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET   -1
#define OLED_ADDRESS 0x3C
#define DHT_TYPE     DHT22

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
DHT dht(PIN_DHT, DHT_TYPE);

void setup() {
  Serial.begin(115200);
  delay(1000); // Allow serial monitor to connect

  // Initialize Pins
  pinMode(PIN_RELAY, OUTPUT);
  digitalWrite(PIN_RELAY, HIGH); // Active LOW relay, start OFF

  // Initialize DHT
  dht.begin();

  // Initialize I2C and OLED with error handling
  Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed. Check I2C wiring and pull-ups."));
    while(true); // Halt execution to prevent I2C bus lockups
  }
  
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.println("System Online");
  display.display();
}

void loop() {
  // Read Sensors
  float humidity = dht.readHumidity();
  float tempC = dht.readTemperature();
  int lightRaw = analogRead(PIN_LDR);
  
  // Map LDR raw value (0-4095) to percentage (0-100)
  int lightPct = map(lightRaw, 0, 4095, 0, 100);

  // Error Handling: Check if DHT reads failed (returns NaN)
  if (isnan(humidity) || isnan(tempC)) {
    Serial.println(F("Failed to read from DHT sensor! Check GPIO 4 wiring."));
    display.clearDisplay();
    display.setCursor(0,0);
    display.println("DHT22 ERROR");
    display.display();
    delay(2000);
    return;
  }

  // Automation Logic: Trigger relay if hot and bright
  if (tempC > 28.0 && lightPct > 60) {
    digitalWrite(PIN_RELAY, LOW);  // Turn ON (Active LOW)
  } else {
    digitalWrite(PIN_RELAY, HIGH); // Turn OFF
  }

  // Update OLED
  display.clearDisplay();
  display.setCursor(0,0);
  display.print("Temp: "); display.print(tempC); display.println(" C");
  display.print("Humi: "); display.print(humidity); display.println(" %");
  display.print("Light: "); display.print(lightPct); display.println(" %");
  display.print("Relay: ");
  display.println(digitalRead(PIN_RELAY) == LOW ? "ON" : "OFF");
  display.display();

  delay(2000); // DHT22 requires 2s between reads
}

Debugging: First Checks and Exact Error Strings

When your ESP32 build fails, do not immediately rewrite your code. Hardware and pin conflicts cause 90% of embedded failures. Here are the first three things to check:

  1. Strapping Pin States at Boot: Disconnect all wires from GPIO 0, 2, 12, and 15. Power cycle the board. If it boots normally, one of your external circuits is pulling a strapping pin to the wrong logic level during startup.
  2. ADC2 vs. Wi-Fi Conflict: If your analog reads return -1 or 0 only after WiFi.begin() is called, you are using an ADC2 pin. Move the sensor to an ADC1 pin (GPIO 32-36).
  3. I2C Pull-Up Resistors: The ESP32's internal pull-ups are often too weak for reliable I2C communication at 400kHz. Ensure your OLED or sensor breakout board has 4.7kΩ physical pull-up resistors on SDA and SCL.

The Exact Error String: ADC2 Wi-Fi Conflict

If you attempt to read an ADC2 pin (like GPIO 25) while the Wi-Fi radio is active, the ESP-IDF underlying the Arduino core will block the read. In the Serial Monitor (with Core Debug Level set to Error or Warn), you will see this exact string:

E (456) adc_common: adc2_get_raw(188): adc2 is used by Wi-Fi, please use adc1

Ranked Causes for this Error:

  1. Wrong Pin Selected (90%): You wired your analog sensor to GPIO 4, 12, 13, 14, 15, 25, 26, or 27. Fix: Rewire to GPIO 32, 33, 34, 35, 36, or 39.
  2. Library Abstraction (8%): A third-party library is hardcoding an ADC2 pin internally for battery voltage monitoring. Fix: Check the library's .cpp file and redefine the battery pin macro.
  3. Bluetooth Coexistence (2%): Classic Bluetooth can also lock out ADC2 in certain ESP32 core versions. Fix: Disable BT in the Arduino IDE Tools menu if not needed.

Extending or Simplifying the Build

To Simplify: If you do not need visual feedback, drop the I2C OLED entirely. Remove the Wire.h and Adafruit includes, delete the display initialization, and rely purely on Serial.print(). This frees up GPIO 21 and 22 and reduces the code footprint by roughly 40KB.

To Extend: Need to add a BME280 pressure sensor and a CCS811 air quality sensor? Both use I2C. Because they share the same bus, you can wire them in parallel to GPIO 21 and 22, provided their I2C addresses do not clash (BME280 is typically 0x76 or 0x77; CCS811 is 0x5A). If you run out of safe I2C addresses, use a TCA9548A I2C Multiplexer on GPIO 21/22 to expand the bus to 8 separate channels.

Frequently Asked Questions

Which ESP32 pins are safe to use for general outputs?

The safest pins for digital outputs (relays, LEDs, buzzers) that will not interfere with boot sequences or internal peripherals are GPIO 4, 16, 17, 18, 19, 23, 25, 26, 27, 32, and 33. Avoid GPIO 6-11 entirely, as they are hardwired to the integrated SPI flash memory on the WROOM module; touching them will crash the chip.

Why does my ESP32 fail to boot when GPIO 12 is pulled high?

GPIO 12 is a strapping pin that configures the internal flash voltage regulator. If GPIO 12 is HIGH during boot, the ESP32 switches the flash VDD to 1.8V. Since the WROOM-32 uses a 3.3V SPI flash chip, the 1.8V supply causes a brownout, and the chip fails to read its own firmware, resulting in a continuous boot loop. Always ensure GPIO 12 is LOW or floating at startup.

Can I use GPIO 34, 35, 36, or 39 as digital outputs?

No. These four pins are physically routed only to the input pad and the ADC multiplexer. They lack output driver transistors on the silicon die. Calling pinMode(34, OUTPUT) and digitalWrite(34, HIGH) will compile without errors, but the pin voltage will remain at 0V. Use them strictly for reading buttons, potentiometers, or LDRs.

How do I fix the "adc2 is used by Wi-Fi" error?

You cannot force ADC2 to work simultaneously with Wi-Fi on the standard ESP32 (non-S3) silicon; it is a hardware limitation documented in the Espressif ESP32 Datasheet. The only fix is to physically move your analog sensor's signal wire from an ADC2 pin (like GPIO 25) to an ADC1 pin (like GPIO 34), and update your code's #define accordingly.