Switch Debouncing Techniques: RC Filter, Counters and Shift Registers

Press a push button once and connect it to a counter, and the counter may jump by three, five or ten. Nothing is wrong with the code; the switch itself is the problem. The metal contacts strike, spring apart and strike again several times before settling. A microcontroller samples fast enough to see every one of those make-and-break events as a separate press. Removing these false transitions is called debouncing.

This article explains what bounce looks like, compares hardware and software fixes, and gives two tested C routines that you can drop into any project.

What Bounce Looks Like

With a pull-up resistor and a button to ground, the pin should go cleanly from high to low when pressed. On an oscilloscope or logic analyzer you instead see a burst of pulses:

Pin level (button to GND, pull-up to VCC)

 VCC ______
           |_| |_|| |___________________   <- settled low
 GND        <-- bounce -->
            a few ms, varies by switch

Bounce duration depends on the switch type, its age and how it is pressed. Small tactile switches often settle within a few milliseconds, while larger toggle switches and worn contacts can take longer. Because the figure varies so much, the honest advice is to capture your own switch on a logic analyzer and design with a margin. Many designs use a settling window of 10 to 20 ms, which is still too short for a human to notice.

Hardware Debouncing

MethodPartsHow it worksNotes
RC filterResistor and capacitorThe capacitor smooths the bounces into a slow rampThe slow edge can cause multiple transitions at a normal input; follow with a Schmitt trigger
RC plus Schmitt triggerRC filter and a Schmitt inverter such as 74HC14Hysteresis turns the slow ramp into one clean edgeRobust and classic; many MCU inputs already have some Schmitt hysteresis
SR latchSPDT switch and two cross-coupled NAND gatesThe first contact sets the latch; bouncing on the same contact cannot change itNeeds a changeover switch, not a two-terminal push button
Debounce ICDedicated debouncer chipsIntegrated filter and timingExtra cost, used in industrial panels

For an RC filter the time constant is tau = R x C. With a 10 kilo-ohm resistor and a 1 microfarad capacitor, tau = 10 ms. The voltage takes a few time constants to cross the threshold, so the filter rejects bounces shorter than roughly one tau. Hardware filtering also helps with electrostatic discharge and long cable noise, which software cannot fix.

Software Debouncing

Software debouncing costs nothing in parts and is the most common choice. Three approaches are widely used:

  1. Delay after edge: on the first change, wait about 20 ms and read again. Simple, but a blocking delay stalls the program, and doing it inside an interrupt is worse.
  2. Counter (integrator): sample the pin at a fixed rate, for example every 1 ms from a timer tick. Count up while the raw level differs from the accepted state and reset the count when it matches. Accept the new level only when the count reaches a threshold.
  3. Shift register: shift each sample into an 8-bit or 16-bit history. The button is pressed only when the history is all zeros and released when it is all ones. Fast, tiny and easy to apply to many buttons.

Both periodic methods require the sampling to happen at a steady rate. Call them from the main loop using a non-blocking 1 ms timer check, or from a timer interrupt. Avoid using a pin-change interrupt alone, because every bounce triggers it.

Two Debounce Routines in C

The program runs on a PC and feeds a recorded noisy sequence through both routines. On hardware, replace the array with a GPIO read and call the functions once per millisecond. The button is active-low: 0 means pressed.

Counter and shift-register debouncers tested with a simulated bouncing input
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

/* ---------- Method 1: counter / integrator ---------- */
#define DEBOUNCE_MS 5          /* samples that must agree (1 sample per ms) */

typedef struct {
    uint8_t stable;            /* accepted level, 1 = released */
    uint8_t count;
} debounce_t;

/* Returns true on the sample where a new stable press is accepted */
static bool debounce_counter(debounce_t *d, uint8_t raw)
{
    if (raw == d->stable) {
        d->count = 0;                    /* no change pending */
        return false;
    }
    if (++d->count >= DEBOUNCE_MS) {
        d->stable = raw;                 /* change confirmed */
        d->count = 0;
        return d->stable == 0;           /* report presses only */
    }
    return false;
}

/* ---------- Method 2: shift-register history ---------- */
static bool debounce_shift(uint8_t *history, bool *pressed, uint8_t raw)
{
    *history = (uint8_t)((*history << 1) | (raw & 1u));
    if (!*pressed && *history == 0x00) {   /* 8 consecutive lows  */
        *pressed = true;
        return true;
    }
    if (*pressed && *history == 0xFF) {    /* 8 consecutive highs */
        *pressed = false;
    }
    return false;
}

int main(void)
{
    /* one sample per ms: idle, bouncy press, held, bouncy release, idle */
    const uint8_t raw[] = {1,1,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
                           0,0,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1};
    const int n = (int)(sizeof raw / sizeof raw[0]);

    debounce_t d = { 1, 0 };
    uint8_t hist = 0xFF;
    bool held = false;
    int edges = 0, presses_a = 0, presses_b = 0;

    for (int t = 0; t < n; t++) {
        if (t > 0 && raw[t] == 0 && raw[t - 1] == 1) edges++;
        if (debounce_counter(&d, raw[t])) {
            presses_a++;
            printf("counter method: press at %d ms\n", t);
        }
        if (debounce_shift(&hist, &held, raw[t])) {
            presses_b++;
            printf("shift method  : press at %d ms\n", t);
        }
    }
    printf("raw falling edges: %d, counter presses: %d, shift presses: %d\n",
           edges, presses_a, presses_b);
    return 0;
}
counter method: press at 12 ms
shift method  : press at 15 ms
raw falling edges: 5, counter presses: 1, shift presses: 1

How the Routines Work

The raw input shows five falling edges for one press and one release. The counter method resets its count whenever the input returns to the accepted level, so it only accepts the press after five consecutive low samples, at 12 ms. The shift method needs eight consecutive lows in its history byte, so it reports slightly later, at 15 ms. Both report exactly one press and ignore the bouncing release.

The trade-off is response time against noise immunity. A longer window rejects worse bounce but delays the response. Delays below about 20 ms feel instant to a user. To debounce eight buttons on one port, the shift method can store one history byte per button, or advanced versions process a whole port byte at once with bitwise logic.

Common Mistakes

  • Using a delay inside an external interrupt ISR to debounce. It blocks every lower-priority interrupt for the whole delay.
  • Sampling irregularly, for example once per main loop when the loop time varies from 1 ms to 50 ms.
  • Debouncing only the press and not the release, so a bouncing release looks like a second press.
  • Leaving the input floating. Enable the internal pull-up or fit an external one, typically a few kilo-ohms to tens of kilo-ohms.

Summary

Mechanical contacts bounce for a few milliseconds, producing several edges per press. Hardware can remove bounce with an RC filter followed by a Schmitt trigger or with an SR latch on a changeover switch. Software can do it for free with a counter or a shift-register history sampled at a fixed rate. Measure your switch, choose a window with margin, and debounce both edges.