Wie funktionieren Interrupts auf dem Arduino Uno und ähnlichen Boards?

11

Bitte erläutern Sie, wie Interrupts auf dem Arduino Uno und verwandten Karten mit dem ATmega328P-Prozessor funktionieren. Boards wie die:

  • Uno
  • Mini
  • Nano
  • Pro Mini
  • Seerosenblatt

Bitte besprechen Sie insbesondere:

  • Wofür Interrupts verwendet werden sollen
  • So schreiben Sie eine Interrupt Service Routine (ISR)
  • Zeitprobleme
  • Kritische Abschnitte
  • Atomarer Zugriff auf Daten

Hinweis: Dies ist eine Referenzfrage .

Nick Gammon
quelle

Antworten:

24

TL; DR:

Beim Schreiben einer Interrupt Service Routine (ISR):

  • Halte es kurz
  • Nicht benutzen delay ()
  • Machen Sie keine seriellen Drucke
  • Machen Sie Variablen, die mit dem Hauptcode geteilt werden, flüchtig
  • Mit dem Hauptcode gemeinsam genutzte Variablen müssen möglicherweise durch "kritische Abschnitte" geschützt werden (siehe unten).
  • Versuchen Sie nicht, Interrupts aus- oder einzuschalten

Was sind Interrupts?

Die meisten Prozessoren haben Interrupts. Mit Interrupts können Sie auf "externe" Ereignisse reagieren, während Sie etwas anderes tun. Wenn Sie beispielsweise das Abendessen kochen, können Sie die Kartoffeln 20 Minuten lang kochen lassen. Anstatt 20 Minuten lang auf die Uhr zu starren, können Sie einen Timer einstellen und dann fernsehen. Wenn der Timer klingelt, "unterbrechen" Sie Ihren Fernseher, um etwas mit den Kartoffeln zu tun.


Beispiel für Interrupts

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
}

Dieses Beispiel zeigt, wie Sie, obwohl die Hauptschleife nichts tut, die LED an Pin 13 ein- oder ausschalten können, wenn der Schalter an Pin D2 gedrückt wird.

Um dies zu testen, schließen Sie einfach ein Kabel (oder einen Schalter) zwischen D2 und Masse an. Das interne Pullup (im Setup aktiviert) zwingt den Pin normal auf HIGH. Wenn es geerdet ist, wird es NIEDRIG. Die Änderung des Pins wird durch einen CHANGE-Interrupt erkannt, wodurch die Interrupt Service Routine (ISR) aufgerufen wird.

In einem komplizierteren Beispiel kann die Hauptschleife etwas Nützliches tun, z. B. Temperaturmessungen durchführen und dem Interrupt-Handler ermöglichen, eine gedrückte Taste zu erkennen.


Pin-Nummern in Interrupt-Nummern umwandeln

Um die Konvertierung von Interrupt-Vektornummern in Pin-Nummern zu vereinfachen, können Sie die Funktion aufrufen digitalPinToInterrupt()und eine Pin-Nummer übergeben. Es gibt die entsprechende Interrupt-Nummer oder NOT_AN_INTERRUPT(-1) zurück.

Auf dem Uno ist Pin D2 auf der Karte beispielsweise Interrupt 0 (INT0_vect aus der folgenden Tabelle).

Somit haben diese beiden Linien den gleichen Effekt:

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

Der zweite ist jedoch leichter zu lesen und für verschiedene Arduino-Typen portabler.


Verfügbare Interrupts

Unten finden Sie eine Liste der Interrupts in Prioritätsreihenfolge für den 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)

Interne Namen (mit denen Sie ISR-Rückrufe einrichten können) stehen in Klammern.

Warnung: Wenn Sie den Namen des Interruptvektors falsch schreiben, wird die Interruptroutine nicht aufgerufen , selbst wenn nur die Groß- und Kleinschreibung falsch geschrieben wird (eine einfache Sache). Es wird kein Compilerfehler angezeigt.


Gründe für die Verwendung von Interrupts

Die Hauptgründe, warum Sie Interrupts verwenden könnten, sind:

  • Zum Erkennen von Stiftwechseln (z. B. Drehgeber, Tastendruck)
  • Watchdog-Timer (z. B. wenn nach 8 Sekunden nichts passiert, unterbrechen Sie mich)
  • Timer-Interrupts - werden zum Vergleichen / Überlaufen von Timern verwendet
  • SPI-Datenübertragungen
  • I2C-Datenübertragungen
  • USART-Datenübertragungen
  • ADC-Konvertierungen (analog zu digital)
  • EEPROM einsatzbereit
  • Flash-Speicher bereit

Die "Datenübertragungen" können verwendet werden, um ein Programm etwas anderes tun zu lassen, während Daten an der seriellen Schnittstelle, der SPI-Schnittstelle oder der I2C-Schnittstelle gesendet oder empfangen werden.

Wecken Sie den Prozessor

Externe Interrupts, Pin-Change-Interrupts und der Watchdog-Timer-Interrupt können ebenfalls verwendet werden, um den Prozessor aufzuwecken. Dies kann sehr praktisch sein, da der Prozessor im Ruhemodus so konfiguriert werden kann, dass er viel weniger Strom verbraucht (z. B. etwa 10 Mikroampere). Ein Interrupt mit steigendem, fallendem oder niedrigem Pegel kann verwendet werden, um ein Gadget zu aktivieren (z. B. wenn Sie eine Taste darauf drücken), oder ein Interrupt mit "Watchdog-Timer" kann es regelmäßig aktivieren (z. B. um die Zeit oder zu überprüfen) Temperatur).

Pin-Change-Interrupts können verwendet werden, um den Prozessor zu aktivieren, wenn eine Taste auf einer Tastatur oder ähnlichem gedrückt wird.

Der Prozessor kann auch durch einen Timer-Interrupt (z. B. einen Timer, der einen bestimmten Wert erreicht oder überläuft) und bestimmte andere Ereignisse, wie z. B. eine eingehende I2C-Nachricht, geweckt werden.


Interrupts aktivieren / deaktivieren

Der Interrupt "Zurücksetzen" kann nicht deaktiviert werden. Die anderen Interrupts können jedoch vorübergehend deaktiviert werden, indem das globale Interrupt-Flag gelöscht wird.

Interrupts aktivieren

Sie können Interrupts mit dem Funktionsaufruf "Interrupts" oder "sei" wie folgt aktivieren:

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

Interrupts deaktivieren

Wenn Sie Interrupts deaktivieren müssen, können Sie das globale Interrupt-Flag wie folgt "löschen":

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

Beide Methoden haben den gleichen Effekt. Wenn Sie interrupts/ verwenden, noInterruptsist es etwas einfacher, sich zu merken, in welcher Richtung sie sich befinden.

Die Standardeinstellung im Arduino ist, dass Interrupts aktiviert werden. Deaktivieren Sie sie nicht für längere Zeit, da sonst Dinge wie Timer nicht richtig funktionieren.

Warum Interrupts deaktivieren?

Möglicherweise gibt es zeitkritische Codeteile, die nicht unterbrochen werden sollen, z. B. durch einen Timer-Interrupt.

Auch wenn Multi-Byte-Felder von einem ISR aktualisiert werden, müssen Sie möglicherweise Interrupts deaktivieren, damit Sie die Daten "atomar" erhalten. Andernfalls wird möglicherweise ein Byte vom ISR aktualisiert, während Sie das andere lesen.

Beispielsweise:

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

Durch das vorübergehende Ausschalten von Interrupts wird sichergestellt, dass sich isrCounter (ein in einem ISR festgelegter Zähler) nicht ändert, während wir seinen Wert erhalten.

Warnung: Wenn Sie nicht sicher sind, ob Interrupts bereits aktiviert sind oder nicht, müssen Sie den aktuellen Status speichern und anschließend wiederherstellen. Der Code aus der Funktion millis () führt beispielsweise Folgendes aus:

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

Beachten Sie, dass in den angegebenen Zeilen das aktuelle SREG (Statusregister) gespeichert ist, das das Interrupt-Flag enthält. Nachdem wir den Timer-Wert (der 4 Bytes lang ist) erhalten haben, setzen wir das Statusregister wieder so, wie es war.


Tipps

Funktionsnamen

Die Funktionen cli/ seiund das Register SREG sind spezifisch für die AVR-Prozessoren. Wenn Sie andere Prozessoren wie die ARM-Prozessoren verwenden, können die Funktionen geringfügig abweichen.

Global deaktivieren oder einen Interrupt deaktivieren

Wenn Sie verwenden cli(), deaktivieren Sie alle Interrupts (einschließlich Timer-Interrupts, serielle Interrupts usw.).

Wenn Sie jedoch nur einen bestimmten Interrupt deaktivieren möchten, sollten Sie das Interrupt-Aktivierungsflag für diese bestimmte Interruptquelle löschen. Rufen Sie beispielsweise für externe Interrupts auf detachInterrupt().


Was ist Interrupt-Priorität?

Da es 25 Interrupts gibt (außer Zurücksetzen), ist es möglich, dass mehr als ein Interrupt-Ereignis gleichzeitig auftritt oder zumindest auftritt, bevor das vorherige verarbeitet wird. Es kann auch ein Interrupt-Ereignis auftreten, wenn Interrupts deaktiviert sind.

Die Prioritätsreihenfolge ist die Reihenfolge, in der der Prozessor nach Interrupt-Ereignissen sucht. Je höher die Liste, desto höher die Priorität. So würde beispielsweise eine externe Interrupt-Anforderung 0 (Pin D2) vor der externen Interrupt-Anforderung 1 (Pin D3) bearbeitet.


Können Interrupts auftreten, während Interrupts deaktiviert sind?

Interrupt- Ereignisse (dh das Erkennen des Ereignisses) können jederzeit auftreten. Die meisten werden durch Setzen eines "Interrupt-Ereignis" -Flag im Prozessor gespeichert. Wenn Interrupts deaktiviert sind, wird dieser Interrupt behandelt, wenn sie in der Prioritätsreihenfolge wieder aktiviert werden.


Wie benutzt man Interrupts?

  • Sie schreiben eine ISR (Interrupt Service Routine). Dies wird aufgerufen, wenn der Interrupt auftritt.
  • Sie teilen dem Prozessor mit, wann der Interrupt ausgelöst werden soll.

ISR schreiben

Interrupt Service Routines sind Funktionen ohne Argumente. Einige Arduino-Bibliotheken sind so konzipiert, dass sie Ihre eigenen Funktionen aufrufen. Sie geben also nur eine normale Funktion an (wie in den obigen Beispielen), z.

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

Wenn eine Bibliothek jedoch noch keinen "Hook" für einen ISR bereitgestellt hat, können Sie Ihren eigenen wie folgt erstellen:

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

In diesem Fall verwenden Sie das Makro "ISR" und geben den Namen des relevanten Interrupt-Vektors an (aus der vorherigen Tabelle). In diesem Fall verarbeitet der ISR den Abschluss einer SPI-Übertragung. (Beachten Sie, dass einige alte Codes SIGNAL anstelle von ISR verwenden, SIGNAL jedoch veraltet ist.)

Anschließen eines ISR an einen Interrupt

Für Interrupts, die bereits von Bibliotheken verarbeitet werden, verwenden Sie einfach die dokumentierte Schnittstelle. Beispielsweise:

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

In diesem Fall ist die I2C-Bibliothek so konzipiert, dass sie eingehende I2C-Bytes intern verarbeitet und dann Ihre bereitgestellte Funktion am Ende des eingehenden Datenstroms aufruft. In diesem Fall ist receiveEvent nicht ausschließlich ein ISR (es hat ein Argument), sondern wird von einem eingebauten ISR aufgerufen.

Ein weiteres Beispiel ist der Interrupt "externer Pin".

// 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

In diesem Fall fügt die Funktion attachInterrupt die Funktion switchPressed zu einer internen Tabelle hinzu und konfiguriert zusätzlich die entsprechenden Interrupt-Flags im Prozessor.

Konfigurieren des Prozessors für die Behandlung eines Interrupts

Der nächste Schritt, sobald Sie einen ISR haben, besteht darin, dem Prozessor mitzuteilen, dass diese bestimmte Bedingung einen Interrupt auslösen soll.

Als Beispiel könnten Sie für External Interrupt 0 (den D2-Interrupt) Folgendes tun:

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

Lesbarer wäre es, die definierten Namen wie folgt zu verwenden:

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

EICRA (External Interrupt Control Register A) würde gemäß dieser Tabelle aus dem Atmega328-Datenblatt festgelegt. Das definiert die genaue Art des gewünschten Interrupts:

  • 0: Der niedrige Pegel von INT0 erzeugt eine Interrupt-Anforderung (LOW-Interrupt).
  • 1: Jede logische Änderung an INT0 erzeugt eine Interrupt-Anforderung (CHANGE-Interrupt).
  • 2: Die fallende Flanke von INT0 erzeugt eine Interruptanforderung (FALLING Interrupt).
  • 3: Die ansteigende Flanke von INT0 erzeugt eine Interrupt-Anfrage (RISING-Interrupt).

EIMSK (External Interrupt Mask Register) aktiviert den Interrupt tatsächlich.

Glücklicherweise müssen Sie sich diese Zahlen nicht merken, da attachInterrupt dies für Sie erledigt. Dies ist jedoch tatsächlich der Fall, und für andere Interrupts müssen Sie möglicherweise Interrupt-Flags "manuell" setzen.


Low-Level-ISRs im Vergleich zu Bibliotheks-ISRs

Um Ihr Leben zu vereinfachen, befinden sich einige gängige Interrupt-Handler tatsächlich im Bibliothekscode (z. B. INT0_vect und INT1_vect). Anschließend wird eine benutzerfreundlichere Oberfläche bereitgestellt (z. B. attachInterrupt). AttachInterrupt speichert tatsächlich die Adresse Ihres gewünschten Interrupt-Handlers in einer Variablen und ruft diese bei Bedarf von INT0_vect / INT1_vect auf. Außerdem werden die entsprechenden Registerflags gesetzt, um den Handler bei Bedarf aufzurufen.


Können ISRs unterbrochen werden?

Kurz gesagt, nein, es sei denn, Sie möchten, dass sie es sind.

Wenn ein ISR eingegeben wird, werden Interrupts deaktiviert . Natürlich müssen sie zuerst aktiviert worden sein, sonst würde der ISR nicht eingegeben. Um jedoch zu vermeiden, dass ein ISR selbst unterbrochen wird, schaltet der Prozessor Interrupts aus.

Wenn ein ISR beendet wird, werden Interrupts wieder aktiviert . Der Compiler generiert auch Code in einem ISR, um Register und Statusflags zu speichern, sodass alles, was Sie zum Zeitpunkt des Interrupts getan haben, nicht beeinträchtigt wird.

Sie können Interrupts jedoch innerhalb eines ISR aktivieren, wenn Sie dies unbedingt müssen, z.

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

}  // end of switchPressed

Normalerweise benötigen Sie dafür einen guten Grund, da ein weiterer Interrupt jetzt zu einem rekursiven Aufruf von pinChange führen kann, was möglicherweise zu unerwünschten Ergebnissen führen kann.


Wie lange dauert die Ausführung eines ISR?

Gemäß dem Datenblatt beträgt die minimale Zeit, um einen Interrupt zu warten, 4 Taktzyklen (um den aktuellen Programmzähler auf den Stapel zu schieben), gefolgt von dem Code, der jetzt an der Interruptvektorposition ausgeführt wird. Dies beinhaltet normalerweise einen Sprung dorthin, wo sich die Interruptroutine tatsächlich befindet, was weitere 3 Zyklen sind. Die Untersuchung des vom Compiler erzeugten Codes zeigt, dass die Ausführung eines mit der "ISR" -Deklaration erstellten ISR etwa 2,625 µs dauern kann, zuzüglich dessen, was der Code selbst tut. Die genaue Menge hängt davon ab, wie viele Register gespeichert und wiederhergestellt werden müssen. Die Mindestmenge wäre 1,1875 us.

Die externen Interrupts (bei denen Sie attachInterrupt verwenden) leisten etwas mehr und benötigen insgesamt etwa 5,125 µs (mit einem 16-MHz-Takt).


Wie lange dauert es, bis der Prozessor einen ISR eingibt?

Dies variiert etwas. Die oben angegebenen Zahlen sind die idealen, bei denen der Interrupt sofort verarbeitet wird. Einige Faktoren können dies verzögern:

  • Wenn der Prozessor schläft, gibt es bestimmte "Weck" -Zeiten, die einige Millisekunden betragen können, während die Uhr wieder auf Geschwindigkeit gespult wird. Diese Zeit hängt von den Einstellungen der Sicherung ab und davon, wie tief der Schlaf ist.

  • Wenn eine Interrupt-Serviceroutine bereits ausgeführt wird, können keine weiteren Interrupts eingegeben werden, bis sie entweder beendet sind oder Interrupts selbst aktivieren. Aus diesem Grund sollten Sie jede Interrupt-Serviceroutine kurz halten, da Sie mit jeder Mikrosekunde, die Sie in einer verbringen, möglicherweise die Ausführung einer anderen verzögern.

  • Einige Codes schalten Interrupts aus. Wenn Sie beispielsweise millis () aufrufen, werden Interrupts kurz deaktiviert. Daher würde sich die Zeit für die Wartung eines Interrupts um die Zeitspanne verlängern, in der Interrupts ausgeschaltet wurden.

  • Interrupts können nur am Ende eines Befehls bedient werden. Wenn also ein bestimmter Befehl drei Taktzyklen benötigt und gerade gestartet wurde, wird der Interrupt um mindestens einige Taktzyklen verzögert.

  • Ein Ereignis, das Interrupts wieder einschaltet (z. B. Rückkehr von einer Interrupt-Serviceroutine), führt garantiert mindestens einen weiteren Befehl aus. Selbst wenn ein ISR endet und Ihr Interrupt ansteht, muss er noch auf eine weitere Anweisung warten, bevor er gewartet wird.

  • Da Interrupts eine Priorität haben, wird möglicherweise ein Interrupt mit höherer Priorität vor dem Interrupt bedient, an dem Sie interessiert sind.


Leistungsüberlegungen

Interrupts können in vielen Situationen die Leistung steigern, da Sie mit der "Hauptarbeit" Ihres Programms fortfahren können, ohne ständig testen zu müssen, ob Schalter gedrückt wurden. Allerdings wäre der Aufwand für die Wartung eines Interrupts, wie oben erläutert, tatsächlich mehr als eine "enge Schleife", die einen einzelnen Eingangsport abfragt. Sie können kaum auf ein Ereignis innerhalb von beispielsweise einer Mikrosekunde reagieren. In diesem Fall können Sie Interrupts (z. B. Timer) deaktivieren und einfach in einer Schleife nach dem zu ändernden Pin suchen.


Wie werden Interrupts in die Warteschlange gestellt?

Es gibt zwei Arten von Interrupts:

  • Einige setzen ein Flag und werden in der Prioritätsreihenfolge behandelt, selbst wenn das Ereignis, das sie verursacht hat, gestoppt wurde. Zum Beispiel ein ansteigender, fallender oder sich ändernder Pegel-Interrupt an Pin D2.

  • Andere werden nur getestet, wenn sie "gerade" stattfinden. Zum Beispiel ein Low-Level-Interrupt an Pin D2.

Diejenigen, die ein Flag setzen, können als in die Warteschlange gestellt angesehen werden, da das Interrupt-Flag gesetzt bleibt, bis die Interrupt-Routine eingegeben wird. Zu diesem Zeitpunkt löscht der Prozessor das Flag. Da es nur ein Flag gibt, wird es natürlich nicht zweimal gewartet, wenn dieselbe Interrupt-Bedingung erneut auftritt, bevor das erste verarbeitet wird.

Beachten Sie, dass diese Flags gesetzt werden können, bevor Sie den Interrupt-Handler anhängen. Zum Beispiel ist es möglich, dass ein Interrupt mit steigendem oder fallendem Pegel an Pin D2 "markiert" wird. Sobald Sie einen AttachInterrupt ausführen, wird der Interrupt sofort ausgelöst, selbst wenn das Ereignis vor einer Stunde aufgetreten ist. Um dies zu vermeiden, können Sie das Flag manuell löschen. Beispielsweise:

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

Die "Low Level" -Interrupts werden jedoch kontinuierlich überprüft. Wenn Sie also nicht vorsichtig sind, werden sie auch nach dem Aufrufen des Interrupts weiter ausgelöst. Das heißt, der ISR wird beendet und der Interrupt wird sofort wieder ausgelöst. Um dies zu vermeiden, sollten Sie sofort nach dem Auslösen des Interrupts einen removeInterrupt ausführen.


Hinweise zum Schreiben von ISRs

Kurz gesagt, halten Sie sie kurz! Während ein ISR ausgeführt wird, können andere Interrupts nicht verarbeitet werden. Wenn Sie versuchen, zu viel zu tun, können Sie leicht Tastendrücke oder eingehende serielle Kommunikation verpassen. Insbesondere sollten Sie nicht versuchen, "Drucke" innerhalb eines ISR zu debuggen. Die dafür benötigte Zeit wird wahrscheinlich mehr Probleme verursachen als lösen.

Es ist sinnvoll, ein Einzelbyte-Flag zu setzen und dieses Flag dann in der Hauptschleifenfunktion zu testen. Oder speichern Sie ein eingehendes Byte von einer seriellen Schnittstelle in einem Puffer. Die eingebauten Timer-Interrupts verfolgen die verstrichene Zeit, indem sie jedes Mal ausgelöst werden, wenn der interne Timer überläuft. Sie können also die verstrichene Zeit berechnen, indem Sie wissen, wie oft der Timer übergelaufen ist.

Denken Sie daran, dass innerhalb eines ISR Interrupts deaktiviert sind. Die Hoffnung, dass sich die von millis () - Funktionsaufrufen zurückgegebene Zeit ändert, führt zu Enttäuschungen. Es ist gültig , die Zeit auf diese Weise zu erhalten. Beachten Sie jedoch, dass der Timer nicht inkrementiert wird. Und wenn Sie zu lange im ISR verbringen, verpasst der Timer möglicherweise ein Überlaufereignis, was dazu führt, dass die von millis () zurückgegebene Zeit falsch wird.

Ein Test zeigt, dass auf einem 16-MHz-Atmega328-Prozessor ein Aufruf von micros () 3,5625 µs dauert. Ein Aufruf von millis () dauert 1,9375 µs. Das Aufzeichnen (Speichern) des aktuellen Timer-Werts ist in einem ISR sinnvoll. Das Auffinden der verstrichenen Millisekunden ist schneller als die verstrichenen Mikrosekunden (die Millisekundenzahl wird nur aus einer Variablen abgerufen). Die Mikrosekundenzahl wird jedoch erhalten, indem der aktuelle Wert des Timer 0-Timers (der weiter erhöht wird) zu einer gespeicherten "Timer 0-Überlaufzahl" addiert wird.

Warnung: Da Interrupts in einem ISR deaktiviert sind und die neueste Version der Arduino IDE Interrupts zum seriellen Lesen und Schreiben sowie zum Inkrementieren des von "millis" und "delay" verwendeten Zählers verwendet, sollten Sie nicht versuchen, diese Funktionen zu verwenden in einem ISR. Um es anders zu sagen:

  • Versuchen Sie nicht zu verzögern, z. delay (100);
  • Sie können die Zeit von einem Anruf an millis abrufen, sie wird jedoch nicht erhöht. Versuchen Sie also nicht zu verzögern, indem Sie darauf warten, dass sie zunimmt.
  • Machen Sie keine seriellen Drucke (z. B. Serial.println ("ISR entered");)
  • Versuchen Sie nicht, seriell zu lesen.

Pinwechsel-Interrupts

Es gibt zwei Möglichkeiten, externe Ereignisse an Pins zu erkennen. Das erste sind die speziellen "externen Interrupt" -Pins D2 und D3. Diese allgemeinen diskreten Interrupt-Ereignisse, eines pro Pin. Sie können zu diesen gelangen, indem Sie attachInterrupt für jeden Pin verwenden. Sie können eine steigende, fallende, sich ändernde oder niedrige Bedingung für den Interrupt angeben.

Es gibt jedoch auch "Pinwechsel" -Interrupts für alle Pins (beim Atmega328 nicht unbedingt alle Pins bei anderen Prozessoren). Diese wirken auf Gruppen von Stiften (D0 bis D7, D8 bis D13 und A0 bis A5). Sie haben auch eine niedrigere Priorität als die externen Ereignisunterbrechungen. Sie sind jedoch etwas umständlicher zu verwenden als die externen Interrupts, da sie in Stapeln gruppiert sind. Wenn der Interrupt ausgelöst wird, müssen Sie in Ihrem eigenen Code genau herausfinden, welcher Pin den Interrupt verursacht hat.

Beispielcode:

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
  }

Um einen Pinwechsel-Interrupt zu behandeln, müssen Sie:

  • Geben Sie an, welcher Pin in der Gruppe enthalten ist. Dies ist die PCMSKn-Variable (wobei n 0, 1 oder 2 aus der folgenden Tabelle ist). Sie können Interrupts an mehr als einem Pin haben.
  • Aktivieren Sie die entsprechende Gruppe von Interrupts (0, 1 oder 2).
  • Stellen Sie einen Interrupt-Handler wie oben gezeigt bereit

Pin-Tabelle -> Pin-Änderungsnamen / Masken

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)

Handler-Verarbeitung unterbrechen

Der Interrupt-Handler müsste herausfinden, welcher Pin den Interrupt verursacht hat, wenn die Maske mehr als einen angibt (z. B. wenn Sie Interrupts für D8 / D9 / D10 wünschen). Dazu müssten Sie den vorherigen Status dieses Pins speichern und herausfinden (indem Sie ein digitalRead oder ähnliches ausführen), ob sich dieser bestimmte Pin geändert hat.


Sie verwenden wahrscheinlich sowieso Interrupts ...

Eine "normale" Arduino-Umgebung verwendet bereits Interrupts, auch wenn Sie dies nicht persönlich versuchen. Die Funktionsaufrufe millis () und micros () verwenden die Funktion "Timerüberlauf". Einer der internen Timer (Timer 0) ist so eingerichtet, dass er ungefähr 1000 Mal pro Sekunde unterbricht und einen internen Zähler inkrementiert, der effektiv zum Millis () -Zähler wird. Es steckt noch ein bisschen mehr dahinter, da die exakte Taktrate angepasst wird.

Auch die serielle Hardwarebibliothek verwendet Interrupts, um eingehende und ausgehende serielle Daten zu verarbeiten. Dies ist sehr nützlich, da Ihr Programm andere Dinge tun kann, während die Interrupts ausgelöst werden, und einen internen Puffer auffüllt. Wenn Sie dann Serial.available () überprüfen, können Sie herausfinden, was, wenn überhaupt, in diesem Puffer abgelegt wurde.


Ausführen der nächsten Anweisung nach dem Aktivieren von Interrupts

Nach einigen Diskussionen und Recherchen im Arduino-Forum haben wir genau geklärt, was passiert, nachdem Sie Interrupts aktiviert haben. Ich kann mir drei Möglichkeiten vorstellen, wie Sie Interrupts aktivieren können, die zuvor nicht aktiviert waren:

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

In allen Fällen garantiert der Prozessor, dass der nächste Befehl nach dem Aktivieren von Interrupts (sofern diese zuvor deaktiviert wurden) immer ausgeführt wird, auch wenn ein Interrupt-Ereignis ansteht. (Mit "weiter" meine ich den nächsten in der Programmsequenz, nicht unbedingt den physisch folgenden. Beispielsweise springt ein RETI-Befehl dorthin zurück, wo der Interrupt aufgetreten ist, und führt dann einen weiteren Befehl aus.)

Auf diese Weise können Sie Code wie folgt schreiben:

sei ();
sleep_cpu ();

Ohne diese Garantie kann der Interrupt auftreten, bevor der Prozessor geschlafen hat, und dann möglicherweise nie geweckt werden.


Leere Interrupts

Wenn Sie lediglich möchten, dass ein Interrupt den Prozessor aufweckt, aber nichts Besonderes tut, können Sie die Definition EMPTY_INTERRUPT verwenden, z.

EMPTY_INTERRUPT (PCINT1_vect);

Dies erzeugt einfach eine "reti" -Anweisung (Rückkehr vom Interrupt). Da nicht versucht wird, Register zu speichern oder wiederherzustellen, ist dies der schnellste Weg, um einen Interrupt zum Aufwecken zu erhalten.


Kritische Abschnitte (Zugriff auf atomare Variablen)

Es gibt einige subtile Probleme in Bezug auf Variablen, die von Interrupt Service Routines (ISRs) und dem Hauptcode (dh dem Code, der nicht in einem ISR enthalten ist) gemeinsam genutzt werden.

Da ein ISR jederzeit ausgelöst werden kann, wenn Interrupts aktiviert sind, müssen Sie beim Zugriff auf solche gemeinsam genutzten Variablen vorsichtig sein, da diese möglicherweise in dem Moment aktualisiert werden, in dem Sie auf sie zugreifen.

Erstens ... wann verwenden Sie "flüchtige" Variablen?

Eine Variable sollte nur dann als flüchtig markiert werden, wenn sie sowohl innerhalb als auch außerhalb eines ISR verwendet wird.

  • Variablen, die nur außerhalb eines ISR verwendet werden, sollten nicht flüchtig sein.
  • Variablen, die nur innerhalb eines ISR verwendet werden, sollten nicht flüchtig sein.
  • Variablen, die sowohl innerhalb als auch außerhalb eines ISR verwendet werden, sollten volatil sein.

z.B.

volatile int counter;

Das Markieren einer Variablen als flüchtig weist den Compiler an, den Inhalt der Variablen nicht in einem Prozessorregister "zwischenzuspeichern", sondern ihn bei Bedarf immer aus dem Speicher zu lesen. Dies kann die Verarbeitung verlangsamen, weshalb Sie nicht jede Variable flüchtig machen, wenn sie nicht benötigt wird.

Schalten Sie Interrupts aus, während Sie auf eine flüchtige Variable zugreifen

Um beispielsweise mit counteiner bestimmten Zahl zu vergleichen , deaktivieren Sie die Interrupts während des Vergleichs, falls ein Byte von countvom ISR aktualisiert wurde und nicht das andere Byte.

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

Lesen Sie das Datenblatt!

Weitere Informationen zu Interrupts, Timern usw. finden Sie im Datenblatt des Prozessors.

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


Weitere Beispiele

Platzüberlegungen (Beschränkung der Postgröße) verhindern, dass ich mehr Beispielcode aufführe. Weitere Beispielcodes finden Sie auf meiner Seite zu Interrupts .

Nick Gammon
quelle
Eine sehr nützliche Referenz - das war eine beeindruckend schnelle Antwort.
Dat Han Bag
Es war eine Referenzfrage. Ich hatte die Antwort vorbereitet und es wäre noch schneller gewesen, wenn die Antwort nicht zu lang gewesen wäre, also musste ich sie zurückschneiden. Weitere Informationen finden Sie auf der verlinkten Website.
Nick Gammon
Über den "Schlafmodus" ist es effizient, den Arduino für etwa 500 ms schlafen zu lassen?
Dat Ha
@ Nick Gammon Ich denke, das Ein- oder Ausschalten (mit oder ohne Automatisierung) für die CPU kann als unkonventioneller Interrupt definiert werden - wenn Sie das wollten. "Ich hatte die Antwort vorbereitet" - Sie haben gerade die ganze Magie dieses Augenblicks herausgenommen, den ich dachte.
Dat Han Bag
1
Ich fürchte, das stimmt nicht. Ich habe ein Beispiel , das Pin-Wechsel-Interrupts verwendet, um aus dem Power-Down-Modus aufzuwachen. Auch wie ich auf meiner Seite über Interrupts erwähne, hat Atmel bestätigt, dass jeder externe Interrupt den Prozessor weckt (dh steigend / fallend / änderend und niedrig).
Nick Gammon