1. Introduction
This guide explains how to connect two mechanical push-button switches to an ESP32 development board and read their state using digital input pins. We will cover the wiring, the role of pull-up resistors, how to write firmware using the Arduino framework, and — most importantly — how to solve a very common beginner problem: a single button press being detected as multiple presses due to switch "bounce" and fast microcontroller loop execution.
INPUT_PULLUP
mode, reading pin state with digitalRead(), and eliminating false/repeated
triggers using delays, blocking while loops, and debounce timing.
2. Circuit Setup
Two push buttons are connected directly to the ESP32. Each switch has two terminals: one terminal connects to a GPIO pin, and the other terminal connects to GND. No external resistor is required because we will enable the ESP32's internal pull-up resistor in software.
| Switch | GPIO Pin | Other Terminal |
|---|---|---|
| Switch 1 | GPIO 12 | GND |
| Switch 2 | GPIO 27 | GND |
Schematic Diagram
3.3V (Internal Pull-Up, enabled in code)
|
[R] internal
|
GPIO12 ------+-------- SWITCH 1 --------GND
|
GPIO27 ------+-------- SWITCH 2 --------GND
Idle state : Pin reads HIGH (pulled up internally)
Pressed state: Pin reads LOW (switch connects pin to GND)
GPIO 12
GPIO 27
3. Understanding Pull-Up Resistors
A digital input pin left "floating" (not connected to anything definite) can randomly read HIGH or LOW due to electrical noise. A pull-up resistor solves this by weakly connecting the pin to a HIGH voltage (3.3V) when nothing else is driving it. When the switch is pressed, it overrides this weak pull-up and forces the pin to GND (LOW).
External vs. Internal Pull-Up
| Mode | Wiring Needed | Behavior |
|---|---|---|
INPUT |
External pull-up/pull-down resistor required | Pin floats if nothing is connected — unreliable without extra hardware |
INPUT_PULLUP |
None — resistor is built into the ESP32 chip | Pin reads HIGH by default, LOW when switch is pressed |
INPUT_PULLUP, the ESP32
enables its internal resistor. This means the pin is HIGH at rest, and only goes LOW when the button
is pressed and connects the pin to GND — eliminating the need for any external resistor.
4. Writing the Basic Sketch
The sketch below declares both switch pins, configures them as pull-up inputs, and continuously checks
their state inside loop().
Full Example Code — "Switch Test"
// ---------- Switch Test Sketch ----------
const int switch1 = 12; // GPIO connected to Switch 1
const int switch2 = 27; // GPIO connected to Switch 2
void setup() {
Serial.begin(115200); // Start serial communication
pinMode(switch1, INPUT_PULLUP); // Enable internal pull-up on Switch 1
pinMode(switch2, INPUT_PULLUP); // Enable internal pull-up on Switch 2
}
void loop() {
if (digitalRead(switch1) == LOW) {
Serial.println("Switch 1 pressed");
delay(500);
}
if (digitalRead(switch2) == LOW) {
Serial.println("Switch 2 pressed");
delay(500);
}
}
Step-by-Step Explanation
- Declare two constants holding the GPIO numbers used for the switches (12 and 27).
- Inside
setup(), initialize the Serial Monitor at 115200 baud for debugging output. - Configure both pins as
INPUT_PULLUPso the internal resistor keeps them HIGH at rest. - Inside
loop(), usedigitalRead()to check each pin's state. - When a pin reads
LOW, it means the corresponding switch is currently pressed. - Print a confirmation message to the Serial Monitor whenever a press is detected.
Commands Used in This Sketch
5. The Repeat-Trigger Problem Important
When you upload and test the sketch above, holding a button down for a longer time causes the message
to print repeatedly, once every delay(500) interval, for as long as the
button remains pressed. This happens because the loop() function keeps
re-checking the pin state continuously.
delay()
call entirely, a single button press can trigger the message many times in rapid succession —
sometimes 5, 8, or more repetitions — because the ESP32's loop executes extremely fast and the
button remains physically closed for many loop cycles.
Button held down over time:
HIGH ─┐ ┌── HIGH
│ LOW (pressed) │
└─────────────────────────────┘
↑ ↑ ↑ ↑
loop() loop() loop() loop() <- each iteration re-detects "pressed"
print print print print = multiple prints for ONE press
6. Solutions to the Repeat-Trigger Problem
6.1 Adding a Delay After Detection
A simple delay(500) placed right after detecting a press slows down how
often the loop can re-check the pin, reducing (but not eliminating) repeated prints while the
button is held.
6.2 Blocking with a while Loop
A more effective approach is to trap the program inside a while loop
for as long as the button remains pressed. The program does nothing until the button is released,
guaranteeing that the action executes exactly once per press.
void loop() {
if (digitalRead(switch1) == LOW) {
Serial.println("Switch 1 pressed");
while (digitalRead(switch1) == LOW); // wait here until released
}
if (digitalRead(switch2) == LOW) {
Serial.println("Switch 2 pressed");
while (digitalRead(switch2) == LOW); // wait here until released
}
}
while loop (note the semicolon
instead of a code block) does nothing but repeatedly re-check the pin. As long as the button stays
pressed (pin is LOW), the program is "stuck" here. The moment the button is released (pin returns
HIGH), the while condition becomes false and the program continues.
Commands Used in This Solution
7. Software Debouncing
Even with the blocking while loop, you may occasionally still see a
double-print when releasing the button. This is caused by contact bounce — a
mechanical switch does not make or break contact instantly. Instead, the metal contacts physically
vibrate for a few milliseconds before settling, producing several rapid HIGH/LOW transitions that
the ESP32 (running extremely fast) can misinterpret as multiple presses.
Real signal during a single press (zoomed in on the transition):
HIGH ─┐ ┌─┐ ┌───────────────── (bounce noise on contact)
│ │ │ │
└───┘ └─┘
Contact "bounces" for ~5-20 ms before settling to a clean LOW
A short debounce delay (commonly around 20 milliseconds) after detecting a state change is enough time for the mechanical contact to fully settle, preventing false re-triggers.
Final Debounced Sketch
const int switch1 = 12;
const int switch2 = 27;
const int debounceDelay = 20; // milliseconds
void setup() {
Serial.begin(115200);
pinMode(switch1, INPUT_PULLUP);
pinMode(switch2, INPUT_PULLUP);
}
void loop() {
if (digitalRead(switch1) == LOW) {
delay(debounceDelay); // wait for contact to settle
Serial.println("Switch 1 pressed");
while (digitalRead(switch1) == LOW);
delay(debounceDelay); // debounce the release too
}
if (digitalRead(switch2) == LOW) {
delay(debounceDelay);
Serial.println("Switch 2 pressed");
while (digitalRead(switch2) == LOW);
delay(debounceDelay);
}
}
Commands Used in This Sketch
8. Summary
| Concept | Purpose |
|---|---|
INPUT_PULLUP | Enables internal resistor so pin idles HIGH without external hardware |
digitalRead() | Reads current pin state (HIGH or LOW) |
delay() | Simple way to slow down repeated detection |
while (pin == LOW); | Blocks execution until button is released — ensures single trigger per press |
| Debounce delay (~20 ms) | Filters out mechanical contact "bounce" noise for clean, reliable readings |