push button/One press, one action/07. Debouncing with millis()
One press, one action · 07 of 9

Debouncing with millis()

Believe a change only once the pin has held still for 20 ms. Bounce never holds still that long, so each press counts once, and nobody notices a 20 ms delay on a button. It takes one timestamp and one comparison, and no library.

Wait for it to hold still

Bounce is a burst of changes a few milliseconds long. A real press is a change that then stays put. So the rule is: when the pin changes, do not believe it yet. Start a clock. If the pin changes again, start the clock again. Only when it has held still for long enough, believe it.

Debouncing
Debounce window
Presses
2
Counted
2
Believed after
20 ms still
A 20 ms window: a change is believed only once the pin has held still for 20 ms. The bounce never holds still that long, so each press is counted once, 20 ms after it settles. Nobody notices a 20 ms delay on a button.

Try the three windows. With 0 ms every change is believed, and two presses count as six: the counter from the last article. With 20 ms, the bounce keeps restarting the clock until it stops, and each press counts once, 20 ms after it settles. With 80 ms, the quick tap in the figure is let go before the clock runs out, and it is never counted at all.

As before, the bounce in the figure is illustrative. The counts are the sketch's own logic, run step by step on that waveform.

Two variables, one clock

The sketch keeps two readings apart. reading is what the pin says now, bounce and all. state is what the sketch has decided the button is, and it only changes when reading has not moved for DEBOUNCE_MS.

  • lastChange is when the pin last moved. Every bounce resets it.
  • now - lastChange >= DEBOUNCE_MS is the same subtraction as the XL LED's blink without delay, and it keeps working when millis() wraps round after about 49 days.
  • state == HIGH, checked only at the moment state changes, is one press. That is the edge detection from the last article, moved from the raw reading to the debounced one.

Choosing the window

It has to be longer than the bounce and shorter than the quickest press you want to count. Bounce on small tactile switches is commonly a few milliseconds, and a deliberate press is held for far longer than that. A window from about 10 ms to 50 ms sits between the two, and 20 ms is a common choice.

This switch's bounce has not been measured. If a press still counts twice now and then, raise the window to 30 or 50 ms. Nobody can feel the difference.

The code

The counter from the last article with a debounce window. reading is what the pin says now; state is what the sketch has decided the button is. state only changes once reading has held still for DEBOUNCE_MS.

push_button_debounce.ino
/*
  Push Button - debounced with millis()                  TK04 / /p/tk04

  Wiring. Count from the square pad on the TinkerBlock board, button
  up, header at the bottom:

    GND    -> GND
    VCC    -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
              (pressed, SIGNAL gives your pin whatever VCC is)
    NC     -> nothing   (unconnected on the board)
    SIGNAL -> D2 on an Uno, GPIO 25 on an ESP32, GPIO 4 on an
              ESP32-S3, GP15 on a Raspberry Pi Pico

  Arduino IDE
    Tools > Board                 your board, e.g. ESP32S3 Dev Module
    Tools > Port                  the one that appears when you plug in
    Tools > USB CDC On Boot       Enabled   (ESP32-S3 only)
    No library needed.
*/

// The GPIO number SIGNAL is wired to.
// Uno: 2. ESP32: 25. ESP32-S3: 4. Pico: 15.
const int BUTTON_PIN = 4;
const unsigned long DEBOUNCE_MS = 20;   // longer than bounce lasts

int lastReading = LOW;          // what the pin said last time round
int state = LOW;                // what we have decided the button is
unsigned long lastChange = 0;   // millis() when the pin last moved
unsigned long presses = 0;

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT);   // the block has its own pull-down
}

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

  if (reading != lastReading) { // the contacts moved: restart the clock
    lastReading = reading;
    lastChange = now;
  }

  // Held still for DEBOUNCE_MS, and different from what we believed?
  if (now - lastChange >= DEBOUNCE_MS && reading != state) {
    state = reading;
    if (state == HIGH) {        // one real press
      presses++;
      Serial.print("presses: ");
      Serial.println(presses);
    }
  }

  // Anything else goes here. None of it waits for the button.
}

lastChange restarts every time the pin moves, so during a bounce it keeps restarting and nothing is believed. 20 ms after the last bounce, state catches up, and a change to HIGH counts once. loop() never waits.

When it does not work

It still counts twice now and then.

Raise DEBOUNCE_MS to 30 or 50 and try again. Bounce varies between switches and grows as a switch wears, and 20 ms is a starting point rather than a figure measured on this part. Anything up to about 50 ms still feels instant.

Fast double-presses are being missed.

The window is too long. A press is only believed after the pin has been HIGH for the whole window, so a tap shorter than it is never counted. Bring DEBOUNCE_MS down towards 20 ms.

Why not just delay(50) after a press?

It works for a single button in a simple sketch, and it stops everything else for 50 ms each time. The millis() version debounces without waiting, so the same loop can also blink an LED, read a sensor or watch a second button.

Should I use a library such as Bounce2?

You can, and it does the same thing. Writing it once yourself is worth it: it is eight lines, it shows exactly what the library is doing, and the same pattern debounces anything else that is noisy, such as a reed switch or a slotted sensor.

Where this goes next

Press once for on, again for off, with the state kept in the sketch.

A toggle

Edit this page — content/books/push-button/debouncing-with-millis.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

Push Button

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum