Learn

Your button fires twice. Here is why, and here is the fix.

Sign up freeor see every lesson
  • 192parts on the bench
  • 25boards running now
  • 1.00xreal time, on every board
Arduino Uno: interrupt counting, with no debouncelive0.000 s 0.00x
Hold the button
Every falling edge on pin 2 adds one to a counter and flips the on-board LED. Open it in the editor, watch the serial monitor, and press the button once.

A tactile switch is a small metal dome pushed down onto a contact, and it does not land and stay still. It lands, springs back a fraction of a millimeter, lands again, and repeats that a few times before it settles, the way a dropped coin does not stop the instant it first touches the table. Electrically, the contact opens and closes several times in the first few milliseconds of a press, and again on release, before it holds steady. A sketch that just counts edges on that pin counts wrong, because a single press can register as two, three or more.

Mokxi models this rather than hiding it. Every pushbutton on the bench bounces by default, on a profile called `typical`: three to eight transitions, settled somewhere between 1 and 5 milliseconds after the press starts. That is close to a real tactile switch, and it means the circuit above is not a staged demonstration of a bug; it is the bug, happening the same way it happens on the desk.

Download the lesson plan (PDF)

What the simulator shows

The program running above is the Uno's built-in `interrupt` example. It wires an interrupt handler to pin 2 that fires on every falling edge (every moment the pin drops from 5 V to 0 V) and adds one to a counter, with no debounce logic anywhere in it on purpose. Open the circuit in the editor, open the serial monitor, and press the button once, cleanly, the way you would press any button. The count printed after a single press is very often two or three, not one, and the on-board LED, which toggles on the counter's parity, sometimes ends up in the state you did not expect. Nothing is broken. The switch bounced, the handler saw every bounce as a separate press, and it counted every one of them faithfully, because that is exactly what the code asks it to do.

This is the entire lesson in one sentence: the switch, not the code, produced the extra presses, and the fix belongs in the code because the switch cannot be made perfectly clean.

Three ways to fix it

The first fix, and the cheapest, is a software debounce window: after any change on the pin, ignore further changes for a short time, long enough to cover the settling time and short enough that a real second press is never mistaken for bounce. Somewhere between 5 and 20 milliseconds covers a typical switch comfortably.

The second is a hardware fix: a small capacitor across the switch. It slows the voltage's rise and fall enough that the bounces never reach a full logic transition, so the pin genuinely only sees one clean edge. This works on the bench because a real capacitor takes real time to charge and discharge, and it works in Mokxi for the same reason: the capacitor here integrates over time with a real exponential RC response rather than switching instantly, so a scaled-down version of this fix actually holds up in the simulator, not just in theory.

The third is debouncing inside an interrupt handler itself, which needs one extra rule: the handler should do as little as possible, because it runs with other interrupts held off. Record the time and get out; do the actual decision-making, including the debounce window, back in `loop()`.

The millis() version

The Uno's built-in `toggle` program is the software fix, in full, on the same pin 2 the bounce demonstration uses. It keeps the last reading and the time it last changed, and it only acts on a change once `DEBOUNCE_MS`, twenty five milliseconds, has passed since the previous one. Press the button once with this program running and the lamp on pin 13 turns on; press it again and it turns off, exactly once per press, which is the entire point.

Notice what the code is actually comparing. It is not counting how long the button has been held; it is measuring how long it has been since the last accepted change, on either edge, press or release. That distinction is what makes the window symmetric: a bounce right after the press is ignored, and so is a bounce right after the release, with the same twenty-five-millisecond rule covering both without any special-casing for which edge is which.

firmware/examples/toggle (the millis() fix, wired the same way)
const int BUTTON_PIN = 2;
const int LAMP_PIN = 13;
const unsigned long DEBOUNCE_MS = 25;

int lastReading = HIGH;
unsigned long lastChange = 0;
bool lampOn = false;

void loop() {
  int reading = digitalRead(BUTTON_PIN);
  unsigned long now = millis();

  if (reading != lastReading && now - lastChange >= DEBOUNCE_MS) {
    lastChange = now;
    lastReading = reading;
    if (reading == LOW) {
      lampOn = !lampOn;
      digitalWrite(LAMP_PIN, lampOn ? HIGH : LOW);
    }
  }
}

The pull-up resistor question

Both the bounce demonstration and its fix wire the button straight to pin 2 with no external resistor, because the sketch switches on the pin's own internal pull-up with `INPUT_PULLUP`. That pull-up holds the pin at 5 V until the button ties it to ground, which is why the pin reads LOW while the button is held rather than HIGH, a detail that trips people up the first time they see it, because it is the opposite of what a button seems like it should do.

The alternative is a real external pull-up, a 10 kilohm resistor from the pin to 5 V, with the button pulling the same net down to ground when pressed. Electrically the two are the same idea; the internal one is simply weaker (about 35 kilohms) and needs no extra part. The page on the pull-up resistor builds the external version from scratch and is worth reading if the internal one ever feels like it appeared from nowhere.

Why the default profile is set the way it is

`typical`, three to eight transitions settling within 1 to 5 milliseconds, is a deliberately ordinary tactile switch, the kind in almost every breadboard kit, not a worst case chosen to make the point look worse than it is. A `worst` profile exists too, going up to twenty transitions over 10 to 20 milliseconds, closer to a tired switch or a long-levered limit switch that has seen a lot of use, and it is worth testing a debounce window against once the typical case is handled: a fix tuned to a 5-millisecond bounce with no margin can still miss a switch that chatters for 15.

Questions

How long should the debounce delay be?

Somewhere between 5 and 20 milliseconds handles a typical tactile switch, which is also the range Mokxi's default bounce profile settles within. Too short and some bounce gets through; too long and a fast double-press starts to feel unresponsive.

Do I need the capacitor if I already debounce in software?

No. The two are alternatives that solve the same problem in different places, and using both is not wrong, just redundant. Pick whichever fits the circuit: software costs nothing extra, hardware works even if the code reading the pin never changes.

Why does my interrupt still fire twice after I added a debounce check?

Almost always because the debounce window check itself is inside the interrupt handler alongside slow work, or because the time is being compared against the wrong stored value. Keep the handler to reading the clock and setting a flag, and do the comparison in loop().

Does INPUT_PULLUP need an external resistor as well?

No. It switches on a pull-up resistor built into the ATmega328P itself, so the button needs only two wires: one to the pin, one to GND.

What value capacitor debounces a typical switch in hardware?

Something around 100 nanofarads across the switch, often paired with a small series resistor to limit the discharge current on the first press, is a common starting point; the right value depends on the switch and is worth checking on the bench with the actual part rather than trusting one number for every switch.

Is debouncing only a problem for buttons?

No. Any mechanical contact bounces, including relays, toggle switches and limit switches on a motor or a door. The same fix applies: a time window after the first change, or a capacitor across the contact, whichever suits the circuit reading it.

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.