Gdy czujnik wykrywa ruch, timer odmierza konkretny moment albo użytkownik naciska przycisk, procesor nie powinien bez końca sprawdzać każdego wejścia w pętli programu. Przerwania systemowe pozwalają zgłosić zdarzenie natychmiast i przekazać sterowanie do krótkiej procedury obsługi. Wyjaśniam, jak działa ten mechanizm w mikrokontrolerach i minikomputerach, kiedy zastępuje odpytywanie, a także jak uniknąć blokowania programu i gubienia danych.
Najważniejsze informacje o obsłudze przerwań
- Przerwanie zatrzymuje bieżący fragment programu na czas obsługi pilnego zdarzenia.
- Źródłem może być pin GPIO, timer, UART, ADC, DMA albo błąd procesora.
- Procedura ISR powinna być krótka, przewidywalna i nieblokująca.
- W mikrokontrolerze reakcja jest zwykle bezpośrednia, a w minikomputerze z Linuksem pośredniczy w niej jądro systemu.
- Najczęstsze problemy to drgania styków, niezerowane flagi, zbyt długi handler i niewłaściwe priorytety.
Co właściwie przerywa pracę procesora
Przerwanie jest sygnałem, który mówi procesorowi, że zaszło zdarzenie wymagające szybkiej reakcji. Układ zapisuje niezbędny stan, odczytuje adres odpowiedniej procedury z tablicy wektorów i wykonuje funkcję obsługi, nazywaną ISR. Po jej zakończeniu wraca do miejsca, w którym przerwał główny program.
W praktyce nie oznacza to, że procesor przestaje działać całkowicie. Zawiesza tylko aktualnie wykonywany kod na czas obsługi zdarzenia. Jeśli handler zajmuje kilka mikrosekund, użytkownik zwykle tego nie zauważy. Jeśli trwa kilka milisekund i blokuje kolejne źródła, skutki mogą być już bardzo konkretne: utrata bajtów z UART, opóźnienia sterowania silnikiem albo niestabilny odczyt enkodera.
Sprzętowe i programowe źródła zdarzeń
Przerwanie sprzętowe pochodzi z urządzenia albo peryferium. Może je wywołać zbocze na wejściu GPIO, przepełnienie timera, odebranie bajtu przez UART, zakończenie konwersji ADC czy sygnał z kontrolera DMA. Taki mechanizm ma sens wtedy, gdy liczy się czas reakcji albo zdarzenie występuje nieregularnie.
Przerwanie programowe jest zgłaszane przez instrukcję procesora, system operacyjny lub kod aplikacji. W mikrokontrolerze może służyć do wywołania usługi systemowej albo testowania wyjątków, natomiast w minikomputerze podobną rolę pełnią między innymi wyjątki procesora i mechanizmy jądra. Trzeba odróżnić je od zwykłego wywołania funkcji, ponieważ przerwanie zmienia bieżący kontekst wykonania i podlega regułom priorytetów.
Maskowalne, niemaskowalne i wyjątkowe
Większość przerwań jest maskowalna, czyli program może je czasowo zablokować. Przydaje się to podczas krótkiej operacji na danych współdzielonych, ale wyłączenie przerwań na długo jest ryzykowne. Procesor może wtedy nie odebrać danych z urządzenia albo zareagować zbyt późno na zdarzenie czasowe.
Przerwanie niemaskowalne, znane jako NMI, jest zarezerwowane dla sytuacji szczególnie ważnych, na przykład poważnego błędu sprzętowego. Osobną grupę tworzą wyjątki, takie jak dzielenie przez zero, błąd dostępu do pamięci czy nieprawidłowa instrukcja. One również zmieniają przepływ programu, ale wynikają z działania procesora, a niekoniecznie z sygnału zewnętrznego urządzenia.
Jak zdarzenie trafia do procedury obsługi
Droga od sygnału do wykonania kodu jest krótka, ale składa się z kilku etapów. Peryferium ustawia flagę zdarzenia, kontroler przerwań sprawdza, czy dana linia jest włączona, a procesor wybiera zgłoszenie zgodnie z priorytetem. Następnie zapisuje część rejestrów na stosie i przechodzi pod adres przypisany do konkretnego wektora.
W układach z rodziny Cortex-M za wybór i priorytety odpowiada zwykle NVIC, czyli Nested Vectored Interrupt Controller. Dzięki niemu przerwania mogą być zagnieżdżane, a zdarzenie o wyższym priorytecie może wyprzedzić obsługę mniej pilnego. Nie każdy projekt potrzebuje zagnieżdżania, ale dobrze ustawiona hierarchia ma znaczenie w sterowaniu czasu rzeczywistego.
Flaga, wektor i ISR
Trzy elementy często decydują o tym, czy program będzie działał stabilnie. Flaga informuje, że zdarzenie wystąpiło, wektor wskazuje właściwą funkcję, a ISR wykonuje minimalny zestaw operacji potrzebnych do jego obsłużenia. W zależności od peryferium flagę trzeba wyzerować programowo albo przez odczyt i zapis konkretnego rejestru.
Jeżeli flaga pozostanie ustawiona, procedura może uruchamiać się ponownie bez końca. To jeden z typowych błędów w projektach STM32, AVR i innych mikrokontrolerów. Objawem bywa zatrzymanie programu w ISR, pozornie losowy reset albo całkowity brak czasu dla pętli głównej.
Priorytet nie rozwiązuje każdego problemu
Wyższy priorytet oznacza szybszą możliwość obsługi, ale nie zwiększa przepustowości procesora. Gdy zdarzeń jest za dużo, nawet najlepiej ustawione priorytety nie pomogą. Wtedy trzeba ograniczyć liczbę operacji w ISR, wykorzystać DMA albo przenieść część pracy do zadania wykonywanego poza procedurą.
Ważne jest też rozróżnienie między latencją a czasem obsługi. Latencja to czas od pojawienia się sygnału do wejścia do ISR, natomiast czas obsługi obejmuje wykonanie kodu handlera. Oba parametry mierzę osobno, najlepiej przez zmianę stanu GPIO i analizę przebiegu oscyloskopem lub analizatorem logicznym.
Mikrokontroler i minikomputer działają tu inaczej
W mikrokontrolerze program często działa bezpośrednio na sprzęcie, bez rozbudowanego systemu operacyjnego. Zdarzenie z timera albo pinu trafia do kontrolera przerwań, a kod użytkownika może zareagować w bardzo przewidywalnym czasie. To dlatego mikrokontrolery dobrze sprawdzają się w sterowaniu silnikami, pomiarach i urządzeniach z ograniczonym budżetem energetycznym.
Minikomputer, taki jak Raspberry Pi, zwykle uruchamia Linuksa. Aplikacja nie obsługuje wtedy sygnału całkowicie samodzielnie. Jądro systemu odbiera przerwanie, zarządza sterownikiem GPIO lub innego urządzenia, a program użytkownika dostaje zdarzenie przez API, deskryptor pliku, kolejkę albo mechanizm biblioteki.
| Cecha | Mikrokontroler | Minikomputer z Linuksem |
|---|---|---|
| Warstwa obsługi | Firmware i kontroler przerwań | Jądro, sterownik i aplikacja |
| Przewidywalność czasu | Zwykle wysoka | Zależna od obciążenia systemu |
| Typowe zastosowanie | Timer, PWM, enkoder, UART | GPIO, sieć, pliki, kamera, automatyka |
| Reakcja aplikacji | Bezpośrednia przez ISR | Pośrednia przez interfejs systemowy |
Ta różnica ma praktyczne konsekwencje. Jeżeli potrzebuję reakcji z dokładnością mikrosekund, wybieram mikrokontroler albo dedykowany układ pomocniczy. Minikomputer jest wygodniejszy, gdy oprócz sygnału trzeba obsłużyć sieć, bazę danych, interfejs użytkownika lub analizę obrazu, ale nie traktuję zwykłego programu w Linuksie jako twardego systemu czasu rzeczywistego.
Polling czy przerwanie w projekcie elektronicznym
Odpytywanie, czyli polling, polega na regularnym sprawdzaniu stanu urządzenia. Jest prostsze do zrozumienia i często wystarcza, gdy zdarzenie pojawia się często, a pętla programu ma stały i krótki czas wykonania. Przerwanie wygrywa wtedy, gdy sygnał jest rzadki, krótki albo nie da się przewidzieć jego momentu wystąpienia.
| Kryterium | Polling | Przerwanie |
|---|---|---|
| Implementacja | Prosta i łatwa do śledzenia | Wymaga konfiguracji wektora, flag i priorytetu |
| Reakcja na krótkie impulsy | Może je pominąć | Może je zarejestrować |
| Obciążenie procesora | Rośnie wraz z częstotliwością sprawdzania | Zwykle mniejsze przy rzadkich zdarzeniach |
| Przewidywalność programu | Łatwiejsza w prostych układach | Trudniejsza przy wielu źródłach |
Nie stosuję przerwań automatycznie do każdego przycisku. Dla prostego interfejsu z odczytem co 10 lub 20 ms polling może być czytelniejszy i odporniejszy na drgania styków. Z kolei licznik impulsów, wejście awaryjne albo odbiór danych z UART zwykle potrzebują mechanizmu zdarzeniowego.
Drgania styków potrafią udawać wiele naciśnięć
Mechaniczny przycisk po naciśnięciu nie zmienia stanu idealnie raz. Przez kilka milisekund styk może wielokrotnie zwierać i rozwierać obwód, a każde takie zbocze wywoła kolejne wejście do ISR. Rozwiązaniem jest debouncing, czyli filtracja programowa lub sprzętowa.
W prostym układzie handler zapisuje tylko czas ostatniego zdarzenia, a resztę wykonuje pętla główna po sprawdzeniu, czy minęło na przykład 20-50 ms. Przy szybkich enkoderach taki filtr może jednak zgubić prawidłowe impulsy, dlatego jego wartość trzeba dobrać do częstotliwości sygnału, a nie kopiować bez namysłu z przykładu dla przycisku.
Jak pisać ISR, żeby nie zatrzymać całego układu
Najbezpieczniejszy handler robi niewiele. Odczytuje rejestr, kasuje flagę, zapisuje dane do bufora kołowego lub ustawia znacznik, po czym kończy działanie. Cięższe obliczenia, komunikaty tekstowe, zapis na kartę SD i transmisję przez sieć przenoszę do kodu głównego albo zadania systemowego.
volatile bool dane_gotowe = false;
volatile uint8_t odebrany_bajt;
void UART_IRQHandler(void) {
odebrany_bajt = UART_READ();
dane_gotowe = true;
}
int main(void) {
while (1) {
if (dane_gotowe) {
dane_gotowe = false;
przetworz_bajt(odebrany_bajt);
}
}
}
Przykład pokazuje prosty wzorzec, ale w prawdziwej transmisji jeden bajt może nadejść przed przetworzeniem poprzedniego. Wtedy pojedyncza zmienna nie wystarczy i potrzebny jest bufor kołowy. Zmienna używana zarówno w ISR, jak i w kodzie głównym powinna być odpowiednio oznaczona, lecz samo słowo `volatile` nie zapewnia atomowości operacji.
Czego nie robić w procedurze przerwania
- Nie wywoływać długich opóźnień ani funkcji oczekujących na gotowość urządzenia.
- Nie wykonywać rozbudowanego formatowania tekstu i zapisu na nośnik danych.
- Nie używać bez sprawdzenia funkcji, które same blokują przerwania.
- Nie modyfikować współdzielonych danych bez ochrony przed częściowym odczytem.
- Nie zakładać, że zdarzenia nie pojawią się ponownie podczas obsługi.
Najczęściej zaczynam od pytania, co musi wydarzyć się natychmiast, a co może poczekać kilka milisekund. Do ISR trafia tylko pierwsza grupa. Taki podział zwykle daje większą poprawę stabilności niż samo podnoszenie priorytetu przerwania.
Praktyczne przykłady i diagnozowanie problemów
W projekcie z Arduino przerwanie zewnętrzne może zliczać impulsy z enkodera albo reagować na zmianę stanu czujnika. Funkcja obsługi powinna zwiększyć licznik lub ustawić znacznik, a obliczenie prędkości i prezentację wyniku lepiej wykonać poza ISR. Przy odczycie licznika wielobajtowego trzeba dodatkowo zadbać o spójność danych.
W STM32 często łączy się przerwanie timera z okresowym zadaniem sterującym. Timer zgłasza zdarzenie co 1 ms, handler aktualizuje zegar programu, a pętla główna wykonuje zadania wynikające z ustawionych znaczników. Taki model jest prosty, dopóki wszystkie operacje mieszczą się w założonym budżecie czasowym.
Na minikomputerze przerwanie GPIO może informować aplikację o zmianie stanu czujnika, ale nie powinno sterować precyzyjnym przebiegiem PWM z poziomu zwykłego procesu użytkownika. Jeżeli potrzebny jest dokładny timing, warto przekazać tę część do mikrokontrolera, a minikomputer zostawić do komunikacji, wizualizacji i logiki wysokiego poziomu.
Przeczytaj również: Cyfrowy potencjometr z Arduino - Podłącz i steruj bez błędów!
Co sprawdzić, gdy przerwanie nie działa
- Sprawdź, czy źródło zdarzenia jest fizycznie podłączone i ma poprawny poziom logiczny.
- Zweryfikuj, czy włączono zegar peryferium, linię przerwania i globalną obsługę przerwań.
- Ustal, czy zdarzenie wyzwala zbocze narastające, opadające, oba zbocza czy poziom.
- Sprawdź flagę w rejestrze oraz sposób jej kasowania opisany w dokumentacji układu.
- Dodaj licznik wejść do ISR albo zmień stan pinu GPIO, aby zmierzyć rzeczywistą częstotliwość.
Jeśli handler wykonuje się bez końca, najpierw szukam niezerowanej flagi. Gdy zdarzenia giną, sprawdzam przepełnienie bufora, czas blokowania przerwań i to, czy kilka źródeł nie korzysta z jednej linii. Oscyloskop często pokazuje problem szybciej niż wielominutowe śledzenie logów.
Dobry projekt zaczyna się od budżetu czasu
Obsługa przerwań jest skuteczna wtedy, gdy znamy częstotliwość zdarzeń i maksymalny czas reakcji. Dla każdego źródła warto zapisać, ile razy na sekundę może się pojawić, jak długo może trwać ISR i ile danych trzeba przechować, gdy kod główny chwilowo nie nadąża.
Nie każde zdarzenie wymaga najwyższego priorytetu. Sygnał awaryjny, kontrola silnika i odbiór strumienia danych mogą mieć różne wymagania, dlatego priorytety ustalam na podstawie konsekwencji opóźnienia. Najważniejsza jest przewidywalność, a nie samo wrażenie, że procesor reaguje natychmiast.
Jeżeli projekt zaczyna gubić impulsy, zwiększanie liczby przerwań zwykle nie jest właściwą drogą. Czasem lepszy okaże się DMA, sprzętowy licznik, kontroler zdarzeń albo osobny mikrokontroler. Dobrze zaprojektowane przerwanie pozostaje małe, mierzalne i podporządkowane całej architekturze urządzenia.