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.
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.
lastChangeis when the pin last moved. Every bounce resets it.now - lastChange >= DEBOUNCE_MSis the same subtraction as the XL LED's blink without delay, and it keeps working whenmillis()wraps round after about 49 days.state == HIGH, checked only at the momentstatechanges, 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 - 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.
The same debounce in MicroPython: ticks_ms is the clock and ticks_diff subtracts two readings of it. state only changes once the reading has held still for DEBOUNCE_MS.
"""
Push Button - debounced, MicroPython TK04 / /p/tk04
Wiring. Count from the square pad on the TinkerBlock board, button
up, header at the bottom:
GND -> GND
VCC -> 3V3 (never 5V: pressed, SIGNAL gives your pin VCC)
NC -> nothing (unconnected on the board)
SIGNAL -> GPIO 25 on an ESP32, GPIO 4 on an ESP32-S3,
GP15 on a Raspberry Pi Pico
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine and time are built in.
"""
from machine import Pin
import time
# The GPIO number SIGNAL is wired to. ESP32: 25. ESP32-S3: 4. Pico: 15.
BUTTON_PIN = 4
DEBOUNCE_MS = 20 # longer than bounce lasts
button = Pin(BUTTON_PIN, Pin.IN) # no pull: the block has its own
last_reading = 0 # what the pin said last time round
state = 0 # what we have decided it is
last_change = time.ticks_ms()
presses = 0
while True:
reading = button.value()
now = time.ticks_ms()
if reading != last_reading: # the contacts moved: restart
last_reading = reading
last_change = now
# Held still for DEBOUNCE_MS, and different from what we believed?
if (time.ticks_diff(now, last_change) >= DEBOUNCE_MS
and reading != state):
state = reading
if state == 1: # one real press
presses += 1
print("presses:", presses)Use ticks_diff, never a plain subtraction: ticks_ms wraps round, and ticks_diff gives the right answer across the wrap. The logic is line for line the Arduino sketch's.
When it does not work
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.
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.
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.
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.
Edit this page — content/books/push-button/debouncing-with-millis.mdx
Questions about this product
See what other owners have asked, and read their solutions.
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.