🧩 Lekcja 18: Wyświetlacz 7-segmentowy — multipleks

Multipleksowanie, odświeżanie, migotanie, sterowanie cyframi. W tej lekcji uruchamiasz wszystkie 4 cyfry bez dekodera BCD — tylko tablice segmentów i timer.

1) Cel lekcji

W lekcji 17 sterowałeś jedną cyfrą. Teraz przechodzisz na wyświetlanie 4 cyfr na wspólnej magistrali segmentów. To klasyczny mechanizm stosowany w zegarkach, licznikach, panelach urządzeń.

  • zrozumiesz, na czym polega multipleks 4 cyfr,
  • zrobisz bufor wyświetlania (4 znaki),
  • zrobisz odświeżanie timerem (bez blokowania pętli),
  • poznasz typowe problemy: migotanie, „ghosting”, jasność.
Po tej lekcji: wyświetlasz stabilnie 4 cyfry (np. 1234) i masz gotowy silnik do licznika / zegara / stopera.

2) Na czym polega multipleks?

Masz 8 linii segmentów (a–g, dp) wspólnych dla wszystkich cyfr oraz 4 linie „wyboru cyfry” (CA1..CA4). W danej chwili świeci tylko jedna cyfra, ale przełączasz cyfry tak szybko, że oko widzi je jako świecące jednocześnie.

Cykl multipleksu (4 cyfry):
  1. wyłącz wszystkie cyfry
  2. ustaw segmenty dla cyfry 1
  3. włącz cyfrę 1 na krótko (np. 2 ms)
  4. powtórz dla cyfr 2,3,4

3) Częstotliwość odświeżania i migotanie

Jeśli każda cyfra świeci np. 2 ms, to pełny cykl 4 cyfr trwa 8 ms:

  • częstotliwość odświeżania całego wyświetlacza ≈ 1 / 8 ms ≈ 125 Hz
  • to zwykle wystarcza, aby nie widzieć migotania
W praktyce:
dąż do ~100–300 Hz „pełnego odświeżania” (czyli każda cyfra co kilka ms). Zbyt wolno → migotanie. Zbyt szybko → mniejszy czas świecenia, spadek jasności.

4) Podłączenie: segmenty i cyfry (jak w Twoim schemacie)

Przyjmujemy mapowanie identyczne jak w lekcji 17:

Segmenty (PORTD)

PinSegmentBit w masce
PD0abit0
PD1bbit1
PD2cbit2
PD3dbit3
PD4ebit4
PD5fbit5
PD6gbit6
PD7dpbit7

Wybór cyfr (PORTC, wspólna anoda przez tranzystory PNP)

PinCyfraWłączenie
PC0CA1LOW
PC1CA2LOW
PC2CA3LOW
PC3CA4LOW
Wspólna anoda: segment świeci, gdy na danym segmencie podasz stan LOW. Dlatego maskę „1 = świeci” będziemy negować przed wysłaniem na PORTD.

5) Najważniejszy problem: ghosting (przebłyski)

Ghosting to sytuacja, gdy podczas przełączania cyfr przez ułamek czasu świeci „zły” wzór na „złej” cyfrze. Najczęstsza przyczyna: włączasz cyfrę zanim ustawisz segmenty (albo zmieniasz segmenty, gdy cyfra jest jeszcze włączona).

Zasada anty-ghosting:
1) wyłącz wszystkie cyfry → 2) ustaw segmenty → 3) włącz tylko jedną cyfrę.

6) Tablica cyfr 0–9 i bufor 4 znaków

Jak w lekcji 17, trzymamy tablicę w logice „1 = segment ma świecić” (a potem negujemy na wyjściu).

// bity: 0=a,1=b,2=c,3=d,4=e,5=f,6=g,7=dp
static const uint8_t SEG_DIGIT[10] = {
    0b00111111, // 0
    0b00000110, // 1
    0b01011011, // 2
    0b01001111, // 3
    0b01100110, // 4
    0b01101101, // 5
    0b01111101, // 6
    0b00000111, // 7
    0b01111111, // 8
    0b01101111  // 9
};

Teraz tworzymy bufor 4 pozycji. Każda pozycja to gotowa maska segmentów (już z dp, jeśli chcesz).

volatile uint8_t seg_buf[4] = { 0, 0, 0, 0 }; // "1 = świeci" (przed negacją)

7) Odświeżanie timerem: przerwanie co 1 ms (Timer0 CTC)

Najwygodniej robić multipleks w ISR (przerwaniu), bo:

  • odświeżanie jest stabilne (nie zależy od tego, co robisz w main),
  • main może liczyć, obsługiwać przyciski, logikę, itp.

Założenia

  • przerwanie co 1 ms
  • każde przerwanie przełącza na kolejną cyfrę
  • pełne odświeżanie: 4 ms → ok. 250 Hz (bardzo dobrze)
OCR dla 1 ms:
• F_CPU=8MHz, preskaler 64 → OCR0A=124
• F_CPU=16MHz, preskaler 64 → OCR0A=249

8) Kompletny silnik multipleksu (ISR + bufor)

Poniższy kod:

  • konfiguruje segmenty i linie cyfr,
  • ustawia Timer0 CTC i przerwanie co 1 ms,
  • w ISR: wyłącza cyfry → ustawia segmenty → włącza jedną cyfrę,
  • udostępnia funkcje do wpisania liczby i ustawienia kropek.
#include <avr/io.h>
#include <avr/interrupt.h>

// ====== Segmenty: PD0..PD7 (a,b,c,d,e,f,g,dp) ======
#define SEG_DDR   DDRD
#define SEG_PORT  PORTD

// ====== Cyfry (wspólna anoda przez PNP): PC0..PC3 ======
#define DIG_DDR   DDRC
#define DIG_PORT  PORTC
#define DIG1      PC0
#define DIG2      PC1
#define DIG3      PC2
#define DIG4      PC3

// Tablica cyfr (1 = segment ma świecić)
static const uint8_t SEG_DIGIT[10] = {
    0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110,
    0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111
};

// Bufor 4 pozycji: maska segmentów (1 = świeci)
volatile uint8_t seg_buf[4] = {0,0,0,0};
volatile uint8_t cur_digit = 0;

// ---- pomocnicze: wyłącz wszystkie cyfry (PNP off = HIGH na bazie) ----
static inline void digits_all_off(void)
{
    DIG_PORT |= (1<<DIG1) | (1<<DIG2) | (1<<DIG3) | (1<<DIG4);
}

// ---- włącz jedną cyfrę (PNP on = LOW na bazie) ----
static inline void digit_on(uint8_t idx)
{
    // idx: 0..3
    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;
    }
}

// ---- ustaw segmenty: wspólna anoda => negacja (ON=0) ----
static inline void seg_set(uint8_t mask_on)
{
    SEG_PORT = (uint8_t)~mask_on;
}

// ====== Timer0 CTC 1ms ======
static void timer0_ctc_1ms_init(void)
{
    // CTC: WGM01=1
    TCCR0A = (1<<WGM01);

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

    // USTAW OCR0A zależnie od F_CPU:
    // 8MHz  -> 124
    // 16MHz -> 249
    OCR0A = 124;

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

// ISR: przełączanie cyfr
ISR(TIMER0_COMPA_vect)
{
    // 1) OFF wszystkie cyfry (anty-ghosting)
    digits_all_off();

    // 2) ustaw segmenty dla aktualnej cyfry
    seg_set(seg_buf[cur_digit]);

    // 3) włącz aktualną cyfrę
    digit_on(cur_digit);

    // 4) następna cyfra
    cur_digit++;
    if (cur_digit >= 4) cur_digit = 0;
}

// ====== Inicjalizacja całego modułu ======
static void seg7_mux_init(void)
{
    SEG_DDR = 0xFF;
    SEG_PORT = 0xFF; // wszystko OFF (wspólna anoda)

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

    timer0_ctc_1ms_init();
    sei(); // globalne przerwania
}

// ====== API do bufora ======
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);
}

9) Program testowy: licznik + kropka jako „separator”

Ten przykład wyświetla licznik rosnący co ~100 ms i zapala kropkę na drugiej pozycji. W pętli używamy prostego opóźnienia, ale ważne: multipleks działa w ISR niezależnie.

#include <util/delay.h>

int main(void)
{
    seg7_mux_init();

    uint16_t counter = 0;

    while (1)
    {
        seg7_set_number_0000_9999(counter);

        // kropka na pozycji 1 (druga cyfra), np. jako separator
        // pos: 0 1 2 3
        // tu robimy "dp" na pos=1:
        // najpierw odczyt maski i dopisanie dp
        uint8_t m = seg_buf[1];
        seg_buf[1] = m | (1<<7);

        counter++;
        if (counter > 9999) counter = 0;

        _delay_ms(100);
    }
}
Check: widzisz płynnie zmieniające się 4 cyfry bez migotania. Jeśli migocze: zwiększ częstotliwość odświeżania (np. 1 ms/pozycję jest OK) albo sprawdź zasilanie/połączenia.

10) Typowe problemy i szybkie diagnozy

Objaw: migotanie
  • odświeżanie za wolne (zbyt duży okres w ISR)
  • zbyt ciężki ISR (za dużo operacji)
  • niestabilne zasilanie / luźne przewody
Objaw: „przebłyski” ghosting
  • brak wyłączania cyfr przed zmianą segmentów
  • złe kolejności: najpierw cyfra, potem segmenty
  • zbyt długie przejścia (np. tranzystory + duże pojemności)
Jasność a multipleks:
Każda cyfra świeci tylko 1/4 czasu, więc średnia jasność spada. To normalne. W praktyce dobiera się prądy/rezystory, albo zwiększa czas świecenia (z zachowaniem braku migotania).

11) Rozszerzenia, które robimy w kolejnych krokach

  • blanking (krótka przerwa między cyframi, np. 50–200 µs) dla jeszcze mniejszego ghostingu,
  • obsługa „pustych” cyfr (leading zeros off),
  • wyświetlanie liczb ujemnych i znaków (minus),
  • licznik / stoper oparty o timer (bez _delay_ms).

12) Zadania obowiązkowe

  1. Wyświetl stałą wartość 1234 (bufor 4 pozycji) i upewnij się, że nie migocze.
  2. Zrób funkcję seg7_set_colon_like() – kropka na wybranej pozycji (dp). Przetestuj 4 pozycje.
  3. Zrób licznik 0..9999 zwiększany co 250 ms (multipleks w ISR, logika w main).

13) Zadania dodatkowe (dla lepszych)

  1. Zrób „leading zeros off”: np. 42 ma wyświetlać 42 (pierwsze dwie cyfry puste).
  2. Zrób miganie całego wyświetlacza (np. co 500 ms ON/OFF) bez zatrzymywania multipleksu.
  3. Zrób efekt „przesuwania” liczby: np. 1234 przesuwa się w prawo/lewo co 200 ms.

14) Zadania PRO (projektowe)

  1. Usuń _delay_ms z main: zbuduj scheduler na ticku (lekcja 13) i zwiększaj licznik co X ms.
  2. Dodaj przycisk: klik start/stop licznika, długie przytrzymanie reset (logika z lekcji 11).
  3. Zrób „sterownik jasności” multipleksu: zmieniaj czas świecenia cyfry (duty w ISR), np. 1 ms vs 2 ms.
To jest fundament Twojego licznika.
Od teraz większość projektów z wyświetlaczem to „bufor + ISR multipleks + logika w main”.