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).
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 |
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)
}
}
- 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
- Zmień mapowanie: CLICK → zmienia tryb blink slow, DOUBLE → OFF/ON, LONG → blink fast.
- Dodaj tryb MODE_ON jako stałe świecenie, a OFF jako gaśnięcie.
- W trybie blink slow ustaw okres na 700 ms zamiast 500 ms.
Poziom 2 menu i wartości
- Utwórz zmienną uint8_t level (0..10). CLICK zwiększa, DOUBLE zmniejsza.
- LONG zeruje level=0.
- REPEAT podczas trzymania: szybkie zwiększanie level (auto-repeat).
Poziom 3 rozbudowa FSM
- Dodaj zdarzenie TRIPLE_CLICK (3 kliknięcia w oknie).
- Dodaj zdarzenie VERY_LONG (np. 2500 ms) jako „reset systemu” (tu: LED 5 razy).
- Zmień FSM tak, aby „długi drugi klik” też generował LONG (wariant logiki).