30 Days Lost in Space → Help Center

Coding Concepts

Debouncing Buttons

When you press a button once and your code registers it five times, that is bounce. Here is what causes it physically, what it looks like in your Serial Monitor, and how to fix it.

What Is Happening Inside the Button

When you press a push button, you expect the metal contacts to touch once and stay connected. That is not what happens. The contacts are tiny springy pieces of metal, and when they come together they literally bounce off each other several times before settling. Same physics as dropping a ball on the ground.

Each bounce is a complete open-close cycle. To your Arduino, each bounce looks like a separate button press. The whole bouncing process takes about 5 to 50 milliseconds, which is too fast for your finger to notice, but an Arduino checks inputs millions of times per second and catches every single bounce.

What you think happens:     What actually happens:

HIGH -----+                 HIGH -----+ ++ ++ +-+
          |                           | || || | |
LOW       +------           LOW       +-++-++-+ +------
          press                       |<-bounce->|settle
                                      ~5-50 ms

What It Looks Like in Your Code

If you are counting button presses, your counter jumps by 3, 5, or 7 instead of 1. If you are toggling an LED, it flips on and off rapidly and lands in a random state. If you are printing to Serial, you see a burst of "pressed!" messages from one tap.

Here is the buggy version that students write first:

// THIS CODE HAS A BOUNCE BUG
void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    counter++;
    Serial.print("Count: ");
    Serial.println(counter);
  }
}

This prints something like "Count: 1" through "Count: 5" from one press. And because the loop runs continuously while the button is held, you also get repeats from holding. There are actually two problems here: bounce (rapid on-off from the contacts) and the need to detect the transition from not-pressed to pressed, rather than checking whether the button is currently down.

Fix 1: The delay() Approach (Quick and Dirty)

When you detect a press, wait a bit and continue. The bounce is over within about 50 milliseconds.

void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    counter++;
    Serial.println(counter);
    delay(50);  // wait for bounce to finish

    // Wait for the button to be released
    while (digitalRead(BUTTON_PIN) == LOW) {
      // do nothing
    }
    delay(50);  // debounce the release too
  }
}

Why this is not great: delay() and the while loop block your entire program. While waiting, your Arduino cannot update LEDs, read sensors, or do anything else. For a basic test this is fine. For a real project, use the millis() approach.

Fix 2: The millis() Approach (The Right Way)

This checks two things: has enough time passed since the last state change (debounce), and has the button state actually changed (transition detection). Nothing gets blocked.

const int BUTTON_PIN = 2;
const unsigned long DEBOUNCE_DELAY = 50;

int buttonState = HIGH;
int lastRawState = HIGH;
unsigned long lastDebounceTime = 0;

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  int rawReading = digitalRead(BUTTON_PIN);

  // If the raw reading changed, reset the debounce timer
  if (rawReading != lastRawState) {
    lastDebounceTime = millis();
  }
  lastRawState = rawReading;

  // If enough time has passed, the reading is stable
  if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
    if (rawReading != buttonState) {
      buttonState = rawReading;

      if (buttonState == LOW) {
        Serial.println("Button pressed!");
      }
    }
  }

  // Rest of your code runs here, never blocked
}

How this works:

  1. Every loop, we read the button. If the reading differs from the last raw reading, we reset a timer.
  2. The timer tracks how long the reading has been stable (unchanged).
  3. Only after the reading has been stable for 50ms do we accept it as real.
  4. We compare the stable state to the previous confirmed state. If it changed, the button was actually pressed or released.

The bounces reset the timer over and over because the reading keeps flipping. Only when the bouncing stops does the debounce "pass."

The Toggle Pattern

The most common place debouncing matters: press a button to turn something on, press again to turn it off. Without debouncing, one press toggles multiple times and you get a random result.

bool ledOn = false;

// Inside the debounce code, where we detect a press:
if (buttonState == LOW) {
  ledOn = !ledOn;  // flip the state
  digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
}

The ! operator inverts the boolean. Each confirmed press flips it once. Without debouncing, the bounces would flip it 5 times and you would end up on the wrong state.

Software vs Hardware Debounce

Everything above is software debounce. You can also fix it with a physical component.

Hardware debounce: Put a small capacitor (0.1uF to 1uF) between the button pin and ground. The capacitor charges and discharges slowly enough to smooth out the rapid bouncing.

Most student projects do fine with software debounce. Use hardware debounce when you have many buttons and do not want debounce code for each one.

Choosing the Right Debounce Time

| Debounce Time | Result | |--------------|--------| | 5-10 ms | Too short for most buttons. Bounce still gets through. | | 20-50 ms | Good range for most push buttons. | | 50-100 ms | Very safe, but starts to feel laggy if you press quickly. | | 200+ ms | Way too long. You will miss real presses. |

Start with 50ms. If you still get double-triggers, increase to 75ms. If the button feels unresponsive, decrease to 30ms.

Common Mistakes

| Mistake | What Happens | Fix | |---------|-------------|-----| | Debounce time too short | Counter still jumps by 2-3 | Use 50ms as a starting point | | Debounce time too long | Fast taps are missed | Bring it down to 50-75ms | | Checking for HIGH/LOW, not the transition | Action fires continuously while held | Compare current stable state to previous | | Declaring variables inside loop() | Variables reset every iteration, debounce never works | Use global variables | | Using int for millis values | Timer math overflows after ~32 seconds | Always use unsigned long |