Arduino i przerwanie
Jeśli przeglądasz w sieci projekty oparte o Arduino, to prędzej czy później natkniesz na jakiś, który wykorzystuje przerwania. Z tego artykułu, dowiesz się co to jest, jak i kiedy użyć w Arduino.
Przerwanie - sygnał z urządzenia peryferyjnego, który powoduje zatrzymanie programu głównego i uruchomienie funkcji obsługującej przerwanie.
Tyle definicja. W praktyce - pomyśl o sytuacji, gdy Twój projekt oczekuje na jakieś zewnętrzne zdarzenia, na które musi zareagować niezwłocznie. Na przykład, rozwarcie jakiegoś kontaktronu podłączonego do wejścia cyfrowego oznacza konieczność natychmiastowego działania. Gdy szkic na Arduino robi jeszcze dużo innych rzeczy, napisanie programu tak by sprawdzał stan wejścia dostatecznie często i reagował w razie potrzeby może być dość kłopotliwe. W takiej sytuacji można użyć przerwania. Gdy nastąpi określone zdarzenie na wejściu cyfrowym Arduino przerywa wykonywanie szkicu i wykonuje zaplanowane na tą okazję zadanie.
Z praktyki Nie używaj przerwań, jeśli potrafisz znaleźć inne rozwiązanie. Programy wykorzystujące przerwania trudniej diagnozować (debuggować) jeżeli coś działa nie tak jak się spodziewasz.
W powyższym przykładzie - jeżeli całość pętli loop wykonuje się poniżej sekundy a opóźnienie rzędu 1 sek. w reakcji na przełączenie kontaktronu nie jest problemem - nie myśl nawet o użyciu przerwań.
Przerwania - od strony kodu
Do obsługi przerwań służy funkcja attachInterrupt
attachInterrupt(int, func, mode);
- int - numer przerwania. Arduino UNO obsługuje dwa przerwania (o numerach 0 i 1)
- func - nazwa fukcji obsługującej przerwanie
- mode - ustala kiedy przerwanie ma być wywołane:
- LOW - kiedy na pinie jest stan niski;
- CHANGE - kiedy zmienia się stan;
- RISING - kiedy zmienia się stan z LOW na HIGH;
- FALLING - kiedy zmienia się stan z HIGH na LOW.
Tutaj się chwilę zatrzymamy. Ważne do zrozumienia jest to, że attachInterrupt
potrzebuje numeru przerwania a nie numeru pinu cyfrowego. I tutaj są spore różnice między modelami Arduino. Liczba dostępnych przerwań, oraz które piny im odpowiadają zależą od procesora Arduino.
I tak modele wyposażone w ATmega328 (Arduino UNO, Nano itp) mają dwa przerwania na pinach cyfrowych 2 i 3. Arduino Leonardo i podobne (ATmega32u4) mają do dyspozycji 5 przerwań (piny 0,1,2,3,7). Arduino Mega (2560, ADK, itp) 6 przerwań na pinach 2,3,18,19,20,21.
Sytuacja w Arduino opartych o procesory ARM jest jeszcze inna - Due ma przerwania na każdym pinie cyfrowym a Zero na każdym, z wyjątkiem 4.
Jak widać każde Arduino wspiera przerwania na pinach 2 i 3. Pisząc kod, który ma być maksymalnie niezależny od modelu Arduino warto korzystać z tych portów. Ale... attachInterrupt
potrzebuje numeru przerwania a nie portu. Przerwanie na porcie 2 ma numer 0 na UNO i Mega, a 2 na Leonardo i Due.
Dlatego Arduino IDE dostarcza pomocniczą funkcję digitalPinToInterrupt
. Zwraca nam numer przerwania dla danego portu, w zależności od płytki Arduino wybranej jako cel kompilacji. Czyli chcąc dopiąć się do przerwania na pinie nr 3 użyj: attachInterrupt(digitalPinToInterrupt(3), NAZWA, TRYB)
. Dzięki temu zostanie użyty właściwy numer przerwania zarówno na UNO jak i Leonardo.
Jednak digitalPinToInterrupt
nie rozwiązuje wszystkich problemów z numerami przerwań. Jeżeli Twój program ma używać więcej niż 2 przerwania to i tak na Arduino z procesorem ATmega328 (jak w UNO) nie da się tego obsłużyć. W takiej sytuacji digtialPinToInterrupt zwróci wartość -1 i oczywiście attachInterrupt
nie zadziała.
Chcąc pisać kod działający na każdym Arduino a korzystający z przerwań, zastosuj się do tych zasad:
- używaj tylko przerwań na pinach 2 i 3
- wywołując
attachInterrupt
korzystaj zdigitalPinToInterrupt
do ustalenia numeru przerwania
Przykład: Arduino liczy impulsy
Jako przykład wyobraźmy sobie Arduino mające liczyć ile razy przełączył się kontaktron. Kontaktron jest niewielkim przełącznikiem, który przewodzi prąd gdy do niego się zbliży magnes. Mocując magnes na ruchomym elemencie możemy wykryć kolejne obroty, gdy magnes spowoduje przewodzenie.
Prosty szkic zliczający impulsy z użyciem przerwań wygląda tak (kontaktron podłączony jest do pinu 2)
volatile word steps;
void setup()
{
Serial.begin(9600);
pinMode(2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), onStep, FALLING);
}
void loop()
{
Serial.println(steps);
delay(5000);
}
void onStep()
{
static unsigned long lastTime;
unsigned long timeNow = millis();
if (timeNow - lastTime < 50)
return;
steps++;
lastTime = timeNow;
}
Przerwanie ma wywoływać funkcję onStep
. Wywołanie nastąpi gdy stan na pinie zmieni się z HIGH na LOW. Funkcja onStep
zaczyna się od fragmentu eliminującego drgania styków. Liczba kroków przechowywana jest w zmiennej globalnej steps
.
W funkcji loop
możesz napisać dowolny fragment programu. Gdy pojawi się impuls na pinie 2, program w loop
zostanie przerwany i wykonana zostanie funkcja onStep
. Po jej zakończeniu program w loop
zacznie pracę tam gdzie skończył.
Fragment eliminujący drgania styków działa tak:
Gdy mechaniczny włącznik (a takim jest kontaktron) zwiera styki, to przez ułamek sekundy one drgają. Te drgania mogą spowodować, że wywoła się kilka przerwań i funkcja zaliczy kilka kroków.
Dlatego początek sprawia, że pierwsze przerwanie będzie zaliczone, a każde kolejne przez 50 ms zostanie zignorowane jako drgania styków i nie zaliczy kroku. Oczywistym wnioskiem jest to, że w ten sposób nie da się poprawnie liczyć szybkich zdarzeń (1 s/50 ms = 20, czyli maksymalnie 20 obrotów na sekundę zostanie zauważone przez Arduino).
Zmienna typu static (mowa o lastTime
) przechowuje wartość nawet po wyjściu z funkcji. Na początku ma wartość 0. Po ponownym uruchomieniu funkcji będzie miała wartość taką, jaka była w niej ostatnio zapisana.
W tej zmiennej (lastTime
) zapisany jest czas ostatniego wywołania przerwania.
W zmiennej timeNow
zapisany jest aktualny czas.
Uwaga
Precyzyjnie mówiąc,millis
, którego wartość jest zapamiętana wtimeNow
zwraca wartość taką jaka była w momencie wywołaniaonStep
. W trakcie działania funkcji obsługującej przerwania działaniemillis
jest zawieszone. Unikaj funkcji obsługujących przerwania mających długi czas wykonywania. Funkcje obsługi przerwań mają więcej ograniczeń, poczytaj o nich w referencji języka Arduino.
Po odjęciu timeNow od lastTime uzyskujemy czas jaki upłynął w milisekundach od ostatniego przerwania.
Jeśli jest on mniejszy niż 50 ms, to wychodzimy z funkcji (return). W ten sposób ignorujemy przypadkowe drgania styków. Jeśli czas jest większy niż 50 ms, zaliczany jest krok i zapisywany jest nowy czas ostatniego przerwania w zmiennej lastTime
.
Jak wyłączyć przerwania
Wspomnieliśmy już, że obsługa przerwań może zaburzyć działanie funkcji millis
i delay
. Jeżeli w programie masz jakiś fragment kodu, który wymaga dokładnych czasów wykonywania, przerwania można na taki krytyczny czas wyłączyć. Służą do tego polecenia noInterrupts()
(wyłączenie obsługi przerwań) i interruts()
(do ich włączenia). Czyli chcąc wykonać jakiś fragment kodu i zagwarantować mu działanie bez żadnych przerw:
noInterrupts();
// miejsce na kod wymagający działania bez przerw
interrupts();
Stosuj to rozważnie, bo wyłączając działanie przerwań, wyłączasz również wewnętrzne przerwania Arduino, a na nich oparte jest działanie funkcji millis
i delay
.
W trakcie wyłączonej obsługi przerwań, zdarzenie które zdefiniowałeś, pozostanie niezauważone. Czyli, po wyłączeniu przerwań w naszym przykładzie, Arduino zgubi ewentualne zamknięcie kontaktronu w tym czasie.
Zmienne używane podczas przerwań
Funkcja obsługująca przerwanie zazwyczaj powinna jakoś przekazywać dane do reszty programu. W naszym przykładzie jest to liczba kroków steps
. Jeśli chcesz by twój szkic zawsze korzystał z najbardziej aktualnej wartości tej zmiennej, przy jej definicji dodaj słowo kluczowe volatile
. Spowoduje to, że za każdym razem gdy sięgniesz do po jej wartość, kod sprawdzi w pamięci jaka jest wartość. W innym przypadku kompilator może zoptymalizować kod i skorzysta z poprzednio odczytanej wartości zmiennej, którą sobie przechowuje w rejestrze procesora.
Zwróć uwagę na problem komunikacji w drugą stronę. Jeżeli program w
loop
miałby zmieniać wartość zmiennych używanych przez funkcję obsługującą przerwania, to warto zapewnić by przerwanie nie zaskoczyło programu w momencie modyfikacji zmiennej. Jeżeli jest to operacja złożona z więcej niż jednej komendy kodu maszynowego procesora, może się zdarzyć, że przerwanie zostanie wywołane "pośrodku" i jako rezultat stan zmiennej będzie błędny.
Czego się nauczyliśmy
Po przeczytaniu tego artykułu i analizie przykładu powinieneś/powinnaś:
- wiedzieć co to jest przerwanie
- umieć zdefiniować obsługę przerwania w programie dla Arduino
- umieć wyłączyć przerwania w istotnym momencie
- rozumieć, czemu należy unikać stosowania przerwań, jeżeli nie są konieczne
Gdyby uzasadnienie ostatniego puntu nie było dla Ciebie oczywiste to - po pierwsze, diagnoza działania programu jest dużo trudniejsza. Po drugie zaburzasz działanie funkcji millis
oraz delay
, zwłaszcza jeżeli funkcje obsługi przerwań nie trwają bardzo krótko.