When you press a mechanical switch, the metal contacts do not simply close and stay shut. They physically collide, rebound, and chatter for a period ranging from 1 to 15 milliseconds before settling. If your microcontroller reads this raw signal, a single button press can register as five or six distinct triggers. The direct fix for 95% of projects is to use the Bounce2 library with a 10ms software interval. For harsh, electrically noisy environments, use a hardware RC low-pass filter (10kΩ resistor and 100nF capacitor) paired with a Schmitt trigger.
This guide breaks down the physics of contact chatter, compares every viable debouncing method with hard data, and provides complete, compilable code targeting the Arduino Nano (ATmega328P).
The Physics of Switch Bounce (And Why delay() Fails)
To understand why debouncing is mandatory, you have to look at a switch closure on an oscilloscope. When the contacts meet, the mechanical momentum causes the moving contact to bounce off the stationary contact. This creates a rapid series of high-frequency open/close transitions—a noisy square wave—before the spring tension finally forces the contacts to remain closed.
The exact duration of this chatter depends on the switch mass and spring stiffness. A lightweight 6x6mm tactile dome switch typically bounces for 1ms to 5ms. A heavy-duty industrial limit switch or a reed switch can bounce for 10ms to 20ms. According to Jack Ganssle's definitive Guide to Debouncing, roughly 30% of switches exhibit bounce times exceeding 5ms, and some can chatter for over 150ms under worst-case conditions.
Many beginners attempt to solve this by inserting delay(50) immediately after reading a LOW state. This is a critical mistake in embedded systems. A 50ms blocking delay halts the microcontroller's main loop, meaning your Arduino cannot read sensors, update displays, or handle serial communication while it waits for the switch to settle. In a system controlling a motor or reading a fast-moving encoder, a blocking delay will cause catastrophic timing failures.
Debouncing Methods Compared
Before writing code or soldering components, choose the right debouncing architecture for your specific constraints. The table below ranks the most common methods by CPU overhead, latency, and reliability.
| Method | CPU Blocking? | Extra Hardware | Latency / Overhead | Best Use Case |
|---|---|---|---|---|
Raw digitalRead() |
No | None | 0ms / 0% | Rotary encoders (requires dedicated hardware decoder) |
delay() Blocking |
Yes | None | 50ms+ / 100% | Absolute beginner toys; single-task blinking LEDs |
millis() State Machine |
No | None | 5-20ms / <1% | Custom firmware where external libraries are forbidden |
| Bounce2 Library | No | None | 5-20ms / <1% | 95% of standard Arduino/ESP32 projects |
| Hardware RC Low-Pass Filter | No | 10kΩ + 100nF | ~2ms analog settle | Noisy industrial environments, long wire runs |
| Schmitt Trigger (e.g., 74HC14) | No | Hex Inverter IC | <100ns / 0% | High-speed precision data entry, MIDI controllers |
Parts List & Pin Mapping
The following build uses a software debouncing approach via the Bounce2 library, with provisions to test a hardware RC filter on the same breadboard. We are targeting the Arduino Nano (ATmega328P, 16MHz) due to its widespread use in permanent breadboard builds, but the code and pin logic are 100% compatible with the Arduino Uno R3 and Mega 2560.
Bill of Materials
- Microcontroller: Arduino Nano (ATmega328P) with pre-soldered headers.
- Switch: 6x6x5mm SPST momentary tactile switch (4-pin DIP package).
- Resistors: 1x 10kΩ (internal pull-up backup or external pull-up), 1x 220Ω (LED current limiting).
- Capacitor: 1x 100nF (0.1µF) ceramic disc capacitor (for hardware RC filter testing).
- Indicator: 5mm standard red LED.
Pin Mapping Table
| Component | Arduino Nano Pin | Pin Mode | Notes |
|---|---|---|---|
| Tactile Switch (Leg 1) | D2 | INPUT_PULLUP |
Active LOW. Internal 20kΩ pull-up enabled. |
| Tactile Switch (Leg 2) | GND | N/A | Connects to common ground. |
| Status LED (Anode) | D13 | OUTPUT |
Use 220Ω series resistor. |
| Status LED (Cathode) | GND | N/A | Connects to common ground. |
The Bounce2 Library Implementation
For robust software debouncing, the Bounce2 library is the industry standard for Arduino. It uses a non-blocking state machine under the hood, tracking the time since the last state change without halting the main loop.
Before compiling, install the library via the Arduino IDE: Go to Sketch > Include Library > Manage Libraries, search for "Bounce2" by Thomas Ouellet Fredericks, and install it.
/*
* Arduino Button Debouncing with Bounce2
* Target Board: Arduino Nano (ATmega328P)
* Author: ElectricalFlux
*
* This sketch toggles an LED on D13 with a cleanly debounced
* button press on D2. It includes serial debugging and edge-case handling.
*/
#include
// --- PIN DEFINITIONS ---
const int BUTTON_PIN = 2;
const int LED_PIN = 13;
// --- DEBOUNCE PARAMETERS ---
// 10ms is the sweet spot for standard 6x6mm tactile switches.
// Increase to 20-50ms for heavy mechanical limit switches.
const unsigned long DEBOUNCE_INTERVAL = 10;
// Instantiate the Bounce object
Bounce debouncer = Bounce();
// State tracking
bool ledState = false;
void setup() {
// Initialize Serial for debugging
Serial.begin(115200);
while (!Serial) {
; // Wait for serial port to connect (needed for native USB boards)
}
Serial.println("System Boot: Initializing GPIO...");
// Configure Button Pin
// INPUT_PULLUP activates the internal 20k-50k ohm resistor,
// preventing the pin from floating and causing ghost triggers.
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Configure LED Pin
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW); // Ensure LED starts OFF
// Attach the debouncer to the pin and set the interval
debouncer.attach(BUTTON_PIN);
debouncer.interval(DEBOUNCE_INTERVAL);
Serial.println("System Ready. Press button on D2.");
}
void loop() {
// Update the Bounce instance (MUST be called every loop iteration)
debouncer.update();
// Check for a falling edge (transition from HIGH to LOW)
// Because we use INPUT_PULLUP, a press pulls the pin LOW.
if (debouncer.fell()) {
ledState = !ledState; // Toggle state
digitalWrite(LED_PIN, ledState);
// Serial output for verification
Serial.print("Button Pressed. LED is now ");
Serial.println(ledState ? "ON" : "OFF");
Serial.print("Bounce duration measured: ");
Serial.print(debouncer.previousDuration());
Serial.println(" ms");
}
// Optional: Check for rising edge (button released)
if (debouncer.rose()) {
Serial.println("Button Released.");
}
// The rest of your non-blocking code goes here.
// The CPU is completely free to handle other tasks.
}
Debugging: First Three Things to Check When It Fails
Even with a solid library, hardware quirks and configuration errors can cause erratic behavior. If your button is misbehaving, run through this ranked diagnostic checklist.
1. Symptom: "Ghost Presses" When the Button is Idle
The Cause: Your input pin is floating. If you wired the switch between D2 and GND but forgot to enable the internal pull-up resistor, the pin acts as an antenna, picking up electromagnetic interference (EMI) from nearby wires or your own body.
The Fix: Ensure your pinMode() is explicitly set to INPUT_PULLUP. If you are using an external pull-up resistor, verify it is connected to 5V (or 3.3V on ESP32 boards) and that its value is between 4.7kΩ and 10kΩ. Refer to the Arduino Digital Pins documentation for specific internal pull-up resistance values per chip.
2. Symptom: Compilation Fails on Include
Exact Error String: fatal error: Bounce2.h: No such file or directory
The Cause: The library is either not installed, or you are using the legacy "Bounce" library instead of the modern "Bounce2" fork. The legacy library has different class instantiation syntax.
The Fix: Open the Library Manager, uninstall any library simply named "Bounce", and install "Bounce2". Ensure your include statement is exactly #include <Bounce2.h> (capital B, number 2).
3. Symptom: Multiple Triggers Still Registering Per Press
The Cause: The debounce interval is set too short for your specific switch hardware, or you are using debouncer.read() instead of edge-detection methods like debouncer.fell().
The Fix: First, verify you are using fell() or rose() to detect the transition, not the continuous state. Second, increase the DEBOUNCE_INTERVAL from 10ms to 20ms or 30ms. You can read the debouncer.previousDuration() output in the Serial Monitor to see exactly how long the physical bounce lasted on your specific switch, then set your interval 5ms higher than that measured value.
Extending and Simplifying the Build
Depending on your final application, you may need to scale this architecture up for a macro pad, or strip it down for a bare-metal ATtiny85 build.
How to Simplify: The Hardware RC Filter
If you are programming a chip with severe memory constraints (like an ATtiny85) and cannot afford the ~2KB flash overhead of the Bounce2 library, move the debouncing to hardware.
Wire a 10kΩ resistor in series with the switch output, and place a 100nF ceramic capacitor from the microcontroller's input pin to GND. This creates a low-pass RC filter. The time constant (τ = R × C) is 1 millisecond. The capacitor absorbs the high-frequency bounce spikes, smoothing the voltage curve. The microcontroller can then use a simple, raw digitalRead() without any software delays. Note: For best results, feed this smoothed signal into a Schmitt trigger inverter (like the 74HC14) to square off the rounded RC curve before it hits the GPIO pin.
How to Extend: Pin Change Interrupts (PCINT)
If your Arduino spends most of its time in deep sleep to save battery, polling the button in the loop() won't work. You must extend the build using Pin Change Interrupts.
Instead of checking the pin state continuously, configure the GPIO pin to trigger a hardware interrupt on a state change. The interrupt service routine (ISR) wakes the CPU, records the millis() timestamp of the event, and goes back to sleep. The main loop then handles the actual debouncing math once awake. This is the standard architecture for battery-powered IoT remotes and wireless doorbells.
Keypad.h, which handles matrix debouncing natively, or implement a single hardware RC filter on the common column lines before they enter the microcontroller.






