← All projects
Basics Beginner

A Button on ESP32: INPUT_PULLUP, Debouncing and Toggling an LED

A button with no resistors thanks to the internal pull-up, why contacts bounce and how software tells one press from twenty. One press turns the LED on, the next one turns it off.

Difficulty
Beginner
Time
30 min
Category
Basics
Updated

About the project

A button is the simplest input, but it comes with two sneaky effects: a floating input and contact bounce. We build a toggle: one press turns the LED on, the next one turns it off.

What you need

  • ESP32 DevKit
  • 6×6 mm tactile button
  • LED and 220 Ω resistor
  • Breadboard and jumper wires

Wiring

Part Connection
Button one contact → GPIO4, the other → GND
LED GPIO23 → 220 Ω resistor → anode; cathode → GND

The button needs no external resistor: we use the built-in pull-up.

Why the input floats

With the button released, GPIO4 is connected to nothing. Such an input picks up noise from your hand or nearby wires and reads 0 one moment and 1 the next. The fix is a pull-up resistor to 3.3 V: at rest it holds the input HIGH, and a pressed button shorts it to ground, giving LOW.

The ESP32 already has such a resistor (~45 kΩ) built in; pinMode(pin, INPUT_PULLUP) turns it on. Hence the "inverted" logic: pressed = LOW.

GPIO34–39 have no internal pull-ups and work as inputs only. A button on those pins needs an external 10 kΩ resistor.

Contact bounce

Mechanical contacts do not close cleanly: for 1–10 ms they bounce several times. Without protection one press becomes several toggles, and the LED switches on and straight back off.

Debouncing ignores changes that come less than 30 ms after the previous one. The time comes from millis(), not delay(), so loop() never blocks.

Code

// A button toggles an LED: INPUT_PULLUP + debouncing
const int BUTTON_PIN = 4;
const int LED_PIN = 23;

bool ledOn = false;
int lastState = HIGH;
unsigned long lastChange = 0;

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

void loop() {
  int state = digitalRead(BUTTON_PIN);
  if (state != lastState && millis() - lastChange > 30) {   // debounce
    lastChange = millis();
    lastState = state;
    if (state == LOW) {   // react only to the moment of pressing
      ledOn = !ledOn;
      digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
      Serial.printf("LED %s\n", ledOn ? "ON" : "OFF");
    }
  }
}

Common mistakes

  • INPUT instead of INPUT_PULLUP without an external resistor: the input floats, and the simulator warns about it.
  • A button between 3.3 V and GND with no resistor: pressing it shorts the supply.
  • A button on GPIO0, GPIO2 or GPIO12 held down at power-up changes the ESP32 boot mode.

What next

  • Detect a long press: if LOW lasts more than 1 s, do something else.
  • Once you have several buttons, move the debouncing into a class, as in the "C++ classes: Led and Button" article.