C-design della macchina a stati [chiuso]


193

Sto realizzando un piccolo progetto in misto C e C ++. Sto costruendo una piccola macchina statale nel cuore di uno dei miei thread di lavoro.

Mi chiedevo se voi guru di SO condivideste le vostre tecniche di progettazione delle macchine a stati.

NOTA: sono principalmente dopo tecniche di implementazione collaudate.

AGGIORNATO: Sulla base di tutti i grandi contributi raccolti su SO, mi sono basato su questa architettura:

Una pompa di eventi punta a un integratore di eventi che punta a un dispatcher.  Il dispatcher punta da 1 a n azioni che rimandano all'integratore di eventi.  Una tabella di transizione con caratteri jolly punta al dispatcher.


4
Le risposte qui sono molto buone. Guarda anche questa domanda duplicata che ha anche molte buone risposte: stackoverflow.com/questions/1371460/state-machines-tutorials
Michael Burr,

2
Questo è anche interessante: stackoverflow.com/questions/133214/...
Daniel Daranas


Risposte:


170

Le macchine a stati che ho progettato in precedenza (C, non C ++) sono state ridotte a un structarray e un loop. La struttura fondamentalmente è costituita da uno stato e un evento (per la ricerca) e una funzione che restituisce il nuovo stato, qualcosa del tipo:

typedef struct {
    int st;
    int ev;
    int (*fn)(void);
} tTransition;

Quindi definisci i tuoi stati ed eventi con definizioni semplici ( ANYquelli sono marcatori speciali, vedi sotto):

#define ST_ANY              -1
#define ST_INIT              0
#define ST_ERROR             1
#define ST_TERM              2
: :
#define EV_ANY              -1
#define EV_KEYPRESS       5000
#define EV_MOUSEMOVE      5001

Quindi si definiscono tutte le funzioni che vengono chiamate dalle transizioni:

static int GotKey (void) { ... };
static int FsmError (void) { ... };

Tutte queste funzioni sono scritte per non accettare variabili e restituire il nuovo stato per la macchina a stati. In questo esempio, le variabili globali vengono utilizzate per trasferire qualsiasi informazione nelle funzioni di stato ove necessario.

L'uso dei globali non è così grave come sembra dal momento che l'FSM di solito è bloccato all'interno di una singola unità di compilazione e tutte le variabili sono statiche per quell'unità (motivo per cui ho usato le virgolette intorno a "globale" sopra - sono più condivise all'interno del FSM, che veramente globale). Come per tutti i globi, richiede cura.

L'array delle transizioni definisce quindi tutte le possibili transizioni e le funzioni che vengono chiamate per quelle transizioni (incluso l'ultimo catch-all):

tTransition trans[] = {
    { ST_INIT, EV_KEYPRESS, &GotKey},
    : :
    { ST_ANY, EV_ANY, &FsmError}
};
#define TRANS_COUNT (sizeof(trans)/sizeof(*trans))

Ciò significa: se sei nello ST_INITstato e ricevi l' EV_KEYPRESSevento, effettua una chiamata aGotKey .

Il funzionamento dell'FSM diventa quindi un ciclo relativamente semplice:

state = ST_INIT;
while (state != ST_TERM) {
    event = GetNextEvent();
    for (i = 0; i < TRANS_COUNT; i++) {
        if ((state == trans[i].st) || (ST_ANY == trans[i].st)) {
            if ((event == trans[i].ev) || (EV_ANY == trans[i].ev)) {
                state = (trans[i].fn)();
                break;
            }
        }
    }
}

Come accennato in precedenza, notare l'uso di caratteri ST_ANYjolly, consentendo a un evento di chiamare una funzione indipendentemente dallo stato corrente.EV_ANYfunziona anche in modo simile, consentendo a qualsiasi evento in uno stato specifico di chiamare una funzione.

Può anche garantire che, se si raggiunge la fine dell'array di transizioni, viene visualizzato un errore che indica che l'FSM non è stato creato correttamente (utilizzando la ST_ANY/EV_ANYcombinazione.

Ho usato codice simile per questo su molti progetti di comunicazione, come un'implementazione precoce di stack e protocolli di comunicazione per sistemi embedded. Il grande vantaggio era la sua semplicità e relativa facilità nel cambiare l'array delle transizioni.

Non ho dubbi che ci saranno astrazioni di livello superiore che potrebbero essere più adatte al giorno d'oggi, ma sospetto che si ridurranno tutti a questo stesso tipo di struttura.


E, come ldogafferma un commento, puoi evitare del tutto i globali passando un puntatore a struttura a tutte le funzioni (e usandolo nel loop degli eventi). Ciò consentirà a più macchine a stati di funzionare fianco a fianco senza interferenze.

Basta creare un tipo di struttura che contenga i dati specifici della macchina (dichiarare al minimo indispensabile) e utilizzarlo al posto dei globali.

Il motivo per cui l'ho fatto raramente è semplicemente perché la maggior parte delle macchine a stati che ho scritto sono state di tipo singleton (ad esempio, una tantum, all'avvio del processo, lettura dei file di configurazione), senza bisogno di eseguire più di un'istanza . Ma ha valore se è necessario eseguirne più di uno.


24
Un interruttore gigante mescola il codice con l'FSM. Anche se c'è solo una chiamata di funzione per transizione, c'è ancora un po 'di codice ed è facile per qualcuno abusarne aggiungendo una piccola transizione in linea a 4 linee. gallina a dieci righe. Quindi sfugge di mano. Con l'array struct, l'FSM rimane pulito: puoi vedere ogni transizione ed effetto (funzione). E ho iniziato quando gli enum erano un luccichio negli occhi di ISO, scrivendo codice per 6809 piattaforme integrate con compilatori che erano, diciamo, meno che perfetti :-)
paxdiablo

5
Hai ragione, gli enum sarebbero migliori, ma preferisco comunque avere l'FSM come array di strutture. Quindi è tutto gestito dai dati piuttosto che dal codice (beh, c'è del codice ma la possibilità di riempire quel ciclo di FSM che ho dato è scarsa).
paxdiablo,

2
Questo va bene, per le macchine a stati di controllo dei processi che usavo sempre aggiungere tre sottostati (possibilmente vuoti) per ogni stato, in modo che la chiamata per una funzione di stato diventasse GotKey (sottostato), dove il sottostato sarebbe: - SS_ENTRY - SS_RUN - SS_EXIT Fondamentalmente, la funzione di stato viene chiamata con un sottostato SS_ENTRY all'entrata, in modo che lo stato possa ricostruire uno stato (ad es. Posizioni degli attuatori). Sebbene non vi sia transizione, viene superato il valore del sottostato SS_RUN. Al momento della transizione, la funzione di stato viene chiamata con il sottostato SS_EXIT, in modo che possa eseguire qualsiasi ripulitura (ad esempio risorse di deallocazione).
Metiu,

13
Hai detto che condividi i dati usando i globali, ma probabilmente sarebbe più pulito se definissi le funzioni di stato int (*fn)(void*);dove si void*trova il puntatore ai dati che ciascuna funzione di stato accetta come parametro. Quindi le funzioni di stato possono utilizzare i dati o ignorarli.
Cane

13
Uso la stessa separazione di dati / codice per la scrittura di FSM, tranne per il fatto che non mi è mai venuto in mente di introdurre stati "jolly". Idea interessante! Tuttavia, iterare l'array di transizioni potrebbe diventare costoso se si hanno molti stati (che è stato il caso per me poiché il codice C è stato generato automaticamente). In tali situazioni, sarebbe più efficiente disporre di una matrice di transizioni per stato. Quindi uno stato non è più un valore enum, ma una tabella di transizione. In questo modo, non è necessario ripetere tutte le transizioni nella macchina, ma solo quelle rilevanti per lo stato corrente.
Frerich Raabe,

78

Le altre risposte sono buone, ma un'implementazione molto "leggera" che ho usato quando la macchina a stati è molto semplice sembra:

enum state { ST_NEW, ST_OPEN, ST_SHIFT, ST_END };

enum state current_state = ST_NEW;

while (current_state != ST_END)
{
    input = get_input();

    switch (current_state)
    {
        case ST_NEW:
        /* Do something with input and set current_state */
        break;

        case ST_OPEN:
        /* Do something different and set current_state */
        break;

        /* ... etc ... */
    }
}

Lo userei quando la macchina a stati è abbastanza semplice da rendere eccessivo l'approccio con il puntatore a funzione e la tabella di transizione dello stato. Questo è spesso utile per l'analisi carattere per carattere o parola per parola.


37

Scusatemi per aver infranto ogni regola dell'informatica, ma una macchina a stati è uno dei pochi posti (posso contare solo due a mano) in cui gotoun'affermazione non è solo più efficiente, ma rende anche il vostro codice più pulito e più facile da leggere. Perchégoto dichiarazioni si basano su etichette, è possibile assegnare un nome ai propri stati anziché dover tenere traccia di un pasticcio di numeri o utilizzare un enum. Inoltre, rende il codice molto più pulito in quanto non è necessaria tutta l'innesto extra di puntatori a funzioni o enormi istruzioni switch e mentre loop. Ho già detto che è anche più efficiente?

Ecco come potrebbe essere una macchina a stati:

void state_machine() {
first_state:
    // Do some stuff here
    switch(some_var) {
    case 0:
        goto first_state;
    case 1:
        goto second_state;
    default:
        return;
    }

second_state:
    // Do some stuff here
    switch(some_var) {
    case 0:
        goto first_state;
    case 1:
        goto second_state;
    default:
        return;
    }
}

Hai un'idea generale. Il punto è che puoi implementare la macchina a stati in modo efficiente e che è relativamente facile da leggere e urla al lettore che stanno guardando una macchina a stati. Nota che se stai usando delle gotoaffermazioni, devi comunque fare attenzione poiché è molto facile spararti nel piede mentre lo fai.


4
Funziona solo se la macchina a stati si trova nell'oggetto di livello superiore. Nel momento in cui qualche altro oggetto a cui vengono occasionalmente sottoposti a polling / messaggi inviati, deve avere uno stato, sei bloccato con questo approccio (che, o devi renderlo molto più complicato)
skrebbel,

1
Questo ti obbliga davvero a utilizzare il multitasking preventivo in tutti i casi tranne il più semplice.
Craig McQueen,

1
Quelle gotos potrebbero essere sostituite con chiamate di funzione. E se un profiler ti dice che il tuo programma sta annegando a causa dell'overhead delle chiamate di funzione, allora puoi sostituire le chiamate con gotos secondo necessità.
Abtin Forouzandeh,

7
@AbtinForouzandeh semplicemente sostituendo le goto con chiamate di funzione causerebbe uno stackoverflow poiché lo stack di chiamate viene cancellato solo in caso di errore.
JustMaximumPower

Sono d'accordo con il metodo goto. Ecco un insieme di macro che lo illustrano. E le macro rendono strutturato il codice come se lo avessi codificato come faresti normalmente. Funziona anche a livello di interruzione che di solito è dove sono necessarie le macchine a stati codeproject.com/Articles/37037/…
eddyq,

30

Potresti prendere in considerazione il compilatore State Machine http://smc.sourceforge.net/

Questa splendida utility open source accetta una descrizione di una macchina a stati in un linguaggio semplice e la compila in una dozzina di lingue, inclusi C e C ++. L'utilità stessa è scritta in Java e può essere inclusa come parte di una build.

La ragione per fare questo, piuttosto che la codifica manuale usando il modello di stato GoF o qualsiasi altro approccio, è che una volta che la macchina a stati è espressa come codice, la struttura sottostante tende a scomparire sotto il peso della piastra di caldaia che deve essere generata per supportarla. L'uso di questo approccio offre un'eccellente separazione delle preoccupazioni e mantiene "visibile" la struttura della macchina a stati. Il codice generato automaticamente va in moduli che non è necessario toccare, in modo da poter tornare indietro e giocherellare con la struttura della macchina a stati senza influire sul codice di supporto che hai scritto.

Mi dispiace, sono troppo entusiasta e senza dubbio rimandando tutti. Ma è un'utilità di prim'ordine e anche ben documentata.


20

Assicurati di controllare il lavoro di Miro Samek (blog State Space , sito Web State Machines & Tools ), i cui articoli nel C / C ++ Users Journal sono stati fantastici.

Il sito Web contiene un'implementazione completa (C / C ++) sia in licenza open source che commerciale di un framework di macchine a stati (QP Framework) , un gestore di eventi (QEP) , uno strumento di modellazione di base (QM) e uno strumento di tracciamento (QSpy) che consentire di disegnare macchine a stati, creare codice ed eseguirne il debug.

Il libro contiene un'ampia spiegazione sul cosa / perché dell'implementazione e su come usarlo ed è anche un ottimo materiale per comprendere i fondamenti delle macchine a stato gerarchico e a stati finiti.

Il sito Web contiene anche collegamenti a numerosi pacchetti di supporto per schede per l'uso del software con piattaforme integrate.


Ho modificato il titolo della domanda in base al tuo gioco di parole.
jldupont,

@jldupont: volevo solo dire che era meglio chiarire. Ho eliminato le parti irrilevanti della mia risposta ora.
Daniel Daranas,

1
Ho aggiunto cosa aspettarmi dal sito Web / libro, avendo usato il software con successo da solo; è il miglior libro sul mio scaffale.
Adriaan,

@Adriann, ottima spiegazione! Ho appena modificato la home del sito Web, il link precedente aveva smesso di funzionare.
Daniel Daranas,

2
I collegamenti sono morti o puntano alla pagina iniziale del sito che sembra aver cambiato direzione verso il software incorporato. Puoi ancora vedere alcuni dei contenuti su state-machine.com/resources/articles.php , ma anche lì la maggior parte dei collegamenti relativi alle macchine a stati sono morti. Questo è uno degli unici buoni collegamenti lì: state-machine.com/resources/…
Tatiana Racheva

11

Ho fatto qualcosa di simile a ciò che descrive paxdiablo, solo invece di un array di transizioni stato / evento, ho impostato un array bidimensionale di puntatori a funzione, con il valore dell'evento come indice di un asse e il valore dello stato corrente come l'altro. Quindi chiamo state = state_table[event][state](params)e succede la cosa giusta. Le celle che rappresentano combinazioni di stato / evento non valide ottengono un puntatore a una funzione che lo dice, ovviamente.

Ovviamente, questo funziona solo se i valori di stato ed evento sono entrambi intervalli contigui e iniziano da 0 o abbastanza vicini.


1
Sembra che questa soluzione non si ridimensioni bene: troppo riempimento della tabella, no?
jldupont,

2
+1. Il problema del ridimensionamento qui è la memoria: la mia soluzione ha un problema di ridimensionamento relativo al tempo, ovvero al tempo impiegato per scansionare la tabella delle transizioni (sebbene sia possibile ottimizzare manualmente per le transizioni più comuni). Questo sacrifica la memoria per la velocità - è solo un compromesso. Probabilmente avresti bisogno di controlli per limiti ma non è una cattiva soluzione.
paxdiablo,

Ragazzi - Il mio commento non è stato pubblicato come previsto: intendevo dire che è molto più laborioso e soggetto a errori. Se si aggiunge uno stato / evento, è necessario eseguire molte modifiche.
jldupont,

3
Nessuno ha detto che l'array 2D è stato inizializzato a mano. Forse c'è qualcosa che legge un file di configurazione e lo crea (o almeno potrebbe esserci).
John Zwinck,

Un modo per inizializzare matrici del genere è utilizzare il preprocessore essendo un raccoglitore tardivo (al contrario del legame anticipato). Si definisce un elenco di tutti gli stati #define STATE_LIST() \STATE_LIST_ENTRY(state1)\STATE_LIST_ENTRY(state2)\...(una nuova riga implicita dopo ciascuno \ ) in cui (ri) si definisce la macro di immissione quando si utilizza la macro STATE_LIST. Esempio - facendo una serie di nomi di stato: #define STATE_LIST_ENTRY(s) #s , \n const char *state_names[] = { STATE_LIST() };\n #undef STATE_LIST_ENTRY. Alcuni lavorano prima per essere installati, ma questo è estremamente potente. Aggiungi nuovo stato -> garantito senza perdere.
hlovdal

9

Stefan Heinzmann offre un bellissimo "framework" per macchine a stati C ++ basato su template nel suo articolo .

Poiché nell'articolo non è presente alcun collegamento a un download completo del codice, mi sono preso la libertà di incollare il codice in un progetto e verificarlo. Le cose sotto sono testate e includono i pochi pezzi mancanti minori ma abbastanza ovvi.

La principale innovazione qui è che il compilatore sta generando un codice molto efficiente. Le azioni vuote di entrata / uscita non hanno alcun costo. Le azioni di entrata / uscita non vuote sono in linea. Il compilatore sta inoltre verificando la completezza dello statechart. Le azioni mancanti generano errori di collegamento. L'unica cosa che non viene catturata è la mancanzaTop::init .

Questa è una bella alternativa all'implementazione di Miro Samek, se puoi vivere senza ciò che manca - questo è ben lungi dall'essere una completa implementazione di Statechart UML, sebbene implementa correttamente la semantica UML, mentre il codice di Samek in base alla progettazione non gestisce l'uscita / transizione / azioni di inserimento nell'ordine corretto.

Se questo codice funziona per quello che devi fare e hai un compilatore C ++ decente per il tuo sistema, probabilmente funzionerà meglio dell'implementazione C / C ++ di Miro. Il compilatore genera un'implementazione della macchina a stati di transizione O (1) appiattita. Se l'audit dell'output dell'assembly conferma che le ottimizzazioni funzionano come desiderato, ci si avvicina alle prestazioni teoriche. La parte migliore: è un codice relativamente piccolo, facile da capire.

#ifndef HSM_HPP
#define HSM_HPP

// This code is from:
// Yet Another Hierarchical State Machine
// by Stefan Heinzmann
// Overload issue 64 december 2004
// http://accu.org/index.php/journals/252

/* This is a basic implementation of UML Statecharts.
 * The key observation is that the machine can only
 * be in a leaf state at any given time. The composite
 * states are only traversed, never final.
 * Only the leaf states are ever instantiated. The composite
 * states are only mechanisms used to generate code. They are
 * never instantiated.
 */

// Helpers

// A gadget from Herb Sutter's GotW #71 -- depends on SFINAE
template<class D, class B>
class IsDerivedFrom {
    class Yes { char a[1]; };
    class No  { char a[10]; };
    static Yes Test(B*); // undefined
    static No Test(...); // undefined
public:
    enum { Res = sizeof(Test(static_cast<D*>(0))) == sizeof(Yes) ? 1 : 0 };
};

template<bool> class Bool {};

// Top State, Composite State and Leaf State

template <typename H>
struct TopState {
    typedef H Host;
    typedef void Base;
    virtual void handler(Host&) const = 0;
    virtual unsigned getId() const = 0;
};

template <typename H, unsigned id, typename B>
struct CompState;

template <typename H, unsigned id, typename B = CompState<H, 0, TopState<H> > >
struct CompState : B {
    typedef B Base;
    typedef CompState<H, id, Base> This;
    template <typename X> void handle(H& h, const X& x) const { Base::handle(h, x); }
    static void init(H&); // no implementation
    static void entry(H&) {}
    static void exit(H&) {}
};

template <typename H>
struct CompState<H, 0, TopState<H> > : TopState<H> {
    typedef TopState<H> Base;
    typedef CompState<H, 0, Base> This;
    template <typename X> void handle(H&, const X&) const {}
    static void init(H&); // no implementation
    static void entry(H&) {}
    static void exit(H&) {}
};

template <typename H, unsigned id, typename B = CompState<H, 0, TopState<H> > >
struct LeafState : B {
    typedef H Host;
    typedef B Base;
    typedef LeafState<H, id, Base> This;
    template <typename X> void handle(H& h, const X& x) const { Base::handle(h, x); }
    virtual void handler(H& h) const { handle(h, *this); }
    virtual unsigned getId() const { return id; }
    static void init(H& h) { h.next(obj); } // don't specialize this
    static void entry(H&) {}
    static void exit(H&) {}
    static const LeafState obj; // only the leaf states have instances
};

template <typename H, unsigned id, typename B>
const LeafState<H, id, B> LeafState<H, id, B>::obj;

// Transition Object

template <typename C, typename S, typename T>
// Current, Source, Target
struct Tran {
    typedef typename C::Host Host;
    typedef typename C::Base CurrentBase;
    typedef typename S::Base SourceBase;
    typedef typename T::Base TargetBase;
    enum { // work out when to terminate template recursion
        eTB_CB = IsDerivedFrom<TargetBase, CurrentBase>::Res,
        eS_CB = IsDerivedFrom<S, CurrentBase>::Res,
        eS_C = IsDerivedFrom<S, C>::Res,
        eC_S = IsDerivedFrom<C, S>::Res,
        exitStop = eTB_CB && eS_C,
        entryStop = eS_C || eS_CB && !eC_S
    };
    // We use overloading to stop recursion.
    // The more natural template specialization
    // method would require to specialize the inner
    // template without specializing the outer one,
    // which is forbidden.
    static void exitActions(Host&, Bool<true>) {}
    static void exitActions(Host&h, Bool<false>) {
        C::exit(h);
        Tran<CurrentBase, S, T>::exitActions(h, Bool<exitStop>());
    }
    static void entryActions(Host&, Bool<true>) {}
    static void entryActions(Host& h, Bool<false>) {
        Tran<CurrentBase, S, T>::entryActions(h, Bool<entryStop>());
        C::entry(h);
    }
    Tran(Host & h) : host_(h) {
        exitActions(host_, Bool<false>());
    }
    ~Tran() {
        Tran<T, S, T>::entryActions(host_, Bool<false>());
        T::init(host_);
    }
    Host& host_;
};

// Initializer for Compound States

template <typename T>
struct Init {
    typedef typename T::Host Host;
    Init(Host& h) : host_(h) {}
    ~Init() {
        T::entry(host_);
        T::init(host_);
    }
    Host& host_;
};

#endif // HSM_HPP

Segue il codice di prova.

#include <cstdio>
#include "hsm.hpp"
#include "hsmtest.hpp"

/* Implements the following state machine from Miro Samek's
 * Practical Statecharts in C/C++
 *
 * |-init-----------------------------------------------------|
 * |                           s0                             |
 * |----------------------------------------------------------|
 * |                                                          |
 * |    |-init-----------|        |-------------------------| |
 * |    |       s1       |---c--->|            s2           | |
 * |    |----------------|<--c----|-------------------------| |
 * |    |                |        |                         | |
 * |<-d-| |-init-------| |        | |-init----------------| | |
 * |    | |     s11    |<----f----| |          s21        | | |
 * | /--| |------------| |        | |---------------------| | |
 * | a  | |            | |        | |                     | | |
 * | \->| |            |------g--------->|-init------|    | | |
 * |    | |____________| |        | |-b->|    s211   |---g--->|
 * |    |----b---^       |------f------->|           |    | | |
 * |    |________________|        | |<-d-|___________|<--e----|
 * |                              | |_____________________| | |
 * |                              |_________________________| |
 * |__________________________________________________________|
 */

class TestHSM;

typedef CompState<TestHSM,0>     Top;
typedef CompState<TestHSM,1,Top>   S0;
typedef CompState<TestHSM,2,S0>      S1;
typedef LeafState<TestHSM,3,S1>        S11;
typedef CompState<TestHSM,4,S0>      S2;
typedef CompState<TestHSM,5,S2>        S21;
typedef LeafState<TestHSM,6,S21>         S211;

enum Signal { A_SIG, B_SIG, C_SIG, D_SIG, E_SIG, F_SIG, G_SIG, H_SIG };

class TestHSM {
public:
    TestHSM() { Top::init(*this); }
    ~TestHSM() {}
    void next(const TopState<TestHSM>& state) {
        state_ = &state;
    }
    Signal getSig() const { return sig_; }
    void dispatch(Signal sig) {
        sig_ = sig;
        state_->handler(*this);
    }
    void foo(int i) {
        foo_ = i;
    }
    int foo() const {
        return foo_;
    }
private:
    const TopState<TestHSM>* state_;
    Signal sig_;
    int foo_;
};

bool testDispatch(char c) {
    static TestHSM test;
    if (c<'a' || 'h'<c) {
        return false;
    }
    printf("Signal<-%c", c);
    test.dispatch((Signal)(c-'a'));
    printf("\n");
    return true;
}

int main(int, char**) {
    testDispatch('a');
    testDispatch('e');
    testDispatch('e');
    testDispatch('a');
    testDispatch('h');
    testDispatch('h');
    return 0;
}

#define HSMHANDLER(State) \
    template<> template<typename X> inline void State::handle(TestHSM& h, const X& x) const

HSMHANDLER(S0) {
    switch (h.getSig()) {
    case E_SIG: { Tran<X, This, S211> t(h);
        printf("s0-E;");
        return; }
    default:
        break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S1) {
    switch (h.getSig()) {
    case A_SIG: { Tran<X, This, S1> t(h);
        printf("s1-A;"); return; }
    case B_SIG: { Tran<X, This, S11> t(h);
        printf("s1-B;"); return; }
    case C_SIG: { Tran<X, This, S2> t(h);
        printf("s1-C;"); return; }
    case D_SIG: { Tran<X, This, S0> t(h);
        printf("s1-D;"); return; }
    case F_SIG: { Tran<X, This, S211> t(h);
        printf("s1-F;"); return; }
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S11) {
    switch (h.getSig()) {
    case G_SIG: { Tran<X, This, S211> t(h);
        printf("s11-G;"); return; }
    case H_SIG: if (h.foo()) {
            printf("s11-H");
            h.foo(0); return;
        } break;
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S2) {
    switch (h.getSig()) {
    case C_SIG: { Tran<X, This, S1> t(h);
        printf("s2-C"); return; }
    case F_SIG: { Tran<X, This, S11> t(h);
        printf("s2-F"); return; }
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S21) {
    switch (h.getSig()) {
    case B_SIG: { Tran<X, This, S211> t(h);
        printf("s21-B;"); return; }
    case H_SIG: if (!h.foo()) {
            Tran<X, This, S21> t(h);
            printf("s21-H;"); h.foo(1);
            return;
        } break;
    default: break;
    }
    return Base::handle(h, x);
}

HSMHANDLER(S211) {
    switch (h.getSig()) {
    case D_SIG: { Tran<X, This, S21> t(h);
        printf("s211-D;"); return; }
    case G_SIG: { Tran<X, This, S0> t(h);
        printf("s211-G;"); return; }
    }
    return Base::handle(h, x);
}

#define HSMENTRY(State) \
    template<> inline void State::entry(TestHSM&) { \
        printf(#State "-ENTRY;"); \
    }

HSMENTRY(S0)
HSMENTRY(S1)
HSMENTRY(S11)
HSMENTRY(S2)
HSMENTRY(S21)
HSMENTRY(S211)

#define HSMEXIT(State) \
    template<> inline void State::exit(TestHSM&) { \
        printf(#State "-EXIT;"); \
    }

HSMEXIT(S0)
HSMEXIT(S1)
HSMEXIT(S11)
HSMEXIT(S2)
HSMEXIT(S21)
HSMEXIT(S211)

#define HSMINIT(State, InitState) \
    template<> inline void State::init(TestHSM& h) { \
       Init<InitState> i(h); \
       printf(#State "-INIT;"); \
    }

HSMINIT(Top, S0)
HSMINIT(S0, S1)
HSMINIT(S1, S11)
HSMINIT(S2, S21)
HSMINIT(S21, S211)

Hmm ... sth manca nel tuo codice. Prima di tutto includi due intestazioni, ma fornisci solo la prima. Quando commento solo l'istruzione "include" ottengo questo errore durante la compilazione: d: \ 1 \ hsm> g ++ test.cpp test.cpp: 195: 1: errore: specializzazione di 'statico vuoto CompState <H, id, B> :: init (H &) [con H = TestHSM; unsigned int id = 0u; B = CompState <TestHSM, 0u, TopState <TestHSM>>] 'dopo l'istanza
Freddie Chopin

Ho dovuto spostare le definizioni di tutto HSMINIT () per essere al di sopra della classe TestHSM e si compila e funziona bene (; L'unica cosa che non va è il fatto che tutte le transizioni sono "esterne", mentre dovrebbero essere "interne" - c'era alcuni dibattiti al riguardo nell'articolo e l'autore ha deciso che "extrenal" era giusto, ma le frecce usate suggeriscono "interno".
Freddie Chopin

5

La tecnica che mi piace per le macchine a stati (almeno quelle per il controllo del programma) è quella di utilizzare i puntatori a funzione. Ogni stato è rappresentato da una diversa funzione. La funzione accetta un simbolo di input e restituisce il puntatore alla funzione per lo stato successivo. I monitor del ciclo di invio centrale accettano l'input successivo, lo alimentano allo stato corrente ed elaborano il risultato.

La digitazione su di essa diventa un po 'strana, poiché C non ha un modo per indicare i tipi di puntatori a funzioni che si restituiscono, quindi le funzioni di stato ritornano void*. Ma puoi fare qualcosa del genere:

typedef void* (*state_handler)(input_symbol_t);
void dispatch_fsm()
{
    state_handler current = initial_handler;
    /* Let's assume returning null indicates end-of-machine */
    while (current) {
        current = current(get_input);
    }
 }

Quindi le singole funzioni di stato possono attivare il loro input per elaborare e restituire il valore appropriato.


Il +1 è davvero bello e offre dei bei posti per la funzionalità all'interno delle funzioni di transizione
Fire Crow,

5

Caso più semplice

enum event_type { ET_THIS, ET_THAT };
union event_parm { uint8_t this; uint16_t that; }
static void handle_event(enum event_type event, union event_parm parm)
{
  static enum { THIS, THAT } state;
  switch (state)
  {
    case THIS:
    switch (event)
    {
      case ET_THIS:
      // Handle event.
      break;

      default:
      // Unhandled events in this state.
      break;
    }
    break;

    case THAT:
    // Handle state.
    break;
  }
}

Punti: lo stato è privato, non solo per l'unità di compilazione ma anche per il gestore_evento. Casi speciali possono essere gestiti separatamente dall'interruttore principale usando qualsiasi costrutto ritenuto necessario.

Caso più complesso

Quando l'interruttore diventa più grande di un paio di schermate piene, dividerlo in funzioni che gestiscono ogni stato, usando una tabella di stato per cercare direttamente la funzione. Lo stato è ancora privato per il gestore eventi. Le funzioni del gestore di stato restituiscono lo stato successivo. Se necessario, alcuni eventi possono ancora ricevere un trattamento speciale nel gestore dell'evento principale. Mi piace lanciare pseudo-eventi per l'ingresso e l'uscita dello stato e forse l'avvio della macchina a stati:

enum state_type { THIS, THAT, FOO, NA };
enum event_type { ET_START, ET_ENTER, ET_EXIT, ET_THIS, ET_THAT, ET_WHATEVER, ET_TIMEOUT };
union event_parm { uint8_t this; uint16_t that; };
static void handle_event(enum event_type event, union event_parm parm)
{
  static enum state_type state;
  static void (* const state_handler[])(enum event_type event, union event_parm parm) = { handle_this, handle_that };
  enum state_type next_state = state_handler[state](event, parm);
  if (NA != next_state && state != next_state)
  {
    (void)state_handler[state](ET_EXIT, 0);
    state = next_state;
    (void)state_handler[state](ET_ENTER, 0);
  }
}

Non sono sicuro di aver inchiodato la sintassi, soprattutto per quanto riguarda l'array di puntatori a funzioni. Non ho eseguito nulla di tutto ciò attraverso un compilatore. Dopo la revisione, ho notato che mi ero dimenticato di scartare esplicitamente lo stato successivo durante la gestione degli pseudo eventi (la parentesi (nulla) prima della chiamata a state_handler ()). Questo è qualcosa che mi piace fare anche se i compilatori accettano silenziosamente l'omissione. Indica ai lettori del codice che "sì, in realtà intendevo chiamare la funzione senza utilizzare il valore restituito" e potrebbe impedire agli strumenti di analisi statica di avvertire al riguardo. Può essere idiosincratico perché non ricordo di aver visto nessuno fare questo.

Punti: aggiungendo un po 'di complessità (controllando se lo stato successivo è diverso da quello attuale), è possibile evitare altro codice duplicato, perché le funzioni del gestore di stato possono godere degli pseudo eventi che si verificano quando uno stato viene inserito e lasciato. Ricordare che lo stato non può cambiare durante la gestione degli eventi pseudo, poiché il risultato del gestore stato viene scartato dopo questi eventi. Ovviamente puoi scegliere di modificare il comportamento.

Un gestore di stato sarebbe simile al seguente:

static enum state_type handle_this(enum event_type event, union event_parm parm)
{
  enum state_type next_state = NA;
  switch (event)
  {
    case ET_ENTER:
    // Start a timer to do whatever.
    // Do other stuff necessary when entering this state.
    break;

    case ET_WHATEVER:
    // Switch state.
    next_state = THAT;
    break;

    case ET_TIMEOUT:
    // Switch state.
    next_state = FOO;
    break;

    case ET_EXIT:
    // Stop the timer.
    // Generally clean up this state.
    break;
  }
  return next_state;
}

Più complessità

Quando l'unità di compilazione diventa troppo grande (qualunque cosa tu pensi che sia, dovrei dire circa 1000 righe), metti ciascun gestore di stato in un file separato. Quando ciascun gestore di stato diventa più lungo di un paio di schermate, dividere ogni evento in una funzione separata, in modo simile al modo in cui l'interruttore di stato è stato diviso. Puoi farlo in diversi modi, separatamente dallo stato o usando una tabella comune o combinando vari schemi. Alcuni sono stati coperti qui da altri. Ordina le tue tabelle e usa la ricerca binaria se la velocità è un requisito.

Programmazione generica

Vorrei che il preprocessore affrontasse problemi come l'ordinamento di tabelle o persino la generazione di macchine a stati dalle descrizioni, consentendo di "scrivere programmi sui programmi". Credo che questo sia ciò per cui le persone di Boost stanno sfruttando i modelli C ++, ma trovo che la sintassi sia criptica.

Tabelle bidimensionali

Ho usato tabelle stato / eventi in passato, ma devo dire che per i casi più semplici non le trovo necessarie e preferisco la chiarezza e la leggibilità dell'istruzione switch anche se si estende oltre una schermata intera. Per casi più complessi le tabelle sfuggono rapidamente di mano, come altri hanno notato. Gli idiomi che presento qui ti consentono di aggiungere una serie di eventi e stati quando ne hai voglia, senza dover mantenere una tabella che consuma memoria (anche se può essere memoria di programma).

disconoscimento

I bisogni speciali possono rendere questi idiomi meno utili, ma li ho trovati molto chiari e mantenibili.


Eviterei "questo" come nome di variabile o simbolo solo per l'associazione, anche se in realtà non è una parola riservata.
XTL

4

Estremamente non testato, ma divertente da programmare, ora in una versione più raffinata della mia risposta originale; versioni aggiornate sono disponibili su mercurial.intuxication.org :

sm.h

#ifndef SM_ARGS
#error "SM_ARGS undefined: " \
    "use '#define SM_ARGS (void)' to get an empty argument list"
#endif

#ifndef SM_STATES
#error "SM_STATES undefined: " \
    "you must provide a list of comma-separated states"
#endif

typedef void (*sm_state) SM_ARGS;
static const sm_state SM_STATES;

#define sm_transit(STATE) ((sm_state (*) SM_ARGS)STATE)

#define sm_def(NAME) \
    static sm_state NAME ## _fn SM_ARGS; \
    static const sm_state NAME = (sm_state)NAME ## _fn; \
    static sm_state NAME ## _fn SM_ARGS

example.c

#include <stdio.h>

#define SM_ARGS (int i)
#define SM_STATES EVEN, ODD
#include "sm.h"

sm_def(EVEN)
{
    printf("even %i\n", i);
    return ODD;
}

sm_def(ODD)
{
    printf("odd  %i\n", i);
    return EVEN;
}

int main(void)
{
    int i = 0;
    sm_state state = EVEN;

    for(; i < 10; ++i)
        state = sm_transit(state)(i);

    return 0;
}

14
Adoro il commento "estremamente non testato". Sembra indicare che ci sono gradi di non testamento e che hai fatto un bel po 'di sforzo per non
provarlo

@Christoph il link in questa risposta è rotto. Inoltre, hai testato questo codice o no? Se è stato testato e funziona, dovresti rimuoverlo dalla risposta. Potresti anche mostrare un esempio del codice che risulta dopo l'espansione delle macro. Mi piace l'idea generale.
Joakim,

4

Mi è piaciuta molto la risposta di paxdiable e ho deciso di implementare tutte le funzionalità mancanti per la mia applicazione come variabili di protezione e dati specifici della macchina a stati.

Ho caricato la mia implementazione su questo sito per condividerla con la community. È stato testato utilizzando IAR Embedded Workbench per ARM.

https://sourceforge.net/projects/compactfsm/


Trovandolo nel 2018 e che è ancora applicabile. Stavo leggendo la risposta di @paxdiablo e in precedenza ho usato con successo quel tipo di implementazione nei sistemi integrati. Questa soluzione aggiunge le cose mancanti dalla risposta di paxdiablos :)
Kristoffer,

4

Un altro interessante strumento open source è Yakindu Statechart Tools su statecharts.org . Fa uso di statechart Harel e quindi fornisce stati gerarchici e paralleli e genera codice C e C ++ (oltre a Java). Non utilizza le librerie ma segue un approccio di "codice semplice". Il codice applica fondamentalmente le strutture a cassa. I generatori di codice possono anche essere personalizzati. Inoltre, lo strumento offre molte altre funzionalità.


3

Venendo a questo in ritardo (come al solito) ma analizzando le risposte fino ad oggi penso che manchi qualcosa di importante;

Nei miei progetti ho scoperto che può essere molto utile non avere una funzione per ogni combinazione stato / evento valida. Mi piace l'idea di avere effettivamente una tabella 2D di stati / eventi. Ma mi piace che gli elementi della tabella siano più di un semplice puntatore a funzione. Invece, provo ad organizzare il mio design in modo che al suo interno comprenda un insieme di semplici elementi o azioni atomici. In questo modo posso elencare quei semplici elementi atomici ad ogni intersezione della mia tabella stato / evento. L'idea è che non lo fai è necessario definire una massa di N funzioni quadrate (in genere molto semplici). Perché qualcosa di così incline agli errori, che richiede tempo, difficile da scrivere, difficile da leggere, lo chiami?

Includo anche un nuovo stato facoltativo e un puntatore a funzione opzionale per ogni cella della tabella. Il puntatore a funzione è lì per quei casi eccezionali in cui non si desidera semplicemente sparare un elenco di azioni atomiche.

Sai che lo stai facendo bene quando puoi esprimere molte funzionalità diverse, semplicemente modificando la tabella, senza un nuovo codice da scrivere.


2
Forse un esempio sarebbe carino, no?
jldupont,

1
Un esempio realistico che può essere presentato in modo isolato è un compito impegnativo che richiederebbe più tempo di quello che sono disposto a dare proprio al momento. C'è qualcosa nel mio post che è particolarmente difficile da capire? Forse posso esprimerlo più chiaramente. L'idea è molto semplice; Non definire un meccanismo di stato che richiede una funzione separata per ogni combinazione evento / stato, in questo modo si ottengono troppe funzioni. Trova invece un altro modo per descrivere la funzionalità desiderata per quella combinazione evento / stato, almeno nella maggior parte dei casi.
Bill Forster,

2
Capito: un esempio di pseudo-codice sarebbe stato buono ma il tuo punto è chiaro.
jldupont,

3

Comunque, penso che il mio sia solo un po 'diverso da quello di tutti gli altri. Un po 'più di separazione di codice e dati rispetto a quanto vedo nelle altre risposte. Ho davvero letto la teoria per scrivere questo, che implementa un linguaggio regolare completo (senza espressioni regolari, purtroppo). Ullman, Minsky, Chomsky. Non posso dire di aver capito tutto, ma ho attinto dai vecchi maestri il più direttamente possibile: attraverso le loro parole.

Uso un puntatore a funzione per un predicato che determina la transizione verso uno stato "sì" o uno "no". Ciò facilita la creazione di un accettore di stato finito per un linguaggio regolare che si programma in un modo più simile a un linguaggio assembly. Per favore, non lasciarti scoraggiare dalle mie sciocche scelte di nome. 'czek' == 'check'. 'grok' == [vai a cercarlo nel Dizionario Hacker].

Quindi per ogni iterazione, czek chiama una funzione predicata con il carattere corrente come argomento. Se il predicato ritorna vero, il personaggio viene consumato (il puntatore è avanzato) e seguiamo la transizione 'y' per selezionare lo stato successivo. Se il predicato restituisce false, il personaggio NON viene consumato e seguiamo la transizione 'n'. Quindi ogni istruzione è un ramo a due vie! All'epoca dovevo aver letto La storia di Mel.

Questo codice proviene direttamente dal mio interprete PostScript e si è evoluto nella sua forma attuale con molte indicazioni dai colleghi su comp.lang.c. Poiché PostScript non ha praticamente sintassi (richiede solo parentesi bilanciate), un Accettatore di linguaggio regolare come questo funziona anche come parser.

/* currentstr is set to the start of string by czek
   and used by setrad (called by israd) to set currentrad
   which is used by israddig to determine if the character
   in question is valid for the specified radix
   --
   a little semantic checking in the syntax!
 */
char *currentstr;
int currentrad;
void setrad(void) {
    char *end;
    currentrad = strtol(currentstr, &end, 10);
    if (*end != '#' /* just a sanity check,
                       the automaton should already have determined this */
    ||  currentrad > 36
    ||  currentrad < 2)
        fatal("bad radix"); /* should probably be a simple syntaxerror */
}

/*
   character classes
   used as tests by automatons under control of czek
 */
char *alpha = "0123456789" "ABCDE" "FGHIJ" "KLMNO" "PQRST" "UVWXYZ";
#define EQ(a,b) a==b
#define WITHIN(a,b) strchr(a,b)!=NULL
int israd  (int c) {
    if (EQ('#',c)) { setrad(); return true; }
    return false;
}
int israddig(int c) {
    return strchrnul(alpha,toupper(c))-alpha <= currentrad;
}
int isdot  (int c) {return EQ('.',c);}
int ise    (int c) {return WITHIN("eE",c);}
int issign (int c) {return WITHIN("+-",c);}
int isdel  (int c) {return WITHIN("()<>[]{}/%",c);}
int isreg  (int c) {return c!=EOF && !isspace(c) && !isdel(c);}
#undef WITHIN
#undef EQ

/*
   the automaton type
 */
typedef struct { int (*pred)(int); int y, n; } test;

/*
   automaton to match a simple decimal number
 */
/* /^[+-]?[0-9]+$/ */
test fsm_dec[] = {
/* 0*/ { issign,  1,  1 },
/* 1*/ { isdigit, 2, -1 },
/* 2*/ { isdigit, 2, -1 },
};
int acc_dec(int i) { return i==2; }

/*
   automaton to match a radix number
 */
/* /^[0-9]+[#][a-Z0-9]+$/ */
test fsm_rad[] = {
/* 0*/ { isdigit,  1, -1 },
/* 1*/ { isdigit,  1,  2 },
/* 2*/ { israd,    3, -1 },
/* 3*/ { israddig, 4, -1 },
/* 4*/ { israddig, 4, -1 },
};
int acc_rad(int i) { return i==4; }

/*
   automaton to match a real number
 */
/* /^[+-]?(d+(.d*)?)|(d*.d+)([eE][+-]?d+)?$/ */
/* represents the merge of these (simpler) expressions
   [+-]?[0-9]+\.[0-9]*([eE][+-]?[0-9]+)?
   [+-]?[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?
   The complexity comes from ensuring at least one
   digit in the integer or the fraction with optional
   sign and optional optionally-signed exponent.
   So passing isdot in state 3 means at least one integer digit has been found
   but passing isdot in state 4 means we must find at least one fraction digit
   via state 5 or the whole thing is a bust.
 */
test fsm_real[] = {
/* 0*/ { issign,  1,   1 },
/* 1*/ { isdigit, 2,   4 },
/* 2*/ { isdigit, 2,   3 },
/* 3*/ { isdot,   6,   7 },
/* 4*/ { isdot,   5,  -1 },
/* 5*/ { isdigit, 6,  -1 },
/* 6*/ { isdigit, 6,   7 },
/* 7*/ { ise,     8,  -1 },
/* 8*/ { issign,  9,   9 },
/* 9*/ { isdigit, 10, -1 },
/*10*/ { isdigit, 10, -1 },
};
int acc_real(int i) {
    switch(i) {
        case 2: /* integer */
        case 6: /* real */
        case 10: /* real with exponent */
            return true;
    }
    return false;
}

/*
   Helper function for grok.
   Execute automaton against the buffer,
   applying test to each character:
       on success, consume character and follow 'y' transition.
       on failure, do not consume but follow 'n' transition.
   Call yes function to determine if the ending state
   is considered an acceptable final state.
   A transition to -1 represents rejection by the automaton
 */
int czek (char *s, test *fsm, int (*yes)(int)) {
    int sta = 0;
    currentstr = s;
    while (sta!=-1 && *s) {
        if (fsm[sta].pred((int)*s)) {
            sta=fsm[sta].y;
            s++;
        } else {
            sta=fsm[sta].n;
        }
    }
    return yes(sta);
}

/*
   Helper function for toke.
   Interpret the contents of the buffer,
   trying automatons to match number formats;
   and falling through to a switch for special characters.
   Any token consisting of all regular characters
   that cannot be interpreted as a number is an executable name
 */
object grok (state *st, char *s, int ns,
    object *src,
    int (*next)(state *,object *),
    void (*back)(state *,int, object *)) {

    if (czek(s, fsm_dec, acc_dec)) {
        long num;
        num = strtol(s,NULL,10);
        if ((num==LONG_MAX || num==LONG_MIN) && errno==ERANGE) {
            error(st,limitcheck);
/*       } else if (num > INT_MAX || num < INT_MIN) { */
/*           error(limitcheck, OP_token); */
        } else {
            return consint(num);
        }
    }

    else if (czek(s, fsm_rad, acc_rad)) {
        long ra,num;
        ra = (int)strtol(s,NULL,10);
        if (ra > 36 || ra < 2) {
            error(st,limitcheck);
        }
        num = strtol(strchr(s,'#')+1, NULL, (int)ra);
        if ((num==LONG_MAX || num==LONG_MIN) && errno==ERANGE) {
            error(st,limitcheck);
/*       } else if (num > INT_MAX || num < INT_MAX) { */
/*           error(limitcheck, OP_token); */
        } else {
            return consint(num);
        }
    }

    else if (czek(s, fsm_real, acc_real)) {
        double num;
        num = strtod(s,NULL);
        if ((num==HUGE_VAL || num==-HUGE_VAL) && errno==ERANGE) {
            error(st,limitcheck);
        } else {
            return consreal(num);
        }
    }

    else switch(*s) {
        case '(': {
            int c, defer=1;
            char *sp = s;

            while (defer && (c=next(st,src)) != EOF ) {
                switch(c) {
                    case '(': defer++; break;
                    case ')': defer--;
                        if (!defer) goto endstring;
                        break;
                    case '\\': c=next(st,src);
                        switch(c) {
                            case '\n': continue;
                            case 'a': c = '\a'; break;
                            case 'b': c = '\b'; break;
                            case 'f': c = '\f'; break;
                            case 'n': c = '\n'; break;
                            case 'r': c = '\r'; break;
                            case 't': c = '\t'; break;
                            case 'v': c = '\v'; break;
                            case '\'': case '\"':
                            case '(': case ')':
                            default: break;
                        }
                }
                if (sp-s>ns) error(st,limitcheck);
                else *sp++ = c;
            }
endstring:  *sp=0;
            return cvlit(consstring(st,s,sp-s));
        }

        case '<': {
            int c;
            char d, *x = "0123456789abcdef", *sp = s;
            while (c=next(st,src), c!='>' && c!=EOF) {
                if (isspace(c)) continue;
                if (isxdigit(c)) c = strchr(x,tolower(c)) - x;
                else error(st,syntaxerror);
                d = (char)c << 4;
                while (isspace(c=next(st,src))) /*loop*/;
                if (isxdigit(c)) c = strchr(x,tolower(c)) - x;
                else error(st,syntaxerror);
                d |= (char)c;
                if (sp-s>ns) error(st,limitcheck);
                *sp++ = d;
            }
            *sp = 0;
            return cvlit(consstring(st,s,sp-s));
        }

        case '{': {
            object *a;
            size_t na = 100;
            size_t i;
            object proc;
            object fin;

            fin = consname(st,"}");
            (a = malloc(na * sizeof(object))) || (fatal("failure to malloc"),0);
            for (i=0 ; objcmp(st,a[i]=toke(st,src,next,back),fin) != 0; i++) {
                if (i == na-1)
                (a = realloc(a, (na+=100) * sizeof(object))) || (fatal("failure to malloc"),0);
            }
            proc = consarray(st,i);
            { size_t j;
                for (j=0; j<i; j++) {
                    a_put(st, proc, j, a[j]);
                }
            }
            free(a);
            return proc;
        }

        case '/': {
            s[1] = (char)next(st,src);
            puff(st, s+2, ns-2, src, next, back);
            if (s[1] == '/') {
                push(consname(st,s+2));
                opexec(st, op_cuts.load);
                return pop();
            }
            return cvlit(consname(st,s+1));
        }

        default: return consname(st,s);
    }
    return null; /* should be unreachable */
}

/*
   Helper function for toke.
   Read into buffer any regular characters.
   If we read one too many characters, put it back
   unless it's whitespace.
 */
int puff (state *st, char *buf, int nbuf,
    object *src,
    int (*next)(state *,object *),
    void (*back)(state *,int, object *)) {
    int c;
    char *s = buf;
    while (isreg(c=next(st,src))) {
        if (s-buf >= nbuf-1) return false;
        *s++ = c;
    }
    *s = 0;
    if (!isspace(c) && c != EOF) back(st,c,src); /* eat interstice */
    return true;
}

/*
   Helper function for Stoken Ftoken.
   Read a token from src using next and back.
   Loop until having read a bona-fide non-whitespace non-comment character.
   Call puff to read into buffer up to next delimiter or space.
   Call grok to figure out what it is.
 */
#define NBUF MAXLINE
object toke (state *st, object *src,
        int (*next)(state *, object *),
        void (*back)(state *, int, object *)) {
    char buf[NBUF] = "", *s=buf;
    int c,sta = 1;
    object o;

    do {
        c=next(st,src);
        //if (c==EOF) return null;
        if (c=='%') {
            if (DUMPCOMMENTS) fputc(c, stdout);
            do {
                c=next(st,src);
                if (DUMPCOMMENTS) fputc(c, stdout);
            } while (c!='\n' && c!='\f' && c!=EOF);
        }
    } while (c!=EOF && isspace(c));
    if (c==EOF) return null;
    *s++ = c;
    *s = 0;
    if (!isdel(c)) sta=puff(st, s,NBUF-1,src,next,back);

    if (sta) {
        o=grok(st,buf,NBUF-1,src,next,back);
        return o;
    } else {
        return null;
    }
}

2
Questo è ciò che ogni generatore di parser o lexer emetterà volentieri per te. Stranamente così. Se si desidera codificarlo manualmente è discutibile. Ha un merito pedagogico, ovviamente.
Ripristina Monica il



2

L'ho visto da qualche parte

#define FSM
#define STATE(x)      s_##x :
#define NEXTSTATE(x)  goto s_##x

FSM {
  STATE(x) {
    ...
    NEXTSTATE(y);
  }

  STATE(y) {
    ...
    if (x == 0)
      NEXTSTATE(y);
    else
      NEXTSTATE(x);
  }
}

1
È interessante, ma nessun voto fino a quando non fai un esempio o due (e forse un risultato de-macro) o qualche discussione sul perché questo potrebbe essere più pratico di un altro. Interessante utilizzo di parentesi orfane e macro. Immagino che qualcosa di simile potrebbe essere fatto su un linguaggio che fa una sorta di ottimizzazione della ricorsione della coda; potresti usare le chiamate di funzione verso l'alto e non preoccuparti di sovraccaricare lo spazio dello stack con la spazzatura di chiamata di funzione (che penso sia ciò che le macro stanno essenzialmente superando qui)
Ape-inago

2
I vantaggi di questo metodo sono ...? Vedo diversi svantaggi, come le macro offuscate, e il loro utilizzo gotocrea una dipendenza da un sistema operativo multitasking preventivo.
Craig McQueen,

2

Dato che implica che puoi usare il C ++ e quindi il codice OO, suggerirei di valutare il modello di stato "GoF" (GoF = Gang of Four, i ragazzi che hanno scritto il libro dei modelli di design che ha portato alla ribalta i modelli di design).

Non è particolarmente complesso ed è ampiamente usato e discusso, quindi è facile vedere esempi e spiegazioni on line.

Sarà anche molto probabilmente riconoscibile da chiunque altro mantenga il codice in un secondo momento.

Se l'efficienza è la preoccupazione, varrebbe la pena fare un benchmark per assicurarsi che un approccio non OO sia più efficiente poiché molti fattori influenzano le prestazioni e non è sempre semplicemente un codice OO cattivo e funzionale. Allo stesso modo, se l'utilizzo della memoria è un limite per te, vale di nuovo la pena fare alcuni test o calcoli per vedere se questo sarà effettivamente un problema per la tua specifica applicazione se usi il modello di stato.

Di seguito sono riportati alcuni collegamenti al modello di stato "Gof", come suggerisce Craig:


sembra più un commento: potrei suggerirti di trattarlo come tale? cioè non posizionarlo nella sezione "risposta".
jldupont,

Sarebbe utile se si potesse fornire un buon collegamento URL per il "modello di stato GoF", per coloro che non hanno familiarità con esso.
Craig McQueen,

1
@jldupont - commento onesto. Ho modificato il testo per renderlo una risposta adeguata, poiché mi sento basato sull'esperienza personale che, a meno che non ci siano specifici problemi di prestazioni, l'approccio GoF funziona bene e avrà una 'base di utenti' relativamente ampia
Mick,

@Craig: aggiunti alcuni collegamenti. Entrambi sembravano precisi e chiari al momento in cui li ho aggiunti.
Mick,

2

Ecco un esempio di una macchina a stati finiti per Linux che utilizza le code di messaggi come eventi. Gli eventi vengono messi in coda e gestiti in ordine. Lo stato cambia in base a ciò che accade per ciascun evento.

Questo è un esempio per una connessione dati con stati come:

  • Non inizializzata
  • Initialized
  • Collegato
  • MTU Negoziato
  • autenticato

Una piccola funzione aggiuntiva che ho aggiunto era un timestamp per ogni messaggio / evento. Il gestore eventi ignorerà gli eventi troppo vecchi (sono scaduti). Questo può accadere molto nel mondo reale in cui potresti rimanere bloccato in uno stato inaspettatamente.

Questo esempio funziona su Linux, usa il Makefile qui sotto per compilarlo e giocarci.

state_machine.c

#include <stdio.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>   // sysconf()
#include <errno.h>    // errno
#include <string.h>   // strerror()
#include <sys/time.h> // gettimeofday()
#include <fcntl.h>    // For O_* constants
#include <sys/stat.h> // For mode constants

#include <mqueue.h>
#include <poll.h>

//------------------------------------------------
// States
//------------------------------------------------
typedef enum
{
    ST_UNKNOWN = 0,
    ST_UNINIT,
    ST_INIT,
    ST_CONNECTED,
    ST_MTU_NEGOTIATED,
    ST_AUTHENTICATED,
    ST_ERROR,
    ST_DONT_CHANGE,
    ST_TERM,
} fsmState_t;

//------------------------------------------------
// Events
//------------------------------------------------
typedef enum
{
    EV_UNKNOWN = 0,
    EV_INIT_SUCCESS,
    EV_INIT_FAIL,
    EV_MASTER_CMD_MSG,
    EV_CONNECT_SUCCESS,
    EV_CONNECT_FAIL,
    EV_MTU_SUCCESS,
    EV_MTU_FAIL,
    EV_AUTH_SUCCESS,
    EV_AUTH_FAIL,
    EV_TX_SUCCESS,
    EV_TX_FAIL,
    EV_DISCONNECTED,
    EV_DISCON_FAILED,
    EV_LAST_ENTRY,
} fsmEvName_t;

typedef struct fsmEvent_type
{
    fsmEvName_t name;
    struct timeval genTime; // Time the event was generated.
                            // This allows us to see how old the event is.
} fsmEvent_t;

// Finite State Machine Data Members
typedef struct fsmData_type
{
    int  connectTries;
    int  MTUtries;
    int  authTries;
    int  txTries;
} fsmData_t;

// Each row of the state table
typedef struct stateTable_type {
    fsmState_t  st;             // Current state
    fsmEvName_t evName;         // Got this event
    int (*conditionfn)(void *);  // If this condition func returns TRUE
    fsmState_t nextState;       // Change to this state and
    void (*fn)(void *);          // Run this function
} stateTable_t;

// Finite State Machine state structure
typedef struct fsm_type
{
    const stateTable_t *pStateTable; // Pointer to state table
    int        numStates;            // Number of entries in the table
    fsmState_t currentState;         // Current state
    fsmEvent_t currentEvent;         // Current event
    fsmData_t *fsmData;              // Pointer to the data attributes
    mqd_t      mqdes;                // Message Queue descriptor
    mqd_t      master_cmd_mqdes;     // Master command message queue
} fsm_t;

// Wildcard events and wildcard state
#define   EV_ANY    -1
#define   ST_ANY    -1
#define   TRUE     (1)
#define   FALSE    (0)

// Maximum priority for message queues (see "man mq_overview")
#define FSM_PRIO  (sysconf(_SC_MQ_PRIO_MAX) - 1)

static void addev                              (fsm_t *fsm, fsmEvName_t ev);
static void doNothing                          (void *fsm) {addev(fsm, EV_MASTER_CMD_MSG);}
static void doInit                             (void *fsm) {addev(fsm, EV_INIT_SUCCESS);}
static void doConnect                          (void *fsm) {addev(fsm, EV_CONNECT_SUCCESS);}
static void doMTU                              (void *fsm) {addev(fsm, EV_MTU_SUCCESS);}
static void reportFailConnect                  (void *fsm) {addev(fsm, EV_ANY);}
static void doAuth                             (void *fsm) {addev(fsm, EV_AUTH_SUCCESS);}
static void reportDisConnect                   (void *fsm) {addev(fsm, EV_ANY);}
static void doDisconnect                       (void *fsm) {addev(fsm, EV_ANY);}
static void doTransaction                      (void *fsm) {addev(fsm, EV_TX_FAIL);}
static void fsmError                           (void *fsm) {addev(fsm, EV_ANY);}

static int currentlyLessThanMaxConnectTries    (void *fsm) {
    fsm_t *l = (fsm_t *)fsm;
    return (l->fsmData->connectTries < 5 ? TRUE : FALSE);
}
static int        isMoreThanMaxConnectTries    (void *fsm) {return TRUE;}
static int currentlyLessThanMaxMTUtries        (void *fsm) {return TRUE;}
static int        isMoreThanMaxMTUtries        (void *fsm) {return TRUE;}
static int currentyLessThanMaxAuthTries        (void *fsm) {return TRUE;}
static int       isMoreThanMaxAuthTries        (void *fsm) {return TRUE;}
static int currentlyLessThanMaxTXtries         (void *fsm) {return FALSE;}
static int        isMoreThanMaxTXtries         (void *fsm) {return TRUE;}
static int didNotSelfDisconnect                (void *fsm) {return TRUE;}

static int  waitForEvent                       (fsm_t *fsm);
static void runEvent                           (fsm_t *fsm);
static void runStateMachine(fsm_t *fsm);
static int newEventIsValid(fsmEvent_t *event);
static void getTime(struct timeval *time);
void printState(fsmState_t st);
void printEvent(fsmEvName_t ev);

// Global State Table
const stateTable_t GST[] = {
    // Current state         Got this event          If this condition func returns TRUE     Change to this state and    Run this function
    { ST_UNINIT,             EV_INIT_SUCCESS,        NULL,                                   ST_INIT,                    &doNothing              },
    { ST_UNINIT,             EV_INIT_FAIL,           NULL,                                   ST_UNINIT,                  &doInit                 },
    { ST_INIT,               EV_MASTER_CMD_MSG,      NULL,                                   ST_INIT,                    &doConnect              },
    { ST_INIT,               EV_CONNECT_SUCCESS,     NULL,                                   ST_CONNECTED,               &doMTU                  },
    { ST_INIT,               EV_CONNECT_FAIL,        &currentlyLessThanMaxConnectTries,      ST_INIT,                    &doConnect              },
    { ST_INIT,               EV_CONNECT_FAIL,        &isMoreThanMaxConnectTries,             ST_INIT,                    &reportFailConnect      },
    { ST_CONNECTED,          EV_MTU_SUCCESS,         NULL,                                   ST_MTU_NEGOTIATED,          &doAuth                 },
    { ST_CONNECTED,          EV_MTU_FAIL,            &currentlyLessThanMaxMTUtries,          ST_CONNECTED,               &doMTU                  },
    { ST_CONNECTED,          EV_MTU_FAIL,            &isMoreThanMaxMTUtries,                 ST_CONNECTED,               &doDisconnect           },
    { ST_CONNECTED,          EV_DISCONNECTED,        &didNotSelfDisconnect,                  ST_INIT,                    &reportDisConnect       },
    { ST_MTU_NEGOTIATED,     EV_AUTH_SUCCESS,        NULL,                                   ST_AUTHENTICATED,           &doTransaction          },
    { ST_MTU_NEGOTIATED,     EV_AUTH_FAIL,           &currentyLessThanMaxAuthTries,          ST_MTU_NEGOTIATED,          &doAuth                 },
    { ST_MTU_NEGOTIATED,     EV_AUTH_FAIL,           &isMoreThanMaxAuthTries,                ST_MTU_NEGOTIATED,          &doDisconnect           },
    { ST_MTU_NEGOTIATED,     EV_DISCONNECTED,        &didNotSelfDisconnect,                  ST_INIT,                    &reportDisConnect       },
    { ST_AUTHENTICATED,      EV_TX_SUCCESS,          NULL,                                   ST_AUTHENTICATED,           &doDisconnect           },
    { ST_AUTHENTICATED,      EV_TX_FAIL,             &currentlyLessThanMaxTXtries,           ST_AUTHENTICATED,           &doTransaction          },
    { ST_AUTHENTICATED,      EV_TX_FAIL,             &isMoreThanMaxTXtries,                  ST_AUTHENTICATED,           &doDisconnect           },
    { ST_AUTHENTICATED,      EV_DISCONNECTED,        &didNotSelfDisconnect,                  ST_INIT,                    &reportDisConnect       },
    { ST_ANY,                EV_DISCON_FAILED,       NULL,                                   ST_DONT_CHANGE,             &doDisconnect           },
    { ST_ANY,                EV_ANY,                 NULL,                                   ST_UNINIT,                  &fsmError               }    // Wildcard state for errors
};

#define GST_COUNT (sizeof(GST)/sizeof(stateTable_t))

int main()
{
    int ret = 0;
    fsmData_t dataAttr;
    dataAttr.connectTries = 0;
    dataAttr.MTUtries     = 0;
    dataAttr.authTries    = 0;
    dataAttr.txTries      = 0;

    fsm_t lfsm;
    memset(&lfsm, 0, sizeof(fsm_t));
    lfsm.pStateTable       = GST;
    lfsm.numStates         = GST_COUNT;
    lfsm.currentState      = ST_UNINIT;
    lfsm.currentEvent.name = EV_ANY;
    lfsm.fsmData           = &dataAttr;

    struct mq_attr attr;
    attr.mq_maxmsg = 30;
    attr.mq_msgsize = sizeof(fsmEvent_t);

    // Dev info
    //printf("Size of fsmEvent_t [%ld]\n", sizeof(fsmEvent_t));

    ret = mq_unlink("/abcmq");
    if (ret == -1) {
        fprintf(stderr, "Error on mq_unlink(), errno[%d] strerror[%s]\n",
                errno, strerror(errno));
    }

    lfsm.mqdes = mq_open("/abcmq", O_CREAT | O_RDWR, S_IWUSR | S_IRUSR, &attr);
    if (lfsm.mqdes == (mqd_t)-1) {
        fprintf(stderr, "Error on mq_open(), errno[%d] strerror[%s]\n",
                errno, strerror(errno));
        return -1;
    }

    doInit(&lfsm);  // This will generate the first event
    runStateMachine(&lfsm);

    return 0;
}


static void runStateMachine(fsm_t *fsm)
{
    int ret = 0;

    if (fsm == NULL) {
        fprintf(stderr, "[%s] NULL argument\n", __func__);
        return;
    }

    // Cycle through the state machine
    while (fsm->currentState != ST_TERM) {
        printf("current state [");
        printState(fsm->currentState);
        printf("]\n");

        ret = waitForEvent(fsm);
        if (ret == 0) {
            printf("got event [");
            printEvent(fsm->currentEvent.name);
            printf("]\n");

            runEvent(fsm);
        }
        sleep(2);
    }
}


static int waitForEvent(fsm_t *fsm)
{
    //const int numFds = 2;
    const int numFds = 1;
    struct pollfd fds[numFds];
    int timeout_msecs = -1; // -1 is forever
    int ret = 0;
    int i = 0;
    ssize_t num = 0;
    fsmEvent_t newEv;

    if (fsm == NULL) {
        fprintf(stderr, "[%s] NULL argument\n", __func__);
        return -1;
    }

    fsm->currentEvent.name = EV_ANY;

    fds[0].fd     = fsm->mqdes;
    fds[0].events = POLLIN;
    //fds[1].fd     = fsm->master_cmd_mqdes;
    //fds[1].events = POLLIN;
    ret = poll(fds, numFds, timeout_msecs);

    if (ret > 0) {
        // An event on one of the fds has occurred
        for (i = 0; i < numFds; i++) {
            if (fds[i].revents & POLLIN) {
                // Data may be read on device number i
                num = mq_receive(fds[i].fd, (void *)(&newEv),
                                 sizeof(fsmEvent_t), NULL);
                if (num == -1) {
                    fprintf(stderr, "Error on mq_receive(), errno[%d] "
                            "strerror[%s]\n", errno, strerror(errno));
                    return -1;
                }

                if (newEventIsValid(&newEv)) {
                    fsm->currentEvent = newEv;
                } else {
                    return -1;
                }
            }
        }
    } else {
        fprintf(stderr, "Error on poll(), ret[%d] errno[%d] strerror[%s]\n",
                ret, errno, strerror(errno));
        return -1;
    }

    return 0;
}


static int newEventIsValid(fsmEvent_t *event)
{
    if (event == NULL) {
        fprintf(stderr, "[%s] NULL argument\n", __func__);
        return FALSE;
    }

    printf("[%s]\n", __func__);

    struct timeval now;
    getTime(&now);

    if ( (event->name < EV_LAST_ENTRY) &&
         ((now.tv_sec - event->genTime.tv_sec) < (60*5))
       )
    {
        return TRUE;
    } else {
        return FALSE;
    }
}


//------------------------------------------------
// Performs event handling on the FSM (finite state machine).
// Make sure there is a wildcard state at the end of
// your table, otherwise; the event will be ignored.
//------------------------------------------------
static void runEvent(fsm_t *fsm)
{
    int i;
    int condRet = 0;

    if (fsm == NULL) {
        fprintf(stderr, "[%s] NULL argument\n", __func__);
        return;
    }

    printf("[%s]\n", __func__);

    // Find a relevant entry for this state and event
    for (i = 0; i < fsm->numStates; i++) {
        // Look in the table for our current state or ST_ANY
        if (  (fsm->pStateTable[i].st == fsm->currentState) ||
              (fsm->pStateTable[i].st == ST_ANY)
           )
        {
            // Is this the event we are looking for?
            if ( (fsm->pStateTable[i].evName == fsm->currentEvent.name) ||
                 (fsm->pStateTable[i].evName == EV_ANY)
               )
            {
                if (fsm->pStateTable[i].conditionfn != NULL) {
                    condRet = fsm->pStateTable[i].conditionfn(fsm->fsmData);
                }

                // See if there is a condition associated
                // or we are not looking for any condition
                //
                if ( (condRet != 0) || (fsm->pStateTable[i].conditionfn == NULL))
                {
                    // Set the next state (if applicable)
                    if (fsm->pStateTable[i].nextState != ST_DONT_CHANGE) {
                        fsm->currentState = fsm->pStateTable[i].nextState;
                        printf("new state [");
                        printState(fsm->currentState);
                        printf("]\n");
                    }

                    // Call the state callback function
                    fsm->pStateTable[i].fn(fsm);
                    break;
                }
            }
        }
    }
}


//------------------------------------------------
//               EVENT HANDLERS
//------------------------------------------------
static void getTime(struct timeval *time)
{
    if (time == NULL) {
        fprintf(stderr, "[%s] NULL argument\n", __func__);
        return;
    }

    printf("[%s]\n", __func__);

    int ret = gettimeofday(time, NULL);
    if (ret != 0) {
        fprintf(stderr, "gettimeofday() failed: errno [%d], strerror [%s]\n",
                errno, strerror(errno));
        memset(time, 0, sizeof(struct timeval));
    }
}


static void addev (fsm_t *fsm, fsmEvName_t ev)
{
    int ret = 0;

    if (fsm == NULL) {
        fprintf(stderr, "[%s] NULL argument\n", __func__);
        return;
    }

    printf("[%s] ev[%d]\n", __func__, ev);

    if (ev == EV_ANY) {
        // Don't generate a new event, just return...
        return;
    }

    fsmEvent_t newev;
    getTime(&(newev.genTime));
    newev.name = ev;

    ret = mq_send(fsm->mqdes, (void *)(&newev), sizeof(fsmEvent_t), FSM_PRIO);
    if (ret == -1) {
        fprintf(stderr, "[%s] mq_send() failed: errno [%d], strerror [%s]\n",
                __func__, errno, strerror(errno));
    }
}
//------------------------------------------------
//           end EVENT HANDLERS
//------------------------------------------------

void printState(fsmState_t st)
{
    switch(st) {
        case    ST_UNKNOWN:
        printf("ST_UNKNOWN");
            break;
        case    ST_UNINIT:
        printf("ST_UNINIT");
            break;
        case    ST_INIT:
        printf("ST_INIT");
            break;
        case    ST_CONNECTED:
        printf("ST_CONNECTED");
            break;
        case    ST_MTU_NEGOTIATED:
        printf("ST_MTU_NEGOTIATED");
            break;
        case    ST_AUTHENTICATED:
        printf("ST_AUTHENTICATED");
            break;
        case    ST_ERROR:
        printf("ST_ERROR");
            break;
        case    ST_TERM:
        printf("ST_TERM");
            break;
        default:
        printf("unknown state");
            break;
    }
}

void printEvent(fsmEvName_t ev)
{
    switch (ev) {
        case    EV_UNKNOWN:
        printf("EV_UNKNOWN");
            break;
        case    EV_INIT_SUCCESS:
        printf("EV_INIT_SUCCESS");
            break;
        case    EV_INIT_FAIL:
        printf("EV_INIT_FAIL");
            break;
        case    EV_MASTER_CMD_MSG:
        printf("EV_MASTER_CMD_MSG");
            break;
        case    EV_CONNECT_SUCCESS:
        printf("EV_CONNECT_SUCCESS");
            break;
        case    EV_CONNECT_FAIL:
        printf("EV_CONNECT_FAIL");
            break;
        case    EV_MTU_SUCCESS:
        printf("EV_MTU_SUCCESS");
            break;
        case    EV_MTU_FAIL:
        printf("EV_MTU_FAIL");
            break;
        case    EV_AUTH_SUCCESS:
        printf("EV_AUTH_SUCCESS");
            break;
        case    EV_AUTH_FAIL:
        printf("EV_AUTH_FAIL");
            break;
        case    EV_TX_SUCCESS:
        printf("EV_TX_SUCCESS");
            break;
        case    EV_TX_FAIL:
        printf("EV_TX_FAIL");
            break;
        case    EV_DISCONNECTED:
        printf("EV_DISCONNECTED");
            break;
        case    EV_LAST_ENTRY:
        printf("EV_LAST_ENTRY");
            break;
        default:
        printf("unknown event");
            break;
    }
}

Makefile

CXX = gcc
COMPFLAGS = -c -Wall -g

state_machine: state_machine.o
    $(CXX) -lrt state_machine.o -o state_machine

state_machine.o: state_machine.c
    $(CXX) $(COMPFLAGS) state_machine.c

clean:
    rm state_machine state_machine.o

1

La tua domanda è piuttosto generica,
ecco due articoli di riferimento che potrebbero essere utili,

  1. Implementazione della macchina a stati integrati

    Questo articolo descrive un approccio semplice all'implementazione di una macchina a stati per un sistema incorporato. Ai fini di questo articolo, una macchina a stati è definita come un algoritmo che può trovarsi in uno di un piccolo numero di stati. Uno stato è una condizione che provoca una relazione prescritta di ingressi / uscite e di ingressi agli stati successivi.
    Un lettore esperto noterà rapidamente che le macchine a stati descritte in questo articolo sono macchine Mealy. Una macchina Mealy è una macchina a stati in cui le uscite sono una funzione sia dello stato presente che dell'input, a differenza di una macchina Moore, in cui le uscite sono solo una funzione di stato.

    • Macchine a stati di codifica in C e C ++

      La mia preoccupazione in questo articolo riguarda i concetti fondamentali delle macchine a stati e alcune semplici linee guida di programmazione per la codifica di macchine a stati in C o C ++. Spero che queste semplici tecniche possano diventare più comuni, in modo che tu (e altri) possiate vedere prontamente la struttura della macchina a stati direttamente dal codice sorgente.



1

Questo è un vecchio post con molte risposte, ma ho pensato di aggiungere il mio approccio alla macchina a stati finiti in C. Ho creato uno script Python per produrre il codice scheletro C per qualsiasi numero di stati. Lo script è documentato su GituHub su FsmTemplateC

Questo esempio si basa su altri approcci di cui ho letto. Non utilizza le istruzioni goto o switch ma ha invece funzioni di transizione in una matrice di puntatori (tabella di ricerca). Il codice si basa su una grande macro di inizializzazione multilinea e funzionalità C99 (inizializzatori designati e valori letterali composti), quindi se non ti piacciono queste cose, potresti non apprezzare questo approccio.

Ecco uno script Python di un esempio di tornello che genera lo scheletro C-code usando FsmTemplateC :

# dict parameter for generating FSM
fsm_param = {
    # main FSM struct type string
    'type': 'FsmTurnstile',
    # struct type and name for passing data to state machine functions
    # by pointer (these custom names are optional)
    'fopts': {
        'type': 'FsmTurnstileFopts',
        'name': 'fopts'
    },
    # list of states
    'states': ['locked', 'unlocked'],
    # list of inputs (can be any length > 0)
    'inputs': ['coin', 'push'],
    # map inputs to commands (next desired state) using a transition table
    # index of array corresponds to 'inputs' array
    # for this example, index 0 is 'coin', index 1 is 'push'
    'transitiontable': {
        # current state |  'coin'  |  'push'  |
        'locked':       ['unlocked',        ''],
        'unlocked':     [        '',  'locked']
    }
}

# folder to contain generated code
folder = 'turnstile_example'
# function prefix
prefix = 'fsm_turnstile'

# generate FSM code
code = fsm.Fsm(fsm_param).genccode(folder, prefix)

L'intestazione di output generata contiene i typedef:

/* function options (EDIT) */
typedef struct FsmTurnstileFopts {
    /* define your options struct here */
} FsmTurnstileFopts;

/* transition check */
typedef enum eFsmTurnstileCheck {
    EFSM_TURNSTILE_TR_RETREAT,
    EFSM_TURNSTILE_TR_ADVANCE,
    EFSM_TURNSTILE_TR_CONTINUE,
    EFSM_TURNSTILE_TR_BADINPUT
} eFsmTurnstileCheck;

/* states (enum) */
typedef enum eFsmTurnstileState {
    EFSM_TURNSTILE_ST_LOCKED,
    EFSM_TURNSTILE_ST_UNLOCKED,
    EFSM_TURNSTILE_NUM_STATES
} eFsmTurnstileState;

/* inputs (enum) */
typedef enum eFsmTurnstileInput {
    EFSM_TURNSTILE_IN_COIN,
    EFSM_TURNSTILE_IN_PUSH,
    EFSM_TURNSTILE_NUM_INPUTS,
    EFSM_TURNSTILE_NOINPUT
} eFsmTurnstileInput;

/* finite state machine struct */
typedef struct FsmTurnstile {
    eFsmTurnstileInput input;
    eFsmTurnstileCheck check;
    eFsmTurnstileState cur;
    eFsmTurnstileState cmd;
    eFsmTurnstileState **transition_table;
    void (***state_transitions)(struct FsmTurnstile *, FsmTurnstileFopts *);
    void (*run)(struct FsmTurnstile *, FsmTurnstileFopts *, const eFsmTurnstileInput);
} FsmTurnstile;

/* transition functions */
typedef void (*pFsmTurnstileStateTransitions)(struct FsmTurnstile *, FsmTurnstileFopts *);
  • enum eFsmTurnstileCheckviene utilizzato per determinare se una transizione è stata bloccata EFSM_TURNSTILE_TR_RETREAT, se è consentita l'avanzamento EFSM_TURNSTILE_TR_ADVANCEo se la chiamata di funzione non è stata preceduta da una transizione conEFSM_TURNSTILE_TR_CONTINUE .
  • enum eFsmTurnstileState è semplicemente l'elenco degli stati.
  • enum eFsmTurnstileInput è semplicemente l'elenco degli input.
  • Il FsmTurnstile struttura è il cuore della macchina a stati con il controllo di transizione, la tabella di ricerca delle funzioni, lo stato corrente, lo stato comandato e un alias per la funzione primaria che esegue la macchina.
  • Ogni puntatore a funzione (alias) FsmTurnstiledeve essere chiamato solo dalla struttura e deve avere il suo primo input come puntatore a se stesso in modo da mantenere uno stile persistente, orientato agli oggetti.

Ora per le dichiarazioni di funzione nell'intestazione:

/* fsm declarations */
void fsm_turnstile_locked_locked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_locked_unlocked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_unlocked_locked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_unlocked_unlocked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_run (FsmTurnstile *fsm, FsmTurnstileFopts *fopts, const eFsmTurnstileInput input);

I nomi delle funzioni sono nel formato {prefix}_{from}_{to}, dove {from}è lo stato precedente (corrente) ed {to}è lo stato successivo. Notare che se la tabella delle transizioni non consente determinate transizioni, verrà impostato un puntatore NULL anziché un puntatore a funzione. Finalmente, la magia accade con una macro. Qui costruiamo la tabella di transizione (matrice di enumerazioni di stato) e le funzioni di transizione di stato osservano la tabella (una matrice di puntatori a funzione):

/* creation macro */
#define FSM_TURNSTILE_CREATE() \
{ \
    .input = EFSM_TURNSTILE_NOINPUT, \
    .check = EFSM_TURNSTILE_TR_CONTINUE, \
    .cur = EFSM_TURNSTILE_ST_LOCKED, \
    .cmd = EFSM_TURNSTILE_ST_LOCKED, \
    .transition_table = (eFsmTurnstileState * [EFSM_TURNSTILE_NUM_STATES]) { \
        (eFsmTurnstileState [EFSM_TURNSTILE_NUM_INPUTS]) { \
            EFSM_TURNSTILE_ST_UNLOCKED, \
            EFSM_TURNSTILE_ST_LOCKED \
        }, \
        (eFsmTurnstileState [EFSM_TURNSTILE_NUM_INPUTS]) { \
            EFSM_TURNSTILE_ST_UNLOCKED, \
            EFSM_TURNSTILE_ST_LOCKED \
        } \
    }, \
    .state_transitions = (pFsmTurnstileStateTransitions * [EFSM_TURNSTILE_NUM_STATES]) { \
        (pFsmTurnstileStateTransitions [EFSM_TURNSTILE_NUM_STATES]) { \
            fsm_turnstile_locked_locked, \
            fsm_turnstile_locked_unlocked \
        }, \
        (pFsmTurnstileStateTransitions [EFSM_TURNSTILE_NUM_STATES]) { \
            fsm_turnstile_unlocked_locked, \
            fsm_turnstile_unlocked_unlocked \
        } \
    }, \
    .run = fsm_turnstile_run \
}

Durante la creazione di FSM, la macro FSM_EXAMPLE_CREATE() è necessario utilizzare .

Ora, nel codice sorgente dovrebbe essere popolata ogni funzione di transizione di stato dichiarata sopra. La FsmTurnstileFoptsstruttura può essere utilizzata per passare i dati alla / dalla macchina a stati. Ogni transizione deve fsm->checkessere impostata su uguale a EFSM_EXAMPLE_TR_RETREATper bloccarla dalla transizione o EFSM_EXAMPLE_TR_ADVANCEper consentirle di passare allo stato comandato. Un esempio funzionante è disponibile all'indirizzo (FsmTemplateC) [ https://github.com/ChisholmKyle/FsmTemplateC] .

Ecco il semplice utilizzo effettivo nel tuo codice:

/* create fsm */
FsmTurnstile fsm = FSM_TURNSTILE_CREATE();
/* create fopts */
FsmTurnstileFopts fopts = {
    .msg = ""
};
/* initialize input */
eFsmTurnstileInput input = EFSM_TURNSTILE_NOINPUT;

/* main loop */
for (;;) {
    /* wait for timer signal, inputs, interrupts, whatever */
    /* optionally set the input (my_input = EFSM_TURNSTILE_IN_PUSH for example) */
    /* run state machine */
    my_fsm.run(&my_fsm, &my_fopts, my_input);
}

Tutto quel business dell'intestazione e tutte quelle funzioni solo per avere un'interfaccia semplice e veloce ne vale la pena.


0

È possibile utilizzare la libreria open source OpenFST .

OpenFst è una libreria per la costruzione, la combinazione, l'ottimizzazione e la ricerca di trasduttori a stato finito (FST) ponderati. I trasduttori a stato finito ponderati sono automi in cui ogni transizione ha un'etichetta di input, un'etichetta di output e un peso. L'accettore di stato finito più familiare è rappresentato come un trasduttore con l'etichetta di input e output di ogni transizione uguale. Gli accettori di stati finiti sono usati per rappresentare insiemi di stringhe (in particolare insiemi regolari o razionali); i trasduttori a stati finiti sono usati per rappresentare relazioni binarie tra coppie di stringhe (in particolare, trasduzioni razionali). I pesi possono essere utilizzati per rappresentare il costo di una particolare transizione.


0
void (* StateController)(void); 
void state1(void);
void state2(void);

void main()
{
 StateController=&state1;
 while(1)
 {
  (* StateController)();
 }
}

void state1(void)
{
 //do something in state1
 StateController=&state2;
}

void state2(void)
{
 //do something in state2
 //Keep changing function direction based on state transition
 StateController=&state1;
}

Puoi ottimizzarlo ulteriormente per sicurezza usando una serie di puntatori di funzioni costanti alle funzioni
AlphaGoku

0

Personalmente uso strutture autoreferenziali in combinazione con array di puntatori. Ho caricato un tutorial su Github qualche tempo fa, link:

https://github.com/mmelchger/polling_state_machine_c

Nota: mi rendo conto che questo thread è piuttosto vecchio, ma spero di ottenere input e riflessioni sulla progettazione della macchina a stati oltre a poter fornire un esempio per un possibile progetto a macchina a stati in C.


0

Si può considerare UML-stato-macchina-a-c , un quadro macchina a stati "leggera" in C. Ho scritto questo quadro per supportare sia la macchina a stati finiti e macchina a stati gerarchici . Confronta con tabelle di stato o semplici casi switch, un approccio framework è più scalabile. Può essere utilizzato per semplici macchine a stati finiti a macchine a stati gerarchici complessi.

La macchina a stati è rappresentata da una state_machine_tstruttura. Contiene solo due membri "Evento" e un puntatore a "state_t".

struct state_machine_t
{
   uint32_t Event;          //!< Pending Event for state machine
   const state_t* State;    //!< State of state machine.
};

state_machine_tdeve essere il primo membro della struttura della macchina a stati. per esempio

struct user_state_machine
{
  state_machine_t Machine;    // Base state machine. Must be the first member of user derived state machine.

  // User specific state machine members
  uint32_t param1;
  uint32_t param2;
  ...
};

state_t contiene un gestore per lo stato e anche gestori opzionali per l'azione di entrata e uscita.

//! finite state structure
struct finite_state{
  state_handler Handler;      //!< State handler to handle event of the state
  state_handler Entry;        //!< Entry action for state
  state_handler Exit;          //!< Exit action for state.
};

Se il framework è configurato per una macchina a stati gerarchica, allora il state_t contiene un puntatore allo stato padre e figlio.

Framework fornisce un'API dispatch_eventper inviare l'evento alla macchina a stati e switch_stateper attivare la transizione di stato.

Per ulteriori dettagli su come implementare una macchina a stati gerarchica, consultare il repository GitHub .

esempi di codice,

https://github.com/kiishor/UML-State-Machine-in-C/blob/master/demo/simple_state_machine/readme.md https://github.com/kiishor/UML-State-Machine-in-C /blob/master/demo/simple_state_machine_enhanced/readme.md


-1

Ecco un metodo per una macchina a stati che utilizza macro in modo tale che ogni funzione possa avere il proprio set di stati: https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code -a

Si chiama "simulate multi tasking" ma non è l'unico uso.

Questo metodo utilizza i callback per rispondere a ciascuna funzione da cui era stata interrotta. Ogni funzione contiene un elenco di stati univoci per ciascuna funzione. Un "loop inattivo" centrale viene utilizzato per eseguire le macchine a stati. Il "ciclo inattivo" non ha idea di come funzionano le macchine a stati, sono le singole funzioni che "sanno cosa fare". Per scrivere il codice per le funzioni, si crea semplicemente un elenco di stati e si usano le macro per "sospendere" e "riprendere". Ho usato queste macro su Cisco quando ho scritto la Transceiver Library per lo switch Nexus 7000.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.