Warum muss das flüchtige Schlüsselwort für globale Variablen verwendet werden, wenn Interrupts in Arduino behandelt werden?

7

Ich bin mit dem Schlüsselwort vertraut Volatile, das zum Deklarieren von Variablen verwendet wird, die von mehreren Threads in einer Softwareanwendung (im Grunde in einer Multithread-Anwendung) gemeinsam genutzt werden.

Aber warum muss ich eine Variable deklarieren, Volatilewenn der Code auf einem Arduino-Interrupt ausgeführt wird? Wird der im Interrupt ausgeführte Code auf einem anderen Thread ausgeführt? Ist noch etwas los?

Nick Gammon
quelle
Sie müssen dem Compiler mitteilen, dass die Variable vorhanden ist, damit Optimierungsalgorithmen sie nicht vollständig aus Ihrer ausführbaren Datei entfernen.
"Ich kenne das Schlüsselwort Volatile". Dann vergiss es und lies noch einmal.
Eugene Sh.
2
Tatsächlich ist der Interrupt-Handler ein von main getrennter Thread. Ohne die flüchtige Deklaration weiß der Compiler nicht, dass main das Global erneut lesen muss (was möglicherweise vom Interrupt-Handler geändert wurde).
MarkU
Is the code running in the interrupt executing on different thread- Nun, irgendwie nur das Gegenteil. Interrupts sind der Mechanismus, die von Betriebssystemprogrammierern zum Implementieren von Threads verwendet wird. Wir müssen also volatileMultithread-Code verwenden, da deren Kontext vom Betriebssystem in einem Interrupt geändert wurde. Wir müssen volatileInterrupts einfach verwenden, weil Interrupts in einem anderen Kontext ausgeführt werden.
Slebetman

Antworten:

11

Erstens ist es volatilenicht Volatile. Ich beschreibe diese Konzepte auf meiner Seite über Interrupts. Um jedoch zu vermeiden, dass nur ein Link beantwortet wird, wiederhole ich die relevanten Bits.


Was sind "flüchtige" Variablen?

Variablen, die zwischen ISR-Funktionen und normalen Funktionen geteilt werden, sollten deklariert werden volatile. Dies teilt dem Compiler mit, dass sich solche Variablen jederzeit ändern können. Daher muss der Compiler die Variable immer dann neu laden, wenn Sie darauf verweisen, anstatt sich auf eine Kopie zu verlassen, die sich möglicherweise in einem Prozessorregister befindet.

Zum Beispiel:

volatile boolean flag;

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

void setup ()
{
  attachInterrupt (0, isr, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  if (flag)
    {
    // interrupt has occurred
    }
}  // end of loop

Kritische Abschnitte ... Zugriff auf flüchtige 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 nur verwendet außerhalb eines ISR sollte nicht flüchtig sein.
  • Variablen nur verwendet innerhalb einer ISR sollte 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.


Atomarer Zugang

Betrachten Sie diesen Code:

volatile byte count;

ISR (TIMER1_OVF_vect)
  {
  count = 10;
  }

void setup ()
  {
  }

void loop () 
  {
  count++;
  }

Der Code, für den generiert wird count++(addiere 1, um zu zählen), lautet wie folgt:

 14c:   80 91 00 02     lds r24, 0x0200
 150:   8f 5f           subi    r24, 0xFF     <<---- problem if interrupt occurs before this is executed
 152:   80 93 00 02     sts 0x0200, r24   <<---- problem if interrupt occurs before this is executed

(Beachten Sie, dass 1 durch Subtrahieren von -1 addiert wird.)

Hier gibt es zwei Gefahrenpunkte, wie markiert. Wenn der Interrupt nach dem lds (Laderegister 24 mit der Variablen count), aber vor den sts (zurück in die Variable countspeichern) ausgelöst wird, kann die Variable vom ISR (TIMER1_OVF_vect) geändert werden, diese Änderung geht jedoch jetzt verloren , da die Variable im Register wurde stattdessen verwendet.

Wir müssen die gemeinsam genutzte Variable schützen, indem wir Interrupts wie folgt kurz ausschalten:

volatile byte count;

ISR (TIMER1_OVF_vect)
  {
  count = 10;
  }  // end of TIMER1_OVF_vect

void setup ()
  {
  }  // end of setup

void loop () 
  {
  noInterrupts ();
  count++;
  interrupts ();
  } // end of loop

Jetzt erfolgt die Aktualisierung von count"atomar" ... das heißt, sie kann nicht unterbrochen werden.


Multi-Byte-Variablen

Lassen Sie uns counteine 2-Byte-Variable erstellen und das andere Problem sehen:

volatile unsigned int count;

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

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

void loop () 
  {
  if (count > 20)
     digitalWrite (13, HIGH);
  } // end of loop

OK, wir ändern uns nicht countmehr. Gibt es also immer noch ein Problem? Leider schon. Schauen wir uns den generierten Code für die "if" -Anweisung an:

 172:   80 91 10 02     lds r24, 0x0210
 176:   90 91 11 02     lds r25, 0x0211  <<---- problem if interrupt occurs before this is executed
 17a:   45 97           sbiw    r24, 0x15   
 17c:   50 f0           brcs    .+20        

Stellen Sie sich vor, countdas wäre 0xFFFF und würde gleich wieder auf Null "umlaufen". Wir laden 0xFF in ein Register, aber bevor wir das zweite 0xFF laden, ändert sich die Variable in 0x00. Jetzt denken wir, countist 0x00FF, was weder der Wert ist, den es vorher (0xFFFF) noch jetzt (0x0000) hatte.

Also müssen wir den Zugriff auf die gemeinsam genutzte Variable wieder wie folgt "schützen":

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

Was ist, wenn Sie nicht sicher sind, ob Interrupts aktiviert oder deaktiviert sind?

Hier gibt es ein letztes "Gotcha". Was ist, wenn Interrupts bereits ausgeschaltet sind? Dann ist es eine schlechte Idee, sie danach wieder einzuschalten.

In diesem Fall müssen Sie das Prozessorstatusregister wie folgt speichern:

unsigned int getCount ()
  {
  unsigned int c;
  byte oldSREG = SREG;   // remember if interrupts are on or off

  noInterrupts ();   // turn interrupts off
  c = count;         // access the shared variable
  SREG = oldSREG;    // turn interrupts back on, if they were on before

  return c;          // return copy of shared variable
  }  // end of getCount

Dieser "sichere" Code speichert den aktuellen Status von Interrupts, schaltet sie aus (sie sind möglicherweise bereits ausgeschaltet), verwandelt die gemeinsam genutzte Variable in eine temporäre Variable, schaltet Interrupts wieder ein - wenn sie bei der Eingabe der Funktion aktiviert waren - und kehrt dann zurück die Kopie der gemeinsam genutzten Variablen.


Zusammenfassung

Code scheint "zu funktionieren", auch wenn Sie die oben genannten Vorsichtsmaßnahmen nicht treffen. Dies liegt daran, dass die Wahrscheinlichkeit, dass der Interrupt genau zum falschen Zeitpunkt auftritt, relativ gering ist (möglicherweise 1 zu 100, abhängig von der Codegröße). Aber früher oder später wird es im falschen Moment passieren und Ihr Code wird entweder abstürzen oder gelegentlich die falschen Ergebnisse zurückgeben.

Achten Sie daher für zuverlässigen Code auf den Schutz des Zugriffs auf gemeinsam genutzte Variablen.

Nick Gammon
quelle
Ich dachte, volatile würde den Mehrbyte-Typ Lese- und Schreibzugriff in der Hauptschleife und im ISR verarbeiten! Ich habe mich also völlig geirrt und sollte die Interrupts ausschalten, bevor ich in der Hauptschleife auf sie zugreife, oder?! ;-(
Shamim
1
Das stimmt. volatileWeist den Compiler lediglich an, Variablen nicht zu optimieren und in Prozessorregistern zu behalten. Es hat keine weiteren Nebenwirkungen (abgesehen davon, dass der Compiler weiß, dass sich diese Variablen jederzeit ändern können, werden flüchtige Variablen niemals optimiert, weil sie "nicht verwendet" werden). Also ja, wie oben dargestellt, "schützen" Sie den Zugriff auf Multi-Byte-Variablen, indem Sie Interrupts ausschalten.
Nick Gammon
6

volatilehat nichts mit Multithreading an sich zu tun, und es scheint zu den am häufigsten verwendeten Keywords in diesem Bereich zu gehören. Es ist niemals ein Ersatz für eine ordnungsgemäße Synchronisation.

Ruft gemäß Standard C das volatilemeist implementierungsdefinierte Verhalten auf, bei dem der Compiler angewiesen wird, die Zugriffszugriffe auf den Speicher, in dem sich eine Variable befindet, nicht zu optimieren, selbst wenn der Compiler der Meinung ist, dass es keine andere geben kann.

Dies kann in den folgenden Situationen nützlich sein, in denen der Compiler nicht weiß, dass externe Mittel eine Variable ändern können:

  • speicherabgebildete Hardwareregister
  • Variablen, auf die von anderen Threads zugegriffen wird, die jedoch in einer engen Schleife überprüft werden
  • Variablen, auf die von Interrupt- oder Signalhandlern zugegriffen wird, die jedoch in einer engen Schleife überprüft werden

quelle