Home · Academy · Robotics & Coding · Arduino · Button Debounce Logic

Button Debounce Logic

Learn to count reliably by handling button bounce with software debounce using millis.

LESSON COMPASS

What will you use this page for?

Core idea

When you press a button once, the Arduino sometimes sees several presses; we will learn to "smooth" the button (debounce) by adding a small time check with millis() so that counting stays reliable.

Evidence to produce

Complete the page task with your own input, test conditions and reasoning.

Control trap

Noting the time in the wrong place The line lastChangeTime = millis(); should run only when the reading changes . If you call it every loop, the stopwatch is reset constantly and millis() - lastChangeTime can never pass the wait time; the button seems to do nothing. Counting the release instead of the press If you…

Next connection

Servo Control: How we turn a servo motor to a given angle and combine it with a button.

Module sources: Python Tutorial · Arduino Learn

LevelBeginner
Age10–16
Duration30–45 min
PrerequisitePWM
ContentStandard lesson · 1,499 words
Last updated

One-sentence summary

When you press a button once, the Arduino sometimes sees several presses; we will learn to "smooth" the button (debounce) by adding a small time check with millis() so that counting stays reliable.

Why does it matter?

You press an elevator button once and the elevator is called for one floor, not two. You press the jump key on a game controller once and the character jumps once. This feels natural, but there is a quiet problem behind it.

Mechanical buttons work by pushing two metal contacts together. At the exact moment you press, the parts do not touch cleanly; they vibrate for a very short time, opening and closing in tiny bounces. This is called bounce. Your eye cannot see it because it lasts only a few milliseconds. But the Arduino checks millions of times per second, so it can mistake each bounce for a separate press.

The result: you write a counter, press the button once, and the screen shows 3 instead of 1. The code is not wrong; the button physically "stuttered". Debounce is the name for fixing that stutter in software, and it appears in every project that works with the real world.

What is bounce and why does it miscount?

What happens inside the button?

A button is a simple switch that connects two metal contact points. When you press, they join; when you release, they separate. But metal is springy. At the moment of the press, the contacts bounce a few times like a ball on the end of a spring:

Moment of press (time ->)
Signal:  1 0 1 0 1 1 1 1 1   (unstable bounce, then steady)
Time:    ~1-10 milliseconds   then constant

Your eye sees a single "press". The Arduino can read all 5–6 of these changes as separate events.

Why does a simple counter get confused?

In its plainest form a counter is thought of as: "If the button is pressed, add one to the counter." But loop() runs very fast. During the bounce the button looks "pressed" many times, so the counter increases several times instead of once. Also, as long as you hold the button down, the counter keeps rising. There are two different problems: the bounce and the holding.

For correct counting we need two things: (1) ignore the bounce, and (2) count only the "not pressed → pressed" transition, one time.

Debounce in software: measuring time

Why isn't delay() a good solution?

The first idea that comes to mind: when you see the button, add delay(50) and wait for the bounce to pass. It works in small projects, but during delay() the Arduino cannot do anything else — it cannot update an LED or read another sensor. In motor control, or in projects that run several jobs at once, this freeze is a problem. So we prefer a non-blocking method: millis().

The idea with millis()

millis() gives the number of milliseconds since the Arduino was turned on. We use it like a stopwatch. Every time the button's state changes, we note the time. When we see a new change, we ask, "Have at least 50 milliseconds passed since the last change?" If not, it is probably bounce, so we ignore it. If yes, it is a real change, so we accept it.

50 milliseconds is very short for a human (you cannot tap your finger twice that fast), but it is much longer than the bounce. So we remove the bounce without missing real presses.

The link to filtering

This is exactly a filter. Remember from the sensor lessons how we averaged a noisy reading to smooth it. Here too we filter out the "noisy" signal from the button over time and pass through only the real event. Debounce is the digital world's filter: it throws away fast, meaningless changes and lets slow, real ones through.

Mini project

Goal: Increase the counter by exactly one on every button press and print the result to the Serial Monitor.

Wiring

The sketch below is written for an external pull-down resistor (HIGH when pressed).

const int buttonPin = 2;
int counter = 0;

int lastStableState = LOW;   // the accepted button state
int lastReading = LOW;       // the most recent raw reading
unsigned long lastChangeTime = 0;
const unsigned long waitTime = 50; // milliseconds

void setup() {
  pinMode(buttonPin, INPUT);
  Serial.begin(9600);
}
void loop() {
  int reading = digitalRead(buttonPin);

  if (reading != lastReading) {
    lastChangeTime = millis();   // bounce started, note the time
  }

  if (millis() - lastChangeTime > waitTime) {
    if (reading != lastStableState) {
      lastStableState = reading;
      if (lastStableState == HIGH) {  // count only on the press
        counter++;
        Serial.println(counter);
      }
    }
  }

  lastReading = reading;
}

Upload the code, open the Serial Monitor at 9600 baud, and press the button. You will see the number rise by exactly one on each press. If you like, lower waitTime to 5 and watch how the bounce brings the miscounting back.

Common mistakes

Noting the time in the wrong place

The line lastChangeTime = millis(); should run only when the reading changes. If you call it every loop, the stopwatch is reset constantly and millis() - lastChangeTime can never pass the wait time; the button seems to do nothing.

Counting the release instead of the press

If you leave out the if (lastStableState == HIGH) check, both the press and the release are counted and every press increases the counter by two. You should count only one direction of the transition.

Forgetting the pull-down or pull-up resistor

If the button's idle leg is connected to nothing, the pin is left "floating" and reads random HIGH/LOW values. Even debounce code cannot fix this randomness. Always use a pull-down/pull-up resistor or INPUT_PULLUP.

Trying to slow it down with delay()

Thinking "if I slow it with delay(200) it will fix itself" both misses fast real presses and stops other jobs. The millis() solution is both more accurate and non-blocking.

Safety note

Lesson summary

Check questions

  1. What is button "bounce" and why does it happen?
  2. Why does a simple counter sometimes rise by 2 or 3 on a single press?
  3. Why do we prefer millis() over delay() for debounce?
  4. When should the line lastChangeTime = millis(); run in the code?
  5. How would the counter behave without the if (lastStableState == HIGH) check?

Answers

  1. Bounce is the metal contacts of the button opening and closing quickly a few times at the moment of the press; because metal is springy, it vibrates for a few milliseconds.
  2. Because loop() runs very fast and during the bounce the button looks "pressed" many times; each appearance is counted separately.
  3. millis() is a non-blocking way to measure time; while waiting, the Arduino can keep doing other jobs (LED, sensor, motor), whereas delay() stops everything.
  4. Only when the raw reading is different from the previous reading; that is, the stopwatch restarts every time the signal changes.
  5. Since both the press and the release would count as a "state change", every full press would increase the counter by two.

Source and verification note

For “Button Debounce Logic”, verification focuses on whether the relationship between What is bounce and why does it miscount? and Why does a simple counter get confused? remains consistent across examples. Pin, voltage and current limits can differ between Arduino-compatible boards. Compiling code does not guarantee a safe circuit; loads such as motors and servos require a suitable driver and external power where appropriate.

Next lesson

Servo Control: How we turn a servo motor to a given angle and combine it with a button.

Start QuizBack to Arduino
QUESTION POOL

Reinforce this lesson with 10 questions

This lesson has a pool of 20 questions. Each attempt selects 10 and reshuffles the choices.