Phase 3: GPIO & Hardware 14 min read

Digital Inputs on ESP32: Complete GPIO Input Guide

Master digital inputs on ESP32 — configure INPUT, INPUT_PULLUP, INPUT_PULLDOWN, use digitalRead(), hardware interrupts, and protect against 5V signals.

Updated June 19, 2026

Introduction to Digital Inputs on ESP32

Digital inputs are the bedrock of interactive ESP32 projects. Every button press, door sensor, limit switch, and motion detector relies on reading a binary HIGH or LOW voltage from a GPIO pin. ESP32 has up to 34 GPIO pins with hardware pull-up and pull-down resistors built in, making it one of the most flexible microcontrollers for digital input applications.

This guide covers everything from basic digitalRead() usage through hardware interrupts, input-only pins, voltage protection, and real-world project examples. After reading this, you will be able to wire and code any digital input scenario confidently.

How Digital Logic Levels Work on ESP32

ESP32 is a 3.3V device. Its GPIO pins interpret voltages in three regions:

Voltage Logic State Result
0 V – 0.66 V LOW digitalRead() returns 0
0.66 V – 2.64 V Undefined Unpredictable — avoid
2.64 V – 3.3 V HIGH digitalRead() returns 1

You must drive the input firmly into the HIGH or LOW region. Leaving a pin floating (no connection, no pull resistor) places it in the undefined zone where noise dominates.

ESP32 GPIO Pin Capabilities at a Glance

GPIO Range Input? Output? Pull Resistors? Notes
GPIO0–GPIO5 Yes Yes Yes GPIO0, GPIO2 affect boot; use carefully
GPIO6–GPIO11 Avoid Avoid Yes Tied to internal SPI flash — do not use
GPIO12–GPIO33 Yes Yes Yes General purpose, safest for projects
GPIO34–GPIO39 Yes No No Input-only; needs external pull resistors

Recommended safe input pins: GPIO4, GPIO13, GPIO14, GPIO16, GPIO17, GPIO18, GPIO19, GPIO21, GPIO22, GPIO23, GPIO25, GPIO26, GPIO27, GPIO32, GPIO33, GPIO34, GPIO35.

Configuring Pins: INPUT, INPUT_PULLUP, INPUT_PULLDOWN

The pinMode() function in setup() sets the electrical mode of a GPIO:

Arduino (C++)
void setup() {
  Serial.begin(115200);

  pinMode(4,  INPUT);           // Floating — needs external resistor
  pinMode(16, INPUT_PULLUP);    // Internal ~47kΩ pull-up → reads HIGH by default
  pinMode(17, INPUT_PULLDOWN);  // Internal ~47kΩ pull-down → reads LOW by default
}

void loop() {
  Serial.printf("GPIO4=%d  GPIO16=%d  GPIO17=%dn",
    digitalRead(4), digitalRead(16), digitalRead(17));
  delay(300);
}

The most common choice for buttons is INPUT_PULLUP with the other end of the button wired to GND. When the button is pressed the pin is pulled LOW — this is called active-LOW logic.

The Floating Pin Problem (and How to Solve It)

A floating pin is a GPIO connected to nothing — no voltage source, no pull resistor. Electromagnetic fields from nearby wires, the MCU itself, and even your hand induce tiny currents that cause the pin to oscillate between HIGH and LOW randomly. The fix is simple:

  • Software pull-up: use INPUT_PULLUP in pinMode()
  • Software pull-down: use INPUT_PULLDOWN in pinMode()
  • External pull-up: connect a 10 kΩ resistor between the pin and 3.3 V
  • External pull-down: connect a 10 kΩ resistor between the pin and GND

External resistors are slightly preferred in noisy environments or when the pin travels off-board on a long wire, because the internal ~47 kΩ resistors are weaker and more susceptible to interference.

Reading a Button: Step-by-Step Project

Wire a momentary push button between GPIO16 and GND. No external resistor needed — we’ll use INPUT_PULLUP.

Button Terminal Connect To
Terminal A ESP32 GPIO16
Terminal B ESP32 GND
Arduino (C++)
#define BUTTON_PIN 16
#define LED_PIN     2   // Built-in LED on most ESP32 dev boards

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);  // HIGH normally, LOW when pressed
  pinMode(LED_PIN,    OUTPUT);
}

void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {   // Active-LOW: button pressed
    digitalWrite(LED_PIN, HIGH);
    Serial.println("PRESSED");
  } else {
    digitalWrite(LED_PIN, LOW);
    Serial.println("released");
  }
  delay(50);
}

Reading Multiple Inputs Simultaneously

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

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < NUM_BTN; i++)
    pinMode(BUTTONS[i], INPUT_PULLUP);
}

void loop() {
  for (int i = 0; i < NUM_BTN; i++) {
    Serial.printf("B%d=%s ", i + 1, digitalRead(BUTTONS[i]) == LOW ? "ON " : "off");
  }
  Serial.println();
  delay(200);
}

Hardware Interrupts for Fast Response

Polling digitalRead() in loop() may miss very brief signals. Hardware interrupts trigger immediately when the pin changes state, regardless of what the main loop is doing.

Arduino (C++)
#define BTN_PIN 16
#define LED_PIN  2

volatile bool btnFlag = false;   // shared between ISR and main

void IRAM_ATTR onButtonPress() { // IRAM_ATTR = runs from RAM, not flash
  btnFlag = true;                // keep ISR short — no delay/Serial here
}

void setup() {
  Serial.begin(115200);
  pinMode(BTN_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(BTN_PIN), onButtonPress, FALLING);
}

void loop() {
  if (btnFlag) {
    btnFlag = false;
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));   // toggle LED
    Serial.println("Interrupt fired — button pressed!");
  }
}

Key rules for ISR functions: always use IRAM_ATTR, keep the function to a few lines, use volatile on any variable the ISR writes, and never call Serial, delay(), or millis() inside the ISR.

Input-Only Pins: GPIO34–GPIO39

These four pins are special — they can only ever be inputs, and they have no internal pull resistors. They are ideal for clean analog signals (they double as high-quality ADC channels) and for digital inputs that arrive from clean, well-driven sources like sensors that actively output 3.3 V or 0 V.

If you must use them with a button or switch, add a 10 kΩ external pull-up (to 3.3 V) or pull-down (to GND) on the breadboard.

Protecting ESP32 from 5 V Signals

Many sensors, modules, and older Arduino peripherals run on 5 V logic. Connecting them directly to ESP32 GPIO pins will exceed the 3.6 V maximum and can permanently damage the chip. Two safe solutions:

Method Parts Needed Best For
Voltage Divider 10 kΩ + 20 kΩ resistors Slow signals, sensors
Logic Level Converter TXS0108E / BSS138 Fast signals, I2C, SPI
Voltage Divider (5 V → 3.3 V)
5V_signal ──── 10kΩ ──── GPIO_pin (3.3V max)
                    |
                   20kΩ
                    |
                   GND

Vout = 5 × (20k / (10k + 20k)) = 3.33 V ✓

PIR Motion Sensor Project

Arduino (C++)
#define PIR_PIN 32
#define LED_PIN  2

void setup() {
  Serial.begin(115200);
  pinMode(PIR_PIN, INPUT);   // PIR actively drives HIGH or LOW
  pinMode(LED_PIN, OUTPUT);
  delay(2000);               // PIR initialisation warm-up
  Serial.println("PIR ready — monitoring motion...");
}

void loop() {
  if (digitalRead(PIR_PIN) == HIGH) {
    Serial.println("Motion detected!");
    digitalWrite(LED_PIN, HIGH);
    delay(1000);
  } else {
    digitalWrite(LED_PIN, LOW);
  }
  delay(100);
}

Summary

Digital inputs on ESP32 revolve around three key concepts: logic levels (3.3 V system, avoid voltages above 3.6 V), pull resistors (always define a resting state — never float), and timing (polling for slow events, interrupts for fast ones). Master these and any button, sensor, or switch becomes straightforward.

Frequently Asked Questions

ESP32 uses 3.3V logic. A pin reads HIGH when voltage is above roughly 2.64V, and LOW when below about 0.66V. The 0.66V–2.64V range is indeterminate — avoid driving inputs into this zone.
Call pinMode(pin, INPUT) in setup() for a bare input. Use INPUT_PULLUP to enable the built-in ~47kΩ pull-up (pin reads HIGH by default), or INPUT_PULLDOWN for the pull-down (pin reads LOW by default). Then call digitalRead(pin) to read the state.
A floating pin is not connected to any defined voltage. Environmental noise causes it to read randomly as HIGH or LOW, triggering false events. Always connect a pull-up or pull-down resistor, or use INPUT_PULLUP / INPUT_PULLDOWN mode.
Most can. GPIO34, 35, 36 (VP), and 39 (VN) are input-only — they cannot be configured as outputs and have no internal pull resistors. GPIO6–GPIO11 are connected to internal flash and should not be used.
Yes — every GPIO supports hardware interrupts. Call attachInterrupt(digitalPinToInterrupt(pin), myISR, RISING/FALLING/CHANGE) and mark the ISR with IRAM_ATTR. Keep ISR functions very short — no Serial.print or delay inside them.
No. ESP32 GPIO pins have a maximum voltage of 3.6V. Applying 5V will permanently damage the chip. Use a voltage divider (10kΩ + 20kΩ) or a dedicated logic-level converter when interfacing with 5V sensors or microcontrollers.
INPUT_PULLUP enables an internal ~47kΩ resistor between the pin and 3.3V, so the pin reads HIGH by default. INPUT_PULLDOWN connects an internal resistor to GND, so the pin reads LOW by default. Use PULLUP when wiring a button to GND, and PULLDOWN when wiring to 3.3V.
This is the classic floating-pin problem. Without a pull resistor, the GPIO picks up electromagnetic interference and toggles unpredictably. The fix is always to add a pull-up or pull-down — either external (10kΩ) or via INPUT_PULLUP / INPUT_PULLDOWN in code.
In Arduino loop mode, practical digitalRead() speed is roughly 1 MHz with no other processing. For faster signals, use hardware interrupts (triggered within ~1–5 µs) or the ESP32 PCNT (Pulse Counter) peripheral which can count MHz-range pulses autonomously.
No. GPIO34, GPIO35, GPIO36, and GPIO39 are input-only and have no internal pull resistors. If you use these pins for buttons or switches, you must add an external pull-up or pull-down resistor on the PCB or breadboard.

Projects to Build

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