Per un pugno di grafcet, ovvero macchine a stati con più stati attivi

Il formalismo grafcet permette di disegnare/descrivere macchine a stati/processi che consentono flussi di esecuzione paralleli con più stati attivi contemporaneamente.

Le macchine a stati più volte discusse in questo forum rappresentano il caso limite (sottoinsieme del grafcet) in cui un solo stato alla volta può essere attivo.

Nel grafcet invece come caso limite possiamo avere anche tutti gli stati contemporaneamente attivi/eseguiti.

È evidente quindi che non basta una singola variabile di fase/stato, ma ne occorre una per ogni stato.

Affinché il flusso globale rappresentato con un grafcet possa funzionare, è necessario valutare tutte le condizioni di transizione prima di iniziare ad aggiornare gli stati, in modo da evitare ad esempio che uno stato non venga mai eseguito se è contemporaneamente vera la sua attivazione e disattivazione, anzi, in questo caso lo stato deve rimanere attivo.

Quindi bisognerebbe in sequenza:

- eseguire gli stati attivi
- valutare tutte le condizioni
- disattivare gli stati a monte delle condizioni vere
- attivare gli stati a valle delle condizioni vere

Mi sono chiesto se ci potesse essere una "sintassi" semplice e vicina al modo consueto di ragionare a stati:

- se stato X
  - se evento Y
    - azione Z, eventuale transizione W

Sono giunto alla seguente struttura generale che fa uso di diversi array (ma potrebbe anche essere un array di struct, cambia poco). L'importante sono i due cicli for posti prima e dopo dell'esecuzione degli stati. In sostanza non si modifica direttamente l'array s[ ] (stati attuali), ma un array nuovi stati (ns[]) che viene azzerato all'inizio di ogni giro, e solo alla fine ricopiato nell'array stati attuali (aggiornamento sincrono). Però, siccome la disattivazione in questo modo diventa implicita, in ogni stato bisogna sempre confermare esplicitamente la permanenza nello stato attuale, e questa è l'unica differenza rispetto alla sintassi "solita":

- se stato X
  - se evento Y
    - azione Z, eventuale transizione W
  - altrienti mantieni stato attuale

La novità maggiore è la sincronizzazione tra più flussi paralleli, che deve garantire il mantenimento dell'attivazione degli stati finali (nell'esempio 4 e 12, oppure 3 5 e 8 ) fino al momento in cui la condizione di transizione della sincronizzazione diventa vera.

Per avere anche l'indicazione di stato appena attivato (per eseguire delle operazioni solo al primo giro) e della durata di uno stato (per gestire dei timeout), si usano gli array sp[] (stato precedente) e t[] (tempi iniziali).

Tramite delle macro è possibile semplificare notevolmente la sintassi.
Esempio1, doppio lampeggiatore:

grafcet02.png

// COLLEGAMENTI HARDWARE
#define  LED1       3      // acceso LOW
#define  LED2       4      // acceso LOW
#define  ONLEVEL    LOW
#define  OFFLEVEL   HIGH
#define  INGRESSO   2      // premuto HIGH
#define  PRESSLEVEL HIGH

// MACRO PER MACCHINA A STATI
#define  TRASCORSO(i)   now - t[i]
#define  INIZIO(i)      s[i] && !sp[i]
#define  ATTIVA(i)      ns[i] = 1
#define  MANTIENI(i)    ns[i] = 1
#define  FASE(i)        s[i]

// DATI DI LAVORO PER MACCHINA A STATI
#define  NSTATI     5
byte     s[NSTATI]  = { 0 };  // stati attuali
byte     sp[NSTATI] = { 0 };  // stati precedenti
byte     ns[NSTATI] = { 0 };  // nuovi stati futuri
uint32_t t[NSTATI];           // tempi di inizio stato

//-----------------------------------------------------------------------------

void setup(){
    pinMode(LED1, OUTPUT);
    digitalWrite(LED1, OFFLEVEL);
    pinMode(LED2, OUTPUT);
    digitalWrite(LED2, OFFLEVEL);
    pinMode(INGRESSO, INPUT);
    s[0] = 1;  // STATO ATTIVO INIZIALE
}

//-----------------------------------------------------------------------------

void loop(){

    byte self;

    uint32_t now = millis();

    bool start = (digitalRead(INGRESSO) == PRESSLEVEL);

    //-------------------------------------------------------------------------
    // AZZERAMENTO STATI FUTURI E SALVA TEMPI INIZIO FASI
    //-------------------------------------------------------------------------
    for (byte i=0; i<NSTATI; i++) { ns[i]=0; if (INIZIO(i)) { t[i]=now; } }


    //-------------------------------------
    // STATO 0
    //-------------------------------------
    if (FASE(0)){
        if (start) { ATTIVA(1);  ATTIVA(3); }
        else       { MANTIENI(0);           }
    }
    
    //-------------------------------------
    // STATO 1     (40ms led1 acceso)
    //-------------------------------------
    self = 1;
    if (FASE(self)){
        if (INIZIO(self)) { digitalWrite(LED1, ONLEVEL); }
        if (TRASCORSO(self)>40) { ATTIVA(2);      }
        else                    { MANTIENI(self); }
    }

    //-------------------------------------
    // STATO 2    (300ms led1 spento)
    //-------------------------------------
    self = 2;
    if (FASE(self)){
        if (INIZIO(self)) { digitalWrite(LED1, OFFLEVEL); }
        if (TRASCORSO(self)>300) { ATTIVA(1);      }
        else                     { MANTIENI(self); }
    }

    //-------------------------------------
    // STATO 3     (400ms led2 acceso)
    //-------------------------------------
    self = 3;
    if (FASE(self)){
        if (INIZIO(self)) { digitalWrite(LED2, ONLEVEL); }
        if (TRASCORSO(self)>400) { ATTIVA(4);      }
        else                     { MANTIENI(self); }
    }

    //-------------------------------------
    // STATO 4    (400ms led2 spento)
    //-------------------------------------
    self = 4;
    if (FASE(self)){
        if (INIZIO(self)) { digitalWrite(LED2, OFFLEVEL); }
        if (TRASCORSO(self)>400) { ATTIVA(3);      }
        else                     { MANTIENI(self); }
    }


    //-------------------------------------------------------------------------
    // AGGIORNAMENTO SINCRONO STATI
    //-------------------------------------------------------------------------
    for (byte i=0; i<NSTATI; i++) { sp[i] = s[i];  s[i] = ns[i]; }
}

La variabile 'self', di Pythonica ispirazione, serve per non riscrivere innumerevoli volte il numero della fase corrente.

continua...

grafcet02.png

Esempio2, premo e lampeggia, ripremo e si spegne:

// COLLEGAMENTI HARDWARE
#define  LED        13     // acceso HIGH
#define  ONLEVEL    HIGH
#define  OFFLEVEL   LOW
#define  INGRESSO   2      // premuto HIGH
#define  PRESSLEVEL HIGH

// MACRO PER MACCHINA A STATI
#define  TRASCORSO(i)   now - t[i]
#define  INIZIO(i)      s[i] && !sp[i]
#define  ATTIVA(i)      ns[i] = 1
#define  MANTIENI(i)    ns[i] = 1
#define  FASE(i)        s[i]

// DATI DI LAVORO PER MACCHINA A STATI
#define  NSTATI     8
byte     s[NSTATI]  = { 0 };  // stati attuali
byte     sp[NSTATI] = { 0 };  // stati precedenti
byte     ns[NSTATI] = { 0 };  // nuovi stati futuri
uint32_t t[NSTATI];           // tempi di inizio stato


void setup() {
    pinMode(INGRESSO, INPUT);
    pinMode(LED, OUTPUT);
    digitalWrite(LED, OFFLEVEL);
    s[0] = 1;  // STATO ATTIVO INIZIALE
}


void loop() {

    byte self;

    uint32_t now = millis();

    bool premuto = (digitalRead(INGRESSO) == PRESSLEVEL);
 
    //-------------------------------------------------------------------------
    // AZZERAMENTO STATI FUTURI E SALVA TEMPI INIZIO FASI
    //-------------------------------------------------------------------------
    for (byte i=0; i<NSTATI; i++) { ns[i]=0; if (INIZIO(i)) { t[i]=now; } }


    //-------------------------------------
    // STATO 0  (attesa prima pressione)
    //-------------------------------------
    if (FASE(0)){
        if (premuto) { ATTIVA(1);  ATTIVA(4); }
        else         { MANTIENI(0);           }
    }
    
    //-------------------------------------
    // STATO 1  (attesa primo rilascio)
    //-------------------------------------
    if (FASE(1)){
        if (!premuto) { ATTIVA(2);   }
        else          { MANTIENI(1); }
    }
    
    //-------------------------------------
    // STATO 2  (attesa seconda pressione)
    //-------------------------------------
    if (FASE(2)){
        if (premuto) { ATTIVA(3);   }
        else         { MANTIENI(2); }
    }

    //-------------------------------------
    // STATO 4  (led acceso 100ms)
    //-------------------------------------
    self = 4;
    if (FASE(self)){
        if (INIZIO(self)) { digitalWrite(LED, ONLEVEL); }
        if      (FASE(3))             { ATTIVA(6);      }
        else if (TRASCORSO(self)>100) { ATTIVA(5);      }
        else                          { MANTIENI(self); }
    }

    //-------------------------------------
    // STATO 5  (led spento 100ms)
    //-------------------------------------
    self = 5;
    if (FASE(self)) {
        if (INIZIO(self)) { digitalWrite(LED, OFFLEVEL); }
        if      (FASE(3))             { ATTIVA(6);      }
        else if (TRASCORSO(self)>100) { ATTIVA(4);      }
        else                          { MANTIENI(self); }
    }

    //-------------------------------------
    // STATO 6  (fine lampeggio)
    //-------------------------------------
    if (FASE(6)){
        digitalWrite(LED, OFFLEVEL);
    }

    //-------------------------------------
    // SINCRONIZZAZIONI (AND-convergenze)
    //-------------------------------------
    if (FASE(3) && FASE(6)) { ATTIVA(7); }
    else 
    {
        if(FASE(3)) { MANTIENI(3); }
        if(FASE(6)) { MANTIENI(6); }
    }
    
    //-------------------------------------
    // STATO 7  (attesa secondo rilascio)
    //-------------------------------------
    if (FASE(7)){
        if (!premuto) { ATTIVA(0);   }
        else          { MANTIENI(7); }
    }


    //-------------------------------------------------------------------------
    // AGGIORNAMENTO SINCRONO STATI
    //-------------------------------------------------------------------------
    for (byte i=0; i<NSTATI; i++) { sp[i] = s[i];  s[i] = ns[i]; }
}

Notare che le macro ATTIVA e MANTIENI svolgono la stessa identica operazione, ma il nome diverso secondo me migliora la comprensione dell'azione che si sta effettuando (transizione a nuove fasi, con disattivazione implicita di quella attuale, oppure mantenimento esplicito di quella attuale).

E ora la domanda... c'è un modo più furbo di scrivere il tutto?

C'è lo svantaggio di un grande consumo di memoria RAM, infatti per ogni stato vengono allocate tutte le variabili di lavoro anche se non usate, ma d'altra parte è proprio questo che permette l'esecuzione anche contemporanea e indipendente di tutti gli stati.

Aggiorno e concludo, questa nuova versione sostituisce in toto le precedenti e semplifica ancora la sintassi perché non richiede il "mantenimento" esplicito dello stato attuale (avevo provato a "tradurre" qualche vecchia macchina ma c'erano troppi else annidati da aggiungere). Gli stati possono andare anche in condizione 'TERMINATO' (set bit array 'te') in attesa di una sincronizzazione di flussi paralleli, e fino a quel momento non vengono più eseguiti (il bit 'te' viene resettato comandando una transizione). Le info di stato (stato attuale, precedente, nuovo e terminato) sono bitmappate in array di 1+(NSTATI-1)/8 elementi. Le funzioni 'bitt' 'seton' setoff' permettono di testare, settare o resettare un singolo bit degli array. Credo che meno di così non si riesca a scrivere :cold_sweat:

//------------------------------------------------------------------------------
// COLLEGAMENTI HARDWARE
//------------------------------------------------------------------------------
#define  LED        13     // acceso HIGH
#define  ONLEVEL    HIGH
#define  OFFLEVEL   LOW
#define  INGRESSO   2      // premuto HIGH
#define  PRESSLEVEL HIGH

//------------------------------------------------------------------------------
// DATI DI LAVORO PER MACCHINA A STATI
//------------------------------------------------------------------------------
#define  NSTATI     6
#define  NELEM      1 + (NSTATI - 1)/8
byte     sa[NELEM]  = { 0 };  // stati attuali bitmapped
byte     sp[NELEM]  = { 0 };  // stati precedenti bitmapped
byte     sn[NELEM]  = { 0 };  // stati nuovi bitmapped
byte     te[NELEM]  = { 0 };  // stati terminati bitmapped
uint32_t t[NSTATI];           // tempi di inizio stato

//------------------------------------------------------------------------------
// FUNZIONI E MACRO PER MACCHINA A STATI
//------------------------------------------------------------------------------
bool bitt(byte arr[], int n)   { return (arr[n>>3] & (1 << (n & 7))) != 0;     }
void setoff(byte arr[], int n) { arr[n>>3] &= ~(1 << (n & 7))                  }
void seton(byte arr[], int n)  { arr[n>>3] |= 1 << (n & 7);                    }
void transit(int da, int a)    { setoff(sn, da); setoff(te, da); seton(sn, a); }
#define  TRASCORSO(i)   (now - t[i])
#define  FASE(i)        bitt(sa, i)
#define  TERMINATO(i)   bitt(te, i)
#define  INIZIO(i)      (bitt(sa, i) and !bitt(sp, i))
#define  TERMINA(i)     { seton(te, i); setoff(sn, i); }

//------------------------------------------------------------------------------

void setup() 
{
    pinMode(INGRESSO, INPUT);
    pinMode(LED, OUTPUT);
    digitalWrite(LED, OFFLEVEL);
    seton(sa, 0);                               // STATO 0 ATTIVO ALL'INIZIO
    for (byte i=0; i<NELEM; i++) sn[i] = sa[i]; // copy attuali in nuovi
}

//------------------------------------------------------------------------------

void loop() 
{
    //------------ SALVA TEMPI INIZIO STATI
    uint32_t now = millis();
    for (byte i=0; i<NSTATI; i++) if (INIZIO(i)) t[i] = now;
    
    bool premuto = (digitalRead(INGRESSO) == PRESSLEVEL);

    //------------ STATO 0  (attesa prima pressione)
    if (FASE(0) and premuto) { transit(0, 1);  transit(0, 3); }
    
    //------------ STATO 1  (attesa primo rilascio)
    if (FASE(1) and !premuto) transit(1, 2);
    
    //------------ STATO 2  (attesa seconda pressione)
    if (FASE(2) and premuto) TERMINA(2);  // attesa sincronizzazione

    //------------ STATO 3  (led acceso 200ms)
    byte x = 3;
    if (FASE(x))
    {
        if (INIZIO(x)) digitalWrite(LED, ONLEVEL);
        if (TRASCORSO(x) > 200) transit(x, 4);
        else if (TERMINATO(2)) 
        { 
            digitalWrite(LED, OFFLEVEL);  
            TERMINA(x);  
        }
    }

    //------------ STATO 4  (led spento 200ms)
    x = 4;
    if (FASE(x))
    {
        if (INIZIO(x)) digitalWrite(LED, OFFLEVEL);
        if      (TRASCORSO(x) > 200) transit(x, 3);
        else if (TERMINATO(2))       TERMINA(x);
    }

    //------------ STATO 5  (attesa secondo rilascio)
    if (FASE(5) and !premuto) transit(5, 0);


    //------------ SINCRONIZZAZIONI (AND-convergenze)
    if (TERMINATO(2) and (TERMINATO(3) or TERMINATO(4)))
    { 
        transit(2, 5); 
        transit(3, 5);
        transit(4, 5); 
    }

    //------------ AGGIORNAMENTO SINCRONO STATI
    for (byte i=0; i<NELEM;  i++) { sp[i] = sa[i];  sa[i] = sn[i]; }
}

Il grafcet per essere formalmente corretto dovrebbe contenere degli stati di attesa dopo gli stati 2, 4 e 3, perché non si dovrebbe uscire con delle transizioni direttamente verso la "barra di join". In realtà quelle transizioni portano gli stati in 'TERMINATO', che equivale a portarli verso "stati di attesa" intermedi privi di funzionalità (se non quella di sincronizzazione). Portando gli stati in 'TERMINATO', i TW si possono risparmiare... e ogni stato risparmiato è memoria guadagnata :stuck_out_tongue:

Ti vorrei ringraziare per aver condiviso questi post.
Mi piacerebbe approfondire ma in questi giorni il tempo è poco, comunque seguirò con interesse ulteriori sviluppi e appena riesco ci metto bene il naso dentro per cercare di capire bene il codice che hai scritto.

Questo post verrà eliminato tra qualche giorno per tenere pulito il thread.