21. Debouncing · Finite State Machine (FSM) Patterns
With peripherals learned, we move to firmware patterns. The first two appear in nearly every project — debouncing and finite state machines (FSM). A mechanical button's contacts bounce for a few ms on press (chatter), so one press reads as many. Debouncing filters the jitter to keep only real input; an FSM expresses behavior cleanly with "state+input→next state". It's pure logic with no hardware, verified exactly with gcc.
What you'll learn
- 1Explain the cause and symptoms of button chatter (bounce)
- 2Implement an integrator (consecutive-count) debouncer
- 3Split the debouncer into a reusable·testable module
- 4Understand an FSM's parts (states·inputs·transitions)
- 5Drive an FSM from confirmed debounced edges
Introduction
A button's metal contacts micro-bounce on contact, so one press is electrically 0→1→0→1… for a few ms. Read raw, it counts as many. This lesson is pure, hardware-independent logic — a debouncer module (debounce.c/.h) with an LED-mode FSM on top.
Key concepts
1) Integrator debouncer
Sample the pin at a fixed period (e.g. SysTick 1–5ms), and change state only when a value differing from the stable state repeats THRESHOLD times. Any return to the original resets the counter, filtering glitches (non-blocking·robust).
uint8_t debounce_update(debounce_t *db, uint8_t raw) {
uint8_t r = raw ? 1u : 0u;
if (r == db->stable) { db->count = 0u; return 0u; } /* no jitter */
if (++db->count >= DEBOUNCE_THRESHOLD) { /* N in a row */
db->stable = r; db->count = 0u; return 1u; /* confirmed change */
}
return 0u;
}2) Finite state machine (FSM)
| Current state | Input (press confirmed) | Next state |
|---|---|---|
| OFF | press | SLOW |
| SLOW | press | FAST |
| FAST | press | OFF |
In C, implement with enum + switch. Explicit states make debugging·extension easy.
The FSM takes only the "press edge" confirmed by the debouncer. Separating input cleanup (debounce) from decision (FSM) keeps code clean. On a board, just feed raw = (GPIOA->IDR >> pin) & 1 from a periodic interrupt.
Core example
/* noisy raw stream → debounce → cycle OFF→SLOW→FAST on each confirmed press edge */
typedef enum { MODE_OFF=0, MODE_SLOW=1, MODE_FAST=2 } led_mode_t;
for (i = 0; i < n; i++) {
if (debounce_update(&db, raw[i]) && debounce_state(&db) == 1u) {
mode = (led_mode_t)((mode + 1) % 3); /* advance only on confirmed edge */
}
}Verify: a short glitch (1 sample) is ignored and the mode changes only on a real press (THRESHOLD in a row). Build: gcc pc_test.c debounce.c -o pc_test.
Common mistakes
Q. Even after debouncing it sometimes double-presses.
A. THRESHOLD is too small or the sample period too fast. Aim for sample period × THRESHOLD = 10–30ms (e.g. 5ms × 3 = 15ms).
Q. The mode keeps changing while I hold the button.
A. Act on the "changed-to-pressed edge", not the "pressed state". Check both debounce_update's return (changed) and that the new state is 1.
Q. My if-else FSM gets tangled.
A. Make states an enum and gather transitions in a switch. Scattered state across the code is hard to trace.
Summary
- Chatter is contact bounce — one press reads as many
- The integrator debouncer confirms only after N consecutive equal values, filtering glitches
- A debouncer module is reusable·testable on any chip
- An FSM expresses behavior with states·inputs·transitions via enum+switch
- Separating input cleanup (debounce) from decision (FSM) keeps code clean
Exercises
- Vary THRESHOLD to 2/5 and observe how glitch filtering changes
- Add a long-press state to the FSM
- Debounce two buttons independently to drive separate FSMs
All lecture materials and example code (with homework and answers) are openly available on GitHub.
View on GitHub ↗