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.
Jens Plappert schrieb:> Nun mal aber die Frage: Wie macht man es denn Richtig?
Das System sieht eigentlich oft immer gleich aus:
1
intmain()
2
{
3
....
4
5
while(1){
6
7
if(Ereignis)
8
....
9
10
11
if(anderesEreignis)
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
intmain()
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.
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.
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
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
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.
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).
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
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
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.
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