🧮 Lekcja 19: Licznik z przyciskami

Zliczanie, reset, start/stop, ergonomia obsługi. Składasz całość: multipleks 7-seg + logika przycisków + odmierzanie czasu bez blokowania.

1) Cel lekcji

Budujesz licznik 0–9999 wyświetlany na 4-cyfrowym 7-seg, sterowany przyciskami:

  • START/STOP — przełącza tryb pracy licznika,
  • RESET — zeruje licznik (krótko) lub zeruje i zatrzymuje (długo),
  • licznik zwiększa się co ustalony czas (np. 1 s),
  • obsługa jest odporna na drgania styków i wygodna w użyciu.
Po tej lekcji: masz działający projekt „panelu”: wyświetlacz + przyciski + logika, gotowy do rozbudowy (stoper, timer, menu).

2) Założenia sprzętowe (zgodne z Twoim schematem)

Zakładamy identyczne mapowanie 7-seg jak w lekcjach 17–18:

  • segmenty PD0..PD7 (a,b,c,d,e,f,g,dp),
  • cyfry (wspólna anoda przez PNP) PC0..PC3 (CA1..CA4),
  • logika wspólnej anody: segment świeci przy stanie LOW.
Przyciski:
W tej lekcji przyjmuję 2 przyciski podłączone do wejść z pull-up (stan spoczynkowy = 1, wciśnięty = 0). Jeśli na Twoim schemacie masz inne piny dla przycisków, w kodzie zmieniasz tylko definicje pinów.

3) Architektura programu (profesjonalnie i czytelnie)

Dzielimy firmware na 3 „warstwy”:

Warstwa 1 — silnik wyświetlacza (ISR)
  • odświeża multipleks co 1 ms
  • czyta bufor 4 pozycji
  • nie zawiera logiki przycisków
Warstwa 2 — tick 1 ms (ISR)
  • licznik czasu (ms)
  • na jego bazie debouncing i long-press
Warstwa 3 — logika aplikacji (main)
  • START/STOP, RESET, auto-zliczanie co 1s
  • aktualizacja bufora wyświetlacza
  • ergonomia (klik vs długie przytrzymanie)

4) Multipleks + tick: jedno ISR, dwie funkcje

Najprościej: Timer0 robi przerwanie co 1 ms. W ISR:

  • zwiększamy g_ms (system tick),
  • robimy jedno przełączenie cyfry multipleksu.
Korzyść: cała reszta programu nie używa _delay_ms() do „czekania na czas”.

5) Debounce i zdarzenia przycisku

W praktyce chcesz dostawać „zdarzenia”:

  • PRESS — wykryto stabilne wciśnięcie
  • RELEASE — wykryto puszczenie
  • CLICK — krótkie wciśnięcie
  • LONG — długie przytrzymanie (np. > 800 ms)
W tej lekcji:
implementujemy prosty, bardzo skuteczny debounce oparty o tick 1 ms.

6) Pełny program lekcji (wyświetlacz + 2 przyciski + licznik)

Funkcjonalność:

  • przycisk START/STOP: klik przełącza liczenie ON/OFF
  • przycisk RESET:
    • klik: licznik = 0 (bez zmiany stanu RUN)
    • długie przytrzymanie (≥ 800 ms): licznik = 0 i STOP
  • zliczanie co 1 s, zakres 0..9999
  • dwukropek „pseudo”: kropka miga na pozycji 1 co 500 ms (sygnalizacja pracy)
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>

// ===================== 7-seg: PORTD segmenty, PORTC cyfry =====================
#define SEG_DDR   DDRD
#define SEG_PORT  PORTD

#define DIG_DDR   DDRC
#define DIG_PORT  PORTC
#define DIG1      PC0
#define DIG2      PC1
#define DIG3      PC2
#define DIG4      PC3

// 1 = segment ma świecić (przed negacją na wspólnej anodzie)
static const uint8_t SEG_DIGIT[10] = {
    0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110,
    0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111
};

volatile uint8_t seg_buf[4] = {0,0,0,0};
volatile uint8_t cur_digit = 0;

// ===================== Tick czasu =====================
volatile uint32_t g_ms = 0;

// ===================== Przyciski (zmień piny jeśli masz inne) =====================
#define BTN_DDR     DDRB
#define BTN_PORT    PORTB
#define BTN_PINREG  PINB

#define BTN_START   PB0   // START/STOP
#define BTN_RESET   PB2   // RESET

// stan wciśnięcia (pull-up): 0 = wciśnięty
static inline uint8_t btn_raw_pressed(uint8_t pin)
{
    return ((BTN_PINREG & (1<<pin)) == 0);
}

// ===================== Pomocnicze: cyfry on/off =====================
static inline void digits_all_off(void)
{
    DIG_PORT |= (1<<DIG1) | (1<<DIG2) | (1<<DIG3) | (1<<DIG4);
}
static inline void digit_on(uint8_t idx)
{
    switch (idx)
    {
        case 0: DIG_PORT &= ~(1<<DIG1); break;
        case 1: DIG_PORT &= ~(1<<DIG2); break;
        case 2: DIG_PORT &= ~(1<<DIG3); break;
        default: DIG_PORT &= ~(1<<DIG4); break;
    }
}
static inline void seg_set(uint8_t mask_on)
{
    // wspólna anoda: ON=0
    SEG_PORT = (uint8_t)~mask_on;
}

// ===================== Timer0 CTC 1ms =====================
static void timer0_ctc_1ms_init(void)
{
    // CTC
    TCCR0A = (1<<WGM01);
    // preskaler /64
    TCCR0B = (1<<CS01) | (1<<CS00);

    // OCR dla 1ms:
    // 8MHz  -> 124
    // 16MHz -> 249
    OCR0A = 124;

    TIMSK0 = (1<<OCIE0A);
}

// ISR: tick + multipleks
ISR(TIMER0_COMPA_vect)
{
    g_ms++;

    digits_all_off();
    seg_set(seg_buf[cur_digit]);
    digit_on(cur_digit);

    cur_digit++;
    if (cur_digit >= 4) cur_digit = 0;
}

// ===================== 7-seg API =====================
static inline void seg7_set_digit(uint8_t pos, uint8_t digit, uint8_t dp_on)
{
    if (pos > 3) return;
    if (digit > 9) digit = 0;

    uint8_t m = SEG_DIGIT[digit];
    if (dp_on) m |= (1<<7);
    seg_buf[pos] = m;
}

static void seg7_set_number_0000_9999(uint16_t v)
{
    if (v > 9999) v = 9999;

    seg7_set_digit(3, v % 10, 0); v /= 10;
    seg7_set_digit(2, v % 10, 0); v /= 10;
    seg7_set_digit(1, v % 10, 0); v /= 10;
    seg7_set_digit(0, v % 10, 0);
}

// leading zeros off (opcjonalnie, ale przydatne)
static void seg7_set_number_trim(uint16_t v)
{
    if (v > 9999) v = 9999;

    uint8_t d3 = v % 10; v /= 10;
    uint8_t d2 = v % 10; v /= 10;
    uint8_t d1 = v % 10; v /= 10;
    uint8_t d0 = v % 10;

    // ustaw cyfry
    seg_buf[0] = SEG_DIGIT[d0];
    seg_buf[1] = SEG_DIGIT[d1];
    seg_buf[2] = SEG_DIGIT[d2];
    seg_buf[3] = SEG_DIGIT[d3];

    // wygaszenie wiodących zer (zostaw co najmniej jedną cyfrę)
    if (d0 == 0) seg_buf[0] = 0;
    if (d0 == 0 && d1 == 0) seg_buf[1] = 0;
    if (d0 == 0 && d1 == 0 && d2 == 0) seg_buf[2] = 0;
    // seg_buf[3] zawsze zostaje (ostatnia cyfra)
}

// ===================== Obsługa przycisków (debounce + click + long) =====================
typedef struct {
    uint8_t stable;          // 0/1: stabilny stan wciśnięcia
    uint8_t last_raw;
    uint16_t stable_cnt;     // ms stabilności
    uint32_t press_time_ms;  // czas wciśnięcia (start)
    uint8_t click;           // flaga kliknięcia
    uint8_t long_press;      // flaga long press
    uint8_t consumed_long;   // aby long nie powtarzał się
} button_t;

#define DEBOUNCE_MS   20
#define LONG_MS       800

static void button_init(button_t* b)
{
    b->stable = 0;
    b->last_raw = 1; // spoczynkowo nie wciśnięty (raw=1 przy pull-up)
    b->stable_cnt = 0;
    b->press_time_ms = 0;
    b->click = 0;
    b->long_press = 0;
    b->consumed_long = 0;
}

static void button_update(button_t* b, uint8_t raw_pressed, uint32_t now_ms)
{
    // raw_pressed: 1 = wciśnięty, 0 = nie wciśnięty
    if (raw_pressed == b->last_raw)
    {
        if (b->stable_cnt <= 1000) b->stable_cnt++;
    }
    else
    {
        b->stable_cnt = 0;
        b->last_raw = raw_pressed;
    }

    // gdy stan utrzymał się DEBOUNCE_MS, uznaj go za stabilny
    if (b->stable_cnt == DEBOUNCE_MS)
    {
        // zmiana stabilnego stanu?
        if (raw_pressed != b->stable)
        {
            b->stable = raw_pressed;

            if (b->stable) // właśnie wciśnięty
            {
                b->press_time_ms = now_ms;
                b->consumed_long = 0;
            }
            else // właśnie puszczony
            {
                // jeśli nie było long, to klik
                if (!b->consumed_long)
                    b->click = 1;
            }
        }
    }

    // long press
    if (b->stable && !b->consumed_long)
    {
        if ((now_ms - b->press_time_ms) >= LONG_MS)
        {
            b->long_press = 1;
            b->consumed_long = 1;
        }
    }
}

static inline uint8_t button_get_click(button_t* b)
{
    if (b->click) { b->click = 0; return 1; }
    return 0;
}
static inline uint8_t button_get_long(button_t* b)
{
    if (b->long_press) { b->long_press = 0; return 1; }
    return 0;
}

// ===================== MAIN =====================
int main(void)
{
    // 7-seg init
    SEG_DDR = 0xFF;
    SEG_PORT = 0xFF;

    DIG_DDR |= (1<<DIG1) | (1<<DIG2) | (1<<DIG3) | (1<<DIG4);
    digits_all_off();

    // przyciski: wejścia + pull-up
    BTN_DDR &= ~((1<<BTN_START) | (1<<BTN_RESET));
    BTN_PORT |= (1<<BTN_START) | (1<<BTN_RESET);

    // timer tick + multipleks
    timer0_ctc_1ms_init();
    sei();

    // logika licznika
    uint16_t count = 0;
    uint8_t running = 0;

    // timery aplikacji
    uint32_t last_step_ms = 0;
    uint32_t last_blink_ms = 0;
    uint8_t blink = 0;

    // przyciski
    button_t b_start, b_reset;
    button_init(&b_start);
    button_init(&b_reset);

    while (1)
    {
        uint32_t now = g_ms;

        // --- aktualizacja przycisków ---
        button_update(&b_start, btn_raw_pressed(BTN_START), now);
        button_update(&b_reset, btn_raw_pressed(BTN_RESET), now);

        // --- START/STOP: klik ---
        if (button_get_click(&b_start))
        {
            running ^= 1;
        }

        // --- RESET: klik / long ---
        if (button_get_click(&b_reset))
        {
            count = 0;
        }
        if (button_get_long(&b_reset))
        {
            count = 0;
            running = 0;
        }

        // --- zliczanie co 1000 ms ---
        if (running && (now - last_step_ms >= 1000))
        {
            last_step_ms = now;
            count++;
            if (count > 9999) count = 0;
        }

        // --- miganie kropki jako sygnał pracy (co 500 ms) ---
        if (now - last_blink_ms >= 500)
        {
            last_blink_ms = now;
            blink ^= 1;
        }

        // --- wyświetlanie (bufor) ---
        // możesz wybrać: seg7_set_number_0000_9999(count);
        seg7_set_number_trim(count);

        // dp na pozycji 1 (druga cyfra) tylko gdy RUNNING i blink=1
        if (running && blink)
            seg_buf[1] |= (1<<7);
        else
            seg_buf[1] &= ~(1<<7);

        // main nie blokuje — ISR odświeża wyświetlacz
    }
}
Co powinno działać:
  • START/STOP przełącza liczenie (kropka miga tylko w RUN),
  • RESET (klik) zeruje licznik,
  • RESET (przytrzymanie ~0.8 s) zeruje i zatrzymuje.

7) Ergonomia obsługi — dlaczego tak?

  • Start/Stop jako toggle jest szybki i intuicyjny.
  • Reset klik jest bezpieczny (nie zmienia trybu pracy).
  • Reset long jest „awaryjny”: zawsze doprowadza do stanu bazowego (0 i STOP).
To jest wzorzec UI w embedded:
krótkie kliknięcie = drobna akcja, długie = „hard reset” / tryb serwisowy.

8) Zadania obowiązkowe

  1. Zmień krok zliczania: zamiast co 1 s zliczaj co 100 ms (tzn. 0.1 s).
  2. Dodaj drugi tryb: START (long press) ustawia count=0 i od razu RUN.
  3. Dodaj „miganie całego wyświetlacza” gdy licznik jest zatrzymany (np. co 1 s ON/OFF).

9) Zadania dodatkowe (dla lepszych)

  1. Dodaj przyspieszenie: jeśli START jest wciśnięty w RUN, licznik przyspiesza do 10 Hz.
  2. Dodaj tryb „odliczanie w dół”: klik RESET w STOP przełącza UP/DOWN.
  3. Dodaj dźwięk (brzęczyk z lekcji 15): beep przy klikach i długi beep przy long press.

10) Zadania PRO (projektowe)

  1. Zrób „stoper”: w RUN zlicza ms (np. wyświetl XX.XX z kropką jako separator).
  2. Zrób „timer”: ustawianie wartości przyciskami, potem odliczanie do 0000 i alarm.
  3. Zrób menu: 3 tryby (Counter / Stopwatch / Timer) przełączane long press START.
Jeśli używasz innego F_CPU:
pamiętaj o OCR0A dla 1 ms. Bez tego: tick będzie zły, debounce będzie zły, a multipleks może migotać.