Forum: Mikrocontroller und Digitale Elektronik Wie FSM mit Events in Tabelle umsetzen (C)?


von Flederdil (Gast)


Lesenswert?

Weder im Wiki-Artikel noch in den gefundenen Threads habe ich eine 
Lösung gesehen, die eine tabellarische FSM mit mehreren Events pro 
Zustand abbildet.

Zwei Ansätze fallen mir ein:

Eine Event-Array bzw. ein Pointer dorthin (was es unübersichtlicher 
macht, da separat definiert)

    STATEx, [EVENTy->[FUNCTION,NEWSTATE], ...]
    ...

Entsprechend viele Einträge und eine Suche (größere Tabelle, langsamer, 
aber schön übersichtlich):

    STATE1, EVENT_A, FUNCTION, NEWSTATE
    STATE1, EVENT_B, FUNCTION, NEWSTATE
    ...

Ist der Ansatz brauchbar, wie geht's "besser"?

von Stephan (Gast)


Lesenswert?

Hallo

wie gefällt dir so ein Ansatz:
1
    CREATE_FSM_TABLE(
2
        CREATE_STATE( STATE_IDLE,
3
            ADD_EVENT( EV_TWI_READ_DATA , STATE_WRITE_ADR , twi_read_wrAdr ),
4
            ADD_EVENT( EV_TWI_WRITE_DATA, STATE_WRITE_LINK, twi_write_link ),
5
        ),
6
        CREATE_STATE( STATE_WRITE_ADR,
7
            ADD_EVENT( EV_TWI_ADR_READY, STATE_READ_DATA, twi_read_data      ),
8
            ADD_EVENT( EV_TWI_I2C_ERROR, STATE_READ_WAIT, twi_startWaitTimer ),
9
            ADD_EVENT( EV_TWI_MAX_ERROR, STATE_IDLE     , twi_job_finished   ),
10
            ADD_EVENT( EV_TWI_DMA_ERROR, STATE_IDLE     , twi_job_finished   ),
11
        ),
12
        CREATE_STATE( STATE_READ_DATA,
13
            ADD_EVENT( EV_TWI_READ_READY   , STATE_READ_DATA, twi_read_data    ),
14
            ADD_EVENT( EV_TWI_READ_COMPLETE, STATE_IDLE     , twi_job_finished ),
15
        ),
16
        CREATE_STATE( STATE_READ_WAIT,
17
            ADD_EVENT( EV_TWI_TIMEOUT_OK, STATE_WRITE_ADR, twi_read_wrAdr ),
18
        ),
19
        CREATE_STATE( STATE_WRITE_LINK,
20
            ADD_EVENT( EV_TWI_WRITE_READY_DMA, STATE_WRITE_WAIT, twi_startWaitTimer ),
21
            ADD_EVENT( EV_TWI_MAX_ERROR      , STATE_IDLE      , twi_job_finished   ),
22
            ADD_EVENT( EV_TWI_I2C_ERROR      , STATE_WRITE_WAIT, twi_startWaitTimer ),
23
            ADD_EVENT( EV_TWI_DMA_ERROR      , STATE_IDLE      , twi_job_finished   ),
24
            ADD_EVENT( EV_TWI_WRITE_COMPLETE , STATE_IDLE      , twi_job_finished   ),
25
        ),
26
        CREATE_STATE( STATE_WRITE_WAIT,
27
            ADD_EVENT( EV_TWI_TIMEOUT_OK, STATE_WRITE_LINK, twi_write_link ),
28
        ),
29
    ),

von Programmierer (Gast)


Lesenswert?

Sehr sauber lässt sich so etwas mit dem State Pattern lösen. Ist OOP und 
damit besser in C++ umzusetzen, aber wo C läuft läuft fast immer auch 
C++, und es vermeidet so ein Schlachtfeld an Makros. Es ist sehr 
flexibel und bietet die Möglichkeit, pro Zustand Daten abzulegen die 
auch nur für diesen Zustand gültig sind, beliebige individuelle 
Übergangsfunktionen oder -Bedingungen und nebenher ist es auch noch 100% 
typsicher ohne Casts umsetzbar.

von Flederdil (Gast)


Lesenswert?

Das sieht gut aus (entspricht vermutlich meiner ersten Variante), aber 
für mich ist es leider nicht offensichtlich, wie die Makros (und Daten) 
dahinter aussehen.

Als Ergänzung zu meiner ersten Idee könnte man natürlich auch einfach 
eine Tabelle entsprechend der maximalen Anzahl von Events anlegen, und 
die "Platzverschwendung" in Kauf nehmen.

von Flederdil (Gast)


Lesenswert?

https://de.wikipedia.org/wiki/Zustand_(Entwurfsmuster) sehe ich mir an, 
möchte mich aber auf C beschränken.

von Stephan (Gast)


Lesenswert?

Flederdil schrieb:
> Als Ergänzung zu meiner ersten Idee könnte man natürlich auch einfach
> eine Tabelle entsprechend der maximalen Anzahl von Events anlegen, und
> die "Platzverschwendung" in Kauf nehmen.
das würde ich nicht tun, da du wirklich zu viel Platz verschenkst

von Flederdil (Gast)


Lesenswert?

Stephan schrieb:
> das würde ich nicht tun, da du wirklich zu viel Platz verschenkst

Ich auch nicht - verrate doch bitte, wie die Makros funktionieren ;-)

von Stephan (Gast)


Lesenswert?

Flederdil schrieb:
> Das sieht gut aus (entspricht vermutlich meiner ersten Variante), aber
> für mich ist es leider nicht offensichtlich, wie die Makros (und Daten)
> dahinter aussehen.

ich habe mir 3 Strukturen dafür aufgebaut.
1
// der Container für die Events
2
struct eventTransTab_s{
3
    void (*transFunc)( event_t* const e );         /* transition function */
4
    const uint16_t eventId;
5
    const uint16_t nextState;
6
};
7
8
// der Container für die States
9
struct fsm_state_data_s
10
{
11
    /* pointer auf ein Array */
12
    const struct    eventTransTab_s ( * const eventTable)[];
13
    const uint16_t  eventTableSize;
14
    const uint16_t  state;
15
};
16
17
// und die FSM
18
struct fsm
19
{
20
   const struct fsm_state_data_s ( * const stateTable )[];
21
   const size_t stateTableSize;
22
   uint16_t currState;
23
};

nun musst du nur die Strukturen mit Daten füllen.
1
#define ADD_EVENT( evNr, nextStNr, cbFunc )     \
2
    { .eventId= (event_id_t)evNr, .nextState = (uint16_t)nextStNr, .transFunc = cbFunc }
3
4
#define CREATE_STATE( stateNr, ...)                                    \
5
{ .state= stateNr, .eventTable= &(struct eventTransTab_s []){ ##__VA_ARGS__ },  \
6
  .eventTableSize = sizeof((struct eventTransTab_s []){ ##__VA_ARGS__ }) / sizeof( struct eventTransTab_s )}
7
8
#define CREATE_FSM_TABLE( ... )      .stateTableSize= sizeof((struct fsm_state_data_s []){ ##__VA_ARGS__ }) / sizeof( struct fsm_state_data_s ),\
9
                                .stateTable    = &(struct fsm_state_data_s []) { ##__VA_ARGS__ }

von Flederdil (Gast)


Lesenswert?

Vielen Dank, ich seh's mir an!

von Stephan (Gast)


Lesenswert?

und hier der Event-Handler:
1
// Return: False = Error
2
//         True  = alles Okay
3
bool_t fsm_state_handler( struct fsm* const fsm, event_t* const e)
4
{
5
    const event_id_t id = e->Id;
6
    const uint16_t   currState = fsm->currState;
7
8
    /*  Iterate through the state transition matrix, checking if there is both a match with the current state
9
        and the event
10
    */
11
    for( size_t i = (size_t)0; i < fsm->stateTableSize; i++ )
12
    {
13
        const struct fsm_state_data_s* tabState= &(*fsm->stateTable)[ i ];
14
15
        if ( tabState->state == currState )
16
        {
17
            /* hier die abgehenden Transitions prüfen */
18
            for( size_t x = (size_t)0; x < tabState->eventTableSize; x++ )
19
            {
20
                const struct eventTransTab_s* eventTransition = &(*tabState->eventTable)[ x ];
21
                if ( eventTransition->eventId == id )
22
                {
23
                    // Transition to the next state
24
                    fsm->currState = eventTransition->nextState;
25
26
                    // Call the function associated with transition
27
                    if ( eventTransition->transFunc != NULL_PTR )
28
                    {
29
                        eventTransition->transFunc( e );
30
                    }
31
32
                    return True;
33
                }
34
            }
35
36
            /* wir haben zwar den State gefunden aber kein passendes Event! */
37
            break;
38
        }
39
    }
40
41
    return False;
42
}

so das war es. Ich hoffe ich habe nichts Kaput gemacht oder vergessen zu 
kopieren. :-)

Es ist ein Ansatz, ob er dir gefällt und du diese Art der Programmierung 
mit den Macros mags, musst du entscheiden.

PS: Du kannst auch diese FSM-Macros umschreiben und mit Hilfe von 
Plantuml ein Diagramm zeichnen lassen. Es sieht nicht schön aus, bei 
mir, aber es hilft beim Programmieren.

von Programmierer (Gast)


Lesenswert?

Stephan schrieb:
> size_t i = (size_t)0

Der Cast nach size_t beim Initialisieren ist unnötig.

von Programmierer (Gast)


Lesenswert?

Stephan schrieb:
> if ( eventTransition->transFunc != NULL_PTR

Und warum nicht einfach NULL? Es gibt keine sinnvolle Situation in C in 
welcher man den Null-Pointer anders definieren würde als NULL, solche 
Konstrukte machen es nur schlechter lesbar & wartbar.

von EAF (Gast)


Lesenswert?

Programmierer schrieb:
> Und warum nicht einfach NULL?
Warum überhaupt irgendeine Null zum Vergleich?

if ( eventTransition->transFunc )
sollte doch reichen.

von Flederdil (Gast)


Lesenswert?

Stephan schrieb:
> Ich hoffe ich habe nichts Kaput gemacht oder vergessen zu
> kopieren. :-)

Es fehlen noch ein paar Typen.. Habe versucht, zu raten, aber das 
funktioniert noch nicht:
1
#include <stdint.h>
2
3
typedef enum {STATE_ZERO, STATE_ONE} state;
4
typedef enum {EV_ZERO, EV_ONE, EV_TWO} event_id_t;
5
typedef void* event_t;
6
typedef uint8_t size_t;
7
8
// der Container für die Events
9
struct eventTransTab_s{
10
    void (*transFunc)( event_t* const e );         /* transition function */
11
    const uint16_t eventId;
12
    const uint16_t nextState;
13
};
14
15
// der Container für die States
16
struct fsm_state_data_s
17
{
18
    /* pointer auf ein Array */
19
    const struct    eventTransTab_s ( * const eventTable)[];
20
    const uint16_t  eventTableSize;
21
    const uint16_t  state;
22
};
23
24
// und die FSM
25
struct fsm
26
{
27
   const struct fsm_state_data_s ( * const stateTable )[];
28
   const size_t stateTableSize;
29
   uint16_t currState;
30
};
31
32
33
34
#define ADD_EVENT( evNr, nextStNr, cbFunc )     \
35
    { .eventId= (event_id_t)evNr, .nextState = (uint16_t)nextStNr, .transFunc = cbFunc }
36
37
#define CREATE_STATE( stateNr, ...)                                    \
38
{ .state= stateNr, .eventTable= &(struct eventTransTab_s []){ ##__VA_ARGS__ },  \
39
  .eventTableSize = sizeof((struct eventTransTab_s []){ ##__VA_ARGS__ }) / sizeof( struct eventTransTab_s )}
40
41
#define CREATE_FSM_TABLE( ... )      .stateTableSize= sizeof((struct fsm_state_data_s []){ ##__VA_ARGS__ }) / sizeof( struct fsm_state_data_s ),\
42
                                .stateTable    = &(struct fsm_state_data_s []) { ##__VA_ARGS__ }
43
44
struct fsm test { 
45
CREATE_FSM_TABLE(
46
    CREATE_STATE( STATE_ZERO,
47
        ADD_EVENT( EV_ZERO , STATE_ONE, NULL ),
48
        ADD_EVENT( EV_ONE, STATE_ONE, NULL ),
49
    ),
50
51
    CREATE_STATE( STATE_ONE,
52
        ADD_EVENT( EV_TWO, STATE_ZERO, NULL),
53
    ),
54
),
55
.currState = STATE_ZERO;
56
};

Sollte ".eventId" nicht eigentlich auch event_id_t sein?

von Stephan (Gast)


Lesenswert?

Flederdil schrieb:
> Stephan schrieb:
>> Ich hoffe ich habe nichts Kaput gemacht oder vergessen zu
>> kopieren. :-)
>
> Es fehlen noch ein paar Typen.. Habe versucht, zu raten, aber das
> funktioniert noch nicht:
>
1
#include <stdint.h>
2
 
3
typedef enum {STATE_ZERO, STATE_ONE} state;
4
typedef enum {EV_ZERO, EV_ONE, EV_TWO} event_id_t;
5
typedef void* event_t;
6
typedef uint8_t size_t;
> Sollte ".eventId" nicht eigentlich auch event_id_t sein?
Ja ist richtig. Das war mal ein Enum.

von Stephan (Gast)


Lesenswert?

Programmierer schrieb:
> Stephan schrieb:
>> size_t i = (size_t)0
>
> Der Cast nach size_t beim Initialisieren ist unnötig.
Ich bekomme hier eine Warnung "Implizierter CAST". daher der cast von 
meiner Seite. Ich glaube, ich hätte auch "0UL" schreiben können.

Programmierer schrieb:
> Stephan schrieb:
>> if ( eventTransition->transFunc != NULL_PTR
>
> Und warum nicht einfach NULL? Es gibt keine sinnvolle Situation in C in
> welcher man den Null-Pointer anders definieren würde als NULL, solche
> Konstrukte machen es nur schlechter lesbar & wartbar.
Bei meinem Compiler ist NULL wirklich eine dezimale 0!
Daher würde das eine Warnung vom Compiler generieren!
1
#define NULL_PTR                ((void*)0)
Dies wird akzeptiert!

EAF schrieb:
> Programmierer schrieb:
>> Und warum nicht einfach NULL?
> Warum überhaupt irgendeine Null zum Vergleich?
>
> if ( eventTransition->transFunc )
> sollte doch reichen.
Die ist kein Boolesche Ausdruck und es gibt eine Warnung!

Mein Compiler hat ein paar MISRA Regeln mit eingebaut und prüft diese 
auch.
Die "##" musste ich bei meinen Compiler auch erstmal freigeben, sind 
auch nicht erlaubt!

von Programmierer (Gast)


Lesenswert?

Stephan schrieb:
> Ich bekomme hier eine Warnung "Implizierter CAST".

Meines Wissens ist das eine Initialisierung und kein Cast, daher völlig 
legal. Das geht mit allen Integer-Typen.

Stephan schrieb:
> Bei meinem Compiler ist NULL wirklich eine dezimale 0!

Das ist auch legitim.

> Daher würde das eine Warnung vom Compiler generieren!

Nö, 0 ist implizit in jeden Pointer-Typ konvertierbar, daher kann man 
alle Pointer-Typen mit 0 vergleichen oder zuweisen. In aktuellen 
C++-Versionen ist es dank nullptr eleganter gelöst, aber in C geht es 
halt so. NULL ist explizit genau dafür gedacht, auf Pointer zugewiesen 
oder verglichen zu werden, daher macht es keinen Sinn etwas eigenes 
dafür zu definieren. Wenn der Compiler davor warnt, etwas genau dafür zu 
benutzen wofür es vorgesehen ist, stimmt mit dem was nicht.

Stephan schrieb:
> Die ist kein Boolesche Ausdruck und es gibt eine Warnung!

Es ist nach bool konvertierbar, und daher ist es absolut korrekt und 
wohldefiniert. Wer hier warnt, ist extrem paranoid, denn hier gibt es 
kein Fehlerpotenzial.

Stephan schrieb:
> Mein Compiler hat ein paar MISRA Regeln mit eingebaut und prüft diese
> auch.

Ah, MISRA. Dann haben wir hier ein paar tolle Beispiele, wie MISRA den 
Code schlechter macht - schlechte Lesbarkeit (NULL_PTR ???) kann eben 
auch Sicherheitsprobleme bewirken, weil man nicht mehr sieht, was 
passiert!

Stephan schrieb:
> Die "##" musste ich bei meinen Compiler auch erstmal freigeben, sind
> auch nicht erlaubt!

Ist auch absolut Standard und erlaubt. Sich so etwas erst zu verbieten 
und dann wieder zu aktivieren...

von Flederdil (Gast)


Lesenswert?

Mit meinem obigen Versuch funktioniert es nicht, d.h. ich erhalte eine 
Reihe an solchen Fehlern:
    error: pasting "{" and "CREATE_STATE" does not give a valid 
preprocessing token

Flederdil schrieb:
> struct fsm test {

Hier fehlt wohl auch noch ein = ?

Und: gab es nicht einen Schalter für den Compiler (GCC), damit er den 
output nach dem Präprozessor nicht löscht? Ich würde mir gerne das 
Resultat ansehen...

von Stephan (Gast)


Lesenswert?

Hi  Flederdil,
1
// hab den include hinzugefügt
2
#include <stdlib.h>
3
// '=' Zeichen fehlte
4
struct fsm test= {
5
// Da gehört ein *KOMMA* hin! :-)
6
.currState = STATE_ZERO,

Hier mal der Code von dir durch den Präprozessor gejagt:
1
struct fsm test= {
2
CREATE_FSM_TABLE(
3
    CREATE_STATE( STATE_ZERO,
4
        ADD_EVENT( EV_ZERO , STATE_ONE, NULL ),
5
        ADD_EVENT( EV_ONE, STATE_ONE, NULL ),
6
    ),
7
    CREATE_STATE( STATE_ONE,
8
        ADD_EVENT( EV_TWO, STATE_ZERO, NULL),
9
    ),
10
),
11
.currState = STATE_ZERO,
12
};

ergibt:
1
struct fsm test= {
2
    . stateTableSize= sizeof((struct fsm_state_data_s []){{ . state= STATE_ZERO, . eventTable= &(struct eventTransTab_s []){{ . eventId= (event_id_t)EV_ZERO, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, { . eventId= (event_id_t)EV_ONE, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, }, . eventTableSize = sizeof((struct eventTransTab_s []){{ . eventId= (event_id_t)EV_ZERO, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, { . eventId= (event_id_t)EV_ONE, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, }) / sizeof( struct eventTransTab_s )}, { . state= STATE_ONE, . eventTable= &(struct eventTransTab_s []){{ . eventId= (event_id_t)EV_TWO, . nextState = (uint16_t)STATE_ZERO, . transFunc = 0 }, }, . eventTableSize = sizeof((struct eventTransTab_s []){{ . eventId= (event_id_t)EV_TWO, . nextState = (uint16_t)STATE_ZERO, . transFunc = 0 }, }) / sizeof( struct eventTransTab_s )}, }) / sizeof( struct fsm_state_data_s ),
3
    . stateTable = &(struct fsm_state_data_s []) 
4
    {
5
        {   . state= STATE_ZERO, 
6
            . eventTable= &(struct eventTransTab_s [])
7
            {
8
                { . eventId= (event_id_t)EV_ZERO, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, 
9
                { . eventId= (event_id_t)EV_ONE, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, 
10
            }, 
11
            . eventTableSize = sizeof((struct eventTransTab_s []){{ . eventId= (event_id_t)EV_ZERO, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, { . eventId= (event_id_t)EV_ONE, . nextState = (uint16_t)STATE_ONE, . transFunc = 0 }, }) / sizeof( struct eventTransTab_s )
12
        },         
13
        {   . state= STATE_ONE, 
14
            . eventTable= &(struct eventTransTab_s [])
15
            {
16
                { . eventId= (event_id_t)EV_TWO, . nextState = (uint16_t)STATE_ZERO, . transFunc = 0 }, 
17
            }, 
18
            . eventTableSize = sizeof((struct eventTransTab_s []){{ . eventId= (event_id_t)EV_TWO, . nextState = (uint16_t)STATE_ZERO, . transFunc = 0 }, }) / sizeof( struct eventTransTab_s )
19
        }, 
20
    },
21
    .currState = STATE_ZERO,
22
};

im Notepad++ sah der Text okay aus. wird hier nicht richtig angezeigt.
Kopiere den Text einfach nochmal.

von Flederdil (Gast)


Angehängte Dateien:

Lesenswert?

Danke dir, aber mein GCC mag es (Anhang) leider immer noch nicht. Die 
Fehler

    error: pasting "{" and "ADD_EVENT" does not give a valid 
preprocessing token

bleiben.

Vielleicht macht der Präprozessor nicht genügend Durchläufe?
1
gcc -v
2
Using built-in specs.
3
COLLECT_GCC=gcc
4
COLLECT_LTO_WRAPPER=c:/mingw/bin/../libexec/gcc/mingw32/8.2.0/lto-wrapper.exe
5
Target: mingw32
6
Configured with: ../src/gcc-8.2.0/configure --build=x86_64-pc-linux-gnu --host=mingw32 --target=mingw32 --prefix=/mingw --disable-win32-registry --with-arch=i586 --with-tune=generic --enable-languages=c,c++,objc,obj-c++,fortran,ada --with-pkgversion='MinGW.org GCC-8.2.0-5' --with-gmp=/mingw --with-mpfr=/mingw --with-mpc=/mingw --enable-static --enable-shared --enable-threads --with-dwarf2 --disable-sjlj-exceptions --enable-version-specific-runtime-libs --with-libiconv-prefix=/mingw --with-libintl-prefix=/mingw --enable-libstdcxx-debug --with-isl=/mingw --enable-libgomp --disable-libvtv --enable-nls --disable-build-format-warnings
7
Thread model: win32
8
gcc version 8.2.0 (MinGW.org GCC-8.2.0-5)

von Stephan (Gast)


Lesenswert?

HI,
dein Code läuft bei mir.
kannst du mal schauen was passiert wenn du die '##' vor dem 
'__VA_ARGS__' beim GCC weg lässt?

von Flederdil (Gast)


Lesenswert?

Danke!

    gcc -E test.c > test_pre.txt

bricht nach den Ausgaben des Präprozessors ab.

von Stephan (Gast)


Angehängte Dateien:

Lesenswert?

Hallo,
so mit dem Code läuft es bei mir mit dieser Version:

gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)

gcc -save-temps test.c -o test1 -Wall -std=c11 -D__USE_MINGW_ANSI_STDIO

Sorry, habe das File doppelt angehängt.

von Flederdil (Gast)


Lesenswert?

Hallo nochmal, ich habe mich wohl nicht klar ausgedrückt: es lief oben 
auch schon bei mir! Der Abbruch bezog sich auf die Ausgabe des 
"präprozessierten" Codes.
Nochmals besten Dank.

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.