Przerwanie na każdym pinie Arduino Uno

Początkujący użytkownicy Arduino mogą nie wiedzieć nawet o istnieniu czegoś takiego jak przerwania, jeżeli jednak budujemy bardziej złożony system na naszym mikrokontrolerze warto się nimi zainteresować, albowiem bardzo ułatwiają one komunikację z różnego rodzaju podzespołami. Niestety na płytkach wyposażonych w Atmegę328 (Uno, Leonardo) mamy do dyspozycji tylko 2 przerwania zewnętrzne. W porównaniu do całkowitej ilości pinów cyfrowych to bardzo mało. Jeżeli jednak nieco bardziej zagłębimy się w temat przerwań na tym procesorze to okaże się, że każdy pin cyfrowy obsługuje PCINT, czyli Pin Change Interrupt. W dzisiejszym artykule dowiemy się jak je wykorzystać.

Przerwanie zewnętrzne vs. PCINT

Od strony niskopoziomowej przerwanie zewnętrzne ma wiele korzyści względem PCINT.

  • jest znacznie szybsze
  • pozwala na wybranie wyzwalacza (FALLING, RISING, LOW, HIGH, CHANGE)
  • każde przerwanie ma swój własny wektor czyli najprościej mówiąc funkcję obsługującą

Stosując PCINT możemy je co prawda zamontować na każdym pinie cyfrowym z tą różnicą, że na wszystkie piny producent przeznaczył zaledwie 3 wektory, oraz wyzwala je każda zmiana stanu pinu. To na barkach programisty spoczywa rozróżnienie który pin wyzwolił przerwanie (jeżeli korzystamy z kilku pinów na jednym wektorze) oraz jaki to był typ wyzwolenia (FALLING, RISING). To oczywiście kosztuje pewną ilość cykli procesora, więc jeszcze bardziej spowalnia kod. Myślę jednak, że w amatorskich oraz niektórych pół-profesjonalnych zastosowaniach takie spowolnienie nie będzie miało znacznego wpływu na ogólną sprawnosć projektu (czym jest kilka cykli w porównaniu do 16 milionów na sekundę, które wykonuje atmega).

Jak używać PCINT?

Oczywiście można to zrobić niskopoziomowo, ustawić odpowiednie rejestry, potem bawić się w rozpoznawanie co przyszło itp., ale przecież nie po to kupilismy sobie Arduino, żeby bawić się w low-level. Z tak dużą społecznością jaką skupia w okół siebie Arduino można liczyć, że czego nie wymyślimy to będzie do tego łatwa w obsłudze biblioteka. Tak też jest i tym razem :) Poprzez menedżer bibliotek ściągnijmy więc taką o nazwie PinChangeInterrupt.

Ustawiamy przerwanie

W funkcji setup należy wywołać taką oto metodę:

attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(INTERRUPT_PIN), interruptRoutine, FALLING);

gdzie INTERRUPT_PIN to numer pinu cyfrowego Arduino dla którego włączamy przerwanie, interruptRoutine to nazwa funkcji bez parametrów, zwracająca typ void, która zostanie wywołana w przypadku nadejścia przerwania. Ostatni parametr to wyzwalacz, czyli co wzbudzi nasze przerwanie (zbocze opadające – FALLING, rosnące – RISING czy zmiana stanu pinu – CHANGE).

Ale zaraz, co tu się dzieje? Przecież dopiero mówiłem że w przypadku PCINT trzeba samemu rozpoznawać jakie zdarzenie wyzwoliło przerwanie i który pin. Odpowiedź jest bardzo prosta: magia wysokopoziomowych bibliotek ;)

Zastosowanie w tym przykładzie funkcji digitalPinToPinChangeInterrupt ułatwia nam robotę. Gdyby nie to musielibyśmy w tym miejscu podać numer PCINT dla danego pinu (obrazek poniżej)

Czhodzi o numer na szarym tle, podczas gdy numer pinu na płytce Arduino oznacozny jest kolorem różowym (albo fioletowym?).

Przerwanie możemy też w dowolnym momencie wyłączyć wywołuąc

detachPinChangeInterrupt(digitalPinToPinChangeInterrupt(INTERRUPT_PIN));

ewentualnie zapauzować oraz wznowić jego wykrywanie:

// pauza
disablePinChangeInterrupt(digitalPinToPinChangeInterrupt(INTERRUPT_PIN));

...

// wznowienie
enablePinChangeInterrupt(digitalPinToPinChangeInterrupt(INTERRUPT_PIN));

Jeżeli jako wyzwalacz wybierzemy CHANGE to w momencie obsługiwania przerwania możemy sprawdzić, czy ta zmiana była ze stanu wysokiego na niski, czy z niskiego na wysoki:

uint8_t trigger = getPinChangeInterruptTrigger(digitalPinToPinChangeInterrupt(INTERRUPT_PIN));

Teraz zmienną trigger możemy porównać ze stałymi RISING lub FALLING, np. tak:

if(trigger == RISING) {
  // Zmiana ze stanu LOW na HIGH
} else {
  // Zmiana ze stanu HIGH na LOW
}

Demonstracja działania

W momencie nadejścia przerwania procesor przestaje robić to, co robił i bezwarunkowo przechodzi do obsługi owego przerwania. Bardzo łatwo sprawdzimy, czy wszystko działa blokując w funkcji loop() procesor na 10 sekund.

#include "PinChangeInterrupt.h"

#define INTERRUPT_PIN 8

void setup() {
  Serial.begin(9600);
  pinMode(INTERRUPT_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(INTERRUPT_PIN), interruptRoutine, CHANGE);
}

void interruptRoutine() {
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void loop() {
  delay(10000);
  Serial.println("Minelo 10 sekund");
}

Po wgraniu powyższego kodu procesor powinien blokować się na 10 sekund, a następnie wypisywać na port szeregowy napis „Minelo 10 sekund” i tak w kółko. Możemy zauważyć, że pomimo blokowania procesora funkcją delay() po wciśnięciu przycisku podłączonego do pinu 8 stan wbudowanej w Arduino diody LED zmienia się. Oznacza to że przerwanie działa.

Jak pisać funkcje przerwań?

Zasada jest jedna: muszą one być szybkie. Unikamy więc tam wypisywania czegokolwiek na port szeregowy oraz wykonywania czasochłonnych rzeczy, a już przede wszystkim nie powinniśmy w takiej funkcji używać opóźniaczy (delay, delayMicroseconds).