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.
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.
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.
krótkie kliknięcie = drobna akcja, długie = „hard reset” / tryb serwisowy.
8) Zadania obowiązkowe
- Zmień krok zliczania: zamiast co 1 s zliczaj co 100 ms (tzn. 0.1 s).
- Dodaj drugi tryb: START (long press) ustawia count=0 i od razu RUN.
- Dodaj „miganie całego wyświetlacza” gdy licznik jest zatrzymany (np. co 1 s ON/OFF).
9) Zadania dodatkowe (dla lepszych)
- Dodaj przyspieszenie: jeśli START jest wciśnięty w RUN, licznik przyspiesza do 10 Hz.
- Dodaj tryb „odliczanie w dół”: klik RESET w STOP przełącza UP/DOWN.
- Dodaj dźwięk (brzęczyk z lekcji 15): beep przy klikach i długi beep przy long press.
10) Zadania PRO (projektowe)
- Zrób „stoper”: w RUN zlicza ms (np. wyświetl XX.XX z kropką jako separator).
- Zrób „timer”: ustawianie wartości przyciskami, potem odliczanie do 0000 i alarm.
- 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ć.
pamiętaj o OCR0A dla 1 ms. Bez tego: tick będzie zły, debounce będzie zły, a multipleks może migotać.