Learning Goals 5 min
By the end of this lesson you will be able to:
- Reproduce the bounce bug by running a press-counter sketch with no debounce — and watch a single physical press register as 2, 3 or even 4 counts on the Serial Monitor.
- Explain in one sentence what physically happens at the metal contacts of a button in the first few milliseconds of a press.
- Add a
delay(20)at the end ofloop()to filter out the bounce — and explain why 20 ms is the right size: long enough to outlast the bounce, short enough to feel instant.
Warm-Up 10 min
The state-change pattern from L01-19 looked clean on paper — and worked perfectly when you pressed the button slowly and steadily. But the 🟡 task asked you to press fast and watch the Serial Monitor, where you may have seen something odd.
Quick-fire puzzle
Faiz wires the L01-19 press-counter sketch and presses the button exactly five times in quick succession. The Serial Monitor shows:
1
2
3
5
6
9
10- How many presses does Faiz think he made, and how many does the Arduino think he made?
- Where did the extra counts come from? Did Faiz actually press the button more than five times?
- What might be happening at the moment Faiz's finger pushes the button down — in the metal contact inside the button — that would explain extra counts?
Reveal the answer
- Faiz pressed it 5 times; the Arduino counted 10. (Or maybe 7, or 11 — the number changes from try to try.)
- The extra counts came from inside the button itself. Faiz didn't press it more than five times; the button "lied" to the Arduino by reporting some presses multiple times.
- When two pieces of metal slap together, they don't make a clean instant connection — they vibrate against each other for a few milliseconds, like a tiny door slamming. The Arduino is fast enough to see each vibration as a separate press.
This effect is called contact bounce, and every mechanical switch in the world does it. Today's whole lesson is two simple ideas: see the bounce, then filter it.
New Concept 20 min
The big idea — buttons aren't as clean as they look
The push button in your kit has two metal pieces inside it. When you press, a tiny spring pushes one piece down onto the other. Because both pieces are slightly springy, they don't touch and stay put — they bounce, like a coin dropped onto a tabletop. For roughly 3 to 20 milliseconds after the initial contact, the connection rapidly opens and closes a few times before settling into a steady "closed" state.
From the Arduino's point of view, those few milliseconds look like a frantic sequence of presses and releases. That's where Faiz's extra counts came from.
What bounce looks like as a signal
Why the L01-19 state-change pattern is fooled
Recall the four-step pattern: read, compare, act on change, remember. During the bounce region, the signal genuinely does change six or seven times. The pattern faithfully detects each one. The pattern isn't broken — the button is "lying" within its specifications, and our code is taking the lies at face value.
The simplest fix — a short delay
Adding a delay(20) at the end of loop() has a magical effect: it slows the loop down so that between two reads, at least 20 milliseconds pass. By the time the second read happens, the bounce region has long finished and the signal is settled. The pattern sees only one transition per press.
Why 20 ms?
- Long enough. Bounce is usually 3–10 ms; 20 ms gives a generous safety margin.
- Short enough. Humans perceive delays under about 50 ms as "instant". The button still feels snappy.
- Tradition. Almost every Arduino tutorial in the world uses 20 or 50 ms. They're both fine.
The pattern
void loop() {
// ... read, compare, act, remember ... (same as L01-19)
delay(20); // hide the bounce
}One line. Always at the very end of loop(). That's it.
Reuse the L01-17 wiring
Today's hardware is unchanged from the last two lessons: button on D7 with INPUT_PULLUP, onboard LED on pin 13. No new wires, no new components.
Why it matters
Every digital input from a mechanical source — buttons, switches, rotary encoders, even reed switches near a magnet — has some kind of bounce. Without debouncing, those inputs are unusable for any sketch that cares about counting events accurately. Today's delay(20) is the simplest, cheapest, most universal fix. Real engineers use it as their first attempt; only if the application demands something faster do they switch to the non-blocking version you'll meet later in Level 2.
Worked Example 20 min
Goal: see the bounce bug for yourself, then fix it with one line of code.
Step 1 — type the broken counter (no debounce)
Open a new sketch called bounce-bug. Type this from scratch — it's the L01-19 press-counter, with the delay(20) deliberately removed:
const int BUTTON_PIN = 7;
const int LED_PIN = 13;
int lastButton = HIGH;
int pressCount = 0;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
int currentButton = digitalRead(BUTTON_PIN);
if (currentButton != lastButton) {
if (currentButton == LOW) {
pressCount = pressCount + 1;
Serial.println(pressCount);
}
}
lastButton = currentButton;
}Step 2 — upload and watch the bug
- Open the Serial Monitor (magnifying-glass icon, top right of the IDE). Set it to 9600 baud.
- Press the button slowly, once. You should see one number appear. Good.
- Press quickly, five times in a row. Look at the column of numbers.
- Repeat the five fast presses a few more times. You'll often see the count jump by 2, 3, or even more on a single physical press.
You're now looking at bounce. Each "extra" count is one of those metal-contact vibrations that the state-change pattern caught.
Step 3 — slow down (or speed up) and notice the pattern
- Slow, deliberate presses (one per second): the count is usually correct. Bounce still happens, but with a 1-second gap between presses, you can't perceive a few extra counts in the noise.
- Fast presses (4 per second): the count is always too high. Bounce becomes obvious.
- A single press where you push down extra-hard: bounce can be longer, more extra counts.
- A single press where you push down extra-soft: sometimes bounces, sometimes doesn't.
The behaviour is not deterministic. Without debouncing, the sketch's behaviour depends on the user's finger pressure, the button's age, the temperature, even ambient humidity. That's unacceptable for a real product.
Step 4 — add the one-line fix
At the very end of loop(), just before the closing brace, add:
delay(20); // hide the bounce
}Re-upload. Try the five-fast-presses test again.
Step 5 — the fixed sketch
const int BUTTON_PIN = 7;
const int LED_PIN = 13;
int lastButton = HIGH;
int pressCount = 0;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
int currentButton = digitalRead(BUTTON_PIN);
if (currentButton != lastButton) {
if (currentButton == LOW) {
pressCount = pressCount + 1;
Serial.println(pressCount);
}
}
lastButton = currentButton;
delay(20); // hide the bounce
}Step 6 — verify the fix
- Press the button 5 times fast. Count: 5. Every time.
- Press 10 times fast. Count: 10.
- Bang the button. Hold and release. Press extra-hard. The count is always equal to the number of physical presses.
- One physical press = one count. Reliable, predictable, deterministic.
Try It Yourself 20 min
Goal: Find the smallest delay() value that still works. Start at delay(2), re-upload, do the five-fast-presses test, and adjust.
delay(____); // experimentTry these values in order, doing the fast-presses test each time:
delay(2)— does it work?delay(5)delay(10)delay(20)delay(50)delay(200)
Questions:
- What's the smallest delay that always gives an accurate count for your specific button? ____
- At what delay value does the button start feeling "sluggish" — like your finger is faster than the Arduino? ____
- Why is the "safe" answer (20–50 ms in textbooks) a bit larger than the absolute minimum you found? (Hint: not every button bounces the same way; the textbook value works for everyone.) ____
Goal: A different debouncing pattern — the "two-read confirmation". Instead of slowing the whole loop, you read the pin twice with a short pause between, and only count a change if both reads agree.
void loop() {
int firstRead = digitalRead(BUTTON_PIN);
delay(5);
int secondRead = digitalRead(BUTTON_PIN);
if (firstRead == secondRead) {
// the read is stable — proceed with state-change logic
if (firstRead != lastButton) {
if (firstRead == LOW) {
pressCount = pressCount + 1;
Serial.println(pressCount);
}
}
lastButton = firstRead;
}
}Questions:
- What does the delay(5) between the two reads do? Why is it shorter than the bounce duration? ____
- If
firstReadandsecondReaddisagree, what does the sketch do? (Hint: look closely at what's NOT inside the outerif.) ____ - Compare this version to the simple end-of-loop
delay(20). Does the button feel more or less responsive? ____
Goal: A "tap-to-light" reflex game. The onboard LED comes on after a random short pause; the player has to press the button as quickly as possible to turn it off. The Serial Monitor prints how many milliseconds you took.
For the random pause use delay(random(1000, 5000)) — Arduino's built-in random(min, max) returns a random integer between min and max-1.
For the reaction time itself, you'll need millis() — the number of milliseconds since the Arduino started. You haven't met it formally; here's just enough to use today.
// Sketch outline — fill in the gaps. Some pieces use ideas from later lessons.
void loop() {
delay(random(1000, 5000)); // random pause
digitalWrite(LED_PIN, HIGH); // challenge begins
unsigned long start = millis();
// wait for the user to press (blocking until the pin goes LOW)
while (digitalRead(BUTTON_PIN) == HIGH) {
// just keep checking
}
unsigned long reactionMs = millis() - start;
digitalWrite(LED_PIN, LOW);
Serial.println(reactionMs);
delay(2000); // rest before the next round
}You don't need debouncing here because we're only checking the first press, not counting multiple. Bounce just means the LED goes off a millisecond early — too small to matter.
Questions:
- Play 10 rounds with a partner. Whose average reaction time is faster? ____
- For comparison, top sprinters react in about 140 ms. How close did you get? ____
- The
whileloop spins waiting for the press. Why isn't this a problem (nodelayneeded)? (Hint:whileloops are L01-areas later, but their job here is just "wait until something changes".)
Mini-Challenge 15 min
The unprotected-LED chase test
Take your light-pattern selector from the L01-19 homework (the one with the 5 modes that the button cycles through). Run it with delay(20) at the end of loop() — and then run it again with the delay removed. The behaviour should be visibly different.
Your task:
- Re-upload your L01-19 light-pattern-selector sketch.
- Run it as it is (with
delay(20)). Press the button five times slowly. The modes should advance 0 → 1 → 2 → 3 → 4 → 0 as expected. - Now remove the
delay(20). Re-upload. Press five times fast. - Record what mode you end up in. Did you make it from mode 0 back to mode 0 in exactly 5 presses, or did you overshoot? Run this test three times.
- Put the
delay(20)back. Verify the predictable behaviour returns.
It works if:
- With the delay: 5 presses gets you exactly through one full cycle.
- Without the delay: 5 presses sometimes lands on mode 0, mode 1, mode 2, or mode 4 — the result is unpredictable from press to press.
- You've now experienced why every commercial product spends a few milliseconds of CPU time on debouncing.
This challenge has no wiring-reveal — the proof is in the comparison. Write 2-3 sentences in your notebook describing what happened in the no-delay case.
Recap 5 min
Mechanical buttons bounce for 3–20 ms after every press, sending a brief burst of state changes that the state-change pattern from L01-19 will faithfully count. The simplest fix is one extra line: delay(20) at the end of loop(). It slows the loop just enough that the bounce has settled by the next read. From now on, every Arduino sketch you write with a button will include this line.
- Bounce / contact bounce
- The rapid open-and-close oscillation that happens at a mechanical switch's contacts immediately after they touch. Lasts roughly 3–20 ms.
- Debouncing
- Any technique that hides bounce from the rest of the sketch, so that one physical press = one detected event. The simplest technique is a short
delayat the end ofloop(). - Settled state
- The level of a signal after the bounce region has finished. The signal we actually care about.
- Blocking vs non-blocking
- A
delay()is blocking — nothing else can happen during it. Later in Level 2, you'll learn themillis()pattern that "remembers when something happened" without blocking, so multiple things can run at once. For Level 1, blocking is fine. - Deterministic
- A program is deterministic if it always produces the same result for the same input. Buttons without debouncing are not deterministic — same physical press might count as 1 or 2 or 3. Adding the delay makes the behaviour deterministic again.
Homework 5 min
The "press log". Build a sketch that times how long each press is held — and reports it via Serial. You'll combine today's debouncing with the millis() idea from the 🔴 stretch task.
- Set up the L01-17 button on D7 with INPUT_PULLUP. Onboard LED on pin 13.
- Use the four-step state-change pattern from L01-19 with the
delay(20)debounce. - Add two new mutable globals:
unsigned long pressStart = 0;and another that you can fill in. - On falling edge (press detected): record
millis()intopressStart, and light the LED. - On rising edge (release detected): compute
millis() - pressStartand print that number to Serial. Turn the LED off.
Open the Serial Monitor and try various press lengths. The numbers (in milliseconds) should match what your watch says.
Also: a design reflection on paper.
- The simple
delay(20)blocks the whole sketch for 20 ms out of every loop pass. If you needed the Arduino to also drive a non-blocking LED chase at the same time as reading a button, why woulddelay(20)ruin the chase? ____ - Estimate: how many bounce-bursts does a typical mechanical button experience over its 10-year lifetime, assuming 50 presses per day? (Hint: 50 × 365 × 10.) ____
Bring back next class:
- The saved
.inofile (call itpress-log). - A phone photo of the Serial Monitor showing a few press durations.
- Your design-reflection answers on a notebook page.
Heads up for next class: L01-20 "if / else" takes the minimal if you've been using and gives it its full set of partners — else, else if, and chained conditions. Your L01-19 homework's nested if blocks will compress to a third of their size.