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.
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
INPUTinstead ofINPUT_PULLUPwithout 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
LOWlasts 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.