Interrupt-Handling in Mikrocontrollern und FSM-Beispiel


9

Erste Frage

Ich habe eine allgemeine Frage zum Umgang mit Interrupts in Mikrocontrollern. Ich verwende den MSP430, aber ich denke, die Frage kann auf andere uCs ausgedehnt werden. Ich möchte wissen, ob es eine gute Praxis ist, Interrupts häufig entlang des Codes zu aktivieren / deaktivieren. Ich meine, wenn ich einen Teil des Codes habe, der nicht empfindlich auf Interrupts reagiert (oder noch schlimmer, aus irgendeinem Grund nicht auf Interrupts hören darf), ist es besser:

  • Deaktivieren Sie die Interrupts vor und aktivieren Sie sie nach dem kritischen Abschnitt wieder.
  • Setzen Sie ein Flag in den jeweiligen ISR und setzen Sie (anstatt den Interrupt zu deaktivieren) das Flag vor dem kritischen Abschnitt auf false und setzen Sie es unmittelbar danach auf true zurück. Um zu verhindern, dass der Code des ISR ausgeführt wird.
  • Keiner der beiden, daher sind Vorschläge willkommen!

Update: Interrupts und Zustandsdiagramme

Ich werde eine bestimmte Situation zur Verfügung stellen. Nehmen wir an, wir möchten ein Zustandsdiagramm implementieren, das aus 4 Blöcken besteht:

  1. Übergänge / Wirkung.
  2. Ausgangsbedingungen.
  3. Einstiegsaktivität.
  4. Aktivität ausführen.

Das hat uns ein Professor an der Universität beigebracht. Wahrscheinlich ist es nicht die beste Möglichkeit, dieses Schema zu befolgen:

while(true) {

  /* Transitions/Effects */
  //----------------------------------------------------------------------
  next_state = current_state;

  switch (current_state)
  {
    case STATE_A:
      if(EVENT1) {next_state = STATE_C}
      if(d == THRESHOLD) {next_state = STATE_D; a++}
      break;
    case STATE_B:
      // transitions and effects
      break;
    (...)
  }

  /* Exit activity -> only performed if I leave the state for a new one */
  //----------------------------------------------------------------------
  if (next_state != current_state)
  {
    switch(current_state)
    {
      case STATE_A:
        // Exit activity of STATE_A
        break;
      case STATE_B:
        // Exit activity of STATE_B
        break;
        (...)
    }
  }

  /* Entry activity -> only performed the 1st time I enter a state */
  //----------------------------------------------------------------------
  if (next_state != current_state)
  {
    switch(next_state)
    {
      case STATE_A:
        // Entry activity of STATE_A
        break;
      case STATE_B:
        // Entry activity of STATE_B
        break;
      (...)
    }
  }

  current_state = next_state;

  /* Do activity */
  //----------------------------------------------------------------------
  switch (current_state)
  {
    case STATE_A:
      // Do activity of STATE_A
      break;
    case STATE_B:
      // Do activity of STATE_B
      break;
    (...)
  }
}

Nehmen wir auch an, dass STATE_Aich beispielsweise empfindlich auf einen Interrupt reagieren möchte, der von einer Reihe von Tasten ausgeht (mit Debouce-System usw. usw.). Wenn jemand eine dieser Tasten drückt, wird ein Interrupt generiert und das Flag für den Eingangsport in eine Variable kopiert buttonPressed. Wenn die Entprellung auf irgendeine Weise auf 200 ms eingestellt ist (Watchdog-Timer, Timer, Zähler, ...), können wir sicher buttonPressednicht vor 200 ms mit einem neuen Wert aktualisiert werden. Das frage ich dich (und mich selbst :) natürlich)

Muss ich die Unterbrechung der DO-Aktivität aktivieren STATE_Aund deaktivieren, bevor ich gehe?

/* Do activity */
//-------------------------------------
switch (current_state)
{
  case STATE_A:
    // Do activity of STATE_A
    Enable_ButtonsInterrupt(); // and clear flags before it
    // Do fancy stuff and ...
    // ... wait until a button is pressed (e.g. LPM3 of the MSP430)
    // Here I have my buttonPressed flag ready!
    Disable_ButtonsInterrupt();
    break;
  case STATE_B:
    // Do activity of STATE_B
    break;
  (...)
}

Auf eine Weise, dass ich sicher bin, dass ich beim nächsten Ausführen von Block 1 (Übergang / Effekte) bei der nächsten Iteration sicher bin, dass die entlang der Übergänge überprüften Bedingungen nicht von einem nachfolgenden Interrupt stammen, der den vorherigen Wert buttonPresseddieses I überschrieben hat brauchen (obwohl es unmöglich ist, dass dies passiert, weil 250 ms vergehen müssen).


3
Es ist schwierig, eine Empfehlung abzugeben, ohne mehr über Ihre Situation zu wissen. Manchmal ist es jedoch erforderlich, Interrupts in eingebetteten Systemen zu deaktivieren. Es ist vorzuziehen, Interrupts nur für kurze Zeit deaktiviert zu lassen, damit Interrupts nicht übersehen werden. Möglicherweise können nur bestimmte Interrupts und nicht alle Interrupts deaktiviert werden. Ich kann mich nicht erinnern, jemals die von Ihnen beschriebene Flag-Inside-ISR-Technik verwendet zu haben, daher bin ich skeptisch, ob dies Ihre beste Lösung ist.
Kkrambo

Antworten:


17

Die erste Taktik besteht darin, die gesamte Firmware so zu gestalten, dass Interrupts jederzeit auftreten können. Das Ausschalten von Interrupts, damit der Vordergrundcode eine atomare Sequenz ausführen kann, sollte sparsam erfolgen. Es gibt oft einen architektonischen Weg darum herum.

Die Maschine ist jedoch da, um Sie zu bedienen, nicht umgekehrt. Allgemeine Faustregeln sollen nur schlechte Programmierer davon abhalten, wirklich schlechten Code zu schreiben. Es ist weitaus besser, genau zu verstehen , wie die Maschine funktioniert, und dann einen guten Weg zu finden, um diese Fähigkeiten für die Ausführung der gewünschten Aufgabe zu nutzen.

Denken Sie daran, dass Sie ansonsten die Klarheit und Wartbarkeit des Codes optimieren möchten, es sei denn, Sie sind wirklich eng mit Zyklen oder Speicherorten (was durchaus passieren kann). Wenn Sie beispielsweise eine 16-Bit-Maschine haben, die einen 32-Bit-Zähler in einem Takt-Tick-Interrupt aktualisiert, müssen Sie sicherstellen, dass die beiden Hälften konsistent sind, wenn der Vordergrundcode den Zähler liest. Eine Möglichkeit besteht darin, Interrupts auszuschalten, die beiden Wörter zu lesen und dann die Interrupts wieder einzuschalten. Wenn die Interrupt-Latenz nicht kritisch ist, ist dies durchaus akzeptabel.

In dem Fall, in dem Sie eine niedrige Interrupt-Latenz haben müssen, können Sie beispielsweise das hohe Wort lesen, das niedrige Wort lesen, das hohe Wort erneut lesen und wiederholen, wenn es sich geändert hat. Dies verlangsamt den Vordergrundcode ein wenig, fügt jedoch überhaupt keine Interrupt-Latenz hinzu. Es gibt verschiedene kleine Tricks. Eine andere Möglichkeit besteht darin, in der Interrupt-Routine ein Flag zu setzen, das angibt, dass der Zähler inkrementiert werden muss, und dies dann in der Hauptereignisschleife im Vordergrundcode zu tun. Dies funktioniert einwandfrei, wenn die Zähler-Interrupt-Rate langsam genug ist, sodass die Ereignisschleife das Inkrement ausführt, bevor das Flag erneut gesetzt wird.

Oder verwenden Sie anstelle einer Flagge einen Ein-Wort-Zähler. Der Vordergrundcode enthält eine separate Variable, die den letzten Zähler enthält, auf den das System aktualisiert wurde. Es wird ein vorzeichenloser Subtrahieren des Live-Zählers abzüglich des gespeicherten Werts durchgeführt, um zu bestimmen, wie viele Ticks gleichzeitig verarbeitet werden müssen. Dadurch kann der Vordergrundcode bis zu 2 N -1 Ereignisse gleichzeitig übersehen , wobei N die Anzahl der Bits in einem nativen Wort ist, die die ALU atomar verarbeiten kann.

Jede Methode hat ihre eigenen Vor- und Nachteile. Es gibt keine einzige richtige Antwort. Verstehen Sie wieder, wie die Maschine funktioniert, dann brauchen Sie keine Faustregeln.


7

Wenn Sie einen kritischen Abschnitt benötigen, müssen Sie sicherstellen, dass der Vorgang, der Ihren kritischen Abschnitt schützt, atomar ist und nicht unterbrochen werden kann.

Daher ist das Deaktivieren der Interrupts, die am häufigsten von einem einzelnen Prozessorbefehl ausgeführt werden (und über eine Compiler-Eigenfunktion aufgerufen werden), eine der sichersten Wetten, die Sie abschließen können.

Abhängig von Ihrem System kann es zu Problemen kommen, z. B. wenn ein Interrupt übersehen wird. Einige Mikrocontroller setzen die Flags unabhängig vom Status der globalen Interrupt-Aktivierung. Nach dem Verlassen des kritischen Abschnitts werden die Interrupts ausgeführt und nur verzögert. Wenn Sie jedoch einen Interrupt haben, der mit einer hohen Rate auftritt, können Sie ein zweites Mal den Interrupt verpassen, wenn Sie die Interrupts zu lange blockieren.

Wenn in Ihrem kritischen Abschnitt nur ein Interrupt nicht ausgeführt werden muss, der andere jedoch ausgeführt werden soll, scheint der andere Ansatz sinnvoll zu sein.

Ich finde mich dabei, die Interrupt-Serviceroutinen so kurz wie möglich zu programmieren. Sie setzen also einfach ein Flag, das dann während der normalen Programmroutinen überprüft wird. Aber wenn Sie das tun, achten Sie auf die Rennbedingungen, während Sie darauf warten, dass diese Flagge gesetzt wird.

Es gibt viele Optionen und sicherlich keine einzige richtige Antwort darauf. Dies ist ein Thema, das sorgfältiges Design erfordert und ein wenig mehr Nachdenken verdient als andere Dinge.


5

Wenn Sie festgestellt haben, dass ein Codeabschnitt ununterbrochen ausgeführt werden muss, sollten Sie Interrupts außer unter ungewöhnlichen Umständen für die minimal mögliche Dauer deaktivieren, um die Aufgabe abzuschließen, und sie anschließend wieder aktivieren.

Setzen Sie ein Flag in den jeweiligen ISR und setzen Sie (anstatt den Interrupt zu deaktivieren) das Flag vor dem kritischen Abschnitt auf false und setzen Sie es unmittelbar danach auf true zurück. Um zu verhindern, dass der Code des ISR ausgeführt wird.

Dies würde immer noch einen Interrupt, einen Codesprung, eine Überprüfung und dann eine Rückkehr ermöglichen. Wenn Ihr Code so viele Unterbrechungen verarbeiten kann, sollten Sie den ISR wahrscheinlich einfach so gestalten, dass er ein Flag setzt, anstatt die Prüfung durchzuführen - es wäre kürzer - und das Flag in Ihrer normalen Coderoutine behandeln. Dies hört sich so an, als würde jemand zu viel Code in den Interrupt einfügen und den Interrupt verwenden, um längere Aktionen auszuführen, die im regulären Code stattfinden sollten.

Wenn Sie mit Code arbeiten, bei dem die Interrupts lang sind, kann das Problem durch ein von Ihnen vorgeschlagenes Flag behoben werden. Es ist jedoch immer noch besser, Interrupts einfach zu deaktivieren, wenn Sie den Code nicht neu faktorisieren können, um den übermäßigen Code im Interrupt zu beseitigen .

Das Hauptproblem dabei ist, dass Sie den Interrupt überhaupt nicht ausführen - was später Auswirkungen haben kann. Die meisten Mikrocontroller verfolgen Interrupt-Flags, auch wenn Interrupts global deaktiviert sind, und führen den Interrupt dann aus, wenn Sie Interrupts wieder aktivieren:

  • Wenn während des kritischen Abschnitts keine Interrupts aufgetreten sind, werden danach keine mehr ausgeführt.
  • Wenn während des kritischen Abschnitts ein Interrupt auftritt, wird einer nach ausgeführt.
  • Wenn während des kritischen Abschnitts mehrere Interrupts auftreten, wird danach nur einer ausgeführt.

Wenn Ihr System komplex ist und Interrupts vollständiger verfolgen muss, müssen Sie ein komplizierteres System entwerfen, um Interrupts zu verfolgen und entsprechend zu arbeiten.

Wenn Sie Ihre Interrupts jedoch immer so gestalten, dass sie die zur Erreichung ihrer Funktion erforderliche Mindestarbeit leisten und alles andere auf die reguläre Verarbeitung verzögern, wirken sich Interrupts selten negativ auf Ihren anderen Code aus. Lassen Sie den Interrupt bei Bedarf Daten erfassen oder freigeben oder die Ausgänge usw. nach Bedarf einstellen / zurücksetzen. Lassen Sie dann den Hauptcodepfad auf Flags, Puffer und Variablen achten, auf die sich der Interrupt auswirkt, damit die lange Verarbeitung in der Hauptschleife durchgeführt werden kann. eher als die Unterbrechung.

Dies sollte alle bis auf sehr wenige Situationen beseitigen, in denen Sie möglicherweise einen ununterbrochenen Codeabschnitt benötigen.


Ich habe den Beitrag aktualisiert, um die Situation, in der ich arbeite, besser zu erklären :)
Umberto D.

1
In Ihrem Beispielcode würde ich in Betracht ziehen, den spezifischen Tasteninterrupt zu deaktivieren, wenn er nicht benötigt wird, und ihn bei Bedarf zu aktivieren. Dies häufig zu tun, ist kein Problem. Lassen Sie globale Interrupts aktiviert, damit Sie dem Code später bei Bedarf weitere Interrupts hinzufügen können. Alternativ können Sie das Flag einfach zurücksetzen, wenn Sie in den Status A wechseln, und es ansonsten ignorieren. Wen interessiert es, wenn die Taste gedrückt und die Flagge gesetzt wird? Ignorieren Sie es, bis Sie wieder zu Zustand A zurückkehren.
Adam Davis

Ja! Dies kann eine Lösung sein, da ich im eigentlichen Design häufig in LPM3 (MSP430) mit aktivierten globalen Interrupts gehe und LPM3 verlasse und die Ausführung wieder aufnehme, sobald ein Interrupt erkannt wird. Es handelt sich also um eine Lösung, die Sie vorgestellt haben und die meines Erachtens im zweiten Teil des Codes beschrieben wird: Aktivieren Sie Interrupts, sobald ich die do-Aktivität eines Status ausführe, der sie benötigt, und deaktivieren Sie sie, bevor Sie zum Übergangsblock wechseln. Könnte eine andere mögliche Lösung darin bestehen, den Interrupt kurz vor dem Verlassen des "Aktivitätsblocks" zu deaktivieren und ihn irgendwann (wann?) Wieder zu aktivieren?
Umberto D.

1

Das Setzen eines Flags in den ISR, wie Sie es beschreiben, funktioniert wahrscheinlich nicht, da Sie das Ereignis, das den Interrupt ausgelöst hat, grundsätzlich ignorieren. Das globale Deaktivieren von Interrupts ist normalerweise die bessere Wahl. Wie andere gesagt haben, sollten Sie dies nicht sehr oft tun müssen. Beachten Sie, dass Lese- oder Schreibvorgänge, die über eine einzelne Anweisung ausgeführt werden, nicht geschützt werden müssen, da die Anweisung entweder ausgeführt wird oder nicht.

Viel hängt davon ab, welche Art von Ressourcen Sie freigeben möchten. Wenn Sie Daten vom ISR in das Hauptprogramm einspeisen (oder umgekehrt), können Sie so etwas wie einen FIFO-Puffer implementieren. Die einzige atomare Operation wäre das Aktualisieren der Lese- und Schreibzeiger, wodurch die Zeit minimiert wird, die Sie mit deaktivierten Interrupts verbringen.


0

Es gibt einen subtilen Unterschied, den Sie berücksichtigen müssen. Sie können die Behandlung eines Interrupts "verzögern" oder "ignorieren" und unterbrechen.

Oft sagen wir im Code, dass wir den Interrupt deaktivieren. Was aufgrund von Hardwarefunktionen wahrscheinlich passieren wird, ist, dass sobald wir Interrupts aktivieren, diese ausgelöst werden. Dies verzögert in gewisser Weise den Interrupt. Damit das System zuverlässig funktioniert, müssen wir wissen, wie lange wir die Behandlung dieser Interrupts maximal verzögern dürfen. Stellen Sie dann sicher, dass alle Fälle, in denen Interrupts deaktiviert sind, in kürzerer Zeit abgeschlossen sind.

Manchmal wollen wir Interrupts ignorieren. Der beste Weg könnte sein, den Interrupt auf Hardwareebene zu blockieren. Es gibt oft einen Interrupt-Controller oder ähnliches, bei dem wir sagen könnten, welche Eingänge Interrupts erzeugen sollen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.