If you are wiring a mechanical switch to a microcontroller, the default choice for arduino button debounce in 2026 is the Bounce2 library with a 10-millisecond polling interval on an Arduino Nano v3. Skip the hardware RC (resistor-capacitor) filters unless you are strictly bound to pin-change interrupts where software polling is impossible. Mechanical contacts do not close cleanly; they chatter. Without debounce, a single button press can register as a dozen rapid triggers, completely breaking your state machine or incrementing a counter out of control.
Bounce2 library. Set the interval to 10 (10ms) for standard 6x6mm tactile switches, and use the microcontroller's internal INPUT_PULLUP resistor to eliminate the need for external pull-down resistors.
The Physics of Contact Bounce: Why Your Code Sees Ghosts
When the metal contacts inside a tactile switch or toggle switch physically collide, they do not mate perfectly on the first impact. The microscopic irregularities on the contact surfaces cause them to bounce apart and slam back together multiple times before settling. According to Jack Ganssle's definitive Guide to Debouncing, a typical switch will bounce for anywhere from 1 to 5 milliseconds, though some heavily worn or low-cost switches can chatter for up to 20ms.
Because an Arduino Nano running at 16 MHz can execute millions of instructions per second, a 5ms bounce window is an eternity. The microcontroller reads the pin transitioning from HIGH to LOW dozens of times during that single physical press. If your code simply checks if (digitalRead(BUTTON_PIN) == LOW), your logic will execute repeatedly, leading to erratic behavior, skipped menu items, or double-firing relays.
Decision Tree: Hardware vs. Software Debounce
Choosing the right debounce method depends entirely on your project's architecture. Use this decision matrix to terminate on the correct approach for your specific build.
| Criteria | Software Delay (delay()) |
Software Library (Bounce2) |
Hardware RC Filter (10kΩ + 100nF) |
|---|---|---|---|
| Blocking? | Yes (halts CPU) | No (non-blocking) | No (analog filtering) |
| Interrupt Safe? | No | No (requires polling) | Yes (cleans signal pre-ISR) |
| Component Count | 0 extra | 0 extra | +2 per button |
| Code Complexity | Low (but bad practice) | Medium (state machine) | Low (hardware does the work) |
fell() and rose()) automatically. Only choose the Hardware RC filter if you are wiring the button directly to a hardware interrupt pin (like D2 or D3 on the Nano) and cannot afford the latency of a software polling loop.
Parts List & Pin Mapping (Arduino Nano v3)
This build targets the Arduino Nano v3 (ATmega328P). It is the standard breadboard-friendly variant. The code relies on the internal pull-up resistors, meaning we wire the button to ground, not VCC.
Bill of Materials
- Microcontroller: Arduino Nano v3 (ATmega328P, 16MHz)
- Switch: 6x6mm Through-hole Tactile Pushbutton (e.g., Omron B3F-1000)
- Indicator: 5mm LED with 220Ω current-limiting resistor
- Wiring: 22 AWG solid core hook-up wire
Pin Mapping Table
| Component | Nano Pin | Mode | Notes |
|---|---|---|---|
| Tactile Button (Leg 1) | D2 | INPUT_PULLUP |
Internal 20kΩ-50kΩ pull-up enabled |
| Tactile Button (Leg 2) | GND | Reference | Completes the circuit to ground |
| LED Anode (+) | D13 | OUTPUT |
Includes 220Ω resistor in series |
| LED Cathode (-) | GND | Reference | Common ground rail |
The Bounce2 Implementation: Complete Code
The following code is fully compilable. It uses the Bounce2 library to track state changes without blocking the main loop. It also includes a heartbeat LED indicator on pin D12 to prove the loop is running and not locked up, which is a critical debugging feature for embedded systems.
#include <Bounce2.h>
// --- PIN DEFINITIONS ---
const int BUTTON_PIN = 2; // Tactile switch wired to GND
const int LED_PIN = 13; // Main indicator LED
const int HEARTBEAT_PIN = 12; // Loop health indicator
// --- DEBOUNCE CONFIGURATION ---
// 10ms is the standard safe interval for most tactile switches.
// If using large mechanical toggle switches, increase to 20-50ms.
const unsigned long DEBOUNCE_INTERVAL = 10;
// Instantiate the Bounce object
Bounce debouncer = Bounce();
// State tracking
bool ledState = false;
unsigned long previousHeartbeat = 0;
const long heartbeatInterval = 500; // 500ms blink for heartbeat
void setup() {
// Initialize Serial for debugging
Serial.begin(115200);
while (!Serial && millis() < 2000) {
// Wait for serial port to connect (max 2 seconds)
}
Serial.println(F("System Boot: Debounce Demo Initialized"));
// Configure Pins
pinMode(LED_PIN, OUTPUT);
pinMode(HEARTBEAT_PIN, OUTPUT);
// INPUT_PULLUP activates the internal ~30k resistor.
// The pin reads HIGH when open, LOW when pressed.
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Attach the debouncer to the pin and set the interval
debouncer.attach(BUTTON_PIN);
debouncer.interval(DEBOUNCE_INTERVAL);
// Initial state sync
digitalWrite(LED_PIN, ledState);
}
void loop() {
// 1. Update the debouncer (MUST be called every loop iteration)
debouncer.update();
// 2. Check for a button press (transition from HIGH to LOW)
if (debouncer.fell()) {
ledState = !ledState; // Toggle state
digitalWrite(LED_PIN, ledState);
Serial.print(F("Button Pressed. Duration: "));
Serial.print(debouncer.previousDuration());
Serial.println(F("ms"));
}
// 3. Check for button release (optional, useful for hold-logic)
if (debouncer.rose()) {
Serial.println(F("Button Released."));
}
// 4. Heartbeat LED (Non-blocking error handling/health check)
unsigned long currentMillis = millis();
if (currentMillis - previousHeartbeat >= heartbeatInterval) {
previousHeartbeat = currentMillis;
digitalWrite(HEARTBEAT_PIN, !digitalRead(HEARTBEAT_PIN));
}
}
Troubleshooting: When Your Button Still Chatters
Even with the right library, physical wiring and configuration errors can mimic bounce. If your system is misbehaving, follow this diagnostic path.
The First Three Things to Check
- Verify the Common Ground: If your button is on a separate breadboard power rail, ensure the ground rail is physically jumpered to the Nano's GND pin. A floating ground will cause the internal pull-up to read random electromagnetic noise as button presses.
- Check the Pull-Up Configuration: If you wired the button to VCC (5V) instead of GND,
INPUT_PULLUPwill not work. You must either rewire to GND or change the code toINPUTand add an external 10kΩ pull-down resistor. - Audit the Interval Value: If you are using heavy-duty arcade buttons or industrial limit switches, 10ms is too short. Increase
debouncer.interval(50)to accommodate the heavier metal contacts.
Compiler Error: fatal error: Bounce2.h: No such file or directory
This is the most common roadblock for beginners. The Arduino IDE cannot find the library. Here are the ranked causes and fixes:
- Cause 1 (Most Likely): The library is not installed. Fix: Go to Sketch > Include Library > Manage Libraries, search for 'Bounce2' by Thomas Ouellet Fredericks, and click Install.
- Cause 2: Typo in the include statement. Fix: Ensure you typed
#include <Bounce2.h>(capital B, number 2). The older, deprecated library was justBounce.h, which lacks the.fell()and.rose()methods used in modern code. - Cause 3: Corrupted IDE cache. Fix: Close the IDE, navigate to your
Documents/Arduino/librariesfolder, delete theBounce2folder, restart the IDE, and reinstall via the Library Manager.
Extending and Simplifying the Build
Once you have a single button debounced reliably, you will inevitably need to scale the interface. Here is how to adapt the architecture without rewriting your core logic.
Scaling Up: Keypad Matrices
If you need more than 4 or 5 buttons, do not wire each to a dedicated GPIO pin; you will run out of pins on the Nano. Instead, use a resistor-diode matrix or a dedicated I2C GPIO expander like the MCP23017. The MCP23017 gives you 16 extra interrupt-capable pins over just two I2C wires. You can attach a Bounce2 instance to each of the 16 expander pins by reading the I2C register states into a local byte array, then feeding those bytes into the debouncer's software logic.
Simplifying: The Hardware RC Fallback
If you are building a simple circuit on a perfboard and want to eliminate the library dependency entirely to save flash memory (the Nano has 32KB, but every byte counts in ultra-compact ATTiny85 builds), you can use a hardware RC filter. Wire a 10kΩ resistor in series with the switch, and place a 100nF ceramic capacitor from the microcontroller pin to ground. This creates a low-pass filter with a time constant (τ = R × C) of 1ms, physically smoothing out the voltage spikes before they ever reach the silicon. However, remember that hardware filters introduce a slight analog ramp-up time, which can cause undefined logic states if the microcontroller samples the pin exactly during the capacitor's charge curve. For pure digital reliability, stick to the Bounce2 library.
By standardizing on the Bounce2 library and INPUT_PULLUP wiring, you eliminate 90% of the ghost-triggering issues that plague embedded projects. Keep your intervals tuned to your specific switch hardware, and always include a heartbeat LED to verify your main loop is executing cleanly.






