⚡ Lekcja 20: Przerwania zewnętrzne

INT0/INT1, maski przerwań, reagowanie na zdarzenia. W tej lekcji przechodzisz z „ciągłego sprawdzania przycisku” na obsługę zdarzeń przez przerwania.

1) Cel lekcji

Przerwania zewnętrzne (INT0/INT1) pozwalają mikrokontrolerowi zareagować natychmiast, gdy na pinie pojawi się określone zbocze (narastające/opadające) lub poziom logiczny.

  • skonfigurujesz INT0 i INT1,
  • nauczysz się ustawiać rodzaj wyzwalania (zbocze/poziom),
  • zrobisz bezpieczny ISR (krótki, bez opóźnień),
  • zbudujesz „flagowy” system zdarzeń: ISR ustawia flagę, main ją obsługuje,
  • zrozumiesz, kiedy INT ma sens, a kiedy wystarczy polling.
Po tej lekcji: potrafisz zrobić przycisk/zdarzenie „na przerwaniu” i podpiąć to pod logikę aplikacji (np. licznik, sceny RGB).

2) INT0 i INT1 na ATmega328P — piny

W ATmega328P zewnętrzne przerwania są na:

  • INT0 → PD2
  • INT1 → PD3
Uwaga praktyczna:
PD2/PD3 mogły być wcześniej używane jako segmenty (np. w 7-seg na PORTD). Jeśli u Ciebie PORTD jest zajęty na segmenty, to przerwania zewnętrzne wykorzystujesz wtedy w innym ćwiczeniu (albo przenosisz segmenty / zmieniasz mapowanie). W tej lekcji skupiamy się na mechanice INT.

3) Tryby wyzwalania: poziom i zbocza

Dla INT0/INT1 wybierasz, kiedy przerwanie ma się uruchomić:

  • LOW level — przerwanie trzymane aktywne, gdy pin jest w stanie 0 (rzadziej używane)
  • Any logical change — każde przejście 0↔1
  • Falling edge — zbocze opadające (1→0)
  • Rising edge — zbocze narastające (0→1)
Dla przycisku z pull-up:
stan spoczynkowy = 1, wciśnięcie = 0 → najczęściej używa się FALLING EDGE.

4) Rejestry: co trzeba ustawić

EICRA
  • ISC01/ISC00 — konfiguracja INT0
  • ISC11/ISC10 — konfiguracja INT1
EIMSK
  • INT0/INT1 — włączenie maski przerwania
W praktyce:
  • ustawiasz pin jako wejście + pull-up
  • ustawiasz tryb wyzwalania w EICRA
  • włączasz INT0/INT1 w EIMSK
  • włączasz globalne przerwania: sei()

5) Zasady pisania ISR (bardzo ważne)

ISR musi być krótki.
Nie używaj _delay_ms(), nie rób długich pętli, nie wypisuj na UART (jeśli go masz). Najczęściej ISR tylko:
  • ustawia flagę,
  • zapisuje licznik czasu,
  • zbiera minimalne dane.

6) Przykład 1: INT0 jako „zdarzenie klik” (flaga)

Konfiguracja: przycisk na PD2 (INT0), pull-up, wyzwalanie na zbocze opadające.

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

#define INT0_DDR    DDRD
#define INT0_PORT   PORTD
#define INT0_PIN    PD2

volatile uint8_t g_int0_event = 0;

static void int0_init_falling_pullup(void)
{
    // PD2 jako wejście + pull-up
    INT0_DDR &= ~(1<<INT0_PIN);
    INT0_PORT |= (1<<INT0_PIN);

    // INT0: zbocze opadające (ISC01=1, ISC00=0)
    EICRA &= ~(1<<ISC00);
    EICRA |=  (1<<ISC01);

    // włącz maskę INT0
    EIMSK |= (1<<INT0);

    // globalne przerwania
    sei();
}

ISR(INT0_vect)
{
    // tylko flaga
    g_int0_event = 1;
}

int main(void)
{
    int0_init_falling_pullup();

    while (1)
    {
        if (g_int0_event)
        {
            g_int0_event = 0;

            // tutaj robisz akcję: zmiana trybu, inkrement, beep, itp.
        }
    }
}
Problem:
Przycisk ma drgania styków, więc może wygenerować wiele przerwań przy jednym kliknięciu. Dlatego potrzebujesz debouncingu „czasowego”.

7) Debounce dla INT: blokada czasowa (lockout)

Najprostsza metoda: po przerwaniu ignorujesz kolejne przerwania przez np. 30 ms. Do tego potrzebujesz ticka czasu (np. z Timer0 jak w lekcjach 18–19).

Wzorzec:
ISR sprawdza now_ms - last_ms. Jeśli za krótko — ignoruje.
// Zakładamy, że masz globalny tick 1 ms:
extern volatile uint32_t g_ms;

volatile uint32_t g_int0_last_ms = 0;
volatile uint8_t  g_int0_click = 0;

#define INT_DEBOUNCE_MS  30

ISR(INT0_vect)
{
    uint32_t now = g_ms;
    if ((now - g_int0_last_ms) < INT_DEBOUNCE_MS)
        return;

    g_int0_last_ms = now;
    g_int0_click = 1;
}

8) Przykład 2: INT0 + licznik z lekcji 19 (start/stop bez pollingu)

Poniżej dostajesz gotowy wariant: INT0 robi START/STOP, a RESET zostaje jako polling (dla porównania). Jeśli chcesz, możesz przenieść RESET na INT1 analogicznie.

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

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

static void timer0_ctc_1ms_init(void)
{
    TCCR0A = (1<<WGM01);
    TCCR0B = (1<<CS01) | (1<<CS00); // /64
    OCR0A  = 124; // 8MHz -> 124, 16MHz -> 249
    TIMSK0 = (1<<OCIE0A);
}

ISR(TIMER0_COMPA_vect)
{
    g_ms++;
}

// ===== INT0: START/STOP =====
#define INT0_DDR   DDRD
#define INT0_PORT  PORTD
#define INT0_PIN   PD2

volatile uint32_t g_int0_last_ms = 0;
volatile uint8_t  g_start_toggle = 0;
#define INT_DEBOUNCE_MS  30

static void int0_init_falling_pullup(void)
{
    INT0_DDR &= ~(1<<INT0_PIN);
    INT0_PORT |= (1<<INT0_PIN);

    EICRA &= ~(1<<ISC00);
    EICRA |=  (1<<ISC01);   // falling
    EIMSK |=  (1<<INT0);
}

ISR(INT0_vect)
{
    uint32_t now = g_ms;
    if ((now - g_int0_last_ms) < INT_DEBOUNCE_MS) return;
    g_int0_last_ms = now;

    g_start_toggle = 1; // zdarzenie do main
}

// ===== RESET jako polling (przykładowo) =====
#define BTN_DDR     DDRB
#define BTN_PORT    PORTB
#define BTN_PINREG  PINB
#define BTN_RESET   PB2

static inline uint8_t reset_pressed(void)
{
    return ((BTN_PINREG & (1<<BTN_RESET)) == 0);
}

int main(void)
{
    // reset input + pull-up
    BTN_DDR &= ~(1<<BTN_RESET);
    BTN_PORT |= (1<<BTN_RESET);

    timer0_ctc_1ms_init();
    int0_init_falling_pullup();

    sei();

    uint16_t count = 0;
    uint8_t running = 0;
    uint32_t last_step = 0;

    while (1)
    {
        uint32_t now = g_ms;

        if (g_start_toggle)
        {
            g_start_toggle = 0;
            running ^= 1;
        }

        if (reset_pressed())
        {
            // prosta blokada aby nie „mieliło” (polling)
            count = 0;
            while (reset_pressed()) { }
        }

        if (running && (now - last_step >= 1000))
        {
            last_step = now;
            count++;
            if (count > 9999) count = 0;
        }

        // tutaj: aktualizacja wyświetlacza/bufora (jak w lekcji 19)
    }
}
Najważniejsze:
ISR nie robi logiki — tylko wystawia zdarzenie. Logika jest w main.

9) INT1: analogicznie do INT0

INT1 jest na PD3. Ustawiasz ISC11/ISC10 w EICRA i bit INT1 w EIMSK.

// INT1: zbocze opadające
EICRA &= ~(1<<ISC10);
EICRA |=  (1<<ISC11);
EIMSK |=  (1<<INT1);

ISR(INT1_vect)
{
    // analogicznie: debounce + flaga
}

10) Kiedy INT ma sens, a kiedy polling?

INT (przerwania)
  • zdarzenia rzadkie, szybka reakcja
  • wybudzanie z uśpienia
  • impulsy z czujników (enkoder, licznik)
Polling
  • proste UI, pętla i tak pracuje szybko
  • wiele przycisków na różnych pinach
  • łatwiejsze debugowanie na start
Uczciwie:
Dla 1–2 przycisków w prostym projekcie polling + debounce jest często wystarczający. INT pokazuje „event-driven firmware” i przydaje się, gdy CPU ma robić coś ciężkiego albo ma spać.

11) Zadania obowiązkowe

  1. Uruchom INT0 na PD2 dla przycisku (pull-up, FALLING). W ISR ustaw flagę, a w main zapal/gaś LED.
  2. Dodaj debounce w ISR (lockout 30 ms) na bazie tick 1 ms.
  3. Zrób INT0 = START/STOP w liczniku (z lekcji 19), a RESET zostaw jako polling.

12) Zadania dodatkowe (dla lepszych)

  1. Przenieś RESET na INT1 (PD3) i zrób: klik=reset, long=reset+stop (wzorując się na lekcji 19).
  2. Zrób licznik zdarzeń: INT0 zwiększa licznik „klików” i pokazuje go na 7-seg.
  3. Zrób „double click”: 2 kliknięcia w 300 ms przełączają tryb.

13) Zadania PRO (projektowe)

  1. Wejdź w tryb uśpienia (sleep) i wybudzaj się INT0 (przycisk) — tylko koncepcja + kod inicjalizacji.
  2. Zrób obsługę enkodera: A na INT0, B jako zwykłe wejście, zliczaj kierunek.
  3. Zbuduj „event queue”: ISR zapisuje zdarzenia do bufora pierścieniowego, main je czyta.