Frage:
Nicht blockierende I2C-Implementierung auf STM32
Alexey Malev
2016-10-24 14:22:05 UTC
view on stackexchange narkive permalink

Die meisten Artikel, die ich finden kann und die im Wesentlichen "I2C für Dummies auf XXX-Mikrocontroller" 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);
/ * auf Bestätigung warten * /
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
 

Diese while -Schleifen sind im Wesentlichen nach jeder Operation vorhanden.Schließlich ist meine Frage - wie man I2C implementiert, 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 kein Codebeispiel finden.Können Sie mir bitte dabei helfen?

Beachten Sie, dass Sie DMA auch mit I²C verwenden können, um Leistungsvorteile zu erzielen und die Anzahl der zu verarbeitenden Interrupts zu verringern.
@AlexeyMalev Hier ist ein Beispiel für eine nicht blockierende I2C-Implementierung für AVR (obwohl ähnliche Prinzipien für STM32 gelten würden): https://github.com/scttnlsn/avr-twi Diese spezielle Implementierung verwendet Interrupts, um eine Zustandsmaschine zu verwalten und eine Rückruffunktion aufzurufen, wennDie Übertragung ist beendet.
Fünf antworten:
jonk
2016-10-24 14:46:21 UTC
view on stackexchange narkive permalink

Sicher.

  1. Richten Sie Ihre Treiber in Form eines oberen und unteren Paares ein, die durch einen gemeinsam genutzten Puffer getrennt sind. Die Funktion lower ist ein Interrupt-gesteuertes Codebit, das auf Unterbrechungsereignisse reagiert, sich um die sofortige Arbeit kümmert, die zur Bereitstellung der Hardware erforderlich ist, und Daten zum gemeinsam genutzten Puffer hinzufügt (wenn es sich um einen Empfänger handelt) oder Extrahiert das nächste Datenbit aus dem gemeinsam genutzten Puffer, um die Hardware weiter zu warten (wenn es sich um einen Sender handelt). Die Funktion obere wird von Ihrem regulären Code aufgerufen und akzeptiert entweder Daten, die einem ausgehenden Puffer hinzugefügt werden sollen, oder sonst sucht nach und extrahiert Daten aus dem eingehenden Puffer. Durch das Koppeln solcher Dinge und das Bereitstellen einer angemessenen Pufferung für Ihre Anforderungen kann diese Anordnung den ganzen Tag ohne Probleme ausgeführt werden und entkoppelt Ihren Hauptcode von Ihrer Hardware-Wartung
  2. Verwenden Sie eine Zustandsmaschine in Ihrem Interrupt-Treiber. Jeder Interrupt liest den aktuellen Status, verarbeitet einen Schritt und wechselt dann entweder in einen anderen Status oder bleibt im selben Status und wird dann beendet. Dies kann so komplex oder so einfach sein, wie Sie es brauchen. Oft wird dies von einem Timer-Ereignis ausgelöst. Das muss aber nicht so sein.
  3. Erstellen Sie ein kooperatives Betriebssystem. Dies kann genauso einfach sein wie das Einrichten einiger kleiner Stapel, die mit malloc () zugewiesen wurden, und das Ermöglichen, dass die Funktionen gemeinsam eine switch () -Funktion aufrufen, wenn sie vorerst mit einer unmittelbaren Aufgabe fertig sind. Sie haben einen separaten Thread für Ihre I2C-Funktion eingerichtet. Wenn jedoch entschieden wird, dass derzeit nichts zu tun ist, wird einfach switch () aufgerufen, um einen Stapelwechsel in einen anderen Thread zu bewirken. Dies kann im Round-Robbin-Modus erfolgen, bis es zurückkommt und von dem von Ihnen getätigten switch () -Aufruf zurückkehrt. Dann kehren Sie zu Ihrem while-Zustand zurück, der erneut überprüft wird. Wenn immer noch nichts zu tun ist, ruft while die switch () erneut auf. Auf diese Weise wird die Verwaltung Ihres Codes nicht viel komplexer und es ist einfach, switch () -Aufrufe überall dort einzufügen, wo Sie dies benötigen. Sie müssen sich auch keine Gedanken über die Vorbelegung von Bibliotheksfunktionen machen, da Sie nur zwischen Stapeln an einer Funktionsaufrufgrenze wechseln, sodass eine Bibliotheksfunktion nicht unterbrochen werden kann. Dies macht die Implementierung sehr einfach.
  4. Vorbeugende Threads. Dies ähnelt # 3, außer dass die switch () - Funktion nicht aufgerufen werden muss. Die Vorbelegung erfolgt basierend auf einem Timer, oder ein Thread kann auch frei wählen, ob er seine Zeit freigeben möchte. Die Schwierigkeit besteht hier darin, Bibliotheksroutinen und / oder spezielle Hardware vorzuenthalten, bei denen möglicherweise bestimmte Befehlssequenzen vom Compiler generiert werden, die nicht unterbrochen werden können (Back-to-Back-E / A, die ein High-Byte gefolgt von a abrufen müssen Zum Beispiel ein niedriges Byte von derselben Speicherzuordnungsadresse.)
  5. ol>

    Ich denke, die ersten beiden Optionen sind wahrscheinlich Ihre besseren Wetten. Viel hängt jedoch davon ab, wie viel Sie von dem von anderen geschriebenen Bibliothekscode abhängig sind. Es ist durchaus möglich, dass der Bibliothekscode nicht für die Aufteilung in Komponenten der oberen und unteren Ebene ausgelegt ist. 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 gehe eher davon aus, dass ich meinen eigenen Code schreibe, um die Arbeit zu erledigen, wenn mir die Art und Weise, wie die Bibliothek die Arbeit erledigt, nicht gefällt (nur Warteschleifen).Das heißt, ich kann eine der oben genannten Methoden anwenden.

    Sie müssen sich jedoch den Bibliothekscode genau ansehen und prüfen, ob er bereits Funktionen enthält, mit denen Sie das Warteverhalten vermeiden können.Möglicherweise unterstützt die Bibliothek etwas anderes.Das ist also der erste Ort, um zu überprüfen, nur für den Fall.Wenn nicht, spielen Sie mit der Idee, die vorhandenen Funktionen entweder als Teil eines oberen / unteren Treibersplits oder als zustandsmaschinengesteuerten Prozess zu verwenden.Möglicherweise können Sie das auch herausfinden.

    Ich habe momentan keine anderen Vorschläge, die mir schnell in den Sinn kommen.

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 einige schwere Berechnungen durchführt, zum Beispiel 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-Behandlungsthread in einer höheren Sprache 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 sich wahrscheinlich in Ihrer Hauptfunktion befinden, die den Code auf _upper_-Ebene aufruft.Der Code der unteren Ebene macht keine Dinge, die eine Pause in der Mitte benötigen.Wenn mehr Dinge passieren, während Ihr Hauptcode den _upper_-Code aufruft oder nach dem Aufrufen verarbeitet, wird der Interrupt weiterhin gewartet und Werte für Sie in den Puffer gestopft.Das hört auch dann nicht auf, wenn Sie rechnen.Sie müssen nicht nach gepufferten Daten suchen, wenn Sie die anderen Arbeiten noch nicht abgeschlossen haben.Wenn Sie nicht mithalten können, haben Sie trotzdem ein Problem.
@AlexeyMalev Die längste Rechenarbeit sollte in Ihrem Hauptcode enthalten sein.Wenn Sie kürzere Bits haben, die zwischenzeitlich ausgeführt werden müssen, können Sie diese für Timer-Ereignisse einrichten, die Ihren langen Berechnungscode unterbrechen, um ihre kurzen Aufgaben zu erledigen.Diese zeitgesteuerten kurzen Aufgaben können die Hälfte jedes Treibers auf der oberen Ebene aufrufen, um auch gepufferte Daten abzurufen.Es geht nur darum, ein Zeitdiagramm zu erstellen.Lassen Sie zwischen den Schritten genügend Zeit, damit die längsten Berechnungen durchgeführt werden können, sowie alle Unterbrechungszeiten.Das ist dein Hauptprozess.Sie können jedoch meine Option Nr. 3 verwenden und Ihren Hauptcode mit switch () -Aufrufen salzen.
Entschuldigung, aber ich muss abstimmen.Diese ausführliche Antwort scheint keine nützlichen Informationen darüber zu liefern, wie eine Interrupt-gesteuerte I²C-Statemaschine 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, habe jedoch versucht, umfassende Antworten zu geben, die im Allgemeinen zutreffen, nicht blockieren und dies sofort getan haben.
@jonk Nono, ich bezog mich auf JimmyBs Kommentar, egal :)
followed Monica to Codidact
2016-10-24 14:52:41 UTC
view on stackexchange narkive permalink

Es gibt Beispiele in den Bibliotheken STM32Cube .Holen Sie sich die für Ihre Controller-Familie geeignete (z. B. STM32CubeF4 oder STM32CubeL1 ) und suchen Sie in den -Projekten nach Beispiele / I2C / I2C_TwoBoards_ComDMA Unterverzeichnis.

AnoE
2016-10-24 18:44:59 UTC
view on stackexchange narkive permalink

Grund

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

Da ich nicht auf viele Details eingehen muss, da ich den STM32 nicht kenne, können Sie dieses Problem je nach Ihren Anforderungen auf zwei Arten lösen.

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * auf Bestätigung warten * /
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
 

In nicht blockierende konvertieren

Entweder implementieren Sie eine Zeitüberschreitung für alle Ihre while -Schleifen. Dies bedeutet:

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * auf Bestätigung warten * /
statischer vorzeichenloser langer Start = now ();
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT) && now () - < TIMEOUT starten);
if (now () - start > = TIMEOUT) {return ERROR_TIMEOUT; }}
 

(Dies ist natürlich ein Pseudocode, Sie haben die Idee. Sie können Ihre Codierungseinstellungen nach Bedarf optimieren oder anpassen.)

Sie müssen die Rückkehrcodes überprüfen, wenn Sie den Stapel hinauffahren, 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 = 1 oder was auch immer festzulegen, damit Sie weitere I2C-Aufrufe schnell abbrechen können, ohne zu viele Argumente übergeben zu müssen.

Diese Änderung ist hoffentlich ziemlich schmerzlos.

Von innen nach außen

Wenn Sie stattdessen wirklich eine andere Verarbeitung durchführen müssen, während Sie auf dieses Ereignis warten, müssen Sie die innere while-Schleife vollständig entfernen. Sie machen das so:

  void main_loop () {
   do_i2c_stuff (); // darf niemals blockieren
   do_other_stuff ();
   ...
}}

// Darf niemals blockieren. Angenommen, alle I2C _... Funktionen blockieren auch nicht.
void do_i2c_stuff () {
  statischer int Zustand = ...;

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

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

Dies funktioniert durch Erstellen einer Zustandsmaschine . Wenn Sie sich Ihren Originalcode ansehen, sieht er folgendermaßen aus:

  nicht blockierender Code
while (! nonblocking_function_call1 ());
nicht blockierender Code
while (! nonblocking_function_call2 ());
 

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

  state 0: nicht blockierender Code
Zustand 1: nonblocking_function_call1 ()
Zustand 2: Nicht blockierender Code
Zustand 3: nonblocking_function_call2 ()
 

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

Beachten Sie, dass die einzelnen while -Schleifen weg sind. Sie haben sie durch die Tatsache ersetzt, dass Sie sowieso Ihre Hauptschleife der obersten Ebene haben, die Ihre Zustandsmaschine wiederholt aufruft.

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

Interrupts

Ehrlich gesagt, für so etwas (d. h. nur unendliches Blockieren loswerden) würde ich mich bemühen, Interrupts zu vermeiden, 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 ziemlich ähnlichen Code hinaus, da Sie innerhalb des Interrupts nicht viel mehr tun möchten, außer einige Flags zu setzen.

schöne (suboptimale) Lösung hier - ich dachte, eine Art Interrupt wäre immer notwendig.
Die ganze Idee war, "while" -Schleifen loszuwerden.Ich habe eine Frage zu Ihrem letzten Codebeispiel - nicht sicher, was genau Sie vorschlagen.Das Problem ist - "I2C_CheckEvent" gibt nach einiger Zeit "true" zurück und blockiert nicht, was im Allgemeinen bedeutet, dass es nicht ausreicht, es einmal aufzurufen, was Sie (wie ich es verstanden habe) vorschlagen.Können Sie das bitte etwas erläutern?
@AlexeyMalev, meine Lösungen werden unendliche "while" -Schleifen los.Der erste hat sie noch, fügt aber eine Zeitüberschreitung hinzu.Der zweite entfernt sie vollständig (vorausgesetzt, Sie haben eine Schleife der obersten Ebene, die sowieso in einer ewigen Schleife ausgeführt wird).Ich habe wie gewünscht eine Erklärung hinzugefügt.i2C_CheckEvent * wird * häufig 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".Nicht blockierend bedeutet, dass Ihre CPU in den Ruhezustand wechselt, wenn keine Arbeit erledigt werden soll.Dies erfolgt normalerweise mithilfe eines Schedulers, 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 Interruptroutine das Semaphor aus und die Aufgabe, die auf das Semaphor gewartet hat, wird aktiviert und erneut ausgeführt.Dies ist nicht blockierend.Die in diesem Beitrag beschriebenen Lösungen sind dies nicht.
@Martin, gibt es verschiedene Aspekte des Blockierens.Meine Antwort spiegelt mein Verständnis der Frage wider, mit der gefragt wird, wie der Programmfluss bei der Eingabe eines i2c-Aufrufs nicht blockiert werden kann.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 leicht auf Interrupt oder Semaphor-gesteuert verallgemeinern, wenn dies erforderlich ist.
Es sei denn, Ihre erste Lösung blockiert nicht und Ihre zweite Lösung erfordert eine 100% ige CPU-Auslastung, während ein i2c-Vorgang ausgeführt wird.Daher sind beide Lösungen für ernsthaften Code auf Produktionsebene ungeeignet.
@Martin, Ich habe das Gefühl, dass meine Antwort so umfangreich ist, dass der Leser sieht, was ich vorhabe.Ich impliziere nicht, dass meine Lösung jedes Problem auf der Welt löst.Ich sehe in der Frage nichts über das Einsparen von CPU-Energie.Mein Ansatz löst ein bestimmtes Problem (wie angegeben) - wie diese Art der Kommunikation mit anderen Verarbeitungen verschachtelt werden kann.Wenn Sie eine andere Lösung bevorzugen, können Sie diese gerne aufschreiben.Wenn Sie konkrete Verbesserungen an meinem Ansatz haben, die nicht zu einer vollständigen Umschreibung führen, können Sie dies gerne kommentieren, und ich werde dies berücksichtigen.
Bence Kaulics
2016-10-24 19:28:22 UTC
view on stackexchange narkive permalink

Hier ist eine Liste von \ $ \ small I ^ 2C \ $ Interrupt-Anforderungen, die auf einem STM32 verfügbar sind.Da ich den genauen Chip, den Sie verwenden, nicht kenne, wird empfohlen, Ihr Referenzhandbuch zu überprüfen, wenn es Unterschiede gibt, aber ich bezweifle, dass dies der Fall sein wird.

enter image description here

Um bei Ihrem Beispielcode zu bleiben, müssen Sie, wenn eine nicht blockierende Version erforderlich ist, das gesendete Start-Bit (Master) -Ereignis mit dem Steuerbit ITEVFEN aktivieren und das SB überprüfen Ereignisflag innerhalb des ISR.

JimmyB
2016-10-24 20:55:53 UTC
view on stackexchange narkive permalink

In Anbetracht des Auszugs von @BenceKaulics aus einem Datenblatt könnte der Pseudocode für eine Interrupt-Serviceroutine (ISR) folgendermaßen aussehen:

  i2c_event_isr () {
  switch (i2c_event) {

    case master_start_bit_sent:

      send_address (...);
      Unterbrechung;

    case master_address_sent:
    case data_byte_finished:

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

      Unterbrechung;
    ...
  }}
}}
 


Diese Fragen und Antworten wurden automatisch aus der englischen Sprache übersetzt.Der ursprüngliche Inhalt ist auf stackexchange verfügbar. Wir danken ihm für die cc by-sa 3.0-Lizenz, unter der er vertrieben wird.
Loading...