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.
2) INT0 i INT1 na ATmega328P — piny
W ATmega328P zewnętrzne przerwania są na:
- INT0 → PD2
- INT1 → PD3
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)
stan spoczynkowy = 1, wciśnięcie = 0 → najczęściej używa się FALLING EDGE.
4) Rejestry: co trzeba ustawić
- ISC01/ISC00 — konfiguracja INT0
- ISC11/ISC10 — konfiguracja INT1
- INT0/INT1 — włączenie maski przerwania
- 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)
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.
}
}
}
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).
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)
}
}
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?
- zdarzenia rzadkie, szybka reakcja
- wybudzanie z uśpienia
- impulsy z czujników (enkoder, licznik)
- proste UI, pętla i tak pracuje szybko
- wiele przycisków na różnych pinach
- łatwiejsze debugowanie na start
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
- Uruchom INT0 na PD2 dla przycisku (pull-up, FALLING). W ISR ustaw flagę, a w main zapal/gaś LED.
- Dodaj debounce w ISR (lockout 30 ms) na bazie tick 1 ms.
- Zrób INT0 = START/STOP w liczniku (z lekcji 19), a RESET zostaw jako polling.
12) Zadania dodatkowe (dla lepszych)
- Przenieś RESET na INT1 (PD3) i zrób: klik=reset, long=reset+stop (wzorując się na lekcji 19).
- Zrób licznik zdarzeń: INT0 zwiększa licznik „klików” i pokazuje go na 7-seg.
- Zrób „double click”: 2 kliknięcia w 300 ms przełączają tryb.
13) Zadania PRO (projektowe)
- Wejdź w tryb uśpienia (sleep) i wybudzaj się INT0 (przycisk) — tylko koncepcja + kod inicjalizacji.
- Zrób obsługę enkodera: A na INT0, B jako zwykłe wejście, zliczaj kierunek.
- Zbuduj „event queue”: ISR zapisuje zdarzenia do bufora pierścieniowego, main je czyta.