Bestes Muster für WFI (Wait-for-Interrupt) auf Cortex (ARM)-Mikrocontrollern

Ich beschäftige mich mit der Entwicklung batteriebetriebener Software mit den EFM Gekko-Controllern (http://energymicro.com/) und möchte, dass der Controller schläft, wenn er nichts Sinnvolles zu tun hat. Zu diesem Zweck wird der Befehl WFI (Wait For Interrupt) verwendet; Es versetzt den Prozessor in den Ruhezustand, bis ein Interrupt auftritt.

Wenn der Schlaf aktiviert würde, indem etwas irgendwo gespeichert wird, könnte man ausschließliche Lade-/Speicher-exklusive Operationen verwenden, um Folgendes zu tun:

  // dont_sleep wird jedes Mal mit 2 geladen, wenn etwas passiert
  // sollte die Hauptschleife zwingen, mindestens einmal zu durchlaufen. Wenn ein Interrupt
  // tritt auf, wodurch es während der folgenden Anweisung auf 2 zurückgesetzt wird,
  // Das Verhalten ist so, als ob der Interrupt danach passiert wäre.

  store_exclusive(load_exclusive(dont_sleep) >> 1);

  while(!dont_sleep)
  {
    // Falls Interrupt zwischen nächster Anweisung und store_exclusive auftritt, nicht schlafen
    load_exclusive(SLEEP_TRIGGER);
    wenn (!dont_sleep)             
      store_exclusive(SLEEP_TRIGGER);
  }

Wenn zwischen den Operationen load_exclusive und store_exclusive ein Interrupt auftritt, würde die Wirkung store_exclusive überspringen, wodurch das System veranlasst wird, die Schleife noch einmal zu durchlaufen (um zu sehen, ob der Interrupt dont_sleep gesetzt hat). Leider verwendet der Gekko einen WFI-Befehl anstelle einer Schreibadresse, um den Schlafmodus auszulösen. Code schreiben wie

  wenn (!dont_sleep)
    WFI();

würde das Risiko eingehen, dass ein Interrupt zwischen dem 'if' und dem 'wfi' auftreten und dont_sleep setzen könnte, aber das wfi würde trotzdem weitermachen und ausführen. Was ist das beste Muster, um das zu verhindern? Setzen Sie PRIMASK auf 1, um zu verhindern, dass Interrupts den Prozessor kurz vor der Ausführung des WFI unterbrechen, und löschen Sie es sofort danach? Oder gibt es einen besseren Trick?

BEARBEITEN

Ich wundere mich über das Event-Bit. Der allgemeinen Beschreibung nach wäre es für die Unterstützung mehrerer Prozessoren gedacht, habe mich aber gefragt, ob so etwas wie das Folgende funktionieren könnte:

  wenn (dont_sleep)
    SEV(); /* Wird das folgende WFE-Ereignis-Flag löschen, aber nicht schlafen */
  WFE();

Jeder Interrupt, der don't_sleep setzt, sollte auch eine SEV-Anweisung ausführen, wenn also der Interrupt nach dem "if"-Test auftritt, würde das WFE das Ereignis-Flag löschen, aber nicht schlafen gehen. Klingt das nach einem guten Paradigma?

Die WFI-Anweisung versetzt den Kern nicht in den Ruhezustand, wenn seine Aufwachbedingung wahr ist, wenn die Anweisung ausgeführt wird. Wenn beispielsweise ein ungeklärter IRQ vorhanden ist, wenn WFI ausgeführt wird, fungiert er als NOP.
@Mark: Das Problem wäre, dass, wenn ein Interrupt zwischen "if (! dont_sleep)" und "WFI" genommen wird, die Interrupt-Bedingung nicht mehr ansteht, wenn das WFI ausgeführt wird, aber der Interrupt möglicherweise dont_sleep gesetzt hat, weil es hat etwas getan, das die Hauptschleife rechtfertigen würde, die eine weitere Iteration ausführt. Bei einer Cypress PSOC-Anwendung von mir würden alle Interrupts, die ein längeres Aufwachen verursachen sollten, den Stack verhexen, wenn der Hauptcode kurz vor dem Schlafen wäre, aber das scheint ziemlich eklig zu sein, und ich verstehe, dass ARM von solchen Stack-Manipulationen abrät.
@supercat Der Interrupt kann gelöscht werden oder nicht, wenn WFI ausgeführt wird. Es liegt an Ihnen und wann/wo Sie den Interrupt löschen. Entfernen Sie die Variable dont_sleep und verwenden Sie einfach einen maskierten Interrupt, um anzuzeigen, wann Sie wach bleiben oder schlafen möchten. Sie können die if-Anweisung einfach alle zusammen loswerden und WFI am Ende der Hauptschleife belassen. Wenn Sie alle Anfragen bedient haben, löschen Sie den IRQ, damit Sie schlafen können. Wenn Sie wach bleiben müssen, lösen Sie den IRQ aus, er ist maskiert, sodass nichts passiert, aber wenn WFI versucht, ihn auszuführen, wird er NOP.
@supercat Auf einer grundlegenderen Ebene scheinen Sie zu versuchen, ein Interrupt-gesteuertes Design mit einem 'Big Main Loop'-Design zu mischen, das normalerweise nicht zeitkritisch ist, häufig auf Abfragen basiert und minimale Interrupts aufweist. Das Mischen dieser kann ziemlich schnell ziemlich hässlich werden. Wählen Sie nach Möglichkeit das eine oder andere Designparadigma. Denken Sie daran, dass Sie mit modernen Interrupt-Controllern im Grunde präventives Multitasking zwischen Interrupts und Aufgabenwarteschlangen erhalten (einen Interrupt bedienen, dann die nächsthöhere Priorität usw.). Nutzen Sie das zu Ihrem Vorteil.
@Mark: Ich habe ein System entwickelt, das einen PIC 18x in einer batteriebetriebenen Anwendung ziemlich gut verwendet; Aufgrund von Stapelbeschränkungen konnte es nicht zu viel innerhalb eines Interrupts verarbeiten, sodass die überwiegende Mehrheit der Dinge in der Hauptschleife nach Belieben behandelt wird. Es funktioniert meistens ziemlich gut, obwohl es ein paar Stellen gibt, an denen die Dinge wegen lang andauernder Operationen für ein oder zwei Sekunden blockiert werden. Wenn ich zu einem ARM migriere, verwende ich möglicherweise ein einfaches RTOS, um die lang andauernden Vorgänge einfacher aufzuteilen, aber ich bin mir nicht sicher, ob ich präventives oder kooperatives Multitasking verwenden soll.
@Mark: Bei einigen 8x51- und TMS320xx-Projekten habe ich selbstgeschriebene kooperative Multitasker sehr gut eingesetzt. Ein großer Vorteil des kooperativen Multitaskings besteht darin, dass das Sperren nur dann erforderlich ist, wenn ein Thread die exklusive Kontrolle über eine Ressource länger benötigt, als er die CPU halten sollte. In den meisten Fällen gab es überhaupt keinen Streit; bei den Gelegenheiten, in denen es auftrat, oder ich konnte entscheiden, dass eine bestimmte Aufgabe immer eine Ressource verwenden durfte und alle anderen Aufgaben sie umgehen mussten. Zum Beispiel ein Zeiger auf einen Puffer, den ein Thread verschieben könnte....
@Mark: ... gehörte dem Thread, der den Puffer gefüllt hat, und dieser Thread konnte ihn verschieben, wann immer er wollte; Der Thread, der aus dem Puffer gelesen hat, hatte angenommen, dass sich der Puffer bei jedem Aufruf von taskswitch() bewegen könnte. In einem preemptiven Tasking-System müsste der gesamte Code, der den Zeiger dereferenziert, eine Sperre darauf erwerben und sicherstellen, dass er freigegeben wird. Im kooperativen Multitasking-System war die implizite Sperre, die in Bereichen ohne Aufruf von taskswitch() erzeugt wurde, ausreichend.

Antworten (5)

Ich habe die Sache nicht ganz verstanden dont_sleep, aber eine Sache, die Sie versuchen könnten, ist, die "Hauptarbeit" im PendSV-Handler zu erledigen, der auf die niedrigste Priorität eingestellt ist. Planen Sie dann einfach jedes Mal, wenn Sie etwas erledigen müssen, einen PendSV von anderen Handlern ein. Sehen Sie hier , wie es geht (es ist für M1, aber M3 ist nicht allzu anders).

Eine andere Sache, die Sie verwenden könnten (vielleicht zusammen mit dem vorherigen Ansatz), ist die Sleep-on-Exit-Funktion. Wenn Sie es aktivieren, geht der Prozessor nach dem Verlassen des letzten ISR-Handlers in den Ruhezustand, ohne dass Sie WFI aufrufen müssen. Sehen Sie hier einige Beispiele .

der WFI-Befehl erfordert keine Freigabe von Interrupts, um den Prozessor aufzuwecken, die F- und I-Bits in CPSR werden ignoriert.
@Mark Ich muss das in den Dokumenten übersehen haben, hast du ein paar Links/Hinweise dazu? Was passiert mit dem Interrupt-Signal, das den Kern aufgeweckt hat? Bleibt es anstehend, bis der Interrupt wieder aktiviert wird?
Das ASM-Referenzhandbuch finden Sie hier: infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489c/… Genauere Informationen zum Cortex-m3 finden Sie hier: infocenter.arm.com/help/ index.jsp?topic=/com.arm.doc.dui0552a/… kurz gesagt, wenn ein maskierter Interrupt anhängig wird, wacht der Kern auf und setzt den Betrieb nach der WFI-Anweisung fort. Wenn Sie versuchten, ein weiteres WFI auszugeben, ohne diesen anstehenden Interrupt zu löschen, würde das WFI als NOP fungieren (der Kern würde nicht schlafen, da die Weckbedingung für WFI wahr ist).
@Mark: Eine Sache, die ich in Betracht gezogen habe, wäre, dass jeder Interrupt-Handler, der dont_sleep einstellt, auch eine SEV-Anweisung ("Set Event") ausführt und dann WFE ("Wait For Event") anstelle von WFI verwendet. Die Gekko-Beispiele scheinen WFI zu verwenden, aber ich würde denken, dass WFE auch funktionieren könnte. Irgendwelche Gedanken?

Legen Sie es in einen kritischen Abschnitt. ISRs werden nicht ausgeführt, sodass Sie nicht das Risiko eingehen, dass sich dont_sleep vor WFI ändert, aber sie wecken den Prozessor trotzdem auf und die ISRs werden ausgeführt, sobald der kritische Abschnitt endet.

uint8 interruptStatus;
interruptStatus = EnterCriticalSection();
if (!dont_sleep)
  WFI();
ExitCriticalSection(interruptStatus);

Ihre Entwicklungsumgebung hat wahrscheinlich kritische Abschnittsfunktionen, aber es ist ungefähr so:

EnterCriticalSection ist:

MRS r0, PRIMASK /* Save interrupt state. */
CPSID i /* Turn off interrupts. */
BX lr /* Return. */

ExitCriticalSection ist:

MSR PRIMASK, r0 /* Restore interrupt states. */
BX lr /* Return. */
Seltsamerweise verwenden eine Reihe von ARM-Bibliotheken eine Critical-Section-Implementierung, die einen globalen Zähler verwendet, anstatt den Status lokal beizubehalten. Ich finde das umwerfend, da der Zähleransatz komplizierter ist und nur funktioniert, wenn der gesamte Code systemweit denselben Zähler verwendet.
Werden Interrupts nicht deaktiviert, bis wir den kritischen Abschnitt verlassen? Wenn ja, wird WFI die CPU nicht dazu bringen, auf unbestimmte Zeit zu warten?
Die Antwort von @Kenzi Shrimp verweist auf einen Linux-Diskussionsthread, der meine vorherige Frage beantwortet. Ich habe seine und Ihre Antwort bearbeitet, um das zu verdeutlichen.
@CorneliuZuzu Das Bearbeiten der Antwort einer anderen Person, um Ihre eigene Diskussion einzufügen, ist keine gute Idee. Das Hinzufügen des Zitats zur Verbesserung einer „Nur-Link“-Antwort ist eine andere Sache. Wenn Sie eine echte Frage zu Ihrem Won haben, stellen Sie sie vielleicht als Frage und verlinken Sie auf diese.
@SeanHoulihane Ich habe nichts aus ihrer Antwort ungültig gemacht oder entfernt. Eine kurze Erläuterung, warum dies funktioniert, ist keine separate Diskussion. Ehrlich gesagt glaube ich nicht, dass diese Antwort ohne die WFI-Klarstellung eine positive Abstimmung verdient, aber sie verdient sie am meisten damit.
@CorneliuZuzu Die Interrupts sind nicht deaktiviert, nur maskiert. WFI wird immer noch zum richtigen Zeitpunkt fortgesetzt, aber die Interrupt-Routinen werden nicht bis zum Ende des kritischen Abschnitts ausgeführt.
@Kenzi Schön! Danke für das Aufklären.

Ihre Idee ist in Ordnung, das ist genau das, was Linux implementiert. Siehe hier .

Nützliches Zitat aus dem oben genannten Diskussionsthread, um zu verdeutlichen, warum WFI auch mit deaktivierten Interrupts funktioniert:

Wenn Sie beabsichtigen, bis zum nächsten Interrupt im Leerlauf zu bleiben, müssen Sie einige Vorbereitungen treffen. Während dieser Vorbereitung kann ein Interrupt aktiv werden. Eine solche Unterbrechung kann ein Weckereignis sein, nach dem Sie suchen.

Egal wie gut Ihr Code ist, wenn Sie Interrupts nicht deaktivieren, werden Sie immer ein Wettrennen zwischen der Vorbereitung auf das Einschlafen und dem tatsächlichen Einschlafen haben, was zu verlorenen Weckereignissen führt.

Aus diesem Grund werden alle mir bekannten ARM-CPUs aufwachen, auch wenn sie an der Kern-CPU (CPSR I-Bit) maskiert sind.

Alles andere und Sie sollten den Leerlaufmodus vergessen.

Sie beziehen sich auf das Deaktivieren von Interrupts zum Zeitpunkt der WFI- oder WFE-Anweisung? Sehen Sie einen sinnvollen Unterschied zwischen der Verwendung von WFI oder WFE für diesen Zweck?
@supercat: Ich würde definitiv WFI verwenden. WFE IMO ist hauptsächlich für Synchronisierungshinweise zwischen Kernen in Multicore-Systemen gedacht (z. B. Ausführen von WFE bei fehlgeschlagenem Spinlock und Ausgeben von SEV nach dem Verlassen von Spinlock). Außerdem berücksichtigt WFE das Interrupt-Masking-Flag, sodass es hier nicht so nützlich ist wie WFI. Dieses Muster funktioniert wirklich gut unter Linux.

Vorausgesetzt, dass:

  1. Der Hauptthread führt Hintergrundaufgaben aus
  2. Die Interrupts führen nur Tasks mit hoher Priorität und keine Hintergrundtasks aus
  3. Der Haupt-Thread kann jederzeit unterbrochen werden (er maskiert normalerweise keine Interrupts)

Dann besteht die Lösung darin, PRIMASK zu verwenden, um Interrupts zwischen der Flag-Validierung und WFI zu blockieren:

mask_interrupts();
if (!dont_sleep)
    wfi();
unmask_interrupts();

Was ist mit dem Sleep on Exit-Modus? Dies geht jedes Mal automatisch in den Ruhezustand, wenn ein IRQ-Handler beendet wird, sodass nach der Konfiguration kein "normaler Modus" mehr ausgeführt wird. Ein IRQ tritt auf, er wacht auf und führt den Handler aus und geht wieder in den Ruhezustand. Kein WFI erforderlich.

Wie sollte man am besten mit der Tatsache umgehen, dass die Art des Ruhezustands, in den der Prozessor fallen sollte, je nach etwas variieren kann, das während eines Interrupts passiert? Beispielsweise könnte ein Pin-Wechsel-Ereignis anzeigen, dass möglicherweise serielle Daten anstehen und dass der Prozessor daher den primären Taktoszillator am Laufen halten sollte, während er auf die Daten wartet. Wenn die Hauptschleife das Ereignis-Flag löscht, untersucht, was vor sich geht, und den Prozessor mit einem WFI in einen geeigneten Schlafmodus versetzt, dann würde jeder Interrupt, der sich auf den geeigneten Modus ausgewirkt haben könnte, das Ereignis-Flag setzen ...
...und den Schlaf abbrechen. Einen Hauptschleifen-Handler zu haben, der den Schlafmodus steuert, scheint sauberer zu sein, als sich bei jedem Interrupt darum kümmern zu müssen. Diesen Teil der Hauptschleife bei jedem Interrupt "drehen" zu müssen, ist möglicherweise nicht optimal effizient, aber es sollte nicht allzu schlimm sein, insbesondere wenn alle Interrupts, die das Schlafverhalten beeinflussen könnten, irgendein Flag treffen.