Forum: Mikrocontroller und Digitale Elektronik Statemachine: Toller Artikel und.


von Robert (profrob)


Lesenswert?

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).

von Udo S. (urschmitt)


Lesenswert?


von Robert (profrob)


Lesenswert?

Aber nur, wenn man die Hintergründe nicht durchschaut und Grundlagen 
ignoriert.

von Chris D. (myfairtux) (Moderator) Benutzerseite


Lesenswert?

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?

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

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
von Robert (profrob)


Lesenswert?

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.

von Rainer W. (rawi)


Lesenswert?

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
von Robert (profrob)


Lesenswert?

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.

von Robert (profrob)


Lesenswert?

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.

von Udo S. (urschmitt)


Lesenswert?

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.

von Falk B. (falk)


Lesenswert?

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.

von Nick (b620ys)


Lesenswert?

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.

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

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
von Hannes J. (pnuebergang)


Lesenswert?

> 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

von Georg M. (g_m)


Lesenswert?

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

von Dergute W. (derguteweka)


Lesenswert?

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
von Nick (b620ys)


Lesenswert?

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.

von Bruno V. (bruno_v)


Lesenswert?

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.

von Chris D. (myfairtux) (Moderator) Benutzerseite


Lesenswert?

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
Noch kein Account? Hier anmelden.