🧠 Lekcja 11: Maszyna stanów (FSM) + przycisk: klik / dwuklik / długie przytrzymanie

W tej lekcji robisz „dorosłą” obsługę przycisku. Zamiast pojedynczego if-a, budujesz maszynę stanów i dostajesz stabilne zdarzenia: CLICK, DOUBLE_CLICK, LONG_PRESS, REPEAT. To jest fundament pod liczniki, menu, sterowanie trybami i większe projekty.

1) Cel lekcji

  • Zrobić stabilny moduł przycisku: debouncing + zdarzenia + czas.
  • Użyć ticka z Timera (z Lekcji 10) jako źródła czasu.
  • Wprowadzić FSM (Finite State Machine) i trzymać logikę w jednym miejscu.
  • Zbudować sterowanie LED w oparciu o zdarzenia (bez blokowania pętli).
Domyślne piny i zegar LED: PB0   |   Przycisk: PD2 (pull-up, wciśnięty=0)   |   Zegar: 8 MHz   |   Timer0: tick 1 ms

2) Co to znaczy „maszyna stanów” w praktyce

Maszyna stanów to kod, który ma kilka jasno zdefiniowanych stanów i przechodzi między nimi według reguł. Dla przycisku to idealny model, bo przycisk ma powtarzalne fazy: spoczynek, wciśnięcie, trzymanie, puszczenie, okno na dwuklik.

Stan Co oznacza Co może się wydarzyć
IDLE Przycisk puszczony, czekamy na start Wciśnięcie → PRESSED
PRESSED Przycisk wciśnięty (po debounce), liczysz czas trzymania Puszczenie → RELEASED_WAIT
Czas ≥ LONG → LONG_SENT
RELEASED_WAIT Puścił po krótkim, czekasz na ewentualny drugi klik Drugi klik → SECOND_PRESSED
Timeout → CLICK
SECOND_PRESSED Drugi klik w oknie czasu Puszczenie → DOUBLE_CLICK
LONG_SENT Długie zdarzenie już wysłane, możesz robić repeat Puszczenie → IDLE
Repeat co N ms → REPEAT
Wniosek FSM robi z „chaosu if-ów” stabilną logikę i zawsze wiesz, gdzie jesteś.

3) Kod: kompletna Lekcja 11 (Timer0 + FSM przycisku + sterowanie LED)

Poniżej masz kompletny plik main.c do wklejenia. Zawiera Timer0 tick 1 ms oraz moduł przycisku oparty o FSM.

#define F_CPU 8000000UL

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>

// ====== PINY (zmień tylko tu, jeśli masz inaczej) ======
#define LED_DDR    DDRB
#define LED_PORT   PORTB
#define LED_PIN    PB0

#define BTN_DDR     DDRD
#define BTN_PORT    PORTD
#define BTN_PIN     PD2
#define BTN_PINREG  PIND

// ====== TICK 1 ms (Timer0 CTC) ======
volatile uint32_t g_ms = 0;

ISR(TIMER0_COMPA_vect)
{
    g_ms++;
}

static inline void timer0_init_ctc_1ms(void)
{
    // CTC: WGM01=1
    TCCR0A = (1 << WGM01);
    TCCR0B = 0;

    // 8 MHz, preskaler 64: 125kHz, OCR0A=124 -> 1ms
    OCR0A  = 124;

    // przerwanie Compare Match A
    TIMSK0 = (1 << OCIE0A);

    // start: preskaler 64 (CS01|CS00)
    TCCR0B = (1 << CS01) | (1 << CS00);
}

// ====== LED helpers ======
static inline void led_init(void)
{
    LED_DDR |= (1 << LED_PIN);
}
static inline void led_on(void)  { LED_PORT |=  (1 << LED_PIN); }
static inline void led_off(void) { LED_PORT &= ~(1 << LED_PIN); }
static inline void led_toggle(void) { LED_PORT ^= (1 << LED_PIN); }

// ====== BUTTON raw + pull-up ======
static inline void button_hw_init(void)
{
    BTN_DDR  &= ~(1 << BTN_PIN);  // input
    BTN_PORT |=  (1 << BTN_PIN);  // pull-up ON
}

// raw: 1 gdy wciśnięty, 0 gdy puszczony (pull-up)
static inline uint8_t button_raw_pressed(void)
{
    return (BTN_PINREG & (1 << BTN_PIN)) == 0;
}

// ====== ZDARZENIA PRZYCISKU ======
typedef enum {
    BTN_EVT_NONE = 0,
    BTN_EVT_CLICK,
    BTN_EVT_DOUBLE_CLICK,
    BTN_EVT_LONG_PRESS,
    BTN_EVT_REPEAT
} button_event_t;

// ====== FSM PRZYCISKU ======
typedef enum {
    ST_IDLE = 0,
    ST_PRESSED,
    ST_RELEASED_WAIT,
    ST_SECOND_PRESSED,
    ST_LONG_SENT
} button_state_t;

typedef struct {
    button_state_t st;

    // debouncing
    uint8_t last_raw;
    uint8_t stable_raw;
    uint32_t t_change;

    // czas wciśnięcia
    uint32_t t_pressed;

    // okno na dwuklik
    uint32_t t_released;

    // repeat po long
    uint32_t t_repeat;
} button_fsm_t;

// Parametry (ms) — można stroić
#define DEBOUNCE_MS      25
#define LONG_MS         800
#define DOUBLE_MS       350
#define REPEAT_START_MS 900
#define REPEAT_PERIOD_MS 150

static inline void btn_fsm_init(button_fsm_t *b)
{
    b->st = ST_IDLE;
    b->last_raw = 0;
    b->stable_raw = 0;
    b->t_change = 0;
    b->t_pressed = 0;
    b->t_released = 0;
    b->t_repeat = 0;
}

// Aktualizacja debounced state na podstawie raw i czasu (g_ms)
static inline void btn_debounce_update(button_fsm_t *b, uint8_t raw_now)
{
    if (raw_now != b->last_raw)
    {
        b->last_raw = raw_now;
        b->t_change = g_ms;
    }

    // jeśli raw się nie zmienia od DEBOUNCE_MS, uznaj za stabilny
    if ((uint32_t)(g_ms - b->t_change) >= DEBOUNCE_MS)
    {
        b->stable_raw = b->last_raw;
    }
}

// Główna funkcja FSM: zwraca pojedyncze zdarzenia
static button_event_t btn_fsm_update(button_fsm_t *b)
{
    uint8_t raw_now = button_raw_pressed(); // 1=pressed, 0=released
    btn_debounce_update(b, raw_now);

    // operujemy na stanie stabilnym
    uint8_t pressed = b->stable_raw;

    switch (b->st)
    {
        case ST_IDLE:
            if (pressed)
            {
                b->st = ST_PRESSED;
                b->t_pressed = g_ms;
            }
            break;

        case ST_PRESSED:
            // long press
            if (pressed && (uint32_t)(g_ms - b->t_pressed) >= LONG_MS)
            {
                b->st = ST_LONG_SENT;
                b->t_repeat = g_ms;
                return BTN_EVT_LONG_PRESS;
            }

            // puszczenie przed long = kandydat na click/dwuklik
            if (!pressed)
            {
                b->st = ST_RELEASED_WAIT;
                b->t_released = g_ms;
            }
            break;

        case ST_RELEASED_WAIT:
            // drugi klik w oknie
            if (pressed)
            {
                b->st = ST_SECOND_PRESSED;
                b->t_pressed = g_ms;
            }
            else
            {
                // timeout = pojedynczy click
                if ((uint32_t)(g_ms - b->t_released) >= DOUBLE_MS)
                {
                    b->st = ST_IDLE;
                    return BTN_EVT_CLICK;
                }
            }
            break;

        case ST_SECOND_PRESSED:
            // czekamy aż puści
            if (!pressed)
            {
                b->st = ST_IDLE;
                return BTN_EVT_DOUBLE_CLICK;
            }
            // opcjonalnie: jeśli ktoś trzyma drugi klik długo, można uznać long — tu zostawiamy prosto
            break;

        case ST_LONG_SENT:
            // repeat po long (np. przy trzymaniu)
            if (pressed)
            {
                // start repeat po REPEAT_START_MS od pierwszego wciśnięcia
                if ((uint32_t)(g_ms - b->t_pressed) >= REPEAT_START_MS)
                {
                    if ((uint32_t)(g_ms - b->t_repeat) >= REPEAT_PERIOD_MS)
                    {
                        b->t_repeat = g_ms;
                        return BTN_EVT_REPEAT;
                    }
                }
            }
            else
            {
                // puszczenie kończy long
                b->st = ST_IDLE;
            }
            break;
    }

    return BTN_EVT_NONE;
}

// ====== LOGIKA LED (przykładowe użycie eventów) ======
typedef enum {
    MODE_OFF = 0,
    MODE_ON,
    MODE_BLINK_SLOW,
    MODE_BLINK_FAST
} app_mode_t;

static inline void mode_apply(app_mode_t mode)
{
    // wyjściowo nic — sterowanie miganiem robimy w pętli z czasem
    if (mode == MODE_OFF) led_off();
    if (mode == MODE_ON)  led_on();
}

int main(void)
{
    led_init();
    button_hw_init();

    timer0_init_ctc_1ms();
    sei();

    button_fsm_t btn;
    btn_fsm_init(&btn);

    app_mode_t mode = MODE_OFF;
    mode_apply(mode);

    uint32_t t_blink = 0;

    while (1)
    {
        // 1) Odbierz zdarzenie z przycisku
        button_event_t ev = btn_fsm_update(&btn);

        // 2) Reakcja aplikacji na zdarzenia
        if (ev == BTN_EVT_CLICK)
        {
            // CLICK: przełącz tryb OFF <-> ON
            mode = (mode == MODE_OFF) ? MODE_ON : MODE_OFF;
            mode_apply(mode);
        }
        else if (ev == BTN_EVT_DOUBLE_CLICK)
        {
            // DOUBLE: przełącz między blink slow i blink fast
            if (mode != MODE_BLINK_SLOW && mode != MODE_BLINK_FAST)
                mode = MODE_BLINK_SLOW;
            else
                mode = (mode == MODE_BLINK_SLOW) ? MODE_BLINK_FAST : MODE_BLINK_SLOW;

            // start migania od razu
            t_blink = g_ms;
        }
        else if (ev == BTN_EVT_LONG_PRESS)
        {
            // LONG: reset do OFF
            mode = MODE_OFF;
            mode_apply(mode);
        }
        else if (ev == BTN_EVT_REPEAT)
        {
            // REPEAT: np. szybkie "piknięcie" (tu: toggle) podczas trzymania
            // (możesz to podpiąć pod regulację wartości w menu itp.)
            led_toggle();
        }

        // 3) Zachowanie zależne od trybu (bez blokowania)
        if (mode == MODE_BLINK_SLOW)
        {
            if ((uint32_t)(g_ms - t_blink) >= 500)
            {
                t_blink = g_ms;
                led_toggle();
            }
        }
        else if (mode == MODE_BLINK_FAST)
        {
            if ((uint32_t)(g_ms - t_blink) >= 120)
            {
                t_blink = g_ms;
                led_toggle();
            }
        }

        // W trybach OFF/ON nic nie robimy w tle (stan utrzymany)
    }
}
Co ten program daje od razu
  • CLICK: OFF ↔ ON
  • DOUBLE_CLICK: blink slow ↔ blink fast
  • LONG_PRESS: reset do OFF
  • REPEAT: toggle podczas trzymania (demo mechanizmu repeat)

4) Jak stroić czasy (bez zgadywania)

Czasami przycisk ma inne drgania i inne odczucie „komfortu” dla użytkownika. Zmieniasz tylko stałe — cała logika FSM zostaje taka sama.

Stała Domyślnie Co robi Kiedy zmienić
DEBOUNCE_MS 25 ile ms musi być stabilny stan jeśli klik „wariuje” → zwiększ do 30–40
LONG_MS 800 próg długiego przytrzymania jeśli long jest za „wolny” → 600–700
DOUBLE_MS 350 okno na dwuklik po puszczeniu jeśli trudno trafić dwuklik → 450–500
REPEAT_PERIOD_MS 150 częstotliwość repeat po long menu/regulacja: 80–200

5) Najczęstsze problemy

  • Nie ma zdarzeń → brak pull-up lub zły pin (czytasz nie ten PIND).
  • CLICK czasem robi DOUBLE → za duże okno DOUBLE_MS lub wahania stanu (zwiększ DEBOUNCE_MS).
  • LONG nie działa → brak ticka (przerwania/Timer0) lub inne F_CPU.
  • Repeat zbyt agresywny → zwiększ REPEAT_PERIOD_MS.

6) Zadania (dużo ćwiczeń — praktyka FSM)

Poziom 1 modyfikacje sterowania

  1. Zmień mapowanie: CLICK → zmienia tryb blink slow, DOUBLE → OFF/ON, LONG → blink fast.
  2. Dodaj tryb MODE_ON jako stałe świecenie, a OFF jako gaśnięcie.
  3. W trybie blink slow ustaw okres na 700 ms zamiast 500 ms.

Poziom 2 menu i wartości

  1. Utwórz zmienną uint8_t level (0..10). CLICK zwiększa, DOUBLE zmniejsza.
  2. LONG zeruje level=0.
  3. REPEAT podczas trzymania: szybkie zwiększanie level (auto-repeat).

Poziom 3 rozbudowa FSM

  1. Dodaj zdarzenie TRIPLE_CLICK (3 kliknięcia w oknie).
  2. Dodaj zdarzenie VERY_LONG (np. 2500 ms) jako „reset systemu” (tu: LED 5 razy).
  3. Zmień FSM tak, aby „długi drugi klik” też generował LONG (wariant logiki).
Co dalej Kolejna lekcja może iść w kierunku: „wielu przycisków + FSM” albo „PWM i sterowanie jasnością jako menu”.