Sind Funktionszeigerzuweisungen in Arduino atomar?

11

Die folgenden Ausschnitte stammen aus dem Quellcode der TimerOne-Bibliothek :

// TimerOne.h:
void (*isrCallback)();

// TimerOne.cpp:
ISR(TIMER1_OVF_vect) // interrupt service routine that wraps a user defined function supplied by attachInterrupt
{
  Timer1.isrCallback();
}

// TimerOne.cpp:
void TimerOne::attachInterrupt(void (*isr)(), long microseconds)
{
  if(microseconds > 0) setPeriod(microseconds);
  isrCallback = isr; // register the user's callback with the real ISR
  TIMSK1 = _BV(TOIE1); // sets the timer overflow interrupt enable bit
  resume();                                                                                            
}

Die Frage: Wenn der Timer bereits läuft und das Hauptprogramm aufruft attachInterrupt(), kann der Timer-Interrupt dort während der Funktionszeigerzuweisung auftreten isrCallback = isr;? Dann Timer1.isrCallback();würde der Funktionszeiger mit etwas Glück teilweise aus der alten und teilweise aus der neuen Adresse bestehen, was dazu führen würde, dass der ISR zu einem falschen Ort springt?

Ich nehme an, dass dies der Fall sein könnte, da Funktionszeiger sicherlich breiter als 1 Byte sind und der Zugriff auf Daten mit> 1 Byte nicht atomar ist. Mögliche Problemumgehungen könnten sein:

  • Immer anrufen detachInterrupt() an, um sicherzustellen, dass der Timer nicht läuft, bevor Sie anrufen attachInterrupt(), dh klären Sie die Timer1-Dokumente.
  • Oder ändern Sie Timer1, indem Sie die Timer-Überlauf-Interrupts kurz zuvor vorübergehend deaktivieren isrCallback = isr;

Ist dies sinnvoll oder gibt es etwas in Timer1Quellen oder Funktionszeigerzuweisungen, das ich verpasst habe?

Joonas Pulakka
quelle

Antworten:

7

Schauen Sie sich den Code für attachInterrupt () und attachInterrupt () in an /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/WInterrupts.c(nun, dort befinden sie sich sowieso auf einem Mac. Die Arduino-Dateistruktur auf anderen Betriebssystemen sieht in den unteren Ebenen des Pfads wahrscheinlich ähnlich aus).

Es scheint, dass attachInterrupt () davon ausgeht, dass der betreffende Interrupt noch nicht aktiviert ist, da er den Funktionszeiger schreibt, ohne Vorsichtsmaßnahmen zu treffen. Beachten Sie, dass attachInterrupts () den Zielinterrupt deaktiviert, bevor ein NULL-Zeiger auf seinen Vektor geschrieben wird. Also würde ich wenigstens ein detachInterrupt()/ attachInterrupt()Paar verwenden

Ich möchte einen solchen Code selbst in einem kritischen Abschnitt ausführen. Es sieht so aus, als würde Ihr erster Weg (Trennen, dann Anhängen) funktionieren, obwohl ich nicht sicher sein kann, ob ein leider zeitgesteuerter Interrupt nicht übersehen werden kann. Das Datenblatt für Ihre MCU enthält möglicherweise mehr dazu. Aber ich bin mir an dieser Stelle auch nicht sicher, ob ein Global cli()/ sei()es auch nicht verpassen würde. Im ATMega2560-Datenblatt, Abschnitt 6.8, heißt es: "Wenn Sie den SEI-Befehl zum Aktivieren von Interrupts verwenden, wird der auf SEI folgende Befehl ausgeführt, bevor anstehende Interrupts ausgeführt werden, wie in diesem Beispiel gezeigt." Dies scheint zu implizieren, dass ein Interrupt während der Interrupts gepuffert werden kann sind weg.

JRobert
quelle
Es ist in der Tat nützlich, zu den Quellen zu tauchen :) TimerOnes Interrupt-Mechanismus zum Anhängen / Trennen scheint ähnlich wie der Standard (WInterrupt) zu sein und hat daher die gleichen "Funktionen".
Joonas Pulakka
0

Es sieht so aus, als hätten Sie einen Punkt. Es ist logisch, Interrupts so zu deaktivieren, dass Sie sie nicht wieder aktivieren, wenn sie überhaupt deaktiviert wurden. Beispielsweise:

  uint8_t oldSREG = SREG;  // remember if interrupts are on
  cli();                   // make the next line interruptible
  isrCallback = isr;       // register the user's callback with the real ISR
  SREG = oldSREG;          // turn interrupts back on, if they were on before

"Wenn Sie den SEI-Befehl zum Aktivieren von Interrupts verwenden, wird der Befehl nach SEI vor anstehenden Interrupts ausgeführt, wie in diesem Beispiel gezeigt."

Damit möchten Sie Code wie folgt schreiben lassen:

  sei ();         // enable interrupts
  sleep_cpu ();   // sleep

Ohne diese Bestimmung könnten Sie eine Unterbrechung zwischen diesen beiden Leitungen bekommen und somit auf unbestimmte Zeit schlafen (weil die Unterbrechung, die Sie wecken würde, aufgetreten ist, bevor Sie geschlafen haben). Die Bestimmung im Prozessor, dass der nächste Befehl, nachdem Interrupts aktiviert wurden, wenn sie vorher nicht aktiviert wurden, immer ausgeführt wird, schützt dagegen.

Nick Gammon
quelle
Wäre es nicht effizienter TIMSK1=0; TIFR1=_BV(TOV1); isrCallback=isr; TIMSK1=_BV(TOIE1);? Es schont ein CPU-Register und trägt nicht zur Interrupt-Latenz bei.
Edgar Bonet
Was ist mit den anderen Bits wie ICIE1, OCIE1B, OCIE1A? Ich verstehe die Latenz, aber Interrupts sollten in der Lage sein, einige Taktzyklen zu bewältigen, in denen sie nicht verfügbar sind. Möglicherweise liegt eine Rennbedingung vor. Der Interrupt wird möglicherweise bereits ausgelöst (dh das Flag in der CPU ist gesetzt), und das Ausschalten von TIMSK1 kann die Verarbeitung möglicherweise nicht stoppen. Möglicherweise müssen Sie auch TOV1 zurücksetzen (indem Sie 1 in TIFR1 schreiben), um sicherzustellen, dass dies nicht geschieht. Die Unsicherheit dort lässt mich denken, dass das globale Ausschalten von Interrupts der sicherere Weg ist.
Nick Gammon