Embedded C track

Lesson 21 · Part 4: Applications & Patterns

Debouncing & Finite State Machines

Shared application patterns

Part 4: Applications & Patterns7 min readFree lesson

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.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/app_debounce_fsm/debounce.cC
#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;
}
src/app_debounce_fsm/debounce.hC header
#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 */
src/app_debounce_fsm/pc_test.cC
#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;
}

Assignment

Both problems are pure logic (PC gcc), so they can be graded without a board.

gcc main.c -o prog && ./prog

Problem 1 — Detecting PRESS/RELEASE Events with a Debouncer

  • File name: homework_01/main.c
  • Key concept: Integrator debounce (THRESHOLD=2), bidirectional edge
  • Platform: Common (PC gcc)

Requirements

  • Process the raw stream {0,1,1,0,0,1,1,1,0,0,1,1} with a debouncer that has THRESHOLD set to 2.
  • When the stable state changes from 0→1, output PRESS; when it changes from 1→0, output RELEASE along with the time.
  • At the end, output the total number of PRESSes. The format must match the expected output character by character.

Expected output

t=2 PRESS
t=4 RELEASE
t=6 PRESS
t=9 RELEASE
t=11 PRESS
total presses = 3

Hint

  • If the input differs from stable for two consecutive times, flip the state. If the same value comes, reset the counter.
  • If the new stable state is 1, it's PRESS; if 0, RELEASE. Only count when it is PRESS.

Problem 2 — 4-Stage Mode FSM

  • File name: homework_02/main.c
  • Key concept: enum state, cyclic transition
  • Platform: Common (PC gcc)

Requirements

  • Cycle through the four states {OFF, LOW, MID, HIGH} on each press event (HIGH goes next to OFF).
  • Input is an array {1,0,1,1,0,1,0,1} indicating 'was press confirmed this tick' (1=pressed).
  • Output the input and current state for each tick. The format must match the expected output character by character.

Expected output

t=0 press=1 state=LOW
t=1 press=0 state=LOW
t=2 press=1 state=MID
t=3 press=1 state=HIGH
t=4 press=0 state=HIGH
t=5 press=1 state=OFF
t=6 press=0 state=OFF
t=7 press=1 state=LOW

Hint

  • Advance the state: state = (state + 1) % 4; (only when press is 1).
  • Output the state name as switch or from the name array.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 1 — Detect PRESS/RELEASE events using a debouncer (THRESHOLD=2)
 *
 *Key Points:
 *- Flip the state if an input different from the stable state occurs twice in a row.
 *If the same value (or the same as the stable state) comes, reset the counter to filter out glitches.
 *- If the new state is 1, it is PRESS; if 0, it is RELEASE.
 *
 *Common mistake:
 *- If the counter reset condition is missed, glitches pass through as is.
 *- Count both PRESS and RELEASE by pressing (count only PRESS).
 */

#define THRESHOLD  2u

int main(void)
{
    uint8_t raw[12] = { 0,1,1,0,0,1,1,1,0,0,1,1 };
    uint8_t stable = 0u;
    uint8_t count  = 0u;
    int presses = 0;
    int i;

    for (i = 0; i < 12; i++) {
        uint8_t r = raw[i] ? 1u : 0u;
        if (r == stable) {
            count = 0u;
        } else {
            count++;
            if (count >= THRESHOLD) {
                stable = r;
                count = 0u;
                if (stable == 1u) {
                    printf("t=%d PRESS\n", i);
                    presses++;
                } else {
                    printf("t=%d RELEASE\n", i);
                }
            }
        }
    }

    printf("total presses = %d\n", presses);
    return 0;
}
homework_02/main.cC
#include <stdio.h>

/*
 *Answer 2 — 4-step mode FSM
 *
 *Key Points:
 *- Specify states with an enum, and cycle with (state+1)%4 for each press event.
 *- After HIGH(3), naturally wrap around to OFF(0).
 *
 *Common mistake:
 *- Missing the %4 can cause the state to go above 4, going out of the name array.
 *- Advancing even when press is 0 (only advance when there is a press).
 */

typedef enum { ST_OFF = 0, ST_LOW = 1, ST_MID = 2, ST_HIGH = 3 } mode_t;

static const char *name_of(mode_t m)
{
    static const char *names[4] = { "OFF", "LOW", "MID", "HIGH" };
    return names[m];
}

int main(void)
{
    int press[8] = { 1, 0, 1, 1, 0, 1, 0, 1 };
    mode_t state = ST_OFF;
    int i;

    for (i = 0; i < 8; i++) {
        if (press[i]) {
            state = (mode_t)((state + 1) % 4);
        }
        printf("t=%d press=%d state=%s\n", i, press[i], name_of(state));
    }
    return 0;
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now