What is Button Bounce?
When you press a mechanical push button, the metal contacts do not make a clean, single connection. Instead, they bounce open and closed multiple times within the first few milliseconds due to their physical elasticity. A high-speed oscilloscope will show 5–50 transitions within the first 5–50 ms of a button press.
This is completely invisible to a human finger, but ESP32 running at 240 MHz can read each bounce as a separate event. Press a button once and your counter may jump by 3 or 7. This is button bounce — and debouncing is the solution.
Visualising Bounce on the Serial Monitor
#define BUTTON_PIN 16
int pressCount = 0;
bool lastState = HIGH;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println("Counting raw presses (no debounce):");
}
void loop() {
bool current = digitalRead(BUTTON_PIN);
if (lastState == HIGH && current == LOW) { // Falling edge
pressCount++;
Serial.printf("Press #%d detectedn", pressCount);
}
lastState = current;
// No delay — catches every bounce
}
Press the button once. You will likely see 2–6 “presses” registered. Run this first to confirm bounce is the problem, then apply a fix.
Method 1: Simple Delay Debounce
The naïve fix: after detecting a press, wait 50 ms and then continue. Simple but blocks the loop:
#define BUTTON_PIN 16
#define DEBOUNCE_MS 50
int count = 0;
bool lastState = HIGH;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
bool current = digitalRead(BUTTON_PIN);
if (lastState == HIGH && current == LOW) {
delay(DEBOUNCE_MS); // Wait for bounce to settle
if (digitalRead(BUTTON_PIN) == LOW) { // Still pressed after wait?
count++;
Serial.printf("Valid press #%dn", count);
}
}
lastState = current;
}
This works but the 50 ms blocking period means the MCU cannot do anything else — bad for Wi-Fi, sensor polling, or OLED updates happening in the same loop.
Method 2: Non-Blocking millis() Debounce (Recommended)
The millis() method records the time of the last state change and only accepts a new state after the debounce window has elapsed. The loop remains unblocked:
#define BUTTON_PIN 16
#define LED_PIN 2
#define DEBOUNCE_MS 50
bool buttonState = HIGH; // Current debounced state
bool lastRawState = HIGH; // Last raw reading
unsigned long lastChangeTime = 0;
int pressCount = 0;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
bool rawReading = digitalRead(BUTTON_PIN);
if (rawReading != lastRawState) { // State changed
lastChangeTime = millis(); // Reset timer
lastRawState = rawReading;
}
if ((millis() - lastChangeTime) > DEBOUNCE_MS) {
if (rawReading != buttonState) { // Stable new state
buttonState = rawReading;
if (buttonState == LOW) { // Debounced press event
pressCount++;
Serial.printf("Debounced press #%dn", pressCount);
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
}
}
// Loop free to do other work here — no blocking
}
Method 3: The Bounce2 Library
The Bounce2 library encapsulates debouncing logic cleanly. Install it from the Arduino Library Manager (Sketch → Include Library → Manage Libraries → search “Bounce2”).
#include <Bounce2.h>
#define BUTTON_PIN 16
#define LED_PIN 2
Bounce debouncer; // One instance per button
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
debouncer.attach(BUTTON_PIN, INPUT_PULLUP);
debouncer.interval(25); // 25 ms debounce window
}
void loop() {
debouncer.update(); // Must call every loop iteration
if (debouncer.fell()) { // Falling edge = button pressed
Serial.println("Button fell (pressed)");
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
if (debouncer.rose()) { // Rising edge = button released
Serial.println("Button rose (released)");
}
}
Multiple Buttons with Bounce2
#include <Bounce2.h>
const int BTNS[] = {16, 17, 18};
const int LEDS[] = { 2, 13, 14};
const int N = 3;
Bounce debouncers[N];
void setup() {
Serial.begin(115200);
for (int i = 0; i < N; i++) {
debouncers[i].attach(BTNS[i], INPUT_PULLUP);
debouncers[i].interval(25);
pinMode(LEDS[i], OUTPUT);
}
}
void loop() {
for (int i = 0; i < N; i++) {
debouncers[i].update();
if (debouncers[i].fell()) {
Serial.printf("Button %d pressedn", i + 1);
digitalWrite(LEDS[i], !digitalRead(LEDS[i]));
}
}
}
Method 4: Hardware RC Debounce Circuit
Hardware debouncing uses a resistor-capacitor (RC) filter to slow down the signal edge, smearing the bounces into a single clean transition:
3.3V ──── 10kΩ (pull-up) ──── GPIO pin
|
100nF cap
|
Button ────────────────────── GND
Time constant τ = R × C = 10,000 × 0.0000001 = 1 ms
Bounce spikes <1ms are smoothed. Only sustained state change passes through.
No software debounce needed — use plain INPUT mode in code.
| Method | Complexity | Blocks Loop? | Best For |
|---|---|---|---|
| delay() debounce | Simplest | Yes | Quick prototypes |
| millis() debounce | Medium | No | Most projects |
| Bounce2 library | Medium | No | Multiple buttons |
| Hardware RC | Hardware | No | Noisy environments |
Interrupt-Safe Debouncing
#define BUTTON_PIN 16
#define DEBOUNCE_US 50000 // 50 ms in microseconds
volatile unsigned long lastISRTime = 0;
volatile int pressCnt = 0;
void IRAM_ATTR onPress() {
unsigned long now = micros();
if (now - lastISRTime > DEBOUNCE_US) {
pressCnt++;
lastISRTime = now;
}
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
}
void loop() {
static int last = 0;
if (pressCnt != last) {
last = pressCnt;
Serial.printf("Interrupt count (debounced): %dn", pressCnt);
}
}
Choosing the Right Debounce Time
| Switch Type | Recommended Debounce |
|---|---|
| Cheap tactile button | 50 ms |
| Quality tactile switch | 20–25 ms |
| Membrane keypad | 5–10 ms |
| Reed switch | 50–100 ms |
| Limit switch / microswitch | 20–50 ms |
| Capacitive touch sensor | None needed |
Summary
Button bounce causes one physical press to register as multiple software events because mechanical contacts oscillate rapidly. For most projects, the non-blocking millis() method (50 ms window) or the Bounce2 library are the best solutions — clean, reliable, and loop-friendly. Hardware RC debounce is ideal when code simplicity matters or for input-only GPIO34–GPIO39 pins where you want zero software overhead.