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.

von Johannes F. (jofe)


Lesenswert?

Bin ich denn der einzige, der sich über den Betreff wundert?
"Toller Artikel und." Was "und."?

Nix für ungut, aber der Eröffnungsbeitrag riecht für meinen Geschmack 
doch sehr nach LLM-erzeugt. Zumal der Account des TO erst wenige Wochen 
alt ist.

von Falk B. (falk)


Lesenswert?

Johannes F. schrieb:
> Nix für ungut, aber der Eröffnungsbeitrag riecht für meinen Geschmack
> doch sehr nach LLM-erzeugt.

Die Vermutung hatte ich auch schon.

von Markus M. (Firma: EleLa - www.elela.de) (mmvisual)


Lesenswert?

Wegen der State Machine mit
1
switch (state)
2
case ...

Ich verwende diese Art ebenfalls sehr gerne und häufig.
Jedoch habe ich meist noch diese Erweiterung:
1
uint32_t stateT = 0; // Schrittzeit
2
uint32_t stateOld = 0; // Zuvoriger Schritt
3
uint32_t stateArr[10]; // Array mit Historie der letzten Schritte
4
uint32_t stateArrPos = 0; // Zeiger in Array
5
6
stateT += ZyklusZeitMs; // Wert der Dauer der Schrittzeit in ms
7
// (ZyklusZeitMs ist die Anzahl ms, die seit dem letzten Aufruf der Funktion vergangen sind.)
8
9
if (Rücksetz Bedingung) {
10
  state = 0; // Sicheres Rücksetzen um Schrittkette neu zu starten
11
}
12
13
switch (state)
14
case 0: // Schritt Grundstellung
15
16
case ...
17
  if (stateT >= 1000) {
18
    // Schritt aktiv für 1000ms, mache was...
19
  }
20
...
21
22
// Erkennung Schrittwechsel
23
// Aufzeichnung Schritt Historie in Array
24
// Rücksetzen Schritt Dauer
25
if (state != stateOld) {
26
  stateOld = state;
27
  stateArrPos = (stateArrPos + 1) % 10; // Erst Zeiger erhöhen
28
  stateArr[stateArrPos] = state; // Dann Wert speichern in Array
29
  stateT = 0; // Lösche Schrittzeit
30
}

Damit kann ich sehr einfach auf einen zeitlichen Ablauf reagieren ohne 
extra Zeitmerker zu bilden/verwalten und mit dem Array weis ich welche 
states zuvor alles durchlaufen wurden, also was die Historie ist um in 
diesen State zu kommen.
Sehr gerne mache ich das in der Programmierung von Maschinensteuerungen 
um im nachhinein bei Störungen sehen zu können was genau alles passiert 
ist. Denn der Kunde sagt ohnehin immer "hab nix gemacht".

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


Lesenswert?

Markus M. schrieb:
> stateT += ZyklusZeitMs;
> // (ZyklusZeitMs ist die Anzahl ms, die seit dem letzten Aufruf der
> Funktion vergangen sind.)
Wo wird dann ZyklusZeitMs überall manipuliert? Oder ist das eine 
Konstante und du rufst diese FSM über einen Timer auf?


Markus M. schrieb:
> stateArrPos = (stateArrPos + 1) % 10; // Erst Zeiger erhöhen
Warum 10? Ich hätte da 8 oder 16 oder sonst eine Zahl mit *2^x* 
genommen, denn das ist das Zahlensystem, in dem sich ein binärer Rechner 
bewegt. Es ist ihm egal, dass du mit 10 Fingern mit dem Zehnersystem 
besser zurecht kommst. Du musst nur 1x denken, er muss den Job ständig 
tun.

Als Tipp: sieh dir einfach mal den Maschinencode an, der bei
1
stateArrPos = (stateArrPos + 1) % 10;
und bei
1
stateArrPos = (stateArrPos + 1) % 16;
erzeugt wird und rechne aus, wieviel Prozent mehr Rechenleistung dafür 
nötig ist.

Es ist mir klar, dass das in diesem speziellen Fall nur wenige 
Maschinenzyklen sind. Aber wenn sowas in jeder zweiten Programmzeile 
"passiert"...

: Bearbeitet durch Moderator
von Robert (profrob)


Lesenswert?

Markus M. schrieb:
> Jedoch habe ich meist noch diese Erweiterung:

Das regt mich an, auch noch ein wenig aus der Schule zu plaudern:
Abgesehen davon, dass ich statt switch/case über einen Funktionszeiger 
verzweige, lege ich auch mehrere Verzweigungsmöglichkeiten an.
Also z.B. "Nächster Zustand" und "Übernächster Zustand" und werte diese 
in den Zuständen aus. Darüber kann man dann in einen bestimmten Zustand 
verzweigen und den dazu anregen, anschließend in einen anderen aktuell 
bestimmten Zustand zu verzweigen. Also Zustände anders zu nutzen, als in 
der ursprünglichen Programmierung vorgesehen war.
Werden mehrere solcher Verzweigungsoptionen geschickt verwaltet, so 
erhält man Abläufe, die sich zur Laufzeit beliebig manipulieren lassen. 
Auch nachladbare Tabellen für spezielle Abläufe sind möglich.

Als weitere Möglichkeit habe auch ich (ähnlich wie Du) einen 
manipulierbaren Zähler (Priorität) vor dem Aufruf eines Zustandes. Den 
nutze ich allerdings meistens, um beim Verlassen eines Zustandes die 
Zeit bis zum Aufruf des Folgezustandes zu setzen. Dadurch kann man 
Zeitabläufe optimieren und zur Laufzeit an sich ändernde Bedingungen 
anpassen.

Für Debug-Zwecke habe ich einen extra Zustand, der bei jedem 
Zustandswechsel aufgerufen wird. Allerdings brauche ich den inzwischen 
nur noch sehr selten.

Der Nachteil beim Einsatz von Funktionszeigern ist allerdings, dass man 
nun keine erkennbaren Bezeichner (Nummern, case ...) mehr hat. Ich muss 
also zusätzlich einen Bezeichner für die Zustände einführen, wenn ich 
Abfolgen sichtbar machen will.

Es freut mich, dass hier auch viele Zustandsmaschinenanwender sind. Und 
vielleicht könnt ihr mir zustimmen, dass in diesem Verfahren enorme 
Möglichkeiten stecken.

von Markus M. (Firma: EleLa - www.elela.de) (mmvisual)


Lesenswert?

Die ZyklusZeitMs berechne ich im OB1 gleich am Anfang mit Hilfe des 
Moduls TIME_TCK() in einer Siemens 1500er Steuerung.
Wenn man andere Systeme hat, oder direkt einen µC, so muss man sich das 
selbst einfallen lassen wie man das ausrechnet. Wenn man RTOS oder 
andere OS verwendet, ist es etwas aufwändiger, da man das für jeden Task 
separat errechnen muss.
Das gute daran ist, man muss es sich nur einmalig einfallen lassen und 
kann diese "ZyklusZeitMs" sehr einfach in allen Programmteilen des Tasks 
weiter verwenden.


Ob 10, 8, 16, oder 1000 ist eigentlich völlig egal, man hat in einer 
Steuerung meist eine überschaubare Anzahl von diesen Aufzeichnungen, und 
der Modulo wird auch nur bei Schrittwechsel berechnet, also auch nicht 
in jedem Durchlauf des Loops. Der Modulo Operator macht das in jedem 
Fall korrekt und sorgt in zusammenhand mit dem "unsigned" Datentyp für 
einen korrekten Bereich innerhalb vom Array.
In meinem Fall kommt es darauf an wie groß mein Bildschirm ist, dass ich 
dieses Array ordentlich in der HMI darstellen kann, für Diagnose über 
Fernwartung ist das ungemein wichtig (Kunde schickt mir z.B. ein Foto 
davon).
Bei einfachen Schrittketten mache ich das Array kleiner, bei komplexen 
Schrittketten hatte ich auch schon mal 50 Arraypunkte. Man kann das 
Array auch 2-Dimensional ausführen (oder Array mit Record), damit kann 
man noch eine weitere Zahl für einen speziellen Zustand speichern.
Ich hatte bei dem Fall mit 50 Array Punkte dann ein zweites Array mit 
nur 10, für die Darstellung in der HMI und die 50 konnte ich dann nur 
online direkt in der SPS Steuerung anschauen.

Ich wollte hier mal die nächste Ausbaustufe der Statemachine zeigen, wie 
man sich das Leben deutlich vereinfachen kann um Fehler zu finden. Ich 
nutze diese Techniken schon über 20 Jahre und kann damit sehr schnell 
Fehler finden und beheben, denn sehr oft finden Fehler nicht am 
aktuellen Schritt statt, sondern schon viel früher wurde z.B. in den 
falschen Schritt gesprungen, was dann einige Schritte später zu dem 
Fehler führt.

von Chris D. (myfairtux) (Moderator) Benutzerseite


Lesenswert?

Falk B. schrieb:
> Johannes F. schrieb:
>> Nix für ungut, aber der Eröffnungsbeitrag riecht für meinen Geschmack
>> doch sehr nach LLM-erzeugt.
>
> Die Vermutung hatte ich auch schon.

Kann ich aber guten Gewissens entkräften. Alles gut.

von Cyblord -. (cyblord)


Lesenswert?

Rainer W. schrieb:
> for (;;) {
>     loop();
>     if (serialEventRun) serialEventRun();
>   }

Wichtig ist dass man immer alles aus der Arduino Welt heraus betrachtet. 
Was gibt es bitte sonst?

von Falk B. (falk)


Lesenswert?

Chris D. schrieb:
>>> Nix für ungut, aber der Eröffnungsbeitrag riecht für meinen Geschmack
>>> doch sehr nach LLM-erzeugt.
>>
>> Die Vermutung hatte ich auch schon.
>
> Kann ich aber guten Gewissens entkräften. Alles gut.

Wie? Bist du Hellseher oder LLM Plagiatsjäger?

von Chris D. (myfairtux) (Moderator) Benutzerseite


Lesenswert?

Falk B. schrieb:
> Chris D. schrieb:
>>>> Nix für ungut, aber der Eröffnungsbeitrag riecht für meinen Geschmack
>>>> doch sehr nach LLM-erzeugt.
>>>
>>> Die Vermutung hatte ich auch schon.
>>
>> Kann ich aber guten Gewissens entkräften. Alles gut.
>
> Wie? Bist du Hellseher oder LLM Plagiatsjäger?

Ich bin Moderator und sehe damit einiges mehr als Ihr Sterblichen ;-)

von Falk B. (falk)


Lesenswert?

Robert schrieb:
> Der Nachteil beim Einsatz von Funktionszeigern ist allerdings, dass man
> nun keine erkennbaren Bezeichner (Nummern, case ...) mehr hat.

Stimmt nicht. Einfach eine Tabelle über ein enum adressieren, schon hat 
man menschenlesbare Zustandsnamen.

> Ich muss
> also zusätzlich einen Bezeichner für die Zustände einführen, wenn ich
> Abfolgen sichtbar machen will.

Ja und? Wo liegt das Problem? Die Methode ist praktisch nicht langsamer, 
wohl aber deutlich besser lesbar. Und das ist eines der Hauptkriterien 
guter Software.

von Andreas S. (Firma: Schweigstill IT) (schweigstill) Benutzerseite


Lesenswert?

Hannes J. schrieb:
> Alles Spaß und Spiel, bis jemand ein Auge verliert. Nämlich wenn man mal
> über so was stolpert CCITT alias ITU-T SDL/GR:

Oh, damit hast Du die Büchse der Pandora geöffnet! Ursprünglich wurde 
SDL nur für sie Spezifikation von Kommunikationsprotokollen erschaffen, 
aber nicht direkt für die Codegenerierung. Diese wurde erst später von 
den Toolherstellern (z.B. Telelogic) und einigen Anwendern (z.B. Hagenuk 
Telecom) dazugebastelt. Und gerade dort gibt es auch Unmengen 
unterschiedlicher Codegeneratoren und Integrationsmodelle. Telelogic 
hatte z.B. den C Basic für Simulation, C Advanced für "große" 
Zielsysteme und C Micro für kleine Systeme wie z.B. Microcontroller. Und 
je nach Codegenerator konnte man dann den Code ohne Betriebssystem (bare 
integration), alle SDL-Prozesse in einem Betriebsystemprozess (minimum 
integration) oder pro SDL-Prozess ein Betriebssystemprozess (maximum 
integration) laufen lassen. Damals(tm) verbrachte ich viel Zeit damit, 
mich mit diesen Integrationen und der entsprechenden Fehlersuche zu 
beschäftigen.

Und wenn man womöglich "eigenen C-Code" aufrufen wollte, konnte man das 
nicht einfach in einem Anweisungskästchen, sondern musste das über sog. 
ADT (abstract data types) realisieren, für die man externe Methoden 
definieren konnte. Daran merkte man sehr deutlich, dass SDL eigentlich 
nie für die Codegenerierung gedacht war.

Und unfassbar teuer, fehlerhaft und ressourcenfressend war der Scheiß 
von Telelogic auch noch.

Neben den Telelogic-Kram gab es auch noch den SDL-Codegenerator der 
Berliner Humboldt-Universität, der sogar Open Source und recht 
durchdacht war. Allerdings war das mehr oder weniger eine "one man show" 
eines fachlich sehr guten Doktoranden (Ralf Schröder). Ich lernte ihn 
mal im Rahmen eines Kundenprojektes kennen, in dem er genau diesen 
SDL-Codegenerator betreute und natürlich alles sehr genau wusste und 
erklären konnte. Schließlich hatte er das Teil über viele Jahre 
entwickelt. Zu der Zeit war er aber natürlich kein HU-Mitarbeiter mehr, 
und die öffentliche Version verwaiste dann.

Ach, und es gibt noch das dänische Cinderella SDL und Cinderella SITE 
(Codegenerator). (Mit-)Entwickelt wurde das durch fjord-e-design, einer 
Ausgründung der FH Flensburg, die es für deren eigene 
TETRA-Protokollstacks und -tester einsetzte. Und da schließt sich auch 
schon wieder der SDL-Kreis: dieser TETRA-Protokollstack wurde in auch in 
den obigen Projekt eingesetzt, und WIMRE basiert Cinderella SITE 
wiederum auf dem HU-Codegenerator...

Angeblich soll das Produkt sogar noch leben:
https://www.cinderella.dk/

: Bearbeitet durch User
von Alexander S. (alesi)


Lesenswert?

Hallo,

in dem ganz oben zitierten Artikel fehlt noch der Link auf

Finite State Machines in Forth von J.V. Noble
https://galileo.phys.virginia.edu/classes/551.jvn.fall01/fsm.html

von Johannes F. (jofe)


Lesenswert?

Bruno V. schrieb:
> 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.

+1 für die LLM-These.

Chris D. schrieb:
> Ich bin Moderator und sehe damit einiges mehr als Ihr Sterblichen ;-)

Hmm, wie schließt du denn aus IP-Adresse u.a. Server-Logdaten darauf, ob 
ein LLM/generative KI/Chatbot/wie auch immer man es nennen will den Text 
der Postings des TO erzeugt hat? Es kann ja auch ein Mensch sich hinter 
dem Account verbergen, der aber seine Beitragstexte KI-generieren lässt, 
aus welchem Grund auch immer.

von Bruno V. (bruno_v)


Lesenswert?

Robert schrieb:
> Als weitere Möglichkeit habe auch ich (ähnlich wie Du) einen
> manipulierbaren Zähler (Priorität) vor dem Aufruf eines Zustandes. Den
> nutze ich allerdings meistens, um beim Verlassen eines Zustandes die
> Zeit bis zum Aufruf des Folgezustandes zu setzen. Dadurch kann man
> Zeitabläufe optimieren und zur Laufzeit an sich ändernde Bedingungen
> anpassen.
Es scheint, dass Du nicht eine sogenannten SPS-Loop nutzt, also wo x 
parallel State-Machines zyklisch durchgerattert werden. Sondern etwas, 
wo Du einen State durchläufst und dann verschieden lange Pausen machst.


> Für Debug-Zwecke habe ich einen extra Zustand, der bei jedem
> Zustandswechsel aufgerufen wird.
Das deutet ebenso darauf hin. Bei Markus ist das (wie üblich) im Vor- 
oder Abspann.

> Der Nachteil beim Einsatz von Funktionszeigern ist allerdings, dass man
> nun keine erkennbaren Bezeichner (Nummern, case ...) mehr hat. Ich muss
> also zusätzlich einen Bezeichner für die Zustände einführen, wenn ich
> Abfolgen sichtbar machen will.
Funktionszeiger zeigen auf Funktionen. Deren Namen sind die Bezeichner. 
Was meinst Du mit "zusätzlich"?

> Es freut mich, dass hier auch viele Zustandsmaschinenanwender sind. Und
> vielleicht könnt ihr mir zustimmen, dass in diesem Verfahren enorme
> Möglichkeiten stecken.
Im Hinblick auf Deine vagen Timer-Ausführunge hier und im Ausgangspost 
halte ich die Aussage für so allgemein wie
> Es freut mich, dass hier auch viele [strukturiert programmieren]. Und
> vielleicht könnt ihr mir zustimmen, dass in [strukturierter Programmierung]
> enorme Möglichkeiten stecken.

von Robert (profrob)


Lesenswert?

Auf sachliche Diskussionen lasse ich mich gerne ein.
Allerdings habe ich leider meistens zu wenig Zeit, um mich intensiv mit 
Beiträgen in Foren zu befassen. Deshalb muss ich oftmals etwas 
oberflächlich bleiben.
Auf persönliche Anmache (wie die Unterstellung der Nutzung eines LLM 
oder anderweitiger unsachlicher Äußerungen) reagiere ich in der Regel 
nicht.

Meine ersten Zustandsmaschinen (vor Jahrzehnten) liefen tatsächlich im 
Timer-Interrupt. Das war eine sehr schöne Möglichkeit, unter DOS ein 
Multitasking zu gestalten. Die Zeitdistanzen waren damals ganzzahlige 
Vielfache des 55ms Zeittaktes. Damit habe ich früher auch ähnliche 
Abläufe aufgebaut, wie sie bei den SPSen üblich waren (EVA-Prinzip). 
Damals hatten die PCs eine parallele Schnittstelle (Centronics/Drucker), 
über die Peripherieankopplungen erfolgen konnten.

Eine grundsätzliche Änderung habe ich vorgenommen, als ich vermehrt 
Mikrocontroller auf Basis der Arduino-Umgebung programmierte. Ich habe 
mich dabei anstelle der Verwendung von Timern auf die Abfrage von 
micros() in der loop()-Funktion eingelassen. Dabei wurde die minimale 
Taktzeit von mir willkürlich auf 50 Mikrosekunden festgelegt. Wenn mir 
der Aufruf von micros() zu lang erschien (z.B. beim Nano BLE 33), habe 
ich ihn durch einen direkten Zugriff auf die Hardware ersetzt (je nach 
CPU oder OS auf den Systemtakt oder auf bereits laufende Timer). Also in 
loop() werte ich konkrete Zeiten (Zeitdifferenzen) aus und verzweige von 
da über eine Zeigervariable in den aktuellen Zustand.

Jeder Zustand hat einen Eingangszähler (virtuell, physikalisch ist da 
nur einer in der run()-Funktion). Hat der einen Wert größer Null, wird 
er nur dekrementiert und die Ausführung des Zustandes über return() 
abgebrochen. Auf diese Weise entscheide ich in einem beliebigen Zustand 
durch Setzen des Zählers, wann ich den nächsten Zustand aufrufe (also 
immer über ganzzahlige Vielfache des Aufruftaktes). So kann sich auch 
ein wiederholt aufgerufener (wartender) Zustand selbst verlangsamen, um 
die CPU-Last zu verringern.

Wenn man den Ablauf einer Zustandsmaschine zur Laufzeit sichtbar machen 
will, dann kann man z.B. jedem Zustand die Möglichkeit geben, sich 
irgendwo (im Speicher oder an einer Schnittstelle) zu markieren (evt. 
mit Zeitstempel). Die Adresse des Zustandes ist für die Identifikation 
nicht geeignet, weil sie sich mit jeder Programmänderung ebenfalls 
ändert. Das ist bei switch/case natürlich nicht der Fall, der case-Wert 
ist eine eindeutige Zustandsidentifikation.

von Johannes F. (jofe)


Lesenswert?

Robert schrieb:
> Auf sachliche Diskussionen lasse ich mich gerne ein.

Schön. Sollte man auch voraussetzen können, wenn jemand einen Thread in 
einem Forum aufmacht. Wozu sonst das ganze?

> Allerdings habe ich leider meistens zu wenig Zeit, um mich intensiv mit
> Beiträgen in Foren zu befassen. Deshalb muss ich oftmals etwas
> oberflächlich bleiben.

Ah, alles klar. Um inkonsistentes KI-Blabla ins Forum zu kopieren, 
reicht deine Zeit gerade noch aus, aber um auf Antworten darauf 
einzugehen, nicht mehr.

Robert schrieb:
> Damals hatten die PCs eine parallele Schnittstelle (Centronics/Drucker),
> über die Peripherieankopplungen erfolgen konnten.

Ach? XD.

Robert schrieb:
> Wenn man den Ablauf einer Zustandsmaschine zur Laufzeit sichtbar machen
> will, dann kann man z.B. jedem Zustand die Möglichkeit geben, sich
> irgendwo (im Speicher oder an einer Schnittstelle) zu markieren (evt.
> mit Zeitstempel). Die Adresse des Zustandes ist für die Identifikation
> nicht geeignet, weil sie sich mit jeder Programmänderung ebenfalls
> ändert. Das ist bei switch/case natürlich nicht der Fall, der case-Wert
> ist eine eindeutige Zustandsidentifikation.

Und hier wieder Feststellungen trivialer Fakten, unpassend hochtrabend 
ausformuliert, wie die restlichen Absätze.

Dass diese Texte nicht von einem LLM stammen, kannst du meiner Oma 
erzählen, und selbst die würde es nicht glauben.

von Georg M. (g_m)


Lesenswert?

Next time, please at least include a few spelling mistakes.

von Robert (profrob)


Lesenswert?

Georg M. schrieb:
> Next time, please at least include a few spelling mistakes.

Es ist schon grotesk, dass gut formulierte Beiträge von manchen hier als 
LLM-Ergebnisse unterstellt werden. Was kommt denn da noch?

Aber vielen Dank für den Tipp!

von Nemopuk (nemopuk)


Lesenswert?

Johannes F. schrieb:
> Nix für ungut, aber der Eröffnungsbeitrag riecht für meinen Geschmack
> doch sehr nach LLM-erzeugt.

Ich wollte es nicht sagen, aber: ja

von Bruno V. (bruno_v)


Lesenswert?

Robert schrieb:
> Es ist schon grotesk, dass gut formulierte Beiträge von manchen hier als
> LLM-Ergebnisse unterstellt werden. Was kommt denn da noch?

Die meisten hier nutzen Statemachines. Im Embeddedbereich allerdings 
eher mit Zykluszeiten von ~1ms und alles schnellere eventgetrieben (aka 
Interrupts). Warum? Weil jegliche Benutzerinteraktion im Bereich von 
mehreren ms schnell genug ist (Tastenentprellung per ms-Tick ist immer 
ausreichend) und weil es meist nur wenige Tasks gibt, die schnellere 
Zykluszeiten brauchen. Motorsteuerung wäre sowas. Kommunikation auch, 
aber da: Interrupts.

Wenn Du dann sowas schreibst:

Robert schrieb:
> anstelle der Verwendung von Timern auf die Abfrage von
> micros() in der loop()-Funktion eingelassen. Dabei wurde die minimale
> Taktzeit von mir willkürlich auf 50 Mikrosekunden festgelegt. Wenn mir
> der Aufruf von micros() zu lang erschien (z.B. beim Nano BLE 33), habe
> ich ihn durch einen direkten Zugriff auf die Hardware ersetzt (je nach
> CPU oder OS auf den Systemtakt oder auf bereits laufende Timer). Also in
> loop() werte ich konkrete Zeiten (Zeitdifferenzen) aus und verzweige von
> da über eine Zeigervariable in den aktuellen Zustand.

ist das *schräg*: Entweder, Du missbrauchst das für einen 
Poor-Mans-Scheduler oder Deine Anwendungen sind *schräg*=ungewöhnlich 
(Spinnmaschine mit 20 unabhängigen Motorsteuerungen) oder Deine 
Statemachines haben mit den üblichen nur wenig gemeinsam.

Da Du das auch nicht auflösen möchtest (kein Beispiel, kein Kontext, 
keine Erklärung, kein Eingehen auf die Antworten) ist Dein Beitrag einer 
KI nicht unähnlich. Viel Muster-Erfahrung aber ohne Bezug zur Realität.

von Johannes F. (jofe)


Lesenswert?

Und sowas hier:

Robert schrieb:
> Das regt mich an, auch noch ein wenig aus der Schule zu plaudern:

Die Wendung "aus der Schule plaudern" ist mir unbekannt und zudem in 
diesem Kontext unpassend. Einzige naheliegende Erklärung: 
LLM-Halluzination.

Robert schrieb:
> Es freut mich, dass hier auch viele Zustandsmaschinenanwender sind. Und
> vielleicht könnt ihr mir zustimmen, dass in diesem Verfahren enorme
> Möglichkeiten stecken.

Ein typischer künstlich-euphorischer Schluss-Absatz, wie man ihn von 
Chatbots wie GPT gewohnt ist.

von Joachim B. (jar)


Lesenswert?

Bruno V. schrieb:
> (Tastenentprellung per ms-Tick ist immer
> ausreichend)

auch IR-Code Bits einsammeln alle 64µs z.B. und dann zählt man halt mit 
ob die Entprellung bearbeitet werden muß.

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.