Now that we've learned how to use peripherals, let's move on to patterns for writing firmware. The first two that appear in almost every embedded project are — debouncing and finite state machines (FSM). Mechanical buttons bounce for several milliseconds after being pressed and released, which can make one press register as multiple presses. Debouncing filters out this bounce and leaves only the "real" inputs. FSMs are a design tool that neatly expresses complex behavior with the simple rule "current state + input → next state."
This chapter is pure logic independent of hardware. We separate the debouncer into a reusable module (debounce.c/.h), place the LED mode FSM on top of it, and verify its operation exactly on a PC using gcc. The debouncer created here can be directly used on 8051 or STM32.
Learning Objectives
- Explain the causes and symptoms of button bouncing.
- Implement a debouncer using an integrator (continuous sample count) method.
- Separate the debouncer into a module to make it reusable and testable.
- Understand the components of a finite state machine (state, input, transition).
- Drive the FSM with the debounced "confirmed edge."
Target Environment
- Compiler: PC + gcc or μVision (pure logic so it works anywhere).
- Device: None (hardware independent). On an actual board, raw samples come from GPIO
IDR. - Verification: Check expected outputs using PC
gcc(pc_test.c).
Key Concepts
1) Chattering (Bounce)
The metal contacts of a mechanical button bounce slightly the moment they touch. So a single press action electrically vibrates for several milliseconds like 0→1→0→1→…→1. If you read this signal as is, one press will be counted as multiple presses.
2) Debouncing Strategies
| Method | Idea | Characteristics |
|---|---|---|
| Time Delay | Ignore for several tens of ms after detecting a change | Simple but blocking; risk of missing inputs |
| Integrator (Sample Count) | Take samples at a regular interval; if the same value appears N times consecutively, confirm | Non-blocking; robust (this one is adopted) |
3) Integrator Debouncer
Sample the pin at a fixed period (e.g., SysTick 1–5ms) and change the state only if it differs from the stable state for THRESHOLD consecutive times. If it returns to the original value even once in the middle, the counter is reset to 0, filtering out glitches.
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 vibration*/
db->count++;
if (db->count >= DEBOUNCE_THRESHOLD) { /*Consecutive N times*/
db->stable = r; db->count = 0u; return 1u; /*Confirmed change*/
}
return 0u;
}
4) Finite State Machine (FSM)
FSM expresses operation with a finite number of states and transition rules that move states based on inputs. For example, "Every time the button is pressed, the LED mode cycles OFF→SLOW→FAST→OFF" can be seen as an FSM:
| Current State | Input (press confirmed) | Next State |
|---|---|---|
| OFF | press | SLOW |
| SLOW | press | FAST |
| FAST | press | OFF |
In C, it is usually implemented with enum + switch. Since the states are explicit, debugging and expansion are easy.
5) Combining Debounce + FSM
The FSM only takes as input the 'press edge' confirmed by the debouncer. If debounce_update returns 1 and the new stable state is 1 (pressed), only then does the mode advance. By separating it this way, input refinement (debounce) and action decision (FSM) are cleanly divided.
6) Actual Hardware Connection
On the board, raw = (GPIOA->IDR >> pin) & 1 is read and put into debounce_update from a periodic interrupt like SysTick. In other words, this convenience logic is kept as is, only the raw source changes to GPIO.
Examples
Example 1 — debounce.h / debounce.c : Reusable Debouncer Module
#ifndef DEBOUNCE_H
#define DEBOUNCE_H
#include <stdint.h>
/*
*Debouncer (integral type).
*If the raw pin value is input at regular intervals, the state only changes when an input different from the stable state occurs THRESHOLD times consecutively.
*Short glitches (chattering) are ignored.
*It is hardware-independent, so it can be tested as is on a PC (gcc).
*/
/*The new input must be consecutive this many times to definitively change the state.*/
#define DEBOUNCE_THRESHOLD 3u
typedef struct {
uint8_t stable; /*Confirmed stable state (0 or 1)*/
uint8_t count; /*Number of consecutive occurrences of input different from the stable state*/
} debounce_t;
/*Resets the debouncer to the initial state (0 or 1).*/
void debounce_init(debounce_t *db, uint8_t init);
/*Processes one raw sample. Returns 1 if the stable state changes, otherwise 0.*/
uint8_t debounce_update(debounce_t *db, uint8_t raw);
/*Currently confirmed stable state (0/1).*/
uint8_t debounce_state(const debounce_t *db);
#endif /* DEBOUNCE_H */
#include "debounce.h"
void debounce_init(debounce_t *db, uint8_t init)
{
db->stable = init ? 1u : 0u;
db->count = 0u;
}
uint8_t debounce_update(debounce_t *db, uint8_t raw)
{
uint8_t r = raw ? 1u : 0u;
if (r == db->stable) {
db->count = 0u; /*No shaking → Counter reset*/
return 0u;
}
db->count++; /*Accumulation of input different from stable state*/
if (db->count >= DEBOUNCE_THRESHOLD) {
db->stable = r; /*Sufficient continuation → Confirmed state change*/
db->count = 0u;
return 1u; /*Changed*/
}
return 0u;
}
uint8_t debounce_state(const debounce_t *db)
{
return db->stable;
}
Example 2 — pc_test.c : Debouncer + Mode FSM
#include <stdio.h>
#include <stdint.h>
#include "debounce.h"
/*
*Lesson 21 PC Verification — Debouncer + LED Mode FSM.
*Debounce the raw button stream with noise, and at each edge 'confirmed as pressed',
*cycle the mode OFF→SLOW→FAST→OFF.
*Build: gcc pc_test.c debounce.c -o pc_test
*/
typedef enum { MODE_OFF = 0, MODE_SLOW = 1, MODE_FAST = 2 } led_mode_t;
static const char *mode_name(led_mode_t m)
{
switch (m) {
case MODE_OFF: return "OFF";
case MODE_SLOW: return "SLOW";
case MODE_FAST: return "FAST";
default: return "?";
}
}
int main(void)
{
/*1=pressed. Glitches (index 2) and short bounces (index 13) are mixed in.*/
uint8_t raw[20] = {
0,0,1,0,1,1,1,0,0,0,
1,1,1,0,1,1,1,1,0,0
};
debounce_t db;
led_mode_t mode = MODE_OFF;
int i;
debounce_init(&db, 0u);
for (i = 0; i < 20; i++) {
uint8_t changed = debounce_update(&db, raw[i]);
if (changed && debounce_state(&db) == 1u) {
mode = (led_mode_t)((mode + 1) % 3); /*Advance only at edges confirmed as pressed*/
}
printf("t=%2d raw=%u stable=%u mode=%s\n",
i, raw[i], debounce_state(&db), mode_name(mode));
}
return 0;
}
Verification on a PC
gcc pc_test.c debounce.c -o pc_test && ./pc_test
Expected output (stdout):
t= 0 raw=0 stable=0 mode=OFF
t= 1 raw=0 stable=0 mode=OFF
t= 2 raw=1 stable=0 mode=OFF
t= 3 raw=0 stable=0 mode=OFF
t= 4 raw=1 stable=0 mode=OFF
t= 5 raw=1 stable=0 mode=OFF
t= 6 raw=1 stable=1 mode=SLOW
t= 7 raw=0 stable=1 mode=SLOW
t= 8 raw=0 stable=1 mode=SLOW
t= 9 raw=0 stable=0 mode=SLOW
t=10 raw=1 stable=0 mode=SLOW
t=11 raw=1 stable=0 mode=SLOW
t=12 raw=1 stable=1 mode=FAST
t=13 raw=0 stable=1 mode=FAST
t=14 raw=1 stable=1 mode=FAST
t=15 raw=1 stable=1 mode=FAST
t=16 raw=1 stable=1 mode=FAST
t=17 raw=1 stable=1 mode=FAST
t=18 raw=0 stable=1 mode=FAST
t=19 raw=0 stable=1 mode=FAST
Observation points: The glitch at index 2 is ignored (stable maintained), and the mode only changes to SLOW from the real press at indexes 4–6. The short bounce at index 13 is also ignored, so the third 'press' does not change the mode since stable is already 1.
Common Mistakes
Q. Even after debouncing, it sometimes behaves as if the button was pressed twice.
A. The THRESHOLD is too small or the sample interval is too fast. Usually, set so that sample interval × THRESHOLD is 10–30ms (e.g., 5ms × 3 = 15ms).
Q. The mode keeps changing while holding the button.
A. Action should occur only on the 'edge that changed to pressed', not the 'pressed state'. Check both debounce_update return value (changed) and whether the new state is 1.
Q. When I built the FSM using if-else dummies, the state gets messed up.
A. Specify the state as enum and gather all transitions to switch in one place. If the state is scattered throughout the code, tracking becomes difficult.
Q. Is it okay to put all the debounce logic inside the interrupt?
A. It is safer to call only debounce_update in the periodic sampling (tick) and handle heavy operations (FSM result processing) in the main loop.
Summary
- Chattering occurs due to mechanical contact vibrations, causing a single press to be read multiple times.
- The integrator debounce confirms the state only when the same value occurs N consecutive times, filtering out glitches.
- If a debouncer is separated as a module, it can be reused and tested on any chip.
- FSM expresses operations through state, input, and transition, and is implemented with
enum+switch. - Separating input refining (debounce) and action decision (FSM) keeps the code clean.
Try it yourself
Solve the assignment below. The reference solution is shown below.