Forum: Projekte & Code Kleiner endlicher Automat (FSM)


von Daniel G. (Gast)


Lesenswert?

Nachdem ich gerade die Steuerlogik eines RGB-LED-Dimmer damit 
fertiggestellt habe: Wielleicht interessiert sich hier jemand für einen 
kleinen (60 Zeilen inkl. Kommentaren in einer Headerdatei), endlichen 
Automaten. Aufgrund der Größe und Einfachheit eignet er sich gut für die 
Verwendung in einem Mikrocontroller-Projekt. Der Code ist recht 
übersichtlich geworden und Compiliert einwandfrei in AVR-GCC.

Hier die genaue Beschreibung: http://www.flashsystems.de/articles/1789
Herunterladen kann man die Headerdatei hier: 
http://www.flashsystems.de/down/files/tiny_fsm.h

Viel Spaß damit.

von Schwitzender Fettsack (Gast)


Lesenswert?

Wozu soll das gut sein? Das ist eine Unsitte mit #defines die 
Programmiersprache zu vergewaltigen. Die State Machine kann man genauso 
gut normal in C schreiben und der Code bleibt viel besser lesbar.

von Daniel G. (Gast)


Lesenswert?

Hallo "Schwitzender Fettsack",

es ist wohl Ansichtssache ob ein Wald aus if- oder switch-Statements 
oder dubiose Tabellen übersichtlicher ist als sprechende Makros. Wem's 
nicht gefällt, der muss es ja nicht verwenden.
Aus meiner Sicht hat es nichts mit vergewaltigen zu tun, die Features 
einer Programmiersprache (in diesem Fall Makros) auch zu verwenden. Es 
kann aber sein, dass ich hier schon etwas zu ATL- und MFC-geschädigt 
bin. Diese Bibliotheken machen das nämlich auch (aus meiner Sicht) sehr 
erfolgreich :)

von Schwitzender Fettsack (Gast)


Lesenswert?

Daniel G. schrieb:
> es ist wohl Ansichtssache ob ein Wald aus if- oder switch-Statements
> oder dubiose Tabellen übersichtlicher ist als sprechende Makros.

Man kann State Machines viel eleganter Programmieren als mit if oder 
switch/case. Stichwort Callback.

Daniel G. schrieb:
> Wem's nicht gefällt, der muss es ja nicht verwenden.

Das sag mal jemandem der dann solchen Code pflegen muss.

von Daniel G. (Gast)


Lesenswert?

Hallo "Schwitzender Fettsack",

über das Wohl und Wehe von Präprozessormakros sind schon zu viele 
Forenseiten vollgeschrieben worden ohne eine Einigung zu erzielen...

Was ich viel interessanter Finde, ist der erwähnte Callback-Ansatz. 
Würde es dir etwas ausmachen, hier ein kleines Beispiel zu posten?

Evtl. einfach die gleiche Funktion wie das von mir verwendete Beispiel. 
Dann kann jemand, der auf diesen Artikel stößt, selbst entscheiden 
welche Variante ihm besser gefällt oder welche für seinen Einsatzzweck 
besser geeignet ist.

von Klaus W. (mfgkw)


Lesenswert?

Daniel G. schrieb:
> über das Wohl und Wehe von Präprozessormakros sind schon zu viele
> Forenseiten vollgeschrieben worden ohne eine Einigung zu erzielen...

Das ist falsch, die Einigung ist nur noch nicht bei allen angekommen:
Sobald es eine sinnvolle Alternative gibt (also in dem meisten Fällen), 
sind Makros nur von Übel - insbesondere für funktionsähnliche Makros.

von Paul Baumann (Gast)


Lesenswert?

Vielleicht hat der schwitzende Fettsack Makros mit Makronen 
verwechselt...

;-)
MfG Paul

von Daniel G. (Gast)


Lesenswert?

Ok, mag sein, dass der Makro-Ansatz nichts taugt. Wie oben schon 
geschrieben fände ich es dann interessant, eine bessere Lösung zu sehen. 
Dann lerne nicht nur ich etwas dazu sondern auch andere, die diesen 
Thread lesen...

Schließlich ist nichts so schlecht, dass es nicht noch als schlechtes 
Beispiel dienen könnte ;)

Die Rahmenbedingungen sind:
- Implementiert das beschriebene Beispiel oder was ähnliches
- C (kein C++)
- keine dynamische Speicherallokation
- übersichtlicher/wartungsfreundlicher als der Makro-Ansatz

von Marko B. (glagnar)


Lesenswert?

Bin zwar nicht "Schwitzender Fettsack", aber sowas hab ich schon mal 
gemacht (LED-Uhr mit 1-Button Interface als State Machine programmiert). 
Folgenden Code hab ich benutzt um das Konzept zu testen. Sollte auf fast 
jedem BS als Konsolenprogramm kompilieren:
1
#include <stdio.h>
2
#include <stdlib.h>
3
4
void * state1(void);
5
void * state2(void);
6
void * quit(void);
7
8
void * (*currentstate)(void) = &state1;
9
10
void * state1(void)
11
{
12
        static int a = 1;
13
14
        printf("we are in state1. a is %d.\n",a);
15
16
        if (++a<5)
17
                return &state1;
18
        else
19
                return &state2;
20
}
21
22
void * state2(void)
23
{
24
        printf("now we're in state2. time to quit.\n");
25
        return &quit;
26
}
27
28
void * quit(void)
29
{
30
        printf("exiting\n");
31
        exit(EXIT_SUCCESS);
32
}
33
34
int main(void)
35
{
36
        for(;;)
37
                currentstate = currentstate();
38
}

und hier die Ausgabe:
1
we are in state1. a is 1.
2
we are in state1. a is 2.
3
we are in state1. a is 3.
4
we are in state1. a is 4.
5
now we're in state2. time to quit.
6
exiting

von Informatikstudent (Gast)


Lesenswert?

@glagnar: Uiuiui.... warum Stackplatz vergeuden, obwohl ein DFA doch per 
Definition gar keinen Stack benötigt?? Warum diese ungewöhnliche, 
umständliche Implementierungsweise? Eine simple Schleife hätte doch auch 
gereicht...?!?

von Yalu X. (yalu) (Moderator)


Lesenswert?

Schwitzender Fettsack schrieb:
> Wozu soll das gut sein? Das ist eine Unsitte mit #defines die
> Programmiersprache zu vergewaltigen.

Das ist keine Vergewaltigung der Programmiersprache, sondern eine DSL,
genauer gesagt eine embedded DSL:

  http://de.wikipedia.org/wiki/Dom%C3%A4nenspezifische_Sprache

Embedded DSLs sind eine durchaus gängige Methode, um den Abstraktions-
grad der Wirtssprache (hier C) für bestimmte Anwendungsbereiche (hier
FSMs) zu erhöhen.

Ok, C bietet nur wenige Sprachkonstrukte an, die zur Erstellung von DSLs
geeignet sind. Eins der wichtigsten ist das Makrosystem (Präprozessor),
das allerdings nicht sehr viel kann.

In C++ geht da schon mehr, vor allem wegen des sehr mächtigen Template-
Systems und der Möglichkeit der Operatorüberladung. Noch mehr in dieser
Richtung ist in Python, Ruby und Konsorten möglich.

Der Klassiker unter den "DSL-fähigen" Programmiersprachen ist aber Lisp.
Lisp hat ein Makrosystem, das mächtiger ist als das aller anderen mir
bekannten Programmiersprachen: Es ist so mächtig wie die Sprache selbst.
Eine Makroexpansion macht dort nicht — wie in C — nur einfache Text-
ersetzungen, sondern praktisch beliebig definierbare (programmierbare)
Transformationen von Programmcode. In Lisp ist es fast normal, dass ein
Entwickler zu Beginn eines größeren Softwareprojekts erst einmal eine
geeignete DSL entwirft und implementiert, um hinterher die eigentliche
Anwendung ganz leicht und in übersichtlicher Weise herunterprogrammie-
ren zu können.

Wie bei allen Dingen sollte man aber klein anfangen, und da ist doch die
FSM-DSL von Daniel ein schönes Beispiel :)

> Die State Machine kann man genauso gut normal in C schreiben

Klar.

> und der Code bleibt viel besser lesbar.

Hmm, findest du? Ich habe noch kein direkt programmiertes gutes Beispiel
gesehen. Es ist generell nicht leicht, die Funktion einer in Textform
(also nichtgrafisch) dargestellten FSM zu verstehen. Das gilt bis zu
einem gewissen Grad auch für DSLs, nur ist dort der Code meistens
kompakter, so dass man einen viel größeren Ausschnitt mit einem Blick
erfassen kann, was die Sache sehr viel übersichtlicher macht.

Besser für das Verständnis ist es natürlich, wenn man aus der textuellen
Beschreibung eine grafische Darstellung generieren kann. Aber auch hier
haben DSLs Vorteile: Ein Parser für Daniels FSM-DSL ist recht schnell
geschrieben, während es nahezu unmöglich ist, aus einem Stück normalem
C-Code, der eine FSM darstellen soll, die FSM-Struktur zu extrahieren.

@Daniel: Lass dich von denen nicht entmutigen, du bist auf dem richtigen
Weg. In welchen Anwendungen sich deine Methode bewährt und in welchen
nicht so sehr, das wirst du von Fall zu Fall selber herausfinden.

von Marko B. (glagnar)


Lesenswert?

Informatikstudent schrieb:
> @glagnar: Uiuiui.... warum Stackplatz vergeuden, obwohl ein DFA doch per
> Definition gar keinen Stack benötigt?? Warum diese ungewöhnliche,
> umständliche Implementierungsweise? Eine simple Schleife hätte doch auch
> gereicht...?!?

Das muss wohl ironisch gemeint sein. Mein Code hat ein Stacklevel von 
genau 1. Es gibt keine tieferen Unteraufrufe und auch kein Reentry. 
Simple Schleife? Ja, man könnte natürlich komplett auf Funktionen 
verzichten ...

Zumindest reichts für eine LED-Uhr mit DCF77 Empfang, mit 7 
Helligkeitsstufen und einem komfortablen Bedieninterface mit nur einem 
Button auf einem ATTiny2313 mit 128 Byte RAM ...

Der Vorteil bei meinem Code ist halt, wie ich finde, dass er modular ist 
und dass die Information für den Programmflow immer schön lokal im 
passenden Kontext ist. Wenn man Features hinzufügen möchte, muss man nur 
den vohergehnden und den nachfolgenden State ändern. Ich nenne es 
Tortellinicode :D

von Yalu X. (yalu) (Moderator)


Lesenswert?

Marko B. schrieb:
> Der Vorteil bei meinem Code ist halt, wie ich finde, dass er modular ist
> und dass die Information für den Programmflow immer schön lokal im
> passenden Kontext ist.

Anstelle der Rückgabe der Funktionszeiger für den nächsten Schritt
könntest du die jeweiligen Funktionen auch direkt aufrufen:
1
#include <stdio.h>
2
#include <stdlib.h>
3
4
void state1(void);
5
void state2(void);
6
void quit(void);
7
8
void state1(void)
9
{
10
        static int a = 1;
11
12
        printf("we are in state1. a is %d.\n",a);
13
14
        if (++a<5)
15
                state1();
16
        else
17
                state2();
18
}
19
20
void state2(void)
21
{
22
        printf("now we're in state2. time to quit.\n");
23
        quit();
24
}
25
26
void quit(void)
27
{
28
        printf("exiting\n");
29
}
30
31
int main(void)
32
{
33
        state1();
34
}

Da jeder anständige C-Compiler heutzutage Tail-Call-Optimization macht,
führt dies zu keinem zusätzlichen Stack-Verbrauch.

von Marko B. (glagnar)


Lesenswert?

Yalu X. schrieb:
> Anstelle der Rückgabe der Funktionszeiger für den nächsten Schritt
> könntest du die jeweiligen Funktionen auch direkt aufrufen:

Hmm. Ich denke es ist kein guter Stil, sich auf eine Optimierung zu 
verlassen damit ein Programm funktioniert. Spätestens wenn man zum 
Debuggen die Optimierung ausschaltet ist es vorbei damit.

Besonders übel wäre es auch, wenn in irgendeinem Spezialfall die 
Optimierung nicht angewandt wird und das Programm crasht. Das wäre ein 
schwer zu lokalisierender Bug.

Da schreibe ich lieber die "korrekte" Version mit Funktionspointer.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Marko B. schrieb:
>> Anstelle der Rückgabe der Funktionszeiger für den nächsten Schritt
>> könntest du die jeweiligen Funktionen auch direkt aufrufen:
>
> Hmm. Ich denke es ist kein guter Stil, sich auf eine Optimierung zu
> verlassen damit ein Programm funktioniert.

Hast recht, das sollte man nicht machen :)

von Chris (Gast)


Lesenswert?

Wenn dein anderer Code auch so schlecht ist, wie die Implementation der 
FSM,
übe noch weiter. An sich nicht so schlecht, nur die Implementierung 
lässt
zu wünschen übrig.

    BEGIN_STATE_MACHINE(main, Standby)
        STATE(main, Running)
            EVENT(main, Done, Standby);
            EVENT(main, EnterSettings, Settings);
        END_STATE()


Bei einer besseren FSM möchte man folgendes machen, dein Code lässt dies
aber nicht zu.

    BEGIN_STATE_MACHINE(main, Standby)
            EVENT(main, Timeout, Standby);
            EVENT(main, External, Standby);
        STATE(main, Running)
            EVENT(main, Done, Standby);
            EVENT(main, EnterSettings, Settings);
        END_STATE()


Weiters sollte die State-Machine eher so lauten:

    BEGIN_STATE_MACHINE(main, Reset)
        STATE(main, Reset)
            do_init();
            STATE_NEXT(main, Standby);
        END_STATE()

Auch deine Namen sind etwas unvorsichtig.
#define HANDLERFUNC(_MACHINENAME, _EVENTNAME) bool 
_MACHINENAME##_##_EVENTNAME()

Was ist, wenn eine state machine den namen "ret" hat und der state
"urn" für Unrecognized Reference Number als Beispiel, dann machst du
daraus return, klingelt da was. Ist natürlich kein gutes Beispiel, es
gibt andere wie gettime, ... .


auch das wiederholte verwenden vom FSM Name ist blöd.

    BEGIN_STATE_MACHINE(main, Standby)
            EVENT(main, Timeout, Standby);
            EVENT(main, External, Standby);
        STATE(main, Running)
            EVENT(main, Done, Standby);
            EVENT(main, EnterSettings, Settings);
        END_STATE()

könnte genausogut
    BEGIN_STATE_MACHINE(main, Standby)
            EVENT(Timeout, Standby);
            EVENT(External, Standby);
        STATE(Running)
            EVENT(Done, Standby);
            EVENT(EnterSettings, Settings);
        END_STATE()

sein,  die Funktion
#define BEGIN_STATE_MACHINE(_MACHINENAME, _STARTSTATE) \
  SMFunc_##_MACHINENAME currentStateFunc = _MACHINENAME##_##_STARTSTATE; 
\
  while (true) \
  { if (false) {}

könnte einfach so definiert sein:

#define BEGIN_STATE_MACHINE(_MACHINENAME, _STARTSTATE) \
  while (true) \
  { static FSM_STATE_TYPE state = _MACHINENAME##_##_STARTSTATE; if 
(false) {}

und anstelle von
#define EVENT(_MACHINENAME, _EVENTNAME, _STATEFUNC) \
      case _EVENTNAME: \
        currentStateFunc = _MACHINENAME##_##_STATEFUNC; \
        break;

machst du
#define EVENT(_EVENTNAME, _STATEFUNC) \
      case _EVENTNAME: \
        state = FSM_##_STATEFUNC; \
        break;

wobei die Switch anweisungen auch problematisch sind, ...
Chris

von Daniel G. (Gast)


Lesenswert?

Chris schrieb:

> Wenn dein anderer Code auch so schlecht ist, wie die Implementation der
> FSM,
> übe noch weiter.

Das musste sein, gell. Ich kommentiere deine Vorschläge doch auch 
sachlich...

[...]
>
>     BEGIN_STATE_MACHINE(main, Standby)
>             EVENT(main, Timeout, Standby);
>             EVENT(main, External, Standby);
>         STATE(main, Running)
>             EVENT(main, Done, Standby);
>             EVENT(main, EnterSettings, Settings);
>         END_STATE()
>

Sehe ich nicht so: Für jeden State sollten aus meiner Sicht die Events 
und der daraus resultierende Folgestatus klar definiert sein. Globale 
Events sind nicht vorgesehen. Ob diese in einer FSM überhaupt sinnvoll 
sind, kann ich ad hoc nicht beurteilen. Evtl. hat hier jemand eine 
Meinung dazu...
Was, wenn ein neuer State dazu kommt, welcher nicht auf den Timeout 
reagieren will. Dann muss der Timeout doch manuell bei allen States 
eingetragen werden.

>
> Weiters sollte die State-Machine eher so lauten:
>
>     BEGIN_STATE_MACHINE(main, Reset)
>         STATE(main, Reset)
>             do_init();
>             STATE_NEXT(main, Standby);
>         END_STATE()
>

Warum? Wie wird zwischen zwei Folgezuständen ausgewählt? So 
implementiert das nur eine sequenzielle Fortschaltung.

> Auch deine Namen sind etwas unvorsichtig.
> #define HANDLERFUNC(_MACHINENAME, _EVENTNAME) bool
> _MACHINENAME##_##_EVENTNAME()
>
> Was ist, wenn eine state machine den namen "ret" hat und der state
> "urn" für Unrecognized Reference Number als Beispiel, dann machst du
> daraus return, klingelt da was. Ist natürlich kein gutes Beispiel, es
> gibt andere wie gettime, ... .

So wie ich das sehe hätte ich bei deinem Beispiel einen Handler mit dem 
Namen ret_urn. Da es in C keine reservierten Schlüsselwörter mit einem _ 
innerhalb des Schlüsselwortes gibt, sehe ich da kein Problem. Natürlich 
kann man immer eine Namenskollision konstruieren, das ist aber bei einem 
selbst gewählten Prefix (dem FSM-Namen) leichter zu vermeiden. Natürlich 
könnte man auch einen festen Prefix (wie weiter unten von dir 
vorgeschlagen) verwenden. Dann können aber keine zwei FSM den gleichen 
State-Namen verwenden.

> auch das wiederholte verwenden vom FSM Name ist blöd.

Stimmt, hier bin ich für eine elegantere Lösung offen. Wenn man aber 
einen statischen Prefix vermeiden will, fällt mir keine andere Lösung 
ein.

[...]
> #define BEGIN_STATE_MACHINE(_MACHINENAME, _STARTSTATE) \
>   while (true) \
>   { static FSM_STATE_TYPE state = _MACHINENAME##_##_STARTSTATE; if
> (false) {}

Werden statische Variablen nicht nur einmal initialisiert? D.h. beim 
nächsten Aufruf der FSM (z.B. in einer Funktion) läuft diese nicht mit 
dem Start-State los sondern mit dem State, welchen sie zuletzt hatte? 
Das wäre - finde ich - "überraschend".

Die Idee einen eigenen Gültigkeitsbereich für die State-Variable zu 
betreten finde ich nicht schlecht. Die Frage ist, welchen Vorteil man 
davon noch hat (außer etwas Stack zu sparen, wenn mehere FSM in einer 
Funktion sind).

> wobei die Switch anweisungen auch problematisch sind, ...

So so...

von Chris (Gast)


Lesenswert?

Beim Namen hast du recht, mein Fehler.

Daniel G. schrieb:
> Werden statische Variablen nicht nur einmal initialisiert? D.h. beim
Ja
> nächsten Aufruf der FSM (z.B. in einer Funktion) läuft diese nicht mit
> dem Start-State los sondern mit dem State, welchen sie zuletzt hatte?
> Das wäre - finde ich - "überraschend".

Das ist jetzt zunächst mal eine Grundsatzfrage. Ich ziehe es vor, wenn
die State-Machine nicht blockiert. Da kann man dann auch mehrere State-
Machines parallel haben und abarbeiten. Deshalb auch mein Ansatz mit
der static Variable. Wenn die State-Machine aber wie bei dir blockieren
sein soll, dann einfach das static weglassen und es ist dann so wie du
es derzeit hast.


Ja, das mit dem Switch ist bei elaborierteren state machines 
problematisch,
da ist ein if Konstrukt besser, in dem Sinne, daß wenn eine Variable 
inizieren will, oder ändern, oder dergleichen das mit einem if 
funktioniert,
mit einem switch nicht.

Dasselbe ist mit dem IF construct,
    if (false) {}

#define STATE(_MACHINENAME, _STATENAME) \
    else if (currentStateFunc == _MACHINENAME##_##_STATENAME) \


besser ist, weil dann ein Semicolon am ende sein kann.
    if (false) {

#define STATE(_MACHINENAME, _STATENAME) \
    } else if (currentStateFunc == _MACHINENAME##_##_STATENAME) \

von Daniel G. (Gast)


Lesenswert?

Chris schrieb:
>
> Das ist jetzt zunächst mal eine Grundsatzfrage. Ich ziehe es vor, wenn
> die State-Machine nicht blockiert. Da kann man dann auch mehrere State-
> Machines parallel haben und abarbeiten. Deshalb auch mein Ansatz mit
> der static Variable. Wenn die State-Machine aber wie bei dir blockieren
> sein soll, dann einfach das static weglassen und es ist dann so wie du
> es derzeit hast.
>

Die Idee die State-Machine immer wieder zu verlassen ist interessant. 
Für diese kleine FSM war es nicht nötig. Wenn man das möchte, kann man 
das static rein und die while-Schleife komplett raus machen. Dann hat 
man eine FSM die nach jedem Schritt zum Hauptprogramm zurückkehrt. Evtl. 
kann man dann einen State als eine Art yield verwenden...

>
> Ja, das mit dem Switch ist bei elaborierteren state machines
> problematisch,
> da ist ein if Konstrukt besser, in dem Sinne, daß wenn eine Variable
> inizieren will, oder ändern, oder dergleichen das mit einem if
> funktioniert,
[...]

In diesem Fall ist ein if/elseif-Konstrukt wirklich vorzuziehen. Ich 
habe mich für das switch entschieden da der Compiler hier (wenige, 
sequenziell aufeinander folgende Werte durch das Enum) eine 
Sprungtabelle verwendet und damit die Performance besser und das Binary 
kleiner ist. Wenn man die FSM weiter ausbaut, könnte sich dass aber in 
der Tat als weniger Flexibel erweisen.

Wer mehr Flexibilität möchte, kann das entsprechende Makro wie von dir 
geschildert einfach anpassen.

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.