Interrupt-Behandlung in Mikrocontrollern und FSM-Beispiel

Erste Frage

Ich habe eine allgemeine Frage zur Behandlung von Interrupts in Mikrocontrollern. Ich verwende den MSP430, aber ich denke, die Frage kann auf andere uCs ausgedehnt werden. Ich würde gerne 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 die jeweilige ISR und setzen Sie (anstatt den Interrupt zu deaktivieren) das Flag vor dem kritischen Abschnitt auf "false" und setzen Sie es gleich danach auf "true" zurück. Um zu verhindern, dass der Code des ISR ausgeführt wird.
  • Keiner von beiden, daher sind Vorschläge willkommen!

Update: Interrupts und Zustandsdiagramme

Ich werde eine bestimmte Situation angeben. Nehmen wir an, wir wollen ein Zustandsdiagramm implementieren, das sich aus 4 Blöcken zusammensetzt:

  1. Übergänge/Effekt.
  2. Ausgangsbedingungen.
  3. Eintrittsaktivität.
  4. Aktivität machen.

Das hat uns ein Professor an der Universität beigebracht. Wahrscheinlich ist es nicht der beste Weg, dies zu tun, indem Sie diesem Schema folgen:

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 von beispielsweise empfindlich auf einen Interrupt reagieren möchte, der von einer Reihe von Tasten kommt (mit Debugging-System usw. usw.). Wenn jemand eine dieser Schaltflächen drückt, wird ein Interrupt generiert und das Flag, das sich auf den Eingangsport bezieht, wird 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 sein, dass buttonPressedsie nicht vor 200 ms mit einem neuen Wert aktualisiert werden kann. Das frage ich dich (und mich natürlich :))

Muss ich den Interrupt in der DO-Aktivität aktivieren STATE_Aund vor dem Verlassen deaktivieren?

/* 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, bei der 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 benötigen (obwohl dies unmöglich sein kann, da 250 ms vergehen müssen).

Es ist schwer, eine Empfehlung abzugeben, ohne mehr über Ihre Situation zu wissen. Aber manchmal ist es notwendig, Interrupts in eingebetteten Systemen zu deaktivieren. Es ist vorzuziehen, Interrupts nur für kurze Zeiträume deaktiviert zu lassen, damit Interrupts nicht verpasst werden. Es kann möglich sein, statt aller Interrupts nur bestimmte Interrupts zu deaktivieren. 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.

Antworten (5)

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. Oft gibt es einen architektonischen Umweg.

Die Maschine ist jedoch dazu da, Ihnen zu dienen, nicht umgekehrt. Allgemeine Faustregeln dienen nur dazu, schlechte Programmierer davon abzuhalten, wirklich schlechten Code zu schreiben. Es ist viel besser, genau zu verstehen , wie die Maschine funktioniert, und dann eine gute Möglichkeit zu entwickeln, diese Fähigkeiten zu nutzen, um die gewünschte Aufgabe auszuführen.

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 sicherlich passieren kann). Wenn Sie beispielsweise eine 16-Bit-Maschine haben, die einen 32-Bit-Zähler in einem Clock-Tick-Interrupt aktualisiert, müssen Sie sicherstellen, dass die beiden Hälften des Zählers konsistent sind, wenn der Vordergrundcode den Zähler liest. Eine Möglichkeit besteht darin, Interrupts abzuschalten, die beiden Wörter zu lesen und die Interrupts dann wieder einzuschalten. Wenn die Interrupt-Latenz nicht kritisch ist, dann ist das vollkommen akzeptabel.

In dem Fall, in dem Sie eine niedrige Unterbrechungslatenz 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 aber überhaupt keine Unterbrechungslatenz hinzu. Es gibt verschiedene kleine Tricks. Eine andere Möglichkeit besteht darin , in der Interrupt-Routine ein Flag zu setzen, das anzeigt, dass der Zähler inkrementiert werden muss, und dies dann in der Hauptereignisschleife im Vordergrundcode zu tun. Das funktioniert gut, wenn die Unterbrechungsrate des Zählers langsam genug ist, damit die Ereignisschleife das Inkrement ausführt, bevor das Flag erneut gesetzt wird.

Oder verwenden Sie anstelle eines Flags einen Ein-Wort-Zähler. Der Vordergrundcode hält eine separate Variable, die den letzten Zähler enthält, auf den er das System aktualisiert hat. Es subtrahiert ohne Vorzeichen den Live-Zähler minus den gespeicherten Wert, um zu bestimmen, wie viele Ticks gleichzeitig verarbeitet werden müssen. Dadurch kann der Vordergrundcode bis zu 2 N – 1 Ereignisse gleichzeitig verpassen, wobei N die Anzahl von Bits in einem nativen Wort ist, die die ALU atomar verarbeiten kann.

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

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

Daher ist das Deaktivieren der Interrupts, das am häufigsten von einem einzelnen Prozessorbefehl behandelt wird (und mit einer Compiler-Intrinsic-Funktion aufgerufen wird), eine der sichersten Möglichkeiten, die Sie eingehen können.

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

Wenn Ihr kritischer Abschnitt erfordert, dass nur ein Interrupt nicht ausgeführt wird, andere jedoch ausgeführt werden sollen, scheint der andere Ansatz praktikabel zu sein.

Ich ertappe mich dabei, die Interrupt-Service-Routinen 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 Möglichkeiten und sicherlich keine einzige richtige Antwort darauf, dies ist ein Thema, das eine sorgfältige Gestaltung erfordert und ein bisschen mehr Nachdenken verdient als andere Dinge.

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

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

Dies würde immer noch ermöglichen, dass eine Unterbrechung auftritt, ein Codesprung, eine Überprüfung und dann eine Rückkehr. Wenn Ihr Code mit so vielen Unterbrechungen umgehen kann, sollten Sie die ISR wahrscheinlich einfach so entwerfen, dass ein Flag gesetzt wird, anstatt die Prüfung durchzuführen - es wäre kürzer - und das Flag in Ihrer normalen Coderoutine zu behandeln. Das hört sich so an, als ob jemand zu viel Code in den Interrupt gesteckt hat und den Interrupt verwendet, um längere Aktionen auszuführen, die im regulären Code stattfinden sollten.

Wenn Sie mit Code zu tun haben, bei dem die Interrupts lang sind, könnte ein von Ihnen vorgeschlagenes Flag das Problem lösen, aber es ist immer noch besser, Interrupts einfach zu deaktivieren, wenn Sie den Code nicht umgestalten können, um den übermäßigen Code im Interrupt zu beseitigen .

Das Hauptproblem bei der Flaggenmethode besteht darin, dass Sie den Interrupt überhaupt nicht ausführen - was später Auswirkungen haben kann. Die meisten Mikrocontroller verfolgen Interrupt-Flags, selbst 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 ausgeführt.
  • Wenn während des kritischen Abschnitts ein Interrupt auftritt, wird danach einer 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 entwerfen, dass sie die minimale Arbeit ausführen, die zum Erreichen ihrer Funktion erforderlich ist, und alles andere auf die reguläre Verarbeitung verzögern, werden Interrupts Ihren anderen Code selten negativ beeinflussen. Lassen Sie den Interrupt bei Bedarf Daten erfassen oder freigeben oder Ausgänge nach Bedarf setzen / zurücksetzen usw., und lassen Sie dann den Hauptcodepfad auf Flags, Puffer und Variablen achten, auf die sich der Interrupt auswirkt, damit die langwierige Verarbeitung in der Hauptschleife durchgeführt werden kann. eher als der Interrupt.

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

Ich habe den Beitrag aktualisiert, um die Situation, an der ich arbeite, besser zu erklären :)
In Ihrem Beispielcode würde ich erwägen, den spezifischen Schaltflächen-Interrupt zu deaktivieren, wenn er nicht benötigt wird, und ihn bei Bedarf zu aktivieren. Dies häufig zu tun, ist kein Problem seines Designs. Lassen Sie globale Interrupts eingeschaltet, damit Sie dem Code später bei Bedarf weitere Interrupts hinzufügen können. Setzen Sie alternativ einfach das Flag zurück, wenn Sie zu Zustand A gehen, und ignorieren Sie es andernfalls. Wenn der Knopf gedrückt und die Flagge gesetzt ist, wen interessiert das? Ignorieren Sie es, bis Sie zu Zustand A zurückkehren.
Ja! Das kann eine Lösung sein, da ich im aktuellen 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. Eine Lösung ist also die, die Sie vorgestellt haben und die meiner Meinung nach im zweiten Teil des Codes gemeldet wird: Aktivieren Sie Interrupts, sobald ich beginne, die Do-Aktivität eines Zustands auszuführen, 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 ausführen" zu deaktivieren und irgendwann (wann?) danach wieder zu aktivieren?

Das Einfügen eines Flags in die ISR, wie Sie es beschreiben, wird wahrscheinlich nicht funktionieren, da Sie das Ereignis, das den Interrupt ausgelöst hat, im Grunde ignorieren. Das globale Deaktivieren von Interrupts ist normalerweise die bessere Wahl. Wie andere gesagt haben, sollten Sie dies nicht sehr oft tun müssen. Denken Sie daran, dass alle Lese- oder Schreibvorgänge, die über eine einzelne Anweisung erfolgen, 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 teilen möchten. Wenn Sie Daten von der ISR in das Hauptprogramm einspeisen (oder umgekehrt), könnten Sie so etwas wie einen FIFO-Puffer implementieren. Die einzige atomare Operation wäre die Aktualisierung der Lese- und Schreibzeiger, was die Zeit minimiert, die Sie mit deaktivierten Interrupts verbringen.

Es gibt einen feinen Unterschied, den Sie berücksichtigen müssen. Sie können wählen, ob Sie die Behandlung eines Interrupts "verzögern" oder "ignorieren" und unterbrechen möchten.

Oft sagen wir im Code, dass wir den Interrupt deaktivieren. Was aufgrund von Hardwarefunktionen wahrscheinlich passieren wird, ist, dass es ausgelöst wird, sobald wir Interrupts aktivieren. Dies verzögert gewissermaßen den Interrupt. Damit das System zuverlässig arbeitet, müssen wir die maximale Zeit kennen, die wir die Bearbeitung dieser Interrupts verzögern dürfen. Und stellen Sie dann sicher, dass alle Fälle, in denen Interrupts deaktiviert sind, in kürzerer Zeit abgeschlossen werden.

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