If you have ever pressed a button on your Arduino project and watched the LED toggle two, three, or five times from a single press, you have experienced arduino button debounce — or rather, the lack of it. Button bounce is a fundamental hardware phenomenon that trips up beginners and experienced makers alike, but once you understand it and have the right fix in your toolbox, it never causes problems again. This guide covers everything: what bounce is, three software debounce approaches with full code, a hardware RC filter solution, and the go-to Bounce2 library for clean reusable code.
Table of Contents
- What Is Button Bounce and Why Does It Happen?
- What Happens Without Debounce
- Method 1: millis()-Based Software Debounce
- Method 2: State Machine Debounce
- Method 3: The Bounce2 Library
- Hardware Debounce: RC Filter + Schmitt Trigger
- Debouncing Multiple Buttons Efficiently
- Frequently Asked Questions
What Is Button Bounce and Why Does It Happen?
Mechanical push buttons contain a spring-loaded metal contact. When you press the button, the contacts slam together — but before settling, they physically bounce off each other 5 to 50 times in the span of 1–10 milliseconds. Each bounce opens and closes the circuit, generating a rapid burst of rising and falling edges.
Arduino’s digital pins sample at up to 16 MHz (one read per 62 ns on a 16 MHz Uno), making them exquisitely sensitive to these transient bounces. A single human button press can look like dozens of presses to the microcontroller. Tactile switches typically bounce for 1–5 ms; toggle switches can bounce for up to 50 ms.
Debouncing is the process of filtering out these spurious transitions and reporting only one clean state change per physical press or release.
What Happens Without Debounce
Here is a simple sketch that increments a counter on each button press — without debounce:
const int BTN_PIN = 2;
const int LED_PIN = 13;
int count = 0;
int lastState = HIGH;
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
}
void loop() {
int state = digitalRead(BTN_PIN);
if (state == LOW && lastState == HIGH) { // falling edge
count++;
Serial.println(count); // you will see 3–10 counts per press!
}
lastState = state;
}
Run this and open Serial Monitor. Press the button ten times slowly and deliberately — you will likely see 20–60 count increments. That is raw bounce in action.
Method 1: millis()-Based Software Debounce
The most common software debounce approach: record the time of the last state change and ignore any further state changes until a debounce delay has passed.
const int BTN_PIN = 2;
const int LED_PIN = 13;
const unsigned long DEBOUNCE_MS = 50; // 50 ms settle time
int buttonState = HIGH;
int lastReading = HIGH;
unsigned long lastChangeTime = 0;
bool ledState = false;
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
}
void loop() {
int reading = digitalRead(BTN_PIN);
if (reading != lastReading) {
lastChangeTime = millis(); // reset the timer on any change
}
if (millis() - lastChangeTime > DEBOUNCE_MS) {
// State has been stable for DEBOUNCE_MS ms
if (reading != buttonState) {
buttonState = reading;
if (buttonState == LOW) { // button pressed (active-low with INPUT_PULLUP)
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
Serial.println("Button pressed!");
}
}
}
lastReading = reading;
}
Key points:
- Use
INPUT_PULLUP— the pin reads HIGH when button is open, LOW when pressed. No external resistor needed. - 50 ms is conservative. For fast-clicking applications (game buttons), 10–20 ms is usually enough. For noisy industrial switches, go up to 100 ms.
- Never use
delay()for debounce in real projects — it blocks the CPU and you miss events on other inputs.
Method 2: State Machine Debounce
A state machine approach makes the logic explicit and easier to extend (e.g., to detect long presses, double clicks):
enum ButtonState { IDLE, PRESSED, HELD, RELEASED };
const int BTN_PIN = 2;
const unsigned long DEBOUNCE_MS = 30;
const unsigned long LONG_PRESS_MS = 700;
ButtonState bState = IDLE;
unsigned long pressStart = 0;
unsigned long lastDebounce = 0;
int lastRaw = HIGH;
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
Serial.begin(115200);
}
void loop() {
int raw = digitalRead(BTN_PIN);
unsigned long now = millis();
switch (bState) {
case IDLE:
if (raw == LOW) {
lastDebounce = now;
bState = PRESSED;
}
break;
case PRESSED:
if (now - lastDebounce > DEBOUNCE_MS) {
if (raw == LOW) {
pressStart = now;
bState = HELD;
Serial.println("Short press detected");
} else {
bState = IDLE; // was just noise
}
}
break;
case HELD:
if (raw == HIGH) {
lastDebounce = now;
bState = RELEASED;
} else if (now - pressStart > LONG_PRESS_MS) {
Serial.println("Long press!");
bState = IDLE; // handled, wait for release
}
break;
case RELEASED:
if (now - lastDebounce > DEBOUNCE_MS) {
bState = IDLE;
}
break;
}
}
This approach cleanly detects short presses, long presses, and filters bounce on both press and release edges.
Method 3: The Bounce2 Library
For production code and larger projects, the Bounce2 library by Thomas O Fredericks is the cleanest solution. Install via Library Manager (search “Bounce2”).
#include <Bounce2.h>
const int BTN_PIN = 2;
const int LED_PIN = 13;
Bounce2::Button button;
bool ledState = false;
void setup() {
button.attach(BTN_PIN, INPUT_PULLUP);
button.interval(25); // debounce interval in ms
button.setPressedState(LOW); // LOW = pressed (active-low)
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
}
void loop() {
button.update();
if (button.pressed()) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
Serial.println("Pressed");
}
if (button.released()) {
Serial.println("Released");
}
// Duration of current press
if (button.isPressed() && button.currentDuration() > 1000) {
Serial.println("Held for 1 second!");
}
}
Bounce2 gives you pressed(), released(), isPressed(), currentDuration(), and previousDuration() — everything you need for rich button interactions with clean debounced signals.
Hardware Debounce: RC Filter + Schmitt Trigger
Sometimes you want debounce guaranteed at the hardware level — especially in noisy industrial environments or when the microcontroller firmware is not yours to modify.
RC Low-Pass Filter
A 10 kΩ resistor in series with the button and a 100 nF (0.1 µF) capacitor to ground creates a low-pass filter with a time constant τ = RC = 1 ms. Bounce transitions (which are high-frequency) are smoothed out. The voltage rises and falls slowly, longer than the bounce duration.
Button ---[10kΩ]--- Pin ---[100nF]--- GND
|
Arduino Input
The RC filter alone may leave slow edges that glitch on digital thresholds. Adding a 74HC14 Schmitt-trigger inverter (threshold hysteresis ~1 V on a 5 V supply) after the RC filter gives a guaranteed clean edge. This combination is used in professional hardware designs.
Dedicated Debounce ICs
The MAX6816/17/18 series provides hardware debounce for 1, 2, or 8 buttons with 40 ms of debounce built in, needing just Vcc and GND plus the input pins. Useful when firmware code space is precious or the application is safety-critical.
Debouncing Multiple Buttons Efficiently
Handling 4+ buttons with individual timer variables becomes messy. Use an array approach with Bounce2:
#include <Bounce2.h>
const int NUM_BUTTONS = 4;
const int BTN_PINS[] = {2, 3, 4, 5};
Bounce2::Button buttons[NUM_BUTTONS];
void setup() {
for (int i = 0; i < NUM_BUTTONS; i++) {
buttons[i].attach(BTN_PINS[i], INPUT_PULLUP);
buttons[i].interval(25);
buttons[i].setPressedState(LOW);
}
Serial.begin(115200);
}
void loop() {
for (int i = 0; i < NUM_BUTTONS; i++) {
buttons[i].update();
if (buttons[i].pressed()) {
Serial.print("Button "); Serial.print(i); Serial.println(" pressed");
}
}
}
This scales cleanly to 8, 16, or more buttons. On memory-constrained boards like the Arduino Nano, keep Bounce2 instances in an array rather than creating per-button logic blocks.
Frequently Asked Questions
What debounce time should I use for Arduino buttons?
For standard 6 mm tactile SMD or through-hole push buttons, 20–50 ms covers virtually all bounce behaviour. If you are using panel-mount toggle switches or micro-switches, increase to 50–100 ms. For capacitive touch buttons there is no mechanical bounce, so 0–5 ms is enough for just noise filtering.
Why does INPUT_PULLUP read HIGH when the button is not pressed?
When you enable the internal pull-up resistor, the Arduino connects a ~20–50 kΩ resistor between the pin and 3.3 V/5 V internally. With no button pressed, no path to ground exists, so the pin reads HIGH. Pressing the button connects the pin to GND through the button (no external resistor needed), pulling it LOW. This is called active-low logic and is the standard for most button circuits.
Can I debounce in an interrupt service routine (ISR)?
Not directly — you should not call millis() or delay() inside an ISR. The standard approach is to set a flag in the ISR and debounce in the main loop, or to check micros() (which works in ISRs) against a threshold. However, for most applications the polling debounce in loop() is simpler and perfectly adequate unless you need ultra-low-latency response.
My button still double-triggers after adding 50 ms debounce. What now?
Check that you are comparing reading != buttonState correctly and updating lastReading every loop iteration — if you only update it inside the debounce branch, the timer never resets properly. Also verify your button wiring: if the button pin is floating (not connected to INPUT_PULLUP or an external resistor), even touching the wire can trigger false reads.
Does debounce code work the same on Arduino Mega and Nano?
Yes. All millis()-based and Bounce2 debounce code is fully portable across the AVR-based Arduino family (Uno, Nano, Mega, Leonardo, Pro Mini) and also works unchanged on ARM-based boards (Arduino Nano 33 IoT, Nano RP2040 Connect, MKR series) because it uses only standard Arduino API calls.
Button bounce is one of those issues that is invisible until it suddenly breaks a critical feature of your project. With the millis() method for simple projects, a state machine for complex multi-press detection, and Bounce2 for large codebases, you now have the right tool for every situation. Hardware debounce with an RC filter is worth knowing for noise-critical or firmware-free contexts. Pick the right approach and bouncy buttons will never frustrate you again.
Build your next Arduino project with confidence. Shop Arduino boards, shields, and components at Zbotic — fast delivery across India with GST invoicing.
Add comment