Table of Contents
KEY TAKEAWAYS
- Mechanical switches bounce for 1–50ms when pressed or released, generating false triggers if not debounced.
- Software debouncing is preferred over hardware RC filters because it’s free, tunable, and doesn’t add board complexity.
- Timer-based debouncing (polling at regular intervals) is the most reliable method for production firmware.
- The vertical counter method debounces multiple buttons simultaneously with just a few bitwise operations.
- Always detect edges (press/release transitions), not just levels — this enables short press, long press, and double-click detection.
Why Buttons Need Debouncing
When you press a mechanical push-button, the metal contacts don’t make a clean, instant connection. Instead, they bounce — rapidly making and breaking contact for anywhere from 1 to 50 milliseconds before settling. To a microcontroller sampling GPIO at megahertz speeds, a single button press looks like dozens of rapid presses.
Without debouncing, a “press button to increment counter” feature might jump from 0 to 7 on a single press. An interrupt-driven button handler might fire 20 times. A state machine might oscillate wildly between states. Debouncing is not optional — it’s required for every mechanical switch in every embedded system.
/* What bouncing looks like to the MCU
*
* Physical press occurs at time T:
*
* GPIO pin: 1 1 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0
* Time: ─────── T ──── T+5ms ── T+10ms ──→
* ↑ bouncing ↑ ↑ settled ↑
*
* A naive "if (pin == LOW)" check during the bounce window
* will see multiple transitions:
* HIGH→LOW (press detected!)
* LOW→HIGH (release detected?!)
* HIGH→LOW (press again?!)
* ... and so on for 5-20ms
*/Method 1: Simple Delay Debounce (Beginner)
The simplest debounce method: when you detect a button press, wait for the bouncing to stop, then read the final state. This works but has a major flaw — it blocks the CPU during the delay.
/* Simple delay-based debouncing
* Works but BLOCKS the CPU — don't use in production
*/
#include
#define GPIOA_IDR (*(volatile uint32_t *)0x40010808)
#define BUTTON_PIN (1 << 0) /* PA0 */
#define DEBOUNCE_DELAY_MS 20
static void delay_ms(uint32_t ms) {
/* Simple busy-wait delay — calibrate for your clock */
while (ms--) {
volatile uint32_t i = 8000; /* ~1ms at 72MHz */
while (i--);
}
}
uint8_t read_button_blocking(void) {
/* Check if button is pressed (active low with pull-up) */
if (!(GPIOA_IDR & BUTTON_PIN)) {
delay_ms(DEBOUNCE_DELAY_MS); /* Wait for bounce to settle */
/* Re-check: is it still pressed? */
if (!(GPIOA_IDR & BUTTON_PIN)) {
return 1; /* Confirmed press */
}
}
return 0;
}
/* Usage — but this misses events during the delay! */
void bad_button_loop(void) {
while (1) {
if (read_button_blocking()) {
/* Button was pressed */
/* Problem: we blocked for 20ms, missing other tasks */
}
}
}Problems with this approach: The CPU is completely blocked during the delay. If you have multiple buttons, LEDs to blink, sensors to read, or communication to handle, they all stall. This method is only acceptable for the simplest possible programs.
Method 2: Timer-Based Polling Debounce (Recommended)
The production-grade approach: sample the button at regular intervals (typically every 5–10ms using a timer interrupt) and only accept a state change after N consecutive identical readings. This method is non-blocking, reliable, and handles multiple buttons efficiently.
/* Timer-based debouncing — the recommended approach
*
* How it works:
* 1. A timer interrupt fires every 5ms
* 2. Each tick, we read the raw button state
* 3. If the reading matches the current debounced state, reset counter
* 4. If different, increment counter
* 5. After N consecutive different readings (e.g., 3), accept the new state
*
* Total debounce time = sample_interval × required_count = 5ms × 3 = 15ms
*/
#include
#define MAX_BUTTONS 4
typedef struct {
volatile uint32_t *port; /* GPIO input data register */
uint32_t pin_mask; /* Pin bit mask */
uint8_t active_low; /* 1 if button pulls pin LOW when pressed */
uint8_t debounce_count; /* Current consecutive-match counter */
uint8_t debounced_state; /* Stable debounced state (1=pressed) */
uint8_t previous_state; /* State at last edge detection */
uint8_t pressed; /* Rising edge flag (press event) */
uint8_t released; /* Falling edge flag (release event) */
} button_t;
#define DEBOUNCE_THRESHOLD 3 /* Consecutive reads to accept change */
static button_t buttons[MAX_BUTTONS];
static uint8_t num_buttons = 0;
/* Register a button */
void button_init(volatile uint32_t *port, uint32_t pin_mask, uint8_t active_low) {
if (num_buttons >= MAX_BUTTONS) return;
button_t *btn = &buttons[num_buttons++];
btn->port = port;
btn->pin_mask = pin_mask;
btn->active_low = active_low;
btn->debounce_count = 0;
btn->debounced_state = 0;
btn->previous_state = 0;
btn->pressed = 0;
btn->released = 0;
}
/* Call this from a 5ms timer interrupt */
void button_debounce_tick(void) {
for (uint8_t i = 0; i port) & btn->pin_mask) ? 1 : 0;
if (btn->active_low) raw = !raw; /* Invert for active-low */
if (raw == btn->debounced_state) {
/* Same as current stable state — reset counter */
btn->debounce_count = 0;
} else {
/* Different from stable state — count up */
btn->debounce_count++;
if (btn->debounce_count >= DEBOUNCE_THRESHOLD) {
/* State has been stable long enough — accept it */
btn->debounced_state = raw;
btn->debounce_count = 0;
/* Detect edges */
if (raw && !btn->previous_state) {
btn->pressed = 1; /* Rising edge: just pressed */
}
if (!raw && btn->previous_state) {
btn->released = 1; /* Falling edge: just released */
}
btn->previous_state = raw;
}
}
}
}
/* Application API — call from main loop */
uint8_t button_is_pressed(uint8_t index) {
if (index >= num_buttons) return 0;
return buttons[index].debounced_state;
}
uint8_t button_was_pressed(uint8_t index) {
if (index >= num_buttons) return 0;
uint8_t flag = buttons[index].pressed;
buttons[index].pressed = 0; /* Clear flag after reading */
return flag;
}
uint8_t button_was_released(uint8_t index) {
if (index >= num_buttons) return 0;
uint8_t flag = buttons[index].released;
buttons[index].released = 0;
return flag;
}
/* Timer interrupt handler (runs every 5ms) */
void TIM2_IRQHandler(void) {
/* Clear timer interrupt flag */
/* TIM2->SR &= ~TIM_SR_UIF; */
button_debounce_tick();
}
/* Example usage */
/*
int main(void) {
#define GPIOA_IDR_ADDR ((volatile uint32_t *)0x40010808)
#define GPIOB_IDR_ADDR ((volatile uint32_t *)0x40010C08)
// Register buttons
button_init(GPIOA_IDR_ADDR, (1 << 0), 1); // BTN0: PA0, active low
button_init(GPIOA_IDR_ADDR, (1 << 1), 1); // BTN1: PA1, active low
button_init(GPIOB_IDR_ADDR, (1 << 5), 1); // BTN2: PB5, active low
// Configure TIM2 for 5ms interrupt (not shown)
while (1) {
if (button_was_pressed(0)) {
// Button 0 was just pressed — handle it
led_toggle();
}
if (button_was_pressed(1)) {
// Button 1 was just pressed
mode_next();
}
// Do other work — we're never blocked!
sensor_read();
display_update();
}
}
*/Method 3: Vertical Counter Debounce (Advanced)
The vertical counter method is an elegant bitwise technique that debounces up to 8 (or 16 or 32) buttons simultaneously using just a few integer variables and bitwise operations. It’s extremely efficient — perfect for systems with many buttons or tight timing constraints.
/* Vertical Counter Debounce — debounce 8 buttons simultaneously
*
* Instead of tracking each button with its own counter,
* we use "vertical" bit counters where each bit position
* in the counter variables corresponds to one button.
*
* For a 2-bit counter (counts 0-3), we need 2 counter bytes.
* Each button's 2-bit counter spans the same bit position
* across both bytes.
*
* Button: 7 6 5 4 3 2 1 0
* count0: b b b b b b b b (bit 0 of each counter)
* count1: b b b b b b b b (bit 1 of each counter)
*
* count value for button N = bit N of count1 : bit N of count0
*/
static uint8_t vc_count0 = 0;
static uint8_t vc_count1 = 0;
static uint8_t vc_state = 0; /* Debounced state of all 8 buttons */
uint8_t debounce_vertical(uint8_t raw_buttons) {
uint8_t delta; /* Bits that differ from debounced state */
uint8_t changes; /* Bits that just changed state */
delta = raw_buttons ^ vc_state; /* Which buttons differ? */
/* Increment 2-bit vertical counters for differing buttons,
* reset counters for matching buttons */
vc_count1 = (vc_count1 ^ vc_count0) & delta;
vc_count0 = ~vc_count0 & delta;
/* When counter reaches 3 (both bits set) AND delta is still set,
* the button has been stable long enough — accept the change */
changes = delta & vc_count0 & vc_count1;
/* Update debounced state */
vc_state ^= changes;
/* Reset counters for buttons that just changed */
vc_count0 &= ~changes;
vc_count1 &= ~changes;
return changes; /* Returns which buttons just changed */
}
/* Usage in timer ISR */
#define GPIOA_IDR (*(volatile uint32_t *)0x40010808)
static uint8_t btn_pressed_flags = 0;
static uint8_t btn_released_flags = 0;
void TIM2_IRQHandler_vc(void) {
/* Read all 8 button pins at once (active low → invert) */
uint8_t raw = ~((uint8_t)(GPIOA_IDR & 0xFF));
uint8_t changes = debounce_vertical(raw);
/* Separate press and release events */
btn_pressed_flags |= (changes & vc_state); /* Changed AND now pressed */
btn_released_flags |= (changes & ~vc_state); /* Changed AND now released */
}
/* Application code reads the flags */
uint8_t get_button_presses(void) {
uint8_t flags = btn_pressed_flags;
btn_pressed_flags = 0;
return flags;
}
/* Example: check specific buttons */
/*
void process_buttons(void) {
uint8_t presses = get_button_presses();
if (presses & (1 << 0)) handle_button0();
if (presses & (1 << 1)) handle_button1();
if (presses & (1 << 2)) handle_button2();
// ... all debounced with just 3 bytes of state!
}
*/Method 4: Shift Register Debounce
Another popular technique uses a shift register (integer) to store the last N readings of a button. A stable pressed state fills the register with all 1s; a stable released state fills it with all 0s. Any mix means the button is still bouncing.
/* Shift register debounce
*
* Store the last 8 readings in a uint8_t.
* All 1s (0xFF) = stable pressed
* All 0s (0x00) = stable released
* Anything else = still bouncing
*/
typedef struct {
uint8_t history; /* Shift register of last 8 samples */
uint8_t debounced; /* Current stable state */
uint8_t edge_press; /* Press event flag */
uint8_t edge_release; /* Release event flag */
} sr_button_t;
void sr_debounce_update(sr_button_t *btn, uint8_t raw_pressed) {
/* Shift history left, add new reading */
btn->history = (btn->history <debounced;
/* Check for stable state */
if (btn->history == 0xFF) {
btn->debounced = 1; /* Stable pressed */
} else if (btn->history == 0x00) {
btn->debounced = 0; /* Stable released */
}
/* else: bouncing, keep previous state */
/* Edge detection */
if (btn->debounced && !prev) btn->edge_press = 1;
if (!btn->debounced && prev) btn->edge_release = 1;
}
/* You can adjust sensitivity by checking fewer bits:
* - 0xFF (8 consecutive) = ~40ms at 5ms sample rate (very stable)
* - 0x0F with mask (4 consecutive) = ~20ms (faster response)
*/
#define SR_MASK_4BIT 0x0F
void sr_debounce_fast(sr_button_t *btn, uint8_t raw) {
btn->history = (btn->history <debounced;
uint8_t masked = btn->history & SR_MASK_4BIT;
if (masked == SR_MASK_4BIT) btn->debounced = 1;
else if (masked == 0x00) btn->debounced = 0;
if (btn->debounced && !prev) btn->edge_press = 1;
if (!btn->debounced && prev) btn->edge_release = 1;
}Advanced: Long Press and Double-Click Detection
Once you have reliable debouncing with edge detection, you can build more sophisticated button behaviors like long press, repeat (auto-repeat while held), and double-click.
/* Advanced button events: short press, long press, repeat, double-click */
#include
typedef enum {
BTN_EVENT_NONE = 0,
BTN_EVENT_SHORT_PRESS,
BTN_EVENT_LONG_PRESS,
BTN_EVENT_REPEAT,
BTN_EVENT_DOUBLE_CLICK
} btn_event_t;
typedef struct {
/* Debounce state */
uint8_t history;
uint8_t debounced;
uint8_t prev_debounced;
/* Timing (in tick counts, each tick = 5ms) */
uint16_t hold_timer; /* How long button has been held */
uint16_t release_timer; /* Time since last release */
uint8_t click_count; /* Clicks within double-click window */
uint8_t long_fired; /* Long press already reported? */
uint16_t repeat_timer; /* Auto-repeat countdown */
} adv_button_t;
/* Timing constants (in 5ms ticks) */
#define LONG_PRESS_TICKS 100 /* 500ms */
#define DOUBLE_CLICK_TICKS 50 /* 250ms window */
#define REPEAT_FIRST_TICKS 80 /* 400ms before first repeat */
#define REPEAT_NEXT_TICKS 20 /* 100ms between repeats */
btn_event_t adv_button_update(adv_button_t *btn, uint8_t raw_pressed) {
btn_event_t event = BTN_EVENT_NONE;
/* Debounce */
btn->prev_debounced = btn->debounced;
btn->history = (btn->history <history & 0x0F) == 0x0F) btn->debounced = 1;
else if ((btn->history & 0x0F) == 0x00) btn->debounced = 0;
/* Press edge */
if (btn->debounced && !btn->prev_debounced) {
btn->hold_timer = 0;
btn->long_fired = 0;
btn->repeat_timer = REPEAT_FIRST_TICKS;
btn->click_count++;
}
/* Held down */
if (btn->debounced) {
btn->hold_timer++;
/* Long press detection */
if (btn->hold_timer >= LONG_PRESS_TICKS && !btn->long_fired) {
btn->long_fired = 1;
btn->click_count = 0; /* Cancel any double-click */
event = BTN_EVENT_LONG_PRESS;
}
/* Auto-repeat */
if (btn->long_fired) {
btn->repeat_timer--;
if (btn->repeat_timer == 0) {
btn->repeat_timer = REPEAT_NEXT_TICKS;
event = BTN_EVENT_REPEAT;
}
}
}
/* Release edge */
if (!btn->debounced && btn->prev_debounced) {
btn->release_timer = 0;
if (!btn->long_fired) {
/* Was a short press — but might be start of double-click */
/* Don't report yet, wait for double-click window */
}
}
/* Released — count down double-click window */
if (!btn->debounced && btn->click_count > 0) {
btn->release_timer++;
if (btn->click_count >= 2) {
event = BTN_EVENT_DOUBLE_CLICK;
btn->click_count = 0;
} else if (btn->release_timer >= DOUBLE_CLICK_TICKS) {
/* Window expired with single click */
event = BTN_EVENT_SHORT_PRESS;
btn->click_count = 0;
}
}
return event;
}
/* Usage example */
/*
static adv_button_t menu_btn = {0};
void timer_isr_5ms(void) {
uint8_t raw = !(GPIOA_IDR & (1 << 0)); // Active low
btn_event_t evt = adv_button_update(&menu_btn, raw);
switch (evt) {
case BTN_EVENT_SHORT_PRESS:
menu_select();
break;
case BTN_EVENT_LONG_PRESS:
menu_back();
break;
case BTN_EVENT_DOUBLE_CLICK:
menu_home();
break;
case BTN_EVENT_REPEAT:
value_increment_fast();
break;
default:
break;
}
}
*/Interrupt-Based Debouncing with Timer
Some designs use GPIO interrupts for initial detection and a timer for debouncing. The GPIO interrupt detects the first edge and starts a debounce timer. During the debounce period, further GPIO interrupts are ignored. When the timer expires, the final state is read.
/* Interrupt + timer hybrid debounce
*
* GPIO interrupt catches the first edge instantly (low latency)
* Timer verifies the state after bounce settles (reliability)
*/
static volatile uint8_t debounce_active = 0;
/* EXTI0 interrupt — fires on PA0 edge */
void EXTI0_IRQHandler(void) {
/* Clear EXTI pending bit */
/* EXTI->PR |= (1 <IMR &= ~(1 <CNT = 0;
TIM3->ARR = 20000; // 20ms at 1MHz timer clock
TIM3->CR1 |= TIM_CR1_CEN; */
}
}
/* Timer 3 interrupt — fires 20ms after button edge */
void TIM3_IRQHandler(void) {
/* Clear timer interrupt flag */
/* TIM3->SR &= ~TIM_SR_UIF; */
/* TIM3->CR1 &= ~TIM_CR1_CEN; // Stop timer */
/* Read the settled button state */
uint8_t pressed = !(GPIOA_IDR & (1 <IMR |= (1 << 0); */
}Hardware Debouncing (For Reference)
While software debouncing is almost always preferred, you should know the hardware approach for completeness. An RC low-pass filter smooths the bouncing signal, and a Schmitt-trigger input on the MCU cleans up the slow edge.
/* Hardware RC debounce circuit
*
* VCC
* |
* [R] 10kΩ (pull-up)
* |
* ├──[R 10kΩ]──┬── MCU GPIO (Schmitt-trigger input)
* | |
* [SW] [C 100nF]
* | |
* GND GND
*
* Time constant: τ = R × C = 10kΩ × 100nF = 1ms
* Debounce time ≈ 5τ = 5ms
*
* Pros:
* - Zero CPU overhead
* - Works even if firmware crashes
*
* Cons:
* - Extra components per button (cost, board space)
* - Fixed timing (can't adjust in software)
* - Adds delay to every read (even non-bouncing situations)
* - Capacitor slows the edge — needs Schmitt-trigger input
*
* Verdict: Use software debouncing unless you have specific
* reasons to need hardware (safety-critical, pre-MCU filtering)
*/Choosing a Debounce Method
Here’s a quick guide for selecting the right debounce method:
- Simple delay: Only for quick prototypes or educational purposes. Never in production.
- Timer-based polling (struct per button): Best for 1–4 buttons. Clear, maintainable code.
- Vertical counter: Best for 5+ buttons or when you need maximum efficiency. Debounces all buttons in ~5 instructions.
- Shift register: Good middle ground — easy to understand, tunable sensitivity, one byte per button.
- Interrupt + timer hybrid: When you need instant response to the first edge (e.g., emergency stop buttons).
Related Articles
- How to Read a Microcontroller Datasheet
- Reset Systems in Microcontrollers
- Brownout Detection in Microcontrollers
- Inline Functions and Macros in Embedded C
- Bitwise Operations and Bit Fields in C
- GPIO in Embedded Systems
- Polling vs Interrupts in Embedded Systems
- Bitwise Operators in C
- State Machine Pattern in C
Related on this site
- Button input first requires correct GPIO configuration (pull-up/pull-down, input mode) — see introduction to GPIO for the setup details.
- Debouncing strategy depends on whether you poll the button in your main loop or use a pin-change interrupt — see polling vs interrupts for the trade-offs.
- Compact macro and inline patterns useful for fast button-handling code are covered in inline functions and macros in embedded C.

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.





