The Hidden Cost of Naive digitalRead()

When developers first write Arduino code push button logic, the instinct is to rely on a simple if (digitalRead(pin) == LOW) statement. While this compiles and may appear functional on a workbench, it is a catastrophic failure point in production environments. According to the Arduino digitalRead Reference, reading a digital pin takes roughly 3 to 5 microseconds. However, the physical reality of mechanical switches introduces transient voltage spikes that naive code interprets as dozens of rapid, phantom button presses.

To combat this, beginners often insert a delay(50) after a button press. In embedded systems, blocking the main loop for 50 milliseconds is a cardinal sin. It starves I2C communication buffers, causes SPI watchdog timeouts, and ruins the timing of software-based PWM or PID control loops. In 2026, with multicore microcontrollers like the ESP32-S3 and RP2040 dominating the DIY and industrial prototyping space, non-blocking, library-driven state machines are mandatory.

The Physics of Contact Chatter

Switch bounce is not a software bug; it is a mechanical reality. When the copper alloy contacts of a standard Omron B3F-4055 tactile switch close, the physical momentum causes the contacts to collide, separate, and collide again. Oscilloscope readings of a fresh Cherry MX mechanical switch reveal 10 to 50 transient voltage spikes spanning 2 to 15 milliseconds before the signal settles. If your microcontroller polls at 1 MHz, it will register every single micro-bounce as a distinct user input.

Library Comparison Matrix: Bounce2 vs OneButton vs EasyButton

Rather than writing custom millis() tracking logic for every pin, the embedded community relies on specialized libraries. Below is a deep-dive comparison of the three dominant libraries for handling push button inputs, profiled on an 8-bit ATmega328P (Arduino Uno/Nano) architecture.

FeatureBounce2OneButtonEasyButton
Primary Use CaseSimple state tracking & debounceComplex gestures (double/long press)Object-oriented hardware abstraction
SRAM Overhead (per instance)~8 bytes~24 bytes~18 bytes
Flash Overhead~850 bytes~1,600 bytes~1,200 bytes
Interrupt (ISR) SupportNo (Polling only)No (Polling only)Yes (Hardware Interrupts)
Multi-Press DetectionNoYes (Highly configurable)Yes (Sequence based)
Active LOW/HIGH ConfigYesYesYes

Bounce2: The Industry Standard for State Tracking

Maintained by Thomas Ouellet Fredericks, Bounce2 remains the gold standard for simple, reliable state tracking. It strips away the complexity of gesture recognition and focuses entirely on signal stabilization and edge detection.

Implementing Edge Detection

The true power of Bounce2 lies in its fell() and rose() methods. Instead of checking if a button is pressed, you check if the button just transitioned to a pressed state. This edge-detection paradigm ensures your trigger logic fires exactly once per physical actuation.

  • Initialization: Attach the pin and set the debounce interval. For standard tactile switches, a 10ms interval is optimal. For larger, heavier mechanical relays or industrial limit switches, increase this to 25ms-50ms.
  • The Loop: You must call button.update() at the very top of your loop() function. This function returns a boolean indicating if the state changed, allowing you to bypass unnecessary conditional checks.
  • Duration Tracking: Bounce2 includes a currentDuration() method, allowing you to measure exactly how long the user has held the button down without writing custom timestamp variables.

OneButton: Unlocking Complex Gestures

If your UI requires double-clicks, long-presses, or hold-and-release sequences, OneButton by Matthias Hertel is the definitive choice. It implements a robust finite state machine (FSM) that tracks the temporal relationship between multiple presses.

Memory vs. Functionality Trade-off

OneButton consumes roughly three times the SRAM of Bounce2 per instance. On an ATmega328P with only 2KB of SRAM, instantiating 20 OneButton objects will consume nearly 500 bytes, potentially causing stack collisions if you are also running large display buffers. However, on modern 32-bit architectures like the STM32 or ESP32, this overhead is negligible.

Configuring Timings for Human Ergonomics

Out of the box, OneButton uses default timings that may feel sluggish. For a snappy, consumer-electronics feel, adjust the library parameters in your setup() block:

  • Click Ticks: Set to 300ms. This is the maximum time between two presses to register as a double-click. Anything over 400ms feels unresponsive to human users.
  • Press Ticks: Set to 800ms. This is the threshold for a long-press. Users typically expect a long-press to trigger between 0.5 and 1.0 seconds.

Hardware Reality: EMI, Pull-Ups, and Capacitance

No software library can fix a fundamentally flawed hardware design. A common failure mode in DIY projects is relying on the microcontroller's internal pull-up resistors. On the ATmega328P, the internal INPUT_PULLUP resistor is not a precise 10kΩ component; it is a silicon trace that varies between 20kΩ and 50kΩ depending on the manufacturing batch and ambient temperature.

The Antenna Effect

In environments with high Electromagnetic Interference (EMI)—such as enclosures housing AC relays, stepper motor drivers, or switching power supplies—a 40kΩ weak pull-up acts as an antenna. Capacitive coupling from nearby AC wires can induce enough voltage fluctuation to pull the pin below the logic LOW threshold (1.5V on 5V logic), triggering phantom button presses that bypass software debouncing entirely.

Expert Wiring Rule: For any push button wired over 6 inches from the microcontroller, or located in an electrically noisy chassis, disable the internal pull-up. Instead, solder a physical 4.7kΩ carbon film resistor to the 5V rail, and place a 100nF (0.1µF) MLCC ceramic capacitor directly across the switch terminals. This creates a hardware low-pass RC filter that absorbs EMI spikes before the software library even evaluates the pin state.

2026 Context: RTOS and Multicore Microcontrollers

As the DIY and prosumer space shifts heavily toward FreeRTOS environments on dual-core ESP32-S3 and RP2040 boards, polling buttons in the main loop is considered an anti-pattern. Blocking a core with a 50ms debounce delay can starve lower-priority RTOS tasks, such as WiFi stack maintenance or BLE beacon broadcasting.

For RTOS implementations, the optimal approach is to utilize EasyButton with hardware interrupts enabled. By attaching the button to an interrupt-capable GPIO pin, the microcontroller can sleep or execute high-priority tasks, waking only on the falling edge of the button press. The ISR (Interrupt Service Routine) then pushes a simple event flag to a FreeRTOS queue, which a dedicated, low-priority UI task reads and processes using Bounce2 or OneButton logic. This guarantees zero latency for critical motor-control or network tasks while maintaining flawless button debouncing.

Summary Decision Framework

  1. Use Native Code: Only for non-critical, one-off prototyping where blocking delays are acceptable.
  2. Use Bounce2: For 90% of applications requiring simple ON/OFF toggling, limit switches, or basic edge detection with minimal memory footprint.
  3. Use OneButton: When building consumer-facing UIs requiring double-taps, long-holds, and multi-press sequences on a single pin.
  4. Use EasyButton + ISRs: For RTOS-based, battery-powered, or deep-sleep applications where every CPU cycle and milliamp matters.