https://www.mikrocontroller.net/articles/Statemachine Ich arbeite schon seit ewigen Zeiten mit solchen Zustandsmaschinen. Es begann mit Apple II und C unter CPM (Softcard), dann PC mit DOS und heute mit diversen Mikrocontrollern in C++ (Arduino) und auch auf dem PC in der Idle-Schleife von Grafikprogrammen (Linux-GTK). Für meine Anwendungen musste ich dabei ein paar spezielle Randbedingungen berücksichtigen, damit meine Zustandsmaschinen (C++) auf Mikrocontrollern und auf dem PC einsetzbar sind. Vielleicht hilft es dem einen oder anderen, wenn er sich auch daran versuchen möchte: Einsatz von Timern Meine Zustandsmaschinen sind meistens Klassen (hin und wieder auch einfache Folgen von Funktionen). Die Klassen haben eine Funktion run(), die ich zyklisch aufrufe. Wobei ich feststellen musste, dass es beim Einsatz von Timern unterschiedliche Probleme gab. Timer sind oft direkte Interrupt-Handler, so dass die Zustandsmaschinen dann auch im Interrupt laufen. Hier müssen Prioritäten beachtet werden, damit die üblichen Schnittstellen mit ihren Interrupt-Routinen sicher funktionieren (ein Schlüsselerlebnis vor längerer Zeit: Bei einem Arduino-DUE mit Ethernet-Shield konnte das Netzwerk nicht aus der Timer-Routine bedient werden). Sicher kennen die meisten von euch das Problem mit Timern in einer Grafik-Umgebung. Direkte Zugriffe sind nicht möglich (Umweg: Run-on-UI-Thread) und geeignete Dispatcher-Timer wackeln wie die Lämmerschwänze. Software-Timer Es ist relativ einfach, sich einen Software-Timer zu basteln, indem vorhandene Zähler ausgewertet werden. Bei Arduino liefert die Funktion micros() die Zeit ab Start in Mikrosekunden, bei Linux reicht eine Zeitabfrage mit entsprechendem Parameter und bei Windows gibt es die Funktion QueryPerformanceCounter. Bei den PCs bekommt man die Zeit allerdings in Nanosekunden. Um gleiches Zeitverhalten beim PC und bei den Mikrocontrollern zu bekommen, implementiert man zunächst auch eine Funktion micros() auf dem PC, die auf Mikrosekunden umrechnet. Zeitdifferenzen berechnet man dann einfach über unsigned-Variablen, die auch bei Zählerüberläufen die korrekte Zeitdistanz liefern. Einbindung in loop() Die Funktion loop() bei Arduino ruft geradezu nach dem Einsatz von Zustandsmaschinen. Sie wird meist alle paar Mikrosekunden aufgerufen (unter 10us beim ESP32, bis zu 100us bei einigen anderen Modellen). Beim Einsatz mancher RTOS wird loop() allerdings eher stiefmütterlich bedient. Eine Frechheit, wenn dann noch die Funktion micros() aus einem Millisekundenzähler liest. Abhilfe bringt meist eine while(1)-Schleife (in loop(), echt doof so etwas) und ein direkter Zugriff auf die Zähler des Mikrocontrollers bzw. das Aufsetzen eines eigenen Mikrosekundenzählers. Beim PC ist die Basis sowieso eine while(run == true)-Schleife in main() oder die Idle-Schleife in der Grafik (da habe ich allerdings nur Erfahrung mit Linux-GTK). Multi-Tasking Wenn man mehrere Software-Timer mit angehängten Zustandsmaschinen implementiert, dann führt das zu einem kooperativen Multi-Task-System. Die Echtzeitfähigkeit bestimmt man selbst durch Vorgaben bei den Aufrufzyklen und für die maximale Dauer eines einzelnen Zustandes. Bei richtiger Dimensionierung ist ein solches Multi-Tasking jedem RTOS haushoch überlegen. Keine Verluste durch den Scheduler, keine Semaphorensteuerung erforderlich, etc. Ich setze bei einigen Arduinos mehrere Zustandsmaschinen gleichzeitig ein, mit Zykluszeiten von 50 Mikrosekunden bis zu mehreren Millisekunden. Es macht Spaß In dem oben genannten Artikel ist deutlich geworden, wie übersichtlich Zustandsmaschinen im Vergleich zu den meist üblichen Ablaufprozeduren sind. Da macht sogar die Dokumentation Freude. Und die Programme sind auch grundsätzlich einfacher und übersichtlicher, zeitliche Abläufe sind trivial und über Zählvariablen hoch genau steuerbar. Ich bin in der langen Zeit meiner Programmiertätigkeiten (C/C++, C#, PHP, JavaScript, ...) noch nicht in Versuchung gekommen, meine Zustandsmaschinen durch ein (noch so tolles) RTOS zu ersetzen. Bei sehr schnellen und umfangreichen Zustandsmaschinen ist es allerdings wichtig, die Zustände nicht über case xyz anzufahren, sondern über umgehängte Verzweigungsvektoren (in C und C++ ziemlich einfach, Stichwort CallBack).
Aber nur, wenn man die Hintergründe nicht durchschaut und Grundlagen ignoriert.
Eine schöne Liste :-) Ich kann das nur unterschreiben: FSMs kann man sehr sauber und übersichtlich aufbauen. Unserer Erfahrung nach gehören sie zu den am wenigsten fehleranfälligen Programmierprinzipien. Dazu kommt, dass man durch entsprechende Graphen-Programme die Programmierung in ein Grafikprogramm verlagern kann und zusätzlich direkt eine sehr gute Beschreibung/Kommentierung des Programms erhält. Man "sieht" quasi sofort, was das Programm macht. Tatsächlich waren hier die einzigen größeren Programmentwicklungen, die auf Anhieb(!) fehlerfrei liefen, FSM-Programme (bspw. eine Revolversteuerung einer CNC-Maschine). Nutzt Du auch grafische Editoren?
Robert schrieb: > Timer sind oft direkte Interrupt-Handler, so dass die Zustandsmaschinen > dann auch im Interrupt laufen. Hier müssen Prioritäten beachtet werden, Deshalb setzt man im Timer auch nur ein Flag, das dann in der zyklisch aufgerufenen FSM abgefragt wird. Oder man zählt einen Zähler hoch (dein Softwaretimer) und vergleicht den in der zyklisch aufgerufenen FSM mit einem anderen Zählerwert oder besser: mit einem Delta zu einem vorher gespeicherten Zählerwert. > Die Funktion loop() bei Arduino ruft geradezu nach dem Einsatz von > Zustandsmaschinen. Sie wird meist alle paar Mikrosekunden aufgerufen Diese Funktion wird sofort dann wieder aufgerufen, wenn sie fertig ist: - https://docs.arduino.cc/language-reference/de/struktur/sketch/loop/ Wenn da ein rechenzeitverschwendendes delay_ms(5000) drin ist, dann dauert diese loop() mindestens 5 Sekunden. Sie kann also gar nicht nach 10µs wieder "aufgerufen" werden. BTW: auch ich liebe FSM und jede SPS läuft mit solchen Automaten. Nur weiß das der Schrauber, der diese SPS mit einem KOP programmiert, gar nicht. Er tut es einfach.
:
Bearbeitet durch Moderator
Chris D. schrieb: > Nutzt Du auch grafische Editoren? Nein, da habe ich mich nicht weiter mit befasst. Ich hatte mich mal intensiv mit UML-Diagrammen und entsprechend abgeleiteten Klassen beschäftigt, aber das war dann doch nicht so produktiv. Da war ich beim direkten Programmieren doch deutlich schneller.
Lothar M. schrieb: > Diese Funktion wird sofort dann wieder aufgerufen, wenn sie fertig > ist: > - https://docs.arduino.cc/language-reference/de/struktur/sketch/loop/ ... es sei denn, serialEventRun ist gesetzt. Dann wird zwischendurch auch noch serialEventRun() aufgerufen, bevor loop() wieder dran ist. So steht es bei Arduino in main():
1 | for (;;) { |
2 | loop(); |
3 | if (serialEventRun) serialEventRun(); |
4 | }
|
:
Bearbeitet durch User
Lothar M. schrieb: > Diese Funktion wird sofort dann wieder aufgerufen, wenn sie fertig > ist: Nein, das ist nur eine Beschreibung des prinzipiellen Verhaltens. Die Details kann man leicht erkennen, wenn man sich mal in der Software zum Aufruf von loop() durchhangelt. Zum Beispiel war es bei den ersten Versionen des Arduino DUE so, dass der UART-Transmit noch nicht über Interrupt abgewickelt wurde, sondern zu Fuß vor dem Aufruf von loop() bedient wurde. Weil ich mit meinen schnellen Zustandsmaschinen sehr vom Timing der loop() abhängig bin, habe ich ein Testprogramm entwickelt, mit dem ich die Zeiten innerhalb und außerhalb von loop() messe. So wähle ich dann die besser geeigneten Boards aus. Manche brauchen auch noch etwas Rechenzeit außerhalb von loop(). Damit meine ich nicht die im Hintergrund laufenden Schnittstellen. > Wenn da ein rechenzeitverschwendendes delay_ms(5000) drin ist Das ist genau der Punkt. Solche Aufrufe sind in einem kooperativen Multi-Tasking nicht erlaubt und bei getakteten Zustandsmaschinen auch unnötig.
Rainer W. schrieb: > So steht es bei Arduino in main() Danke für den Hinweis. Oft setze ich auch nicht nur Original-Arduinos ein, sondern mehr oder weniger kompatible Boards mit Arduino-Toolchain. Da kann man noch ganz andere Dinge erleben.
Chris D. schrieb: > Ich kann das nur unterschreiben: FSMs kann man sehr sauber und > übersichtlich aufbauen. Unserer Erfahrung nach gehören sie zu den am > wenigsten fehleranfälligen Programmierprinzipien. Zweifellos. Aber eine FSM ist nicht die Lösung für jedes Problem. Und so habe ich vor allem den letzten Absatz des TOs verstanden.
Robert schrieb: >> Wenn da ein rechenzeitverschwendendes delay_ms(5000) drin ist > > Das ist genau der Punkt. Solche Aufrufe sind in einem kooperativen > Multi-Tasking nicht erlaubt und bei getakteten Zustandsmaschinen auch > unnötig. Das ist nichts Neues unter der Sonne und wurde schon ausführlich im Artikel Multitasking beschrieben. Es ist erstaunlich, wie alten Hasen wie du sich noch über derartige Grundlagen noch freuen können.
Ich bin auch ein absoluter fan von FSMs! Ich hab das inzwischen weiter getrieben. Meine FSMs kommunizieren ausschließlich über Messages. D.h. es gibt keine calls in die FSMs. Will eine FSM einer anderen etwas mitteilen (ADC-Wert liegt vor, UART-Puffer hat Zeichen, ...) dann schickt sie eine Message an einen Messagehandler mit der Adresse des Empfängers (ist ein enum der aber aus einem Namen wie "ADC", "TCP", "SDCARD" ...) entsteht. Diese Message kommt in eine inbox (queue), die die empfangende FSM, wenn sie in der Taskliste wieder dran ist, abfrägt. Ist eine Message da, wird die abgearbeitet, liegt nix vor ist die FSM auch schon wieder fertig. Es kann also sein, dass eine FSM mehrere Messages anstehen hat, aber nicht genug Zeit hatte. Dadurch werden die FSMs entkoppelt und blockieren sich nicht gegenseitig. Weitergesponnen ist ein Taskmanager dazugekommen. Der verwaltet die Liste der FSMs/Tasks. Ein Task kann sich dort selbst als schlafend eintragen und wird erst dann wieder aufgeweckt, wenn eine Message für ihn da ist. Die Messages haben Prioritäten. Das kann man als Messagestack innerhalb einer FSM "missbrauchen". Wenn sich eine Aufgabe in mehrere Unteraufgaben aufteilen lässt, dann schiebt die FSM sich selbst messages mit einer höheren Priorität in die Messagequeue. Damit das funktioniert, gibt es eine Standard-Priorität an die man sich strikt halten muss. Nochmal weitergesponnen kann der Taskmanager Tasks für x µs schlafen legen (warten auf UART-Timeout) oder erst zu einem festgelegten Zeitpunkt (chron-mässig) aufgerufen werden und der Task wird erst nach Ablauf der Zeit aufgerufen. So lässt sich das Timing von z.B. LCDs einhalten. Hängt man zwischen Quelle (z.B. Messwerte) und Ziel (z.B. TCP) einen queue-Task, dann kann der queue-Task die Länge der Messageliste des Ziels (TCP) beobachten und, wenns irgendwie hängt, auf eine SD-Card zwischenspeichern. Die Messages müssen aber allokiert werden (das wird einigen Puristen nicht gefallen), damit sie effektiv verwaltet werden können. Das Schöne an den Messages ist, dass die Verknüpfungen der Tasks auch aus einer Konfiguration kommen können und sogar zur Laufzeit verändert werden können. Also nix projektspezifisch (im eingeschränkten Rahmen) neucompilieren. Dadurch kann natürlich toter code im µC/PC rumliegen, DLL geht halt nicht auf dem µC. Die FSMs sind so aufgebaut (falls nötig), dass es auch mehrere Instanzen davon geben kann. Also z.B. "ADC.1", "ADC.2". Der code wird dadurch nicht dupliziert, lediglich die struct die eine Instanz der FSM benötigt.
Robert schrieb: > Weil ich mit meinen schnellen Zustandsmaschinen sehr vom Timing der > loop() abhängig bin Das hört sich irgendwie eigenartig an. Natürlich läuft eine FSM im theoretischen Idealfall in der Zeit 0 durch. In der echten Welt dauert die Bearbeitung aber länger und deshalb kommt der Begriff "Zykluszeit" ins Spiel, den auch der erwähnte Schrauber von seiner SPS kennt. Und er weiß: wenn irgendwas in der seinem Programm (seiner "Schrittkette", seiner "Merkerkette", seiner FSM) zu viel Rechenzeit beötigt, dann muss es so aufgeteilt werden, dass diese Rechenarbeit auf mehrere Zyklusdurchläufe aufgeteilt wird. Also muss diese Rechnarbeit wiederum mit einer FSM abgehandelt werden (auch ein Zähler der dann z.B. die Durchläufe zählt, ist eine FSM). Udo S. schrieb: > Aber eine FSM ist nicht die Lösung für jedes Problem. Aber viele Probleme wären gar nicht aufgetreten, wenn man das mit den FSM verstanden hätte. Nick schrieb: > Meine FSMs kommunizieren ausschließlich über Messages. Das ist dann lediglich die logische Weiterentwicklung der Kommunikation über einzelne Bits (aka "Flags").
:
Bearbeitet durch Moderator
> grafische Editoren? Alles Spaß und Spiel, bis jemand ein Auge verliert. Nämlich wenn man mal über so was stolpert CCITT alias ITU-T SDL/GR: https://de.wikipedia.org/wiki/Specification_and_Description_Language
Falk B. schrieb: > Es ist erstaunlich, wie alten Hasen wie du sich noch über derartige > Grundlagen noch freuen können. Geteilte Freude ist doppelte Freude. https://i.pinimg.com/736x/a8/98/70/a898702aa79c59b3ad33d7b2e9578578.jpg
Moin, Man kann auch einfach sagen: So eine FSM ist mindestens so wichtig wie Zinkoxid. https://www.youtube.com/watch?v=uRdZg-ph_U4 scnr, WK
:
Bearbeitet durch User
Lothar M. schrieb: > Das ist dann lediglich die logische Weiterentwicklung der Kommunikation > über einzelne Bits (aka "Flags"). Mit dem Vorteil, dass man sich lediglich in der FSM die den INT empfängt Sorgen um atomare Zugriffe machen muss. Der weitere Transport ist dann INT-sicher und auch für beliebig große Strukturen möglich. Zusätzlich geht keine Zustandsänderung verloren, falls die beobachtende FSM zu langsam war.
Robert schrieb: > Multi-Tasking > > Wenn man mehrere Software-Timer mit angehängten Zustandsmaschinen > implementiert, dann führt das zu einem kooperativen Multi-Task-System. > Die Echtzeitfähigkeit bestimmt man selbst durch Vorgaben bei den > Aufrufzyklen und für die maximale Dauer eines einzelnen Zustandes. Bei > richtiger Dimensionierung ist ein solches Multi-Tasking jedem RTOS > haushoch überlegen. Keine Verluste durch den Scheduler, keine > Semaphorensteuerung erforderlich, etc. Ich setze bei einigen Arduinos > mehrere Zustandsmaschinen gleichzeitig ein, mit Zykluszeiten von 50 > Mikrosekunden bis zu mehreren Millisekunden. > > Es macht Spaß Deine Ausführungen sind auf der einen Seite so allgemein, teils trivial, auf der anderen Seite so schräg, dass es kaum zu unterscheiden ist, ob Du etwas geniales entdeckt oder nur wenig Erfahrung mit RTOS hast. Die 3 RTOS-Ansätze "Kooperierend", "Unterbrechend" und z.B. bei Autosar "basic Task" (ohne Warten oder Kooperation) sind z.B. so unterschiedlich, dass ein Vergleich nur Sinn macht, wenn Du ihn präzisierst.
Udo S. schrieb: > Chris D. schrieb: >> Ich kann das nur unterschreiben: FSMs kann man sehr sauber und >> übersichtlich aufbauen. Unserer Erfahrung nach gehören sie zu den am >> wenigsten fehleranfälligen Programmierprinzipien. > > Zweifellos. > Aber eine FSM ist nicht die Lösung für jedes Problem. Natürlich nicht. Niemand möchte bspw. mit 1000 Knoten hantieren :-) Aber WENN sie eine brauchbare Lösung ist, dann ist die Entwicklung immer sehr sauber und "rock stable" möglich. Im Studium haben wir mal einen einfachen grafischen Editor/Compiler für FSMs als simple SPS geschrieben. Diese bestanden im Wesentlichen aus einem Eprom mit einem winzigen RAM (für den Status), Timer und Goodies wie 8-Bit-Schieberegister usw. Damit konnte man ganz ohne Controller schon richtig viel machen.
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.