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);

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 z digitalPinToInterrupt 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.

Kontaktron i Arduino

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 w timeNow zwraca wartość taką jaka była w momencie wywołania onStep. W trakcie działania funkcji obsługującej przerwania działanie millis 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ś:

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.