Forum: Mikrocontroller und Digitale Elektronik Probleme bei der Interrupt-Steuerung mit sei() und cli()


von Lukas Bommes (Gast)


Lesenswert?

Hallo Zusammen,

ich entwickle gerade eine Software, die Daten in Form von 
CSV-formatierten ASCII-Strings zwischen einer GUI und einem Arduino 
austauscht. Dabei werden Daten vom Arduino zur GUI innerhalb einer 
Interrupt-Routine alle 100 Millisekunden versendet und Daten von der GUI 
innerhalb der main-loop des Arduinos empfangen, sobald Daten an der 
seriellen Schnittstelle ankommen. In etwa so:

// Daten an die GUI senden, Aufruf alle 100 ms
ISR {
 Serial.print(variable1);
 Serial.print(",");
 Serial.print(variable2);
 // [usw.]
}

// Daten von der GUI empfangen
void loop {
 while (Serial.available() > 0) {
  var1 = Serial.parseInt();
  var2 = Serial.parseInt();
  // [usw.]
 }
}

Wird in der GUI z.B. ein Button angeklickt, so wird ein csv-String an 
den µC geschickt, der daraufhin eine Aktion ausführt und zur Bestätigung 
einen csv-String an die GUI zurückschickt.
Das Problem liegt nun darin, dass teilweise das Senden und Empfangen von 
Daten vermischt wird. D.h. noch während in der main-loop der empfangene 
String geparst wird, wird die ISR aufgerufen und der µC sendet. DAs 
resultliert dann natürlich in fehlerhaften Verhalten. Als Lösung wollte 
ich die Interrupts im Block in der main-loop mittels cli() und sei() 
sperren. Also so:

void loop {
 while (Serial.available() > 0) {
  cli();
  var1 = Serial.parseInt();
  var2 = Serial.parseInt();
  // [usw.]
  sei();
 }
}

Das funktioniert aber scheinbar nicht, denn sobald man einen csv-string 
an den µC sendet, wird die ISR gar nicht mehr ausgeführt, d.h. es werden 
keine Daten mehr an die GUI gesendet.

Hat jemand eine Idee, woran das liegen könnte? Habe ich die 
Funktionsweise von sei() und cli() u.U. falsch verstanden?

Das gewollte Verhalten ist ja, dass das Interrupt während des Parsen des 
empfangenen Strings pausiert und nach Abschluss des Parsens wieder 
ausgeführt werden kann.

Wäre echt toll, wenn mir diesebzüglich jemand auf die Sprünge helfen 
könnte.

Viele Grüße
Lukas

von The D. (thedaz)


Lesenswert?

Ich kapiere es nicht: warum und wie sollten sich gesendete und 
empfangene Daten vermischen? Echoed der Empfänger etwa die vom Arduino 
gesendeten Daten?

von Lukas Bommes (Gast)


Lesenswert?

Hallo The Daz, danke erstmal für deine Antwort!
Die vom Arduino gesendeten Daten werden in der GUI wieder eingelesen. 
Konkret sieht es z.B. so aus, dass ein Wert in eine Edit-Box geschrieben 
wird und im Edit-Box-Callback der Wert an den Arduino gesendet wird. 
Dieser sendet den gleichen Wert wieder an die GUI zurück (dort gibt es 
einen Event-Handler, der bei ankommende Daten ausgeführt wird) und wird 
in die Edit-Box eingetragen. Sprich, es liegt ein Echo vor. Im Grunde 
könnte ich natürlich auch einfach die Daten an den Arduino senden und 
sie nicht zurückübertragen. Aber im diesem Fall ist nicht 
sichergestellt, dass die Daten in der GUI und auf dem µC konsistent 
sind.

Ich müsste ja nur die ISR im µC kurz pausieren und nach dem Einlesen der 
ankommmenden Daten wieder aktivieren. Aber das funktioniert wie gesagt 
aus irgendeinem Grund nicht wie erwartet.

Die ISR enthält zugegebenermaßen auch relativ viele Befehle (35x 
Serial.print() und einen PID-Regelalgorithmus). Ich suche gerade nach 
einer Möglichkeit, diese zu verkürzen.

von H.Joachim S. (crazyhorse)


Lesenswert?

Ich habs auch nicht verstanden, aber es könnte gut sein, dass genau das 
dein Problem ist:

Lukas Bommes schrieb:

> Die ISR enthält zugegebenermaßen auch relativ viele Befehle (35x
> Serial.print() und einen PID-Regelalgorithmus). Ich suche gerade nach
> einer Möglichkeit, diese zu verkürzen.

Aber ich weiss nicht, wie serial.print arbeitet. Interruptgesteuert oder 
polling?

Die übliche Lösungsweise: serielle Schnittstelle im Interruptbetrieb mit 
ausreichend Ringbuffer für Senden und Empfangen.

Interruptroutinen selbst sind kurzzuhalten. Ja, kurz ist relativ. Aber 
deine ist definitiv nicht "kurz".

von Peter D. (peda)


Lesenswert?

Dein Problem ist, Du benutzt kein Protkoll.
Du must Dir erstmal ein Protokoll ausdenken, was die Synchronisation 
sicherstellt und Übertragungsfehler erkennt und korrigiert.
Oder Du benutzt irgendein Standardprotokoll.

von (prx) A. K. (prx)


Lesenswert?

Serielle Ausgaben in einer ISR sind selten eine gute Idee.

von Einer K. (Gast)


Lesenswert?

H.Joachim S. schrieb:
> Aber ich weiss nicht, wie serial.print arbeitet. Interruptgesteuert oder
> polling?

Wenn das Arduino Zeugs ist, dann kann ich da aushelfen.

Serial arbeitet Interruptgesteuert.

Es ist also verboten Serial.print() in einer ISR aufzurufen.
Das geht so lange gut, bis der Buffer voll ist. Dann gibts einen 
Deadlock.

ISRs schlank halten.
Kein Serial, kein delay!

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Lukas Bommes schrieb:
> ISR {
>  Serial.print(variable1);
>  Serial.print(",");
>  Serial.print(variable2);
>  // [usw.]
> }

Setze Dir in der ISR nur ein Flag, dass Du etwas zu senden hast, also:
1
static volatile uint8_t send_data = 0;
2
...
3
ISR
4
{
5
    send_data = 1;
6
}

Und dann in der Main-Loop:
1
if (send_data)
2
{
3
    send_data = 0;
4
    Serial.print(variable1);
5
    Serial.print(",");
6
    Serial.print(variable2);
7
    ...
8
}

von Lukas Bommes (Gast)


Lesenswert?

Hallo ihr alle,

herzlichsten Dank erstmal für eure hilfreichen Antworten!

@Peter: Ein Protokoll wäre natürlich die solidere Lösung für das Problem 
gewesen, aber ich dachte mir, die Lösung mit den versendeten 
Zeichenketten funktioniert auch und ist (für mich persönlich) einfacher 
zu verstehen und implementieren.

Ansonsten besten Dank für den Tipp mit dem Status-Flag. Damit tritt das 
Problem nicht mehr auf. Ich bin zwar mir nicht ganz sicher, warum die 
längere ISR zu dem Problem geführt hat, aber offensichtlich war sie die 
Ursache.

Viele Grüße
Lukas

von H.Joachim S. (crazyhorse)


Lesenswert?

So, wie es anfänglich war:
-in der 100ms-ISR werden Zeichen verschickt, aber nicht direkt, sondern 
in einen Buffer geschrieben
-eine serielle ISR (hier unsichtbar, Arduino intern) schickt die Zeichen 
aus dem Buffer dann tatsächlich raus.

Nun ist ein Buffer nicht beliebig gross. Nehmen wir einfach mal 16Byte 
an.
Normalerweise verschickst du alle 100ms nur sagen wir 12Byte, alles 
läuft.
Die 100ms-ISR kann die 12Byte in den Buffer schreiben und wird 
verlasssen. Jetzt kann die serielle ISR diesen Buffer abarbeiten.

Nun kommt der Fall, dass schon ein par Byte im Sendebuffer stehen, wenn 
die 100ms-ISR aufgerufen wird, d.h. die zu sendenden 12Byte passen nicht 
rein. Und dann wird dort solange gewartet, bis es passt. Passiert aber 
nie, da du dich in einer ISR befindest und demzufolge nicht ein einziges 
Byte aus dem Buffer verschickt wird, da der serielle Int nicht zum Zuge 
kommt -> dead lock.

von W.S. (Gast)


Lesenswert?

Lukas Bommes schrieb:
> Dabei werden Daten vom Arduino zur GUI innerhalb einer
> Interrupt-Routine alle 100 Millisekunden versendet und Daten von der GUI
> innerhalb der main-loop des Arduinos empfangen

Du machst es grundfalsch. Das ist alles.

Also:
1. arbeite mit Events und einer sinnvoll eingerichteten Systemuhr in 
deinem Arduino. Wie man ein Event-System schreibt und es mit Events 
versorgt (z.B. alle 100 ms ein "event100ms_um" oder so), kannst du in 
der Lernbetty nachlesen.

2. arbeite dir ein Übertragungsprotokoll für den Kanal aus. (serielle 
Schnittstelle oder was such immer du verwenden willst) Also was welche 
Seite senden darf und was die Bedeutung dessen ist und wie die 
Gegenseite darauf reagieren muß und wie du es einrichtest, daß du beide 
Seiten auch bei Synchron-Verlust wieder synchronisiert bekommst.

3. mache längliche Arbeiten (Senden längerer Texte, Berechnungen usw.) 
NIE NIE NIE in irgendeinem Interrupt-Handler, sondern verlagere dieses 
in Funktionen, die du von der Grundschleife in main aus aufrufst. Eben 
so, daß Interrupts niemals blockiert werden.

4. richte dir größere Ringpuffer für's Senden ein.

W.S.

von Mein grosses V. (vorbild)


Lesenswert?

Frank M. schrieb:
> Setze Dir in der ISR nur ein Flag, dass Du etwas zu senden hast,
> also:static volatile uint8_t send_data = 0;
> ...
> ISR
> {
>     send_data = 1;
> }
>
> Und dann in der Main-Loop:if (send_data)
> {
>     send_data = 0;
>     Serial.print(variable1);
>     Serial.print(",");
>     Serial.print(variable2);
>     ...
> }

Auf Umwegen kommt man auch ans Ziel.

Eine ISR, in der nur ein Flag gesetzt wird, welches dann in der Mainloop 
abgefragt wird, kann man sich mitsamt dem Interrupt ganz sparen und das 
Interruptflag auch in der Mainloop abfragen:
1
if(IRQREG & (1 << INTFLAG))
2
{
3
     IRQREG |= (1 << INTFLAG);
4
     Serial.print(variable1);
5
     Serial.print(",");
6
     Serial.print(variable2);
7
}

W.S. schrieb:
> NIE NIE NIE in irgendeinem Interrupt-Handler

Immer diese Dogmen. Naja, meistens ist es ja richtig.

von Einer K. (Gast)


Lesenswert?

Mein grosses V. schrieb:
> Immer diese Dogmen. Naja, meistens ist es ja richtig.

Du meinst "Wiedereintrittsfähigkeit"?
Da gilt es dann aber einiges an Extrawürsten zu backen...

von Peter D. (peda)


Lesenswert?

Mein grosses V. schrieb:
> if(IRQREG & (1 << INTFLAG))
> {
>      IRQREG |= (1 << INTFLAG);

Da schlägt die AVR-Falle zu und löscht alle Interrupts in dem Register.
Beim AVR muß es heißen:
1
if(IRQREG & (1 << INTFLAG))
2
{
3
     IRQREG = (1 << INTFLAG);

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Mein grosses V. schrieb:
> Auf Umwegen kommt man auch ans Ziel.

Der "Umweg" hat einen nennenswerten Vorteil: Er beschreibt ein Konzept, 
ist leicht um weitere "Handler" erweiterbar und ist überdies 
plattformunabhängig und damit auch portabel. Vollkommen egal, ob Du 
ATmega, Arduino, STM32 oder ESP8266 programmierst.

Dein "direkter" Weg schlägt sich mit prozessorspezifischen 
Interrupt-Registern rum, die dann auch mal falsch bedient werden können 
- wie Peter gezeigt hat.

von Pandur S. (jetztnicht)


Lesenswert?

Fuer solche Geschichten, dh ein zusammengemurkstes Protokoll ist 
erhoehter Aufwand erforderlich. Dh ein aufwendigeres Verfahren.

Minimale Arbeit macht ein MasterSlave Protokol. Der Master(PC) sendet 
Anfragen, der Slave(Controller) antwortet. Dh der Slave sendet nie von 
selbst. Dann haben die Meldungen einen direkten Bezug, kein Vermischen, 
der Master macht die Priorisierung der Anfragen.

Falls der Controller nun periodisch senden soll, so muessen diese 
Packete als solche gekennzeichnet sein. Anfragen des PC haben nun 
wahrscheinlich Prioritaet. Dh der Controller muss in Bloecken arbeiten. 
Dh kommt eine Anfrage rein, so wird diese verarbeitet, und das Resultat 
in den Sendebuffer gefuellt, hinter den bereits zu sendenden Daten. Die 
periodischen Daten werden nicht mehr strikt nach Timing gesendet, 
sondern niedriger priorisiert gegenueber den direkten Anfragen, dh wenn 
die Anfrage vorbei ist, kommt wieder ein anstehendes Periodisches Packet 
dran. Die Antwortpackete werden auch als solche gekennzeichnet.

So einfach CSV Packete auf dem Schirm zu lesen sind, so schwierig sind 
sie zu verarbeiten. Man kann einer Anfrage eine feste dem Befehl 
entsprechende Nummer mitgeben, die in der Antwort wieder enthalten ist. 
So kann man Anfrage und Antwort zuordnen.

von Mein grosses V. (vorbild)


Lesenswert?

Peter D. schrieb:
> Da schlägt die AVR-Falle zu und löscht alle Interrupts in dem Register.

Hast recht. Wahr wohl schon zu spät.

von Peter D. (peda)


Lesenswert?


von Mein grosses V. (vorbild)


Lesenswert?

Frank M. schrieb:
> Dein "direkter" Weg schlägt sich mit prozessorspezifischen
> Interrupt-Registern rum, die dann auch mal falsch bedient werden können
> - wie Peter gezeigt hat.

Den Fehler habe ich auch schon bemerkt. Klar, daß du dein Konzept 
verteidigst. Aber es ist nicht das allein gültige, sondern meine 
Alternative ist die mit der besten Performance. Und was die ewig 
zitierte Platformunabhängigkeit angeht: Es muß nicht alles zu allem 
kompatibel sein.

von Einer K. (Gast)


Lesenswert?

Mein grosses V. schrieb:
> Und was die ewig
> zitierte Platformunabhängigkeit angeht: Es muß nicht alles zu allem
> kompatibel sein.

Hmm...

https://de.wikipedia.org/wiki/Softwarekrise

von W.S. (Gast)


Lesenswert?

Frank M. schrieb:
> Setze Dir in der ISR nur ein Flag, dass Du etwas zu senden hast,
> also:static volatile uint8_t send_data = 0;
> ...
> ISR
> {
>     send_data = 1;
> }

Das war wohl nix.
Merke: Wir haben hier zwei voneinander unabhängige Prozesse. Wenn du in 
einem nur ein Flag setzen willst, was der andere dann nach Kenntnisnahme 
wieder löschen muß, hast du das klassische Konkurrenzproblem.

Das Event-Prinzip geht anders:
1. es gibt nen Fifo für "events". Was ein event ist, ist 
Geschmackssache, z.B. eine Integerzahl ungleich Null.

2. es gibt eine Funktion zum einstapeln von Events, also sowas wie void 
AddEvent(int event); und die ist saukurz und schnell.

3. Die Interrupt-Routine plaziert über AddEvent eben so einen Event in 
dem Fifo.

4. in der Grundschleife in main gibt es eine Abfrage:
 if (EventAvailable()) DispatchEvent(GetEvent());

Damit wird der Fifo wieder geleert und die entsprechende Aktion 
ausgeführt. Das Ganze ähnelt einem flankengetriggerten Bauteil oder auch 
der Abarbeitung von Tastendrücken.

W.S.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.