Nicht blockierende I2C-Implementierung auf STM32

Die meisten Artikel, die ich finden kann und die im Wesentlichen "I2C für Dummies auf XXX-Mikrocontrollern" sind, schlagen vor, die CPU zu blockieren, während auf Bestätigungsereignisse gewartet wird. Zum Beispiel , (Kommentare und Beschreibung sind auf Russisch, aber das spielt eigentlich keine Rolle). Hier ist das Beispiel für "Blockieren", von dem ich spreche:

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));

Diese whileSchleifen sind im Wesentlichen nach jeder Operation vorhanden. Abschließend meine Frage: Wie kann ich I2C implementieren, ohne die MCU mit diesen While-Schleifen zu "hängen"? Auf einer sehr hohen Ebene verstehe ich, dass ich Interrupts anstelle von Whiles verwenden muss, aber ich kann mir kein Codebeispiel einfallen lassen. Kannst du mir bitte dabei helfen?

Beachten Sie, dass Sie DMA auch mit I²C verwenden können, um einige Leistungsvorteile zu erzielen und die Anzahl der zu verarbeitenden Interrupts zu reduzieren.
@AlexeyMalev Hier ist ein Beispiel für eine nicht blockierende I2C-Implementierung für AVR (obwohl ähnliche Prinzipien für STM32 gelten würden): github.com/scttnlsn/avr-twi Diese spezielle Implementierung verwendet Interrupts, um eine Zustandsmaschine zu verwalten und eine Rückruffunktion aufzurufen, wenn die Übertragung erfolgt fertig.

Antworten (5)

Sicher.

  1. Richten Sie Ihre Treiber als oberes und unteres Paar ein, getrennt durch einen gemeinsamen Puffer. Die untere Funktion ist ein interruptgesteuertes Codebit, das auf unterbrechende Ereignisse reagiert, sich um die unmittelbare Arbeit kümmert, die zum Bedienen der Hardware erforderlich ist, und Daten in den gemeinsam genutzten Puffer hinzufügt (wenn es sich um einen Empfänger handelt) oder das nächste Datenbit daraus extrahiert den gemeinsam genutzten Puffer, um die Hardware weiter zu bedienen (wenn es sich um einen Sender handelt). Der obereDie Funktion wird von Ihrem regulären Code aufgerufen und akzeptiert entweder Daten, die einem ausgehenden Puffer hinzugefügt werden sollen, oder sucht nach Daten und extrahiert Daten aus dem eingehenden Puffer. Durch die Paarung solcher Dinge und die Bereitstellung einer angemessenen Pufferung für Ihre Anforderungen kann diese Anordnung den ganzen Tag ohne Probleme ausgeführt werden und entkoppelt Ihren Hauptcode von Ihrer Hardwarewartung.
  2. Verwenden Sie eine Zustandsmaschine in Ihrem Interrupt-Treiber. Jeder Interrupt liest den aktuellen Zustand, verarbeitet einen Schritt und geht dann entweder in einen anderen Zustand über oder bleibt im selben Zustand und wird dann beendet. Dies kann so komplex oder so einfach sein, wie Sie es benötigen. Häufig wird dies von einem Timer-Ereignis gesteuert. Aber es muss nicht so sein.
  3. Erstellen Sie ein kooperatives Betriebssystem. Dies kann genauso einfach sein wie das Einrichten einiger kleiner Stacks, die mit malloc() zugewiesen werden, und den Funktionen erlauben, kooperativ eine switch()-Funktion aufzurufen, wenn sie vorerst mit einer unmittelbaren Aufgabe fertig sind. Sie richten einen separaten Thread für Ihre I2C-Funktion ein, aber wenn sie entscheidet, dass im Moment nichts zu tun ist, ruft sie einfach switch() auf, um einen Stapelwechsel zu einem anderen Thread zu veranlassen. Dies kann Round-Robin erfolgen, bis es zurückkommt und von dem von Ihnen getätigten switch()-Aufruf zurückkehrt. Dann kehren Sie zu Ihrer While-Bedingung zurück, die erneut überprüft. Wenn immer noch nichts zu tun ist, ruft while erneut switch() auf. Etc. Auf diese Weise wird Ihr Code nicht viel komplexer in der Wartung und es ist einfach, switch()-Aufrufe überall dort einzufügen, wo Sie es für nötig halten. Sie müssen sich auch keine Gedanken über die Vorbelegung von Bibliotheksfunktionen machen, Da Sie nur an einer Funktionsaufrufgrenze zwischen Stapeln wechseln, ist es unmöglich, dass eine Bibliotheksfunktion unterbrochen wird. Das macht die Umsetzung sehr einfach.
  4. Vorbeugende Threads. Dies ist ähnlich wie #3, außer dass die Funktion switch() nicht aufgerufen werden muss. Die Vorbelegung erfolgt basierend auf einem Timer, oder ein Thread kann auch frei entscheiden, seine Zeit freizugeben. Die Schwierigkeit besteht hier darin, Bibliotheksroutinen und/oder spezialisierter Hardware vorzubeugen, wo bestimmte Anweisungssequenzen vom Compiler generiert werden können, die nicht unterbrochen werden können (aufeinanderfolgende E/A, die ein hohes Byte abrufen müssen, gefolgt von einer niedriges Byte von derselben speicherabgebildeten Adresse, zum Beispiel.)

Ich denke, die ersten beiden Optionen sind wahrscheinlich Ihre besseren Wetten. Vieles hängt jedoch davon ab, wie sehr Sie von Bibliothekscode abhängig sind, der von anderen geschrieben wurde. Es ist durchaus möglich, dass ihr Bibliothekscode nicht dafür ausgelegt ist, in Komponenten der oberen und unteren Ebene aufgeteilt zu werden. Und es ist auch gut möglich, dass Sie sie nicht basierend auf Timer-Ereignissen oder anderen auf Zustandsmaschinen basierenden Ereignissen aufrufen können.

Ich neige dazu anzunehmen, dass ich meinen eigenen Code schreiben muss, um die Arbeit zu erledigen, wenn mir die Art und Weise, wie die Bibliothek die Arbeit erledigt, nicht gefällt (nur Busy-Wait-Schleifen). Das bedeutet, dass ich frei bin, jede der oben genannten Methoden zu verwenden.

Aber Sie müssen sich den Bibliothekscode genau ansehen und sehen, ob er bereits über Funktionen verfügt, mit denen Sie das geschäftige Warteverhalten vermeiden können. Es ist möglich, dass die Bibliothek etwas anderes unterstützt. Das ist also der erste Ort, an dem Sie nachsehen können, nur für den Fall. Wenn nicht, spielen Sie mit der Idee, die vorhandenen Funktionen entweder als Teil einer Aufteilung des oberen / unteren Treibers oder als einen von einer Zustandsmaschine gesteuerten Prozess zu verwenden. Es ist möglich, dass Sie das auch herausfinden können.

Mir fallen jetzt keine anderen Vorschläge auf die Schnelle ein.

Bitte korrigieren Sie mich, wenn ich die Idee von # 1 falsch verstanden habe - nehmen wir an, ich habe einen Code, der einige Daten vom I2C-Bus liest und schwere Berechnungen anstellt, z. B. die Fakultät einer Zahl berechnet. Da ich hier keine Threads habe, sollte mein Hauptcode, der die Fakultät berechnet, manchmal "pausieren" (anstatt dem Interrupt-Handling-Thread in einer Hochsprache nachzugeben) und prüfen, ob sich Daten in dem von Ihnen erwähnten Puffer befinden, die dort vom Interrupt-Handler hinzugefügt wurden ?
@AlexeyMalev Dieser Fakultätscode sollte wahrscheinlich in Ihrer Hauptfunktion enthalten sein, die den Code der oberen Ebene aufruft. Der Code auf niedrigerer Ebene macht keine Dinge, die mittendrin eine Pause benötigen. Wenn mehr Dinge passieren, während Ihr Hauptcode den oberen Code aufruft oder nach dem Aufruf verarbeitet, wird der Interrupt weiterhin bedient und Werte für Sie in den Puffer gepackt. Das hört nicht auf, selbst wenn Sie Rechenaufgaben machen. Es ist nicht erforderlich, nach weiteren gepufferten Daten zu suchen, wenn Sie die anderen Arbeiten nicht abgeschlossen haben. Wer da nicht mithalten kann, hat sowieso ein Problem.
@AlexeyMalev Die längste Berechnungsarbeit sollte in Ihrem Hauptcode liegen. Wenn Sie kürzere Bits haben, die zwischendurch erledigt werden müssen, können Sie diese auf Timer-Ereignisse setzen, die Ihren langen Berechnungscode unterbrechen, um ihre kurzen Aufgaben zu erledigen. Diese zeitgesteuerten kurzen Tasks können die obere Hälfte jedes Treibers aufrufen , um auch gepufferte Daten zu erhalten. Es geht nur darum, ein Zeitdiagramm zu erstellen. Lassen Sie genügend Zeit zwischen den Schritten, damit die längsten Berechnungen erledigt werden können, plus alle Unterbrechungszeiten. Das ist Ihr Hauptprozess. Sie können jedoch meine Option Nr. 3 verwenden und Ihren Hauptcode mit switch()-Aufrufen salzen.
Entschuldigung, aber ich muss ablehnen. Diese lange Antwort scheint keine nützlichen Informationen darüber zu liefern, wie eine Interrupt-gesteuerte I²C-Zustandsmaschine auf einem STM32 implementiert werden kann.
Die Frage war - wie man eine nicht blockierende i2c-Interaktion implementiert, ich erwähnte Interrupts als Option. Interrupt-basiert in nur einem der möglichen Ansätze.
@AlexeyMalev Ich kenne die STM32-Umgebung nicht speziell, aber ich habe versucht, allgemeine Antworten zu geben, die allgemein gelten, nicht blockieren, und habe dies sofort getan.
@jonk Nono, ich bezog mich auf JimmyBs Kommentar, egal :)

Beispiele gibt es in den STM32CubeBibliotheken. Holen Sie sich das für Ihre Controller-Familie geeignete (z. B. STM32CubeF4oder STM32CubeL1) und suchen Sie Examples/I2C/I2C_TwoBoards_ComDMAim ProjectsUnterverzeichnis.

Grund

Nun, der Grund ist einfach: Das Blockieren ist einfach und auf den ersten Blick scheint es zu funktionieren. Wehe dir, wenn du in der Zwischenzeit etwas anderes machen willst.

Ohne auf viele Einzelheiten einzugehen, da ich den STM32 nicht kenne, können Sie dieses Problem im Allgemeinen auf zwei Arten lösen, je nach Ihren Anforderungen.

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));

In nicht blockierend umwandeln

Entweder Sie implementieren ein Timeout für alle Ihre whileSchleifen. Das heisst:

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
static unsigned long start = now();
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT) && now()-start < TIMEOUT);  
if (now()-start >= TIMEOUT) { return ERROR_TIMEOUT; }

(Das ist natürlich Pseudocode, Sie verstehen schon. Fühlen Sie sich frei, ihn nach Bedarf zu optimieren oder an Ihre Codierungspräferenzen anzupassen.)

Sie müssen die Rückgabecodes überprüfen, wenn Sie den Stapel nach oben bewegen, und die richtige Stelle auswählen, an der Sie Ihre Timeout-Behandlung durchführen. Beachten Sie, dass es hilfreich ist, auch eine globale Variable i2c_timeout_occured=1oder was auch immer zu setzen, damit Sie weitere I2C-Aufrufe schnell abbrechen können, ohne zu viele Argumente herumreichen zu müssen.

Diese Änderung ist hoffentlich ziemlich schmerzlos.

Von innen nach außen

Wenn Sie stattdessen wirklich andere Verarbeitungen durchführen müssen, während Sie auf dieses Ereignis warten, müssen Sie die innere While-Schleife vollständig entfernen. Das machst du so:

void main_loop() {
   do_i2c_stuff();    // must never block
   do_other_stuff();
   ...
}

// Must never block. Assuming all I2C_... functions do not block either.
void do_i2c_stuff() {
  static int state=...;

  if (state==0) {
    I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
    state=1;
  } else if (state==1) {
    if (I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 
      state=2;
  } else ...
}

Es ist nicht unbedingt sehr kompliziert, abhängig von Ihrer anderen Logik. Sie können viel mit dem richtigen Einrücken/Kommentieren/Formatieren tun, damit Sie nicht den Überblick darüber verlieren, was Sie programmieren.

Die Art und Weise, wie dies funktioniert, besteht darin, eine Zustandsmaschine zu erstellen . Wenn Sie sich Ihren ursprünglichen Code ansehen, sieht er so aus:

non-blocking code
while (!nonblocking_function_call1());
non-blocking code
while (!nonblocking_function_call2());

Um das in eine Zustandsmaschine umzuwandeln, haben Sie jeweils einen Zustand:

state 0: non-blocking code
state 1: nonblocking_function_call1()
state 2: non-blocking code
state 3: nonblocking_function_call2()

Dann rufen Sie, wie im obigen Beispiel gezeigt, diesen Code in einer Endlosschleife (Ihrer Hauptschleife) auf und führen nur den Code aus, der Ihrem aktuellen Zustand entspricht (getrackt in einer statischen Variablen state). Der nicht blockierende Code ist trivial, er ist unverändert. Der Blockierungscode wird durch eine Variante ersetzt, die nicht blockiert, sondern erst aktualisiert, statewenn sie fertig ist.

Beachten Sie, dass die einzelnen whileSchleifen verschwunden sind; Sie haben sie durch die Tatsache ersetzt, dass Sie sowieso Ihre Hauptschleife auf oberster Ebene haben, die Ihre Zustandsmaschine wiederholt aufruft.

Diese Lösung kann schmerzhaft sein, wenn Sie viel Legacy-Code haben, da Sie nicht einfach die innerste Blockierungsfunktion anpassen können, wie in der ersten Lösung. Es glänzt, wenn Sie anfangen, frischen Code zu schreiben und diesen Weg von Anfang an gehen. Kombinieren Sie es mit vielen anderen Dingen, die ein µC tun könnte (z. B. auf Tastendrücke warten usw.); Wenn Sie sich daran gewöhnen, es die ganze Zeit so zu machen, erhalten Sie beliebige Multitasking-Fähigkeiten kostenlos.

Unterbricht

Ehrlich gesagt würde ich für so etwas (dh unendliches Blockieren einfach loswerden) versuchen, mich von Interrupts fernzuhalten, es sei denn, Sie haben extreme Timing-Anforderungen. Interrupts machen es kompliziert, schnell, Sie haben vielleicht sowieso nicht genug davon, und es läuft sowieso auf einen ziemlich ähnlichen Code hinaus, da Sie innerhalb des Interrupts nicht viel mehr tun wollen, als einige Flags zu setzen.

nette (suboptimale) Lösung hier - ich dachte, eine Art Interrupt wäre immer notwendig.
Die ganze Idee war, Schleifen loszuwerden while. Ich habe eine Frage zu Ihrem letzten Codebeispiel - ich bin mir nicht sicher, was Sie genau vorschlagen. Das Problem ist - kehrt nach einiger Zeit I2C_CheckEventzurück trueund blockiert nicht, was bedeutet, dass es im Allgemeinen nicht ausreicht, es einmal aufzurufen, was Sie (so wie ich es verstanden habe) vorschlagen. Können Sie das bitte ein wenig erläutern?
@AlexeyMalev, meine Lösungen beseitigen Endlosschleifen while. Der erste hat sie noch, fügt aber ein Timeout hinzu. Der zweite entfernt sie vollständig (vorausgesetzt, Sie haben eine Schleife der obersten Ebene, die sowieso in einer Dauerschleife ausgeführt wird). Ich habe, wie gewünscht, eine Erklärung hinzugefügt. i2C_CheckEvent wird oft aufgerufen, aber Ihre Methode kehrt unabhängig vom Ergebnis sofort zurück, sodass auch die anderen Teile Ihres Programms Zeit zum Ausführen haben.
Dies ist nicht "nicht blockierend". Nonblocking bedeutet, dass Ihre CPU in den Ruhezustand übergeht, wenn keine Arbeit zu erledigen ist. Dies geschieht normalerweise mit einem Scheduler, der es der Aufgabe ermöglicht, auf einem Semaphor zu schlafen, anstatt die CPU in einer While-Schleife zu blockieren. Wenn i2c überhaupt auftritt, gibt die Interrupt-Routine die Semaphore und die Task, die auf die Semaphore gewartet hat, wacht auf und läuft erneut. Dies ist nicht blockierend. Die in diesem Beitrag beschriebenen Lösungen sind dies nicht.
@Martin, es gibt verschiedene Aspekte des Blockierens. Meine Antwort spiegelt mein Verständnis der Frage wider, in der es darum geht, wie verhindert werden kann, dass der Programmfluss blockiert wird, wenn ein i2c-Aufruf eingegeben wird. OP beschreibt eine blockierende Variante, und meine skizzierte Lösung ist die entsprechende nicht blockierende. Meine Lösung löst das unmittelbare Problem und lässt sich bei Bedarf einfach auf Interrupt- oder Semaphor-Ansteuerung verallgemeinern.
Außer dass Ihre erste Lösung nicht blockierungsfrei ist und Ihre zweite Lösung 100 % CPU-Auslastung erfordert, während ein i2c-Vorgang ausgeführt wird. Daher sind beide Lösungen für ernsthaften Code auf Produktionsebene ungeeignet.
@Martin, ich denke, dass meine Antwort umfangreich genug ist, damit der Leser sieht, worauf ich hinaus will. Ich behaupte nicht, dass meine Lösung jedes Problem der Welt löst. Ich sehe in der Frage nichts über das Einsparen von CPU-Leistung. Mein Ansatz löst ein spezifisches Problem (wie gesagt) – wie diese Art der Kommunikation mit anderer Verarbeitung verschachtelt werden kann. Wenn Sie eine andere Lösung bevorzugen, zögern Sie nicht, eine zu schreiben. Wenn Sie konkrete Verbesserungen an meinem Ansatz haben, die nicht auf eine vollständige Neufassung hinauslaufen, können Sie dies gerne kommentieren, und ich werde sie berücksichtigen.

Hier ist eine Liste von ICH 2 C Interrupt-Anforderung auf einem STM32 verfügbar. Da ich den genauen Chip, den Sie verwenden, nicht kenne, wird empfohlen, in Ihrem Referenzhandbuch nachzusehen, ob es einen Unterschied gibt, aber ich bezweifle, dass es einen geben wird.

Geben Sie hier die Bildbeschreibung ein

Um bei Ihrem Beispielcode zu bleiben, müssen Sie, wenn eine nicht blockierende Version benötigt wird, das Ereignis Startbit gesendet (Master)ITEVFEN mit dem Steuerbit aktivieren und das SBEreignisflag in der ISR überprüfen.

Betrachtet man den Auszug von @BenceKaulics aus einem Datenblatt, könnte der Pseudo-Code für eine Interrupt-Service-Routine (ISR) so aussehen:

i2c_event_isr() {
  switch( i2c_event ) {

    case master_start_bit_sent: 

      send_address(...);
      break;

    case master_address_sent:
    case data_byte_finished:

      if ( has_more_data() ) {
        send_next_data_byte(); 
      } else {
        send_stop_condition();
      }

      break;
    ...
  }
}