Forum: Mikrocontroller und Digitale Elektronik Wie warte ich "richtig" auf Daten?


von Jens P. (Gast)


Lesenswert?

Moin Leute,

man bekommt ja immer wieder mit dass sog. "blockeirende" Programmierung 
verpöhnt ist (wohl auch zurecht).

Nun mal aber die Frage: Wie macht man es denn Richtig?

Beispiel: Ich habe eine Steuerung, die über eine Schnittstelle einen 
Befehl sendet und dann auf die Antwort wartet, um diese auszuwerten.

Dazwischen bzw nebenher passiert eigentlich in der Zeit garnichts.

Aufgrund des Aufbaus und der doch recht unterschiedlichen Nachrichten 
finde ich es hier am konkreten Beispiel mal einfach übersichtlicher mit 
einzelnen UART_READ() in den Subroutinen zu Arbeiten statt einem riesen 
Automaten in der ISR.

Wie gesagt, für die eigentliche Funktion des Programmes hat es 
eigentlich auch keine Bedeutung. Jetzt stellt sich nur die Frage: Was 
mache ich, wenn die Gegenstelle mal garnicht Antwortet? Mit der 
ISR-Variante steht mein Programm ja auch erst mal.

Wie löst man das "richtig"? In einer Schleife das Receive-Flag abfragen 
und einen Timer loslaufen lassen, der dann irgendwann die Schleife 
unterbricht?

Würd mich einfach mal interessieren, was man da so empfiehlt, da ich es 
bisher halt immer ungefähr so gemacht habe.

von Karl H. (kbuchegg)


Lesenswert?

Jens Plappert schrieb:

> Nun mal aber die Frage: Wie macht man es denn Richtig?

Das System sieht eigentlich oft immer gleich aus:
1
int main()
2
{
3
  ....
4
5
  while( 1 ) {
6
7
    if( Ereignis )
8
       ....
9
10
11
    if( anderes Ereignis )
12
       .....
13
14
    etc. etc.
15
  }
16
}

d.h. in der Hauptschleife werden nacheinander alle möglichen 
Ereignisauslöser geprüft und wenn eines davon vorliegt, wird es 
bearbeitet.

> Beispiel: Ich habe eine Steuerung, die über eine Schnittstelle einen
> Befehl sendet und dann auf die Antwort wartet, um diese auszuwerten.

Auch das geht nach dem gleichen Schema.
Denn das eintreffen eines Zeichens ist auch nichts anderes als ein 
Ereignis. Das Zeichen hängt man zb an einen String an, in dem die 
bisherig empfangenen Zeichen gesammelt wurden und prüft vielleicht noch, 
ob damit die Antwort vollständig ist. Wenn ja, dann setzt man zb 
seinerseits ein Ereignis. Denn: niemand sagt, dass ein 'Ereignis' etwas 
ist, dass nur von externer Hardware ausgelöst werden kann. Ein 
'Ereignis', das kann genausogut eine Variable sein, die einen bestimmten 
Wert annimmt und welches später bei der Ereignisprüfung auf genau diesen 
Wert geprüft wird.

Eine Statemachine ist eine andere Variante, die in eine ähnliche 
Richtung geht. Zudem sagt ja auch niemand, dass ein Programm nur aus 
einer einzigen Statemaschine bestehen darf. Da drfen ja ruhig auch 
mehrere derartige Mechanismen gleichzeitig (hintereinander) am Werk 
sein.

Im konkreten Beispiel der UART bietet sich dann auch noch eine UART für 
die ISR an, die das Zeichen sammeln übernimmt.

Aber wie auch immer man es macht. Der kleinste gemeinsame Nenner besteht 
immer darin, dass es nur eine einzige Schleife gibt, die ständig läuft 
und das ist die Hauptschleife in main
1
int main()
2
{
3
   ...
4
5
   while( 1 ) {
6
7
     ...
8
   }
9
}

natürlich kann es vorkommen, dass zwischendurch mal kurz eine Schleife 
etwas mehr Zeit verbrutzelt, wenn sicher gestellt ist, dass es bei 
dieser kurzen Zeit bleibt. Aber sobald das nicht mehr erfüllt ist, 
wandelt man seine Aufgabe um in eine Abfolge von Ereignissen und die 
Hauptschleife fungiert wie ein Schrittschalter, der durch diese 
Ereigniskette durcharbeitet und so auch anderen Programmteilen die 
Gelegenheit gibt, ihr Ding zu machen.

> Würd mich einfach mal interessieren, was man da so empfiehlt, da ich es
> bisher halt immer ungefähr so gemacht habe.

Jedes Programm ist anders. Genau das ist es ja, was Programmierung so 
interessant macht. Wie gut eine bestimmte Technik funktioniert, hängt 
von der Aufgabenstellung und von den Nebenbedingungen ab. Das kann auch 
bedeuten, dass man sich ab und an mal etwas Neues einfallen lassen muss. 
Aber so im Großen und Ganzen funktioniert Ereignisbasiertes Arbeiten 
bzw. Statemaschines in den meisten Fällen sehr gut. Zumindest als 
Ausgangspunkt ist das immer gut geeignet.

von Läubi .. (laeubi) Benutzerseite


Lesenswert?

Wenn es nix anderes zu tun gibt, kann man ruhig warten. Der Funktion 
einen "TimeOut" Parameter zu spendieren kann nützlich sein, aber nur 
wenn du dann was damit anfangen kannst. In diesem Konkretem Beispiel 
wäre vermutlich ein Watchdog eine Möglichkeit, falls ein Reset in 
Ordnung ist, oder alternativ den WD Interrupt nutzen um den User vorher 
nochmal darüber zu informieren, dass ein Timeout auftrat oder ein 
Schleife via Flag zu unterbrechen.

von Stefan (Gast)


Lesenswert?

Blockierende Warteschleifen sind völlig Ok, wenn der Mikrocontroller in 
dieser Zeit nicht anderes zu tun hat.

Ansonsten lies Dich mal in das Thema "Endlicher Automat" bzw "State 
Machine" ein.

Der Forscher Adam Dunkels hat zu diesem Zweck ein Rahmenwerk 
konstruiert, welches den Code leichter lesbar machen soll. Ob er sein 
Ziel erreicht hat, mag jeder selbst beurteilen. Ich habe diese 
"Protothreads" von ihm mal auf deutsch beschrieben: 
http://stefanfrings.de/avr_io/protosockets.html

von oldmax (Gast)


Lesenswert?

Hi
Wenn es um Datenkomunikation geht, Empfang mit ISR, senden aus Programm 
heraus. Hintergrund: Ein Controller, der nur auf einen Empfang wartet 
und nur dann aktiv werden soll ist vielleicht speziell hier der Fall, 
aber nicht immer Usus. Der Aufwand, Empfang in ISR ist dagegen minimal:
ISR:
   Zeichen aus Empfangspuffer holen
   In Ringpuffer schreiben
   Schreibzeiger Ringpuffer erhöhen
   Wenn Grenzwert erreicht, dann Anfang Ringpuffer
Ende ISR
Im Haupt programm gibt es dann einen Lesezeiger auf den Ringpuffer. Sind 
Daten eingetroffen, sind Lese- und Schreibzeiger unterschiedlich.
Bearbeite Empfang:
   Vergleiche Lese- und Schreibzeiger
   Gleich, dann Ende
   Hole Zeichen aus Ringpuffer
   Bearbeite Zeichen
   Erhöhe Lesezeiger
   Wenn Grenzwert erreicht, dann Lesezeiger auf Anfang Ringpuffer
ende

Damit verlierst du kein einziges Zeichen, egal welche Baudrate.
Gruß oldmax

von Simon K. (simon) Benutzerseite


Lesenswert?

Stefan schrieb:
> Blockierende Warteschleifen sind völlig Ok, wenn der Mikrocontroller in
> dieser Zeit nicht anderes zu tun hat.

Sehe ich gar nicht so! Man sollte von Anfang an "richtig" (siehe Karl 
Heinz) programmieren. Genau das ist der Gedankenfehler, den man oft 
macht.
Man bekommt ein Stück bestehende Firmware und jemand sagt "Bau doch noch 
mal ganz schnell XYZ ein, der Rest funktioniert".

Und dann läuft es auf ein Re-design hinaus, weil der ursprüngliche 
Programmierer eben genau so gedacht hat.

von Simon K. (simon) Benutzerseite


Lesenswert?

oldmax schrieb:
> Damit verlierst du kein einziges Zeichen, egal welche Baudrate.
> Gruß oldmax

Naja ;-) Außer, wenn die Daten (über längere Zeit) schneller 
hereinkommen als sie verarbeitet werden.

Ein Ringpuffer ist aber auch nicht die Eierlegende WoMiSau. Ich habe in 
ein anderes Projekt vor kurzem eine XMODEM Implementierung eingebaut und 
mich u.A. deswegen gegen einen Ringpuffer entschieden, weil ich den 
Datenpuffer direkt an die datenverarbeitende Applikation übergeben kann 
ohne (erneut) zu kopieren.
Zumal die Blockgröße fix ist und nach dem Empfang eines Blocks sowieso 
genug Zeit zur Verarbeitung ist (also dort keine weiteren Zeichen mehr 
eintreffen).

von Reinhard Kern (Gast)


Lesenswert?

Hallo,

solche Probleme werden normalerweise mit einer "Statusmaschine" 
bearbeitet, auch wenn das dem Programmierer nicht immer bewusst ist. Es 
gibt dazu auch eine Theorie der Mealy/Moore-Maschinen, da kannst du dich 
auch theoretisch damit beschäftigen.

Kurz gesagt: es gibt einen Status (z.B. idle,waitforirgendwas) und es 
treten Ereignisse ein (Zeichen empfangen), und zu jeder Kombination von 
Status und Ereignis gibt es eine Reaktion und einen neuen Status. Wenn 
man das bewusst so nach einer Tabelle macht (es gibt auch Software 
dafür, oder man packt das ganze in eine Sprungtabelle), dann hat man den 
RIESIGEN Vorteil, dass das System vollständig definiert ist, Aufhänger 
durch unerwartete Ereignisse gibt es nicht.

Die Hauptschleife in main wartet auf Ereignisse und ruft beim Eintreffen 
die passende Maschine auf. Die jeweilige Reaktion darf die Hauptschleife 
nicht lange ausser Betrieb setzen, das ist der Grund, warum lokale 
Warteschleifen so böse sind. Es können ganz unterschiedliche Maschinen 
bearbeitet werden und auch verschachtelte: eine Maschine empfängt 
einzelne Zeichen vom UART (wird also bei jedem Zeichen aufgerufen) und 
packt sie in einen Puffer, bis eine Message komplett ist, diese Message 
ist dann ein Ereignis für die übergeordnete Maschine, die den Austausch 
mit einem Client darstellt. So ist das auch gut wartbar, man kann die 
Maschine für den zeichenweisen Empfang austauschen ohne die 
übergeordnete Protokollebene zu ändern.

Soweit hat das ganze noch keinen Zeitbezug, daher ist es zweckmässig, 
für die Maschine ein Timerereignis zu definieren, damit zählt die 
Maschine ihren eigenen Zeitzähler hoch und generiert ev. ein 
Timeout-Ereignis - das Mindeste wäre dann alles rücksetzen auf Null.

Wie oben schon erwähnt - macht man das mit einer Status-Ereignistabelle 
und setzt da gleich überall eine Dummy-Reaktion ein, ist die Logik von 
Anfang an vollständig definiert und man kann sich den einzelnen 
Reaktionen widmen, die den Ablauf festlegen.

Die Statusmaschinen gab es schon lange vor der Objekt-Orientierung, aber 
für die sind sie hervorragend geeignet, eine Maschine = ein Objekt mit 
Status, Reaktionstabelle und den Reaktions-prozeduren.

Gruss Reinhard

von oldmax (Gast)


Lesenswert?

Hi
>Naja ;-) Außer, wenn die Daten (über längere Zeit) schneller
>hereinkommen als sie verarbeitet werden.

In einem solchen Fall verlierst du auf jeden Fall die Daten....
Der Ringpuffer ist natürlich nicht die EiWoSau, aber damit erschlägt man 
mindestens 50-60%. Und wenn er groß genug ist, hält er länger durch, als 
ein statischer Puffer.
Aber egal, ich hab nur manchmal das Gefühl, das Panik ausbricht, sobald 
das Wort "Interrupt" erwähnt wird. Vielleicht liegt hier auch der Grund 
für diese Frage. Dabei ist es überhaupt kein Hexenwerk. Wenn der 
Postbote klingelt, dann unterbrechen wir doch auch die aktuelle 
Tätigkeit, um ihm seine Post abzunehmen. Kein Mensch erwartet, das diese 
auch sofort gelesen wird. Die Vorgehensweise ist vielfältig beschrieben. 
Einfach mal ausprobieren und feststellen, "das geht ja gut ".  Wer es 
durchschaut hat, und das ist gar nicht sooo schwierig, wird mit 
Interrupts gern arbeiten, zumindest beim Datenempfang.
Gruß oldmax

von Jens P. (Gast)


Lesenswert?

Nicht falsch verstehen: Ich habe das schon oft genug gemacht, meistens 
auch mit Interrupts, mir gehts eigentlich nur um die Prinzipfrage an 
sich.

von Udo S. (urschmitt)


Lesenswert?

Jens Plappert schrieb:
> mir gehts eigentlich nur um die Prinzipfrage an
> sich.

Es gibt keine Prinzipfrage an sich.
Es gibt unterschiedliche Probleme und unterschiedliche Lösungen. Es gibt 
keine einheitlich 'beste' Lösung aka Eierlegende Wollmilchsau am besten 
aber vegetarisch.

Was 'good practices' betrifft stimme ich dem zu was Karl Heinz, Oldmax 
und Simon gesagt haben.

von Carsten R. (kaffeetante)


Lesenswert?

Es gibt dafür sogar einen mathematischen Beweis.
Ich habe darüber einmal in einem Seminar referiert.
Es gibt keine allgemeinoptimalen Algorithmen!

"Ob ISR oder nicht" hat nicht so viel mit dem blockierenden 
Programmieren zu tun wie man zunächst meint. Was das einfach oder nicht 
betrifft:

Die ISR läßt sich auch in der UART_READ() verbergen. Ich kann also 
genauso einfach und/oder blockierend mit ISR programmieren. Es sieht nur 
intern anders aus.

Man muß aber im Hinterkopf behalten, daß die Codeausführung jederzeit 
unterbrochen werden kann, sobald man irgendwo Interrupts zuläßt. Dabei 
ist es irrelevant ob der aktuelle Code-Abschnitt selbst ISR verwendet. 
Umgekehrt kann ich damit fremden Code der für eine interruptfreihe 
Umgebung entwickelt wurde, auch wenn ich ihn nicht anrühre, durch 
Hinzufügen von Interrupts in meinem Code verhackstücken. Meine ISR 
unterbrechen dann den ansonsten fehlerfreien fremden Code bei der 
Ausführung und erzegen so sporadische kaum auffindbare Fehler zur 
Laufzeit, natürlich im Einsatz und nicht im der Entwicklungsabteilung. 
Sollte das ein Problem darstellen, so gibt es auch dafür Mittel und 
Wege. Aber das wäre ein anderes Thema.

Blockierend oder nicht ist eine Frage des Designs/der Planung.
ISR sind "nur" ein mächtiges Werkzeug, das man richtig oder falsch 
benutzen kann.

Das Thema ist zu komplex um es in einem Beitrag zu erklären. Selbst wenn 
man es aus einen Vorlesungsscript herauskopiert, so ist damit nicht viel 
gewonnen. Um es zu verstehen und sinnvoll einzusetzen ist der Stoff der 
gesamten Vorlesung nötig. Konkrete Fragen daraufhin sind in Beiträgen zu 
lösbar.

Erste Stichpunkte wäre in einem Script zu Betriebssystemen:
asynchron, atomar, Mutex, Monitore, Semaphore ...

Sicher kann man auch ISR ohne das verwenden. Lustig wird dann aber die 
Fehlersuche wenn einmal eine ISR genau da reinhaut, wo etwas eigentlich 
keine Unterbrechug verträgt. Das sind dann schwer reproduzierbare Fehler 
die mitunter extrem schwer zu finden sind.

Gruß

Carsten

von Peter D. (peda)


Lesenswert?

Der Anfänger schreibt gerne:
1
void task1()
2
{
3
  while( nix_zu_tun )
4
    ;                  // drehe Däumchen
5
  tue_was();
6
}
Damit ist klar, das nichts anderes mehr zum Zuge kommt.

Der Trick ist nun, immer abweisend zu warten:
1
void task1()
2
{
3
  if( nix_zu_tun )
4
    return;            // gibt die Wartezeit zurück zur Main-Loop
5
  tue_was();
6
}

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.