Jak przerwania działają na Arduino Uno i podobnych płytach?

11

Proszę wyjaśnić, w jaki sposób przerwania działają na Arduino Uno i pokrewnych płytach przy użyciu procesora ATmega328P. Tablice takie jak:

  • ONZ
  • Mini
  • Nano
  • Pro Mini
  • Lilypad

W szczególności prosimy o omówienie:

  • Do czego używać przerwań
  • Jak napisać procedurę przerwania usługi (ISR)
  • Problemy z czasem
  • Sekcje krytyczne
  • Atomowy dostęp do danych

Uwaga: jest to pytanie referencyjne .

Nick Gammon
źródło

Odpowiedzi:

25

TL; DR:

Podczas pisania procedury przerwania usługi (ISR):

  • Mów krótko
  • Nie używaj delay ()
  • Nie rób wydruków seryjnych
  • Zmienne make wspólny główny kod lotnej
  • Zmienne współdzielone z głównym kodem mogą wymagać ochrony przez „krytyczne sekcje” (patrz poniżej)
  • Nie próbuj włączać i wyłączać przerwań

Co to są przerwy?

Większość procesorów ma przerwania. Przerwania pozwalają reagować na zdarzenia „zewnętrzne”, robiąc coś innego. Na przykład, jeśli gotujesz obiad, możesz gotować ziemniaki przez 20 minut. Zamiast wpatrywać się w zegar przez 20 minut, możesz ustawić minutnik, a następnie oglądać telewizję. Gdy minutnik dzwoni, „przerywasz” oglądanie telewizji, aby zrobić coś z ziemniakami.


Przykład przerwań

const byte LED = 13;
const byte SWITCH = 2;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // end of switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // so we can update the LED
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  // loop doing nothing
}

Ten przykład pokazuje, w jaki sposób, mimo że główna pętla nic nie robi, można włączyć lub wyłączyć diodę LED na pinie 13, po naciśnięciu przełącznika na pinie D2.

Aby to przetestować, wystarczy podłączyć przewód (lub przełącznik) między D2 a uziemieniem. Wewnętrzne pullup (włączony w konfiguracji) wymusza normalnie pin WYSOKI. Po uziemieniu staje się NISKI. Zmiana w pinie jest wykrywana przez przerwanie CHANGE, które powoduje wywołanie Procedury Przerwania Usługi (ISR).

W bardziej skomplikowanym przykładzie główna pętla może robić coś pożytecznego, na przykład odczyty temperatury i umożliwiać obsłudze przerwań wykrycie naciśnięcia przycisku.


Konwersja numerów pinów na numery przerwań

Aby uprościć konwersję numerów wektorów przerwań na numery pinów, możesz wywołać tę funkcję digitalPinToInterrupt(), przekazując numer pin. Zwraca odpowiedni numer przerwania lub NOT_AN_INTERRUPT(-1).

Na przykład w Uno, pin D2 na płycie to przerwanie 0 (INT0_vect z tabeli poniżej).

Zatem te dwie linie mają ten sam efekt:

  attachInterrupt (0, switchPressed, CHANGE);    // that is, for pin D2
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);

Jednak drugi jest łatwiejszy do odczytania i bardziej przenośny dla różnych typów Arduino.


Dostępne przerwania

Poniżej znajduje się lista przerwań w kolejności priorytetowej dla Atmega328:

 1  Reset
 2  External Interrupt Request 0  (pin D2)          (INT0_vect)
 3  External Interrupt Request 1  (pin D3)          (INT1_vect)
 4  Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
 5  Pin Change Interrupt Request 1 (pins A0 to A5)  (PCINT1_vect)
 6  Pin Change Interrupt Request 2 (pins D0 to D7)  (PCINT2_vect)
 7  Watchdog Time-out Interrupt                     (WDT_vect)
 8  Timer/Counter2 Compare Match A                  (TIMER2_COMPA_vect)
 9  Timer/Counter2 Compare Match B                  (TIMER2_COMPB_vect)
10  Timer/Counter2 Overflow                         (TIMER2_OVF_vect)
11  Timer/Counter1 Capture Event                    (TIMER1_CAPT_vect)
12  Timer/Counter1 Compare Match A                  (TIMER1_COMPA_vect)
13  Timer/Counter1 Compare Match B                  (TIMER1_COMPB_vect)
14  Timer/Counter1 Overflow                         (TIMER1_OVF_vect)
15  Timer/Counter0 Compare Match A                  (TIMER0_COMPA_vect)
16  Timer/Counter0 Compare Match B                  (TIMER0_COMPB_vect)
17  Timer/Counter0 Overflow                         (TIMER0_OVF_vect)
18  SPI Serial Transfer Complete                    (SPI_STC_vect)
19  USART Rx Complete                               (USART_RX_vect)
20  USART, Data Register Empty                      (USART_UDRE_vect)
21  USART, Tx Complete                              (USART_TX_vect)
22  ADC Conversion Complete                         (ADC_vect)
23  EEPROM Ready                                    (EE_READY_vect)
24  Analog Comparator                               (ANALOG_COMP_vect)
25  2-wire Serial Interface  (I2C)                  (TWI_vect)
26  Store Program Memory Ready                      (SPM_READY_vect)

Nazwy wewnętrzne (których można użyć do skonfigurowania wywołań zwrotnych ISR) są w nawiasach.

Ostrzeżenie: Jeśli źle wpiszesz nazwę wektora przerwań, nawet po pomyleniu wielkich liter (łatwa rzecz do zrobienia), procedura przerwania nie zostanie wywołana i nie pojawi się błąd kompilatora.


Powody używania przerwań

Główne powody, dla których możesz używać przerwań to:

  • Aby wykryć zmiany pinów (np. Enkodery obrotowe, naciśnięcia przycisków)
  • Strażnik (np. Jeśli nic się nie wydarzy po 8 sekundach, przerwij mi)
  • Przerwania timera - używane do porównywania / przepełnienia timerów
  • Transfer danych SPI
  • Przesyłanie danych I2C
  • Transfer danych USART
  • Konwersje ADC (analogowe na cyfrowe)
  • EEPROM gotowy do użycia
  • Gotowa pamięć flash

„Transfer danych” może być użyty, aby pozwolić programowi zrobić coś innego, gdy dane są wysyłane lub odbierane przez port szeregowy, port SPI lub port I2C.

Obudź procesor

Przerwania zewnętrzne, przerwania zmiany pinów i przerwania timera watchdoga mogą być również użyte do wybudzenia procesora. Może to być bardzo przydatne, ponieważ w trybie uśpienia procesor można skonfigurować tak, aby zużywał znacznie mniej energii (np. Około 10 mikroamperów). Przerwanie na wznoszenie, opadanie lub niski poziom może być użyte do przebudzenia gadżetu (np. Po naciśnięciu przycisku), lub przerwanie „watchdog timer” może go okresowo budzić (np. Aby sprawdzić czas lub temperatura).

Przerwania zmiany pinów mogą być wykorzystane do wybudzenia procesora, jeśli zostanie naciśnięty klawisz na klawiaturze lub podobny.

Procesor może również zostać przebudzony przez przerwanie timera (np. Zegar osiągający określoną wartość lub przepełnienie) i pewne inne zdarzenia, takie jak przychodzący komunikat I2C.


Włączanie / wyłączanie przerwań

Przerwania „reset” nie można wyłączyć. Inne przerwania można jednak tymczasowo wyłączyć, usuwając flagę globalnego przerwania.

Włącz przerwania

Możesz włączyć przerwania za pomocą wywołania funkcji „przerwania” lub „sei” w następujący sposób:

interrupts ();  // or ...
sei ();         // set interrupts flag

Wyłącz przerwania

Jeśli chcesz wyłączyć przerwania, możesz „wyczyścić” globalną flagę przerwań w następujący sposób:

noInterrupts ();  // or ...
cli ();           // clear interrupts flag

Każda z metod ma ten sam efekt, użycie interrupts/ noInterruptsjest nieco łatwiejsze do zapamiętania, w którą stronę się znajdują.

Domyślnie w Arduino włącza się przerwania. Nie wyłączaj ich na dłuższy czas, ponieważ takie rzeczy jak timery nie będą działać poprawnie.

Po co wyłączać przerwania?

Mogą istnieć fragmenty kodu o krytycznym czasie, których nie chcesz przerywać, na przykład przez przerwanie timera.

Także jeśli ISR ​​aktualizuje pola wielobajtowe, może być konieczne wyłączenie przerwań, aby uzyskać dane „atomowo”. W przeciwnym razie ISR może aktualizować jeden bajt podczas czytania drugiego.

Na przykład:

noInterrupts ();
long myCounter = isrCounter;  // get value set by ISR
interrupts ();

Tymczasowe wyłączenie przerwań zapewnia, że ​​isrCounter (licznik ustawiony w ISR) nie zmienia się podczas uzyskiwania jego wartości.

Ostrzeżenie: jeśli nie masz pewności, czy przerwania są już włączone, czy nie, musisz zapisać bieżący stan i przywrócić go później. Na przykład kod z funkcji millis () robi to:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- save status register

  // disable interrupts while we read timer0_millis or we might get an
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- restore status register including interrupt flag

  return m;
}

Uwaga: wskazane linie zapisują bieżący SREG (rejestr stanu), który zawiera flagę przerwania. Po uzyskaniu wartości timera (która ma 4 bajty) przywracamy rejestr statusu z powrotem.


Porady

Nazwy funkcji

Funkcje cli/ seii rejestr SREG są specyficzne dla procesorów AVR. Jeśli używasz innych procesorów, takich jak ARM, funkcje mogą się nieco różnić.

Wyłączenie globalne a wyłączenie jednego przerwania

Jeśli używasz cli(), wyłącz wszystkie przerwania (w tym przerwania czasowe, szeregowe itp.).

Jeśli jednak chcesz tylko wyłączyć określone przerwanie, powinieneś usunąć flagę przerwania dla tego konkretnego źródła przerwania. Na przykład w przypadku zewnętrznych przerwań zadzwoń detachInterrupt().


Co jest priorytetem przerwania?

Ponieważ istnieje 25 przerwań (innych niż reset), możliwe jest, że więcej niż jedno zdarzenie przerwania może wystąpić jednocześnie lub przynajmniej nastąpi przed przetworzeniem poprzedniego. Może również wystąpić zdarzenie przerwania, gdy przerwania są wyłączone.

Kolejność priorytetów to sekwencja, w której procesor sprawdza zdarzenia przerwania. Im wyżej na liście, tym wyższy priorytet. Na przykład zewnętrzne żądanie przerwania 0 (pin D2) byłoby obsługiwane przed zewnętrznym żądaniem przerwania 1 (pin D3).


Czy przerwania mogą wystąpić, gdy przerwania są wyłączone?

Zdarzenia przerwania (czyli zauważenie zdarzenia) mogą wystąpić w dowolnym momencie, a większość z nich jest zapamiętywana przez ustawienie flagi „przerwanie zdarzenia” wewnątrz procesora. Jeśli przerwania są wyłączone, to przerwanie zostanie obsłużone, gdy zostaną ponownie włączone, w kolejności priorytetowej.


Jak korzystasz z przerwań?

  • Piszesz ISR (rutynowa usługa przerwań). Jest to wywoływane, gdy nastąpi przerwanie.
  • Informujesz procesor, kiedy chcesz uruchomić przerwanie.

Pisanie ISR

Procedury obsługi przerwań są funkcjami bez argumentów. Niektóre biblioteki Arduino są zaprojektowane do wywoływania własnych funkcji, więc podajesz zwykłą funkcję (jak w powyższych przykładach), np.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
 flag = true;
}  // end of switchPressed

Jeśli jednak biblioteka nie zapewniła jeszcze „haka” do ISR, możesz utworzyć własny:

volatile char buf [100];
volatile byte pos;

// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

W tym przypadku używasz makra „ISR” i podajesz nazwę odpowiedniego wektora przerwań (z tabeli wcześniej). W takim przypadku ISR obsługuje zakończenie transferu SPI. (Uwaga: niektóre stare kody używają SIGNAL zamiast ISR, jednak SIGNAL jest przestarzałe).

Podłączanie ISR do przerwania

W przypadku przerwań już obsługiwanych przez biblioteki wystarczy użyć udokumentowanego interfejsu. Na przykład:

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // do something with the incoming byte
    }
}  // end of receiveEvent

void setup ()
  {
  Wire.onReceive(receiveEvent);
  }

W tym przypadku biblioteka I2C jest zaprojektowana do wewnętrznego przetwarzania przychodzących bajtów I2C, a następnie wywoływania podanej funkcji na końcu strumienia danych przychodzących. W tym przypadku receiveEvent nie jest ściśle ISR (ma argument), ale jest wywoływany przez wbudowany ISR.

Innym przykładem jest przerwanie „pin zewnętrzny”.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
}  // end of switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // attach interrupt handler for D2
}  // end of setup

W tym przypadku funkcja attachInterrupt dodaje przełącznik switchPressed do wewnętrznej tabeli, a ponadto konfiguruje odpowiednie flagi przerwań w procesorze.

Konfigurowanie procesora do obsługi przerwania

Następnym krokiem, po uzyskaniu ISR, jest poinformowanie procesora, że ​​ten konkretny warunek ma wywołać przerwanie.

Na przykład dla zewnętrznego przerwania 0 (przerwanie D2) możesz zrobić coś takiego:

EICRA &= ~3;  // clear existing flags
EICRA |= 2;   // set wanted flags (falling level interrupt)
EIMSK |= 1;   // enable it

Bardziej czytelne byłoby użycie zdefiniowanych nazw, takich jak to:

EICRA &= ~(bit(ISC00) | bit (ISC01));  // clear existing flags
EICRA |= bit (ISC01);    // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0);     // enable it

EICRA (zewnętrzny rejestr kontroli przerwania A) zostałby ustawiony zgodnie z tą tabelą z arkusza danych Atmega328. To określa dokładny rodzaj przerwania:

  • 0: Niski poziom INT0 generuje żądanie przerwania (LOW interrupt).
  • 1: Każda zmiana logiczna w INT0 generuje żądanie przerwania (CHANGE interrupt).
  • 2: Zbocze opadające INT0 generuje żądanie przerwania (przerwanie FALLING).
  • 3: Zbocze narastające INT0 generuje żądanie przerwania (przerwanie RISING).

EIMSK (rejestr maski zewnętrznej przerwania) faktycznie włącza przerwanie.

Na szczęście nie musisz pamiętać tych liczb, ponieważ attachInterrupt robi to za Ciebie. Jednak tak właśnie się dzieje i dla innych przerwań może być konieczne „ręczne” ustawienie flag przerwań.


ISR niskiego poziomu vs. biblioteczne ISR

Aby uprościć życie, niektóre popularne procedury obsługi przerwań znajdują się w kodzie biblioteki (na przykład INT0_vect i INT1_vect), a następnie zapewniony jest interfejs bardziej przyjazny dla użytkownika (np. AttachInterrupt). W rzeczywistości attachInterrupt zapisuje adres żądanego modułu obsługi przerwań w zmiennej, a następnie wywołuje go z INT0_vect / INT1_vect w razie potrzeby. Ustawia także odpowiednie flagi rejestru, aby w razie potrzeby wywoływały moduł obsługi.


Czy można przerwać ISR?

Krótko mówiąc, nie, chyba że tego chcesz.

Po wprowadzeniu ISR przerwania są wyłączone . Oczywiście musiały one być przede wszystkim włączone, w przeciwnym razie ISR nie zostałby wpisany. Jednak, aby uniknąć przerwania samego ISR, procesor wyłącza przerwania.

Kiedy ISR kończy pracę, przerwania są ponownie włączane . Kompilator generuje również kod wewnątrz ISR w celu zapisywania rejestrów i flag statusu, aby nie miało to wpływu na to, co robiłeś w momencie wystąpienia przerwania.

Jakkolwiek można włączyć na przerwania wewnątrz ISR jeśli absolutnie muszą, np.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
  interrupts ();  // allow more interrupts

}  // end of switchPressed

Zwykle potrzebny byłby do tego dobry powód, ponieważ kolejne przerwanie może spowodować rekurencyjne wywołanie funkcji pinChange, co może być niepożądane.


Jak długo trwa wykonanie ISR?

Zgodnie z arkuszem danych, minimalny czas do obsługi przerwania wynosi 4 cykle zegarowe (w celu przesunięcia licznika bieżącego programu na stos), a następnie kod wykonywany teraz w miejscu wektora przerwania. Zwykle zawiera skok do miejsca, w którym tak naprawdę jest procedura przerwania, czyli kolejne 3 cykle. Analiza kodu wygenerowanego przez kompilator pokazuje, że wykonanie ISR za pomocą deklaracji „ISR” może zająć około 2,625 µs, a także cokolwiek robi sam kod. Dokładna kwota zależy od liczby rejestrów, które należy zapisać i przywrócić. Minimalna ilość wynosiłaby 1,1875 µs.

Zewnętrzne przerwania (w przypadku korzystania z funkcji attachInterrupt) wykonują nieco więcej i zabierają łącznie około 5,125 µs (działające z zegarem 16 MHz).


Jak długo zanim procesor zacznie wprowadzać ISR?

To się nieco różni. Podane liczby są idealne, gdy przerwanie jest natychmiast przetwarzane. Kilka czynników może opóźnić:

  • Jeśli procesor śpi, są wyznaczone czasy „budzenia”, które mogą wynosić kilka milisekund, podczas gdy zegar jest buforowany z powrotem do prędkości. Ten czas zależy od ustawień bezpiecznika i głębokości snu.

  • Jeśli procedura obsługi przerwań jest już wykonywana, nie można wprowadzić kolejnych przerwań, dopóki się nie zakończy lub nie umożliwi samych przerwań. Dlatego powinieneś utrzymywać krótki czas wykonywania każdej usługi przerwania, ponieważ każda mikrosekunda wydana w jednej z nich potencjalnie opóźnia wykonanie innej.

  • Niektóre kody wyłączają przerwania. Na przykład wywołanie millis () na krótko wyłącza przerwania. Dlatego czas obsługi przerwania zostałby wydłużony o czas, w którym przerwania były wyłączone.

  • Przerwania mogą być obsługiwane tylko na końcu instrukcji, więc jeśli dana instrukcja wymaga trzech cykli zegara i dopiero się rozpoczęła, wówczas przerwanie będzie opóźnione co najmniej o kilka cykli zegara.

  • Zdarzenie, które ponownie włącza przerwania (np. Powrót z procedury usługi przerwania), gwarantuje wykonanie co najmniej jednej dodatkowej instrukcji. Więc nawet jeśli ISR ​​zakończy się, a twoje przerwanie jest w toku, nadal musi czekać na jeszcze jedną instrukcję, zanim zostanie obsłużone.

  • Ponieważ przerwania mają priorytet, przerwanie o wyższym priorytecie może być obsługiwane przed przerwaniem, które Cię interesuje.


Uwagi dotyczące wydajności

Przerwania mogą zwiększyć wydajność w wielu sytuacjach, ponieważ możesz zacząć od „głównej pracy” programu bez konieczności ciągłego testowania, aby sprawdzić, czy naciśnięto przełączniki. Powiedziawszy to, narzut związany z obsługą przerwania, jak omówiono powyżej, w rzeczywistości byłby czymś więcej niż robieniem „ciasnej pętli” odpytywania pojedynczego portu wejściowego. Ledwo reagujesz na zdarzenie w ciągu, powiedzmy, mikrosekundy. W takim przypadku możesz wyłączyć przerwania (np. Timery) i po prostu zapętlić szukając pinezki do zmiany.


Jak kolejki przerwań są w kolejce?

Istnieją dwa rodzaje przerwań:

  • Niektóre ustawiają flagę i są obsługiwane w kolejności priorytetowej, nawet jeśli zdarzenie, które je spowodowało, zostało zatrzymane. Na przykład wzrost, spadek lub zmiana poziomu przerywa na pinie D2.

  • Inne są testowane tylko wtedy, gdy mają miejsce „teraz”. Na przykład przerwanie niskiego poziomu na pinie D2.

Te, które ustawiają flagę, można uznać za ustawione w kolejce, ponieważ flaga przerwania pozostaje ustawiona do momentu wprowadzenia procedury przerwania, kiedy procesor usunie flagę. Oczywiście, ponieważ istnieje tylko jedna flaga, jeśli ten sam warunek przerwania wystąpi ponownie przed przetworzeniem pierwszego, nie będzie on obsługiwany dwa razy.

Należy pamiętać, że flagi te można ustawić przed dołączeniem modułu obsługi przerwań. Na przykład, możliwe jest, aby przerwanie poziomu rosnącego lub spadającego na pinie D2 było „oflagowane”, a następnie, jak tylko wykonasz załącznik, przerwij natychmiast przerwanie, nawet jeśli zdarzenie miało miejsce godzinę temu. Aby tego uniknąć, możesz ręcznie usunąć flagę. Na przykład:

EIFR = bit (INTF0);  // clear flag for interrupt 0
EIFR = bit (INTF1);  // clear flag for interrupt 1

Jednak przerwania „niskiego poziomu” są stale sprawdzane, więc jeśli nie będziesz ostrożny, będą kontynuować ostrzał, nawet po wywołaniu przerwania. Oznacza to, że ISR zakończy działanie, a następnie przerwanie natychmiast zostanie ponownie uruchomione. Aby tego uniknąć, powinieneś wykonać odłączanie przerwania natychmiast po tym, jak wiesz, że przerwanie zostało uruchomione.


Wskazówki dotyczące pisania ISR

Krótko mówiąc, streszczaj je! Podczas wykonywania ISR inne przerwania nie mogą być przetwarzane. Możesz więc łatwo pominąć naciskanie przycisków lub przychodzącą komunikację szeregową, jeśli spróbujesz zrobić zbyt wiele. W szczególności nie należy próbować „debugować” wydruków wewnątrz ISR. Czas potrzebny na ich wykonanie może spowodować więcej problemów niż ich rozwiązanie.

Rozsądną rzeczą jest ustawienie flagi jednobajtowej, a następnie przetestowanie tej flagi w funkcji głównej pętli. Lub zapisz przychodzący bajt z portu szeregowego w buforze. Wbudowane przerwania timera śledzą upływający czas, strzelając za każdym razem, gdy wewnętrzny zegar się przepełnia, dzięki czemu możesz obliczyć upływ czasu, wiedząc, ile razy zegar się przepełnił.

Pamiętaj, że wewnątrz ISR przerwania są wyłączone. Zatem nadzieja, że ​​czas zwracany przez wywołania funkcji millis () ulegnie zmianie, doprowadzi do rozczarowania. Ważne jest, aby uzyskać czas w ten sposób, należy tylko pamiętać, że czasomierz nie zwiększa. A jeśli spędzasz zbyt długo w ISR, zegar może przegapić zdarzenie przepełnienia, co spowoduje, że czas zwrócony przez millis () stanie się nieprawidłowy.

Test pokazuje, że w procesorze Atmega328 16 MHz wywołanie do micros () zajmuje 3,5625 µs. Wywołanie do millis () zajmuje 1,9375 µs. Zapisywanie (zapisywanie) bieżącej wartości timera jest rozsądną rzeczą do zrobienia w ISR. Znalezienie upływających milisekund jest szybsze niż upłynięcie mikrosekund (liczba milisekund jest właśnie pobierana ze zmiennej). Jednak liczbę mikrosekund uzyskuje się poprzez dodanie bieżącej wartości timera Timer 0 (który będzie stale zwiększał) do zapisanej „Liczby przepełnienia Timera 0”.

Ostrzeżenie: Ponieważ przerwania są wyłączone w ISR, a ponieważ najnowsza wersja Arduino IDE używa przerwań do szeregowego odczytu i zapisu, a także do zwiększania licznika używanego przez „millis” i „opóźnienie”, nie powinieneś próbować używać tych funkcji wewnątrz ISR. Innymi słowy:

  • Nie próbuj opóźniać, np .: delay (100);
  • Możesz uzyskać czas od połączenia do millis, jednak nie będzie on zwiększany, więc nie próbuj opóźniać, czekając, aż wzrośnie.
  • Nie rób wydruków seryjnych (np. Serial.println ("ISR entered");)
  • Nie próbuj czytać seryjnie.

Przerwania zmiany pinów

Istnieją dwa sposoby wykrywania zewnętrznych zdarzeń na pinach. Pierwszy to specjalne piny „zewnętrznego przerwania”, D2 i D3. Te ogólne dyskretne zdarzenia przerwania, po jednym na pin. Możesz do nich dotrzeć, używając attachInterrupt dla każdego pinu. Możesz określić warunek wzrostu, spadku, zmiany lub niskiego poziomu przerwania.

Istnieją jednak również przerwania „zmiany pinów” dla wszystkich pinów (w Atmega328, niekoniecznie wszystkich pinów w innych procesorach). Działają one na grupy pinów (D0 do D7, D8 do D13 i A0 do A5). Mają również niższy priorytet niż przerwania zdarzeń zewnętrznych. Są jednak nieco bardziej skomplikowane w użyciu niż zewnętrzne przerwania, ponieważ są pogrupowane w partie. Więc jeśli zadziała przerwanie, musisz wypracować w swoim własnym kodzie dokładnie, który pin spowodował przerwanie.

Przykładowy kod:

ISR (PCINT0_vect)
 {
 // handle pin change interrupt for D8 to D13 here
 }  // end of PCINT0_vect

ISR (PCINT1_vect)
 {
 // handle pin change interrupt for A0 to A5 here
 }  // end of PCINT1_vect

ISR (PCINT2_vect)
 {
 // handle pin change interrupt for D0 to D7 here
 }  // end of PCINT2_vect


void setup ()
  {
  // pin change interrupt (example for D9)
  PCMSK0 |= bit (PCINT1);  // want pin 9
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to D13
  }

Aby obsłużyć przerwanie zmiany pinów, musisz:

  • Określ, który pin w grupie. Jest to zmienna PCMSKn (gdzie n oznacza 0, 1 lub 2 z poniższej tabeli). Możesz mieć przerwania na więcej niż jednym pinie.
  • Włącz odpowiednią grupę przerwań (0, 1 lub 2)
  • Podaj moduł obsługi przerwań, jak pokazano powyżej

Tabela pinów -> nazwy pinów / maski

D0    PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1    PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2    PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3    PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4    PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5    PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6    PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7    PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8    PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9    PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10   PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11   PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12   PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13   PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0    PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1    PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2    PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3    PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4    PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5    PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Przerwij przetwarzanie modułu obsługi

Program obsługi przerwań musiałby ustalić, który pin spowodował przerwanie, jeśli maska ​​określa więcej niż jeden (np. Jeśli chcesz przerwań na D8 / D9 / D10). Aby to zrobić, musisz zapisać poprzedni stan tego pinu i poćwiczyć (wykonując digitalRead lub podobny), jeśli ten konkretny pin się zmienił.


Prawdopodobnie i tak używasz przerwań ...

„Normalne” środowisko Arduino używa już przerwań, nawet jeśli nie próbujesz tego osobiście. Wywołania funkcji millis () i micros () wykorzystują funkcję „przepełnienia timera”. Jeden z wewnętrznych timerów (timer 0) jest skonfigurowany do przerywania z grubsza 1000 razy na sekundę i zwiększania wewnętrznego licznika, który faktycznie staje się licznikiem millis (). Jest w tym nieco więcej, ponieważ dostosowuje się dokładną częstotliwość zegara.

Również sprzętowa biblioteka szeregowa używa przerwań do obsługi przychodzących i wychodzących danych szeregowych. Jest to bardzo przydatne, ponieważ twój program może wykonywać inne czynności podczas uruchamiania przerwań i zapełniania bufora wewnętrznego. Następnie, sprawdzając Serial.available (), możesz dowiedzieć się, co, jeśli w ogóle, zostało umieszczone w tym buforze.


Wykonanie następnej instrukcji po włączeniu przerwań

Po krótkiej dyskusji i badaniach na forum Arduino wyjaśniliśmy dokładnie, co dzieje się po włączeniu przerwań. Są trzy główne sposoby, w jakie myślę, że możesz włączyć przerwania, które wcześniej nie były włączone:

  sei ();  // set interrupt enable flag
  SREG |= 0x80;  // set the high-order bit in the status register
  reti  ;   // assembler instruction "return from interrupt"

We wszystkich przypadkach procesor gwarantuje, że następna instrukcja po przerwie (jeśli były wcześniej wyłączone) będzie zawsze wykonywana, nawet jeśli zdarzenie przerwania jest w toku. (Przez „następny” rozumiem następny w sekwencji programu, niekoniecznie fizycznie następujący. Na przykład instrukcja RETI przeskakuje z powrotem do miejsca, w którym nastąpiło przerwanie, a następnie wykonuje jeszcze jedną instrukcję).

Pozwala to na napisanie takiego kodu:

sei ();
sleep_cpu ();

Gdyby nie ta gwarancja, przerwanie może nastąpić przed uśpieniem procesora, a następnie może nigdy nie zostać przebudzone.


Puste przerwy

Jeśli chcesz, aby przerwanie obudziło procesor, ale nie robisz nic konkretnego, możesz użyć EMPTY_INTERRUPT, np.

EMPTY_INTERRUPT (PCINT1_vect);

To po prostu generuje instrukcję „reti” (return from interrupt). Ponieważ nie próbuje zapisywać ani przywracać rejestrów, byłby to najszybszy sposób na przerwanie w celu wznowienia działania.


Sekcje krytyczne (dostęp do zmiennych atomowych)

Istnieją pewne subtelne problemy dotyczące zmiennych, które są wspólne między procedurami obsługi przerwań (ISR) i kodem głównym (to znaczy kodu nie znajdującego się w ISR).

Ponieważ ISR może być uruchamiany w dowolnym momencie, gdy włączone są przerwania, należy zachować ostrożność przy uzyskiwaniu dostępu do takich współdzielonych zmiennych, ponieważ mogą one być aktualizowane w momencie dostępu do nich.

Po pierwsze ... kiedy używasz zmiennych „niestabilnych”?

Zmienna powinna być oznaczona jako lotna tylko wtedy, gdy jest używana zarówno wewnątrz ISR, jak i poza nią.

  • Zmienne używane tylko poza ISR nie powinny być niestabilne.
  • Zmienne używane tylko wewnątrz ISR nie powinny być lotne.
  • Zmienne stosowane zarówno wewnątrz, jak i na zewnątrz ISR powinny być niestabilne.

na przykład.

volatile int counter;

Oznaczenie zmiennej jako lotnej informuje kompilator, aby nie „buforował” zawartości zmiennych do rejestru procesora, ale zawsze w razie potrzeby czytał ją z pamięci. Może to spowolnić przetwarzanie, dlatego nie wszystkie zmienne są zmienne, gdy nie są potrzebne.

Wyłącz przerwania podczas uzyskiwania dostępu do zmiennej lotnej

Na przykład, aby porównać countz pewną liczbą, wyłącz przerwania podczas porównywania, jeśli jeden bajt countzostał zaktualizowany przez ISR, a nie drugi bajt.

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop ()
  {
  noInterrupts ();    // <------ critical section
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ end critical section
  } // end of loop

Przeczytaj kartę danych!

Więcej informacji na temat przerwań, timerów itp. Można uzyskać z karty danych procesora.

http://www.atmel.com/images/Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.pdf


Dalsze przykłady

Względy miejsca (limit rozmiaru posta) uniemożliwiają zamieszczenie więcej przykładowego kodu. Aby uzyskać więcej przykładowego kodu, zobacz moją stronę o przerwaniach .

Nick Gammon
źródło
Bardzo przydatne odniesienie - to była imponująco szybka odpowiedź.
Dat Han Bag
To było pytanie referencyjne. Przygotowałem odpowiedź i byłoby jeszcze szybciej, gdyby odpowiedź nie była zbyt długa, więc musiałem ją przyciąć. Zobacz link do strony po więcej szczegółów.
Nick Gammon
Jeśli chodzi o „tryb uśpienia”, czy Arduino może spać przez, powiedzmy, 500 ms?
Dat Ha
@Nick Gammon Myślę, że włączenie lub wyłączenie zasilania (z automatyzacją lub bez) dla procesora może być zdefiniowane jako niekonwencjonalne przerwanie - jeśli chcesz to zrobić. „Miałem przygotowaną odpowiedź” - właśnie wyjąłeś całą magię z tej chwili, o której myślałem, że tak.
Dat Han Bag
1
Obawiam się, że to nieprawda. Mam przykład, który używa przerwań zmiany pinów, aby obudzić się z trybu wyłączania. Również, jak wspomniałem na mojej stronie o przerwaniach, Atmel potwierdził, że każde zewnętrzne przerwanie obudzi procesor (tj. Wzrost / spadek / zmiana i niski).
Nick Gammon