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
Ich kapiere es nicht: warum und wie sollten sich gesendete und
empfangene Daten vermischen? Echoed der Empfänger etwa die vom Arduino
gesendeten Daten?
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.
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".
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.
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!
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:
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
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.
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.
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.
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...
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:
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.
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.
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.
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.