A portable, well-documented C library implementing three debounce algorithms for mechanical switch inputs in embedded systems. Includes Arduino/ESP32 examples and a Python simulation that generates comparison waveforms.
When a mechanical switch closes, its contacts physically bounce against each other for 1–10 ms. A microcontroller running at MHz speeds sees this as dozens of rapid HIGH/LOW transitions — i.e., many false button presses from a single physical click.
Raw signal seen by MCU:
____ _ _ __________________________
____| || | ||
(idle) (bounce!) (settled)
This library cleans that up entirely in software.
debounce-lib/
├── src/
│ ├── debounce.h ← Header: all types, constants, function prototypes
│ └── debounce.c ← Implementation of all three algorithms
├── examples/
│ ├── arduino/
│ │ └── button_debounce.ino ← Arduino Uno/Nano example
│ └── esp32/
│ └── button_debounce_esp32.ino ← ESP32 + FreeRTOS example
├── simulation/
│ └── simulate_debounce.py ← Python waveform generator & visualiser
└── README.md
File: src/debounce.c → timer_debounce_*
When the first edge is detected, a lockout window starts (DEBOUNCE_TIMER_MS, default 20 ms). Any transitions during the window are ignored. After the window, if the signal is still at the new level, it is confirmed as a valid transition.
- ✅ Simple and low overhead
- ✅ Deterministic latency
⚠️ Misses a press if it's shorter than the window
TimerDebounce btn;
timer_debounce_init(&btn, PIN_HIGH, millis());
// In your loop (call every 1ms):
timer_debounce_update(&btn, read_pin(), millis());
if (timer_debounce_event(&btn)) {
// confirmed transition!
}File: src/debounce.c → integrator_debounce_*
A saturating counter is incremented on every HIGH sample and decremented on every LOW sample. The output only changes state when the counter hits its maximum (all HIGH) or zero (all LOW). Requires DEBOUNCE_INTEGRATOR_MAX consecutive same-level samples to confirm a transition.
- ✅ No time dependency — works on any polling rate
- ✅ Naturally filters short spikes
- ✅ Used in Jack Ganssle's famous debounce guide
IntegratorDebounce btn;
integrator_debounce_init(&btn, PIN_HIGH);
// In your loop (call at fixed interval):
integrator_debounce_update(&btn, read_pin());
if (integrator_debounce_event(&btn)) {
// confirmed transition!
}File: src/debounce.c → sm_debounce_*
Tracks four states: IDLE → PRESSED → HELD → RELEASED. Combines timer-based filtering with long-press detection. Emits distinct events (BTN_EVENT_PRESS, BTN_EVENT_HOLD, BTN_EVENT_RELEASE).
- ✅ Detects short press AND long press
- ✅ Production-quality pattern (used in real firmware)
- ✅ Easiest to integrate with UI/UX logic
StateMachineDebounce btn;
sm_debounce_init(&btn, millis());
// In your loop (call every 1ms):
sm_debounce_update(&btn, read_pin(), millis());
switch (sm_debounce_get_event(&btn)) {
case BTN_EVENT_PRESS: /* single tap */ break;
case BTN_EVENT_HOLD: /* long press */ break;
case BTN_EVENT_RELEASE: /* finger lifted */ break;
}Ran the simulation to visualise all three algorithms on a synthetic bouncing signal:
pip install matplotlib numpy
cd simulation
python simulate_debounce.pyThis generates debounce_comparison.png — a 4-panel plot showing:
- Raw bouncing signal
- Timer-based output
- Integrator output
- State-machine output with annotated PRESS/HOLD/RELEASE events
Example output:
Raw: 5 apparent presses | Timer: 1 | Integrator: 1 | State machine: 1
- Copy
src/debounce.handsrc/debounce.cinto your sketch folder. - Open
examples/arduino/button_debounce.ino. - Wire a tactile switch between Pin 2 and GND.
- Upload and open Serial Monitor at 115200 baud.
- Press the button — you should see exactly one
PRESSper physical press.
- Copy the
src/files alongside the sketch. - Open
examples/esp32/button_debounce_esp32.inoin Arduino IDE (with ESP32 board package installed). - Wire a switch between GPIO 15 and GND.
- Upload and open Serial Monitor at 115200 baud.
The ESP32 example uses a FreeRTOS task for the button polling and a queue to deliver events to the main loop — a production-grade pattern.
All tuning constants are in src/debounce.h:
| Constant | Default | Description |
|---|---|---|
DEBOUNCE_TIMER_MS |
20 | Lockout window for timer algorithm (ms) |
DEBOUNCE_INTEGRATOR_MAX |
10 | Samples needed for integrator to confirm |
DEBOUNCE_HELD_MS |
500 | Long-press threshold for state machine (ms) |
The library has no platform dependencies. The only requirement is a uint32_t millisecond counter. Replace millis() in the examples with your platform's equivalent:
| Platform | Time function |
|---|---|
| Arduino / ESP32 | millis() |
| STM32 HAL | HAL_GetTick() |
| FreeRTOS | xTaskGetTickCount() * portTICK_PERIOD_MS |
| Linux (test) | clock_gettime(CLOCK_MONOTONIC) |
| Tool | Purpose |
|---|---|
| Wokwi | Online Arduino/ESP32 simulator — test without hardware |
| Arduino IDE | Upload to physical boards |
| PlatformIO | VS Code extension for professional embedded workflow |
| Python + Matplotlib | Waveform simulation and visualisation |
| TinkerCAD Circuits | Circuit schematic and breadboard layout |
- Jack Ganssle, A Guide to Debouncing — the definitive resource
- Elliot Williams, Debounce your Switches — Hackaday two-part series
- The Art of Electronics, Horowitz & Hill — Chapter 9
MIT — free to use in personal and commercial projects.
