Macchina a stati finiti (FSM) basata su switch case

Ne avrebbe dedotto, giustamente, che si tratta di spunti e non pappa fatta

Considerando le possibili 'azioni' su un pin in uscita

Che sono 3, non due

Accendi, spegni, lascia inalterato

Anche per gli ingressi le condizioni sono 3 Deve essere alto, deve essere basso, ignorato

Che cosa mi consigliate per stivare un array di azioni? Fossero solo 2 userei un numero binario Deve essere una cosa facile da scrive e e da comprendere

Belle le luci di cortesia, ma senza per ci provo

Ne avrebbe dedotto, giustamente, che si tratta di spunti e non pappa fatta

Che male c'è a fornire degli esempio per implementare una macchina a stati funzionante, macchina che poi dovrà comunque adattare all'applicazione e per farlo dovrà capire come funziona, per cui non si tratta di pappa fatta.

Torno indietro e mi pongo la seguente domanda: Come si divide una applicazione in stati? Mi rispondo che non lo so, accade lavorando di fantasia, di certo c'è un inizio, potrebbe anche esserci una fine, e nel mezzo ci sono tanti stati quanti ne servono. Questo punto è fondamentale per cui si dovrebbe provare a scrivere qualcosa in merito.

Belle le luci di cortesia, ma senza per ci provo

::)

Il mio è solo un esempio al fine di non giudicare totalmente inutile ciò che appare inutile.

Ciao.

Maurotec: Torno indietro e mi pongo la seguente domanda: Come si divide una applicazione in stati?

Uh, allora vale anche: Come si divide una applicazione in macchine? Come comunicano? Come si sincronizzano ?

Mi rispondo che non lo so, accade lavorando di fantasia, di certo c'è un inizio, potrebbe anche esserci una fine, e nel mezzo ci sono tanti stati quanti ne servono.

Questo mi sembra semplice: uno stato per ogni diversa situazione di attesa (in cui si può permanere da un solo ciclo all'infinito a seconda degli eventi che accadono).

AmericanDreamer: Che cosa mi consigliate per stivare un array di azioni? Fossero solo 2 userei un numero binario Deve essere una cosa facile da scrive e e da comprendere

È qui il difficile... dentro uno stato si può aver bisogno di fare un sacco di cose (rimanendo sempre nello stesso stato), non solo attendere un evento e cambiare stato, ad esempio il mio stato di modifica ora dell'orologio non capisco come possa essere facilmente parametrizzato

Si Claudio_FF, grazie per l'intervento, in effetti sono domande che sorgono spontanee quando affronti il problema, si può tentare di inventare qualcosa al momento per sincronizzarle ma alla fine ci si accorge che tutto è già stato inventato e sperimentato.

Ora può sembrare facile dividere una applicazione in stati e ancora più semplice se esiste una sola macchina a stati detta appunto Main Machine. Ma tu e io sappiamo benissimo che non è sempre così evidente specie per il principiante (e non solo).

Allora potrei dire che abbiamo almeno due strade da seguire: 1) Pianifichiamo tutto a tavolino, scriviamo codice di test, scriviamo simulatori, scriviamo diagrammi, ci lavoriamo in tanti fino a quando non appare chiaro che la macchina a stati funzionerà come ci si aspetta. Tradotto vuol dire che investiamo la maggiore parte del tempo per pianificare, per assurdo potremmo ritrovarci a lavora per 2 mesi senza avere scritto una riga di codice.

2) Studiamo il problema in modo superficiale, scriviamo la macchina a stati inserendo gli stati che ci appaiono evidenti, due, tre, quattro comunque pochi. Ritorniamo a studiare il problema e modifichiamo se necessario il codice e ripetiamo questo processo fino a quando otteniamo il risultato desiderato.

Il secondo metodo è quello che si intraprende istintivamente, il primo richiede tanto lavoro, tanta documentazione da produrre e chissà cos'altro prima di potere scrivere una riga di codice.

Per coloro i quali voglio osservare una applicazione e immaginarsi gli stati di questa, consiglio di mettere avanti la proprio lavatrice.

PS: Fate ciò quando siete soli, perché solitamente accende il televisore per guardarlo, avviare la lavatrice per guardarla lavorare può allarmare i vostri cari. :D

Ciao.

mi sono risposto da solo
per memorizzare semplicemente più di 2 possibili azioni su un pin serve un enum
che tanto è un intero

allora tantovale un byte

byte per byte ho pensato prima ad un carattere e poi ad una stringa

typedef bool (* verifica)(void);
typedef void (* esegui)(void);

struct Regola
{
    int stato;
    esegui azione;
    unsigned long int tempo;
    verifica condizione;
    char output[11];
    char input[11];

    int prossimo;
} ;

byte pinoutput[10] = {6, 7, 13, 14};
byte pininput[10] = {2, 3, 4, 5};



byte stato = 0; // lo stato attuale
unsigned long int timestart;


bool vero()
{
    return 1;
}
void nulla()
{
    return;
}

Regola regole[] = {{0, nulla, 10000, vero, "111", "10", 1 }};
#define REGOLE sizeof regole / sizeof regole[0]


void setup()
{
    Serial.begin(9600);
}

void loop()
{
    for (byte i = 0; i < REGOLE; i++)
    {
        if (regole[i].stato == stato)
        {
            if (millis() - timestart >= regole[i].tempo)
            {
                byte j = 0;
                byte test = 1;

                while (regole[i].input[j])
                {
                    if (regole[i].input[j] == '1' && digitalRead(pininput[j] == LOW))
                    {
                        test = 0;
                    }

                    if (regole[i].input[j] == '0' && digitalRead(pininput[j] == HIGH))
                    {
                        test = 0;
                    }

                    j++;

                    if (regole[i].condizione() || test == 1)
                    {
                        stato = regole[i].prossimo;
                        timestart = millis();
                    }
                }
            }
        }

        agisci(stato);
    }
}
void agisci(int stato)
{
    static byte vecchio = 255;

    if (stato != vecchio)
    {
        vecchio = stato;
        Serial.print("inizio stato: ");
        Serial.println(stato);
        regole[stato].azione();
        byte i = 0;

        while (regole[stato].output[i])
        {
            if (regole[stato].output[i] == '1')
            {
                digitalWrite(pinoutput[i], HIGH);
            }

            if (regole[stato].output[i] == '0')
            {
                digitalWrite(pinoutput[i], LOW);
            }

            i++;
        }
    }
}

come vedete ho aggiunto un puntatore a funzione per l’azione dello stato, che verrà eseguita assieme alla stampa

e poi due stringhe, di numeri, 1 vale alto, 0 vale basso, qualsiasi altra cosa (compreso stringa terminata prima della lunghezza massima) vale NON TOCCARE NON INFLUENTE

all’inizio di uno stato assieme alle stampe viene eseguita una volta sola
la stampa
la funzione di ingresso
e un ciclo che legge la stringa degli output e li aggiorna, senza toccare quelli che sono su NON TOCCARE

durante lo stato viene testata sia
la funzione di uscita che
un ciclo che confronta i piedini di ingresso con la stringa di riferimento, ignorando quelli che sono su NON INFLUENTE

per correttezza non lo ho provato, e anche mancano i pinMode, gli stati successivi al primo e via così

è solo uno spunto, ripeto, che comunque la compilazione va a buon fine

anche questo viene dal mio pensare al mio semaforo

prossimo passo
farne una classe, istanziabile (si dice così?) più volte
per avere più macchine in contemporanea, anche che potrebbero interagire, leggendosi tra di loro i piedini
(oddio, scritto così semba una cosa oscena…

Claudio_FF: .....ad esempio il mio stato di modifica ora dell'orologio non capisco come possa essere facilmente parametrizzato

guarda, non mi ci metto nemmeno

non mi piace come è scritto

ho già abbastanza problemi a leggere il c come raccontavano i vecchi (K&R)

ma scritto come lo hai scritto tu non è leggibile per me, mi spiace

Secondo me ogni regola dovrebbe avere un identificativo come primo byte, l'idea è di potere passare un puntatore void all'attuatore di regole il quale ricava il primo byte è pertanto sa come castare il puntatore void, cioè almeno lo deve sapere. Cioè una cosa simile a ciò che si fa con le macchine event driver, dove un evento ha un ID e da questo si ricava la struttura che non è detto sia unica per tutti gli eventi.

Comunque partire dalle regole mi mette in difficoltà le stesse che ho dovuto affrontare al tempo quando ho dovuto scrivere un generatore di makefile.

Ora cosa vogliamo ottenere mi sfugge, forse una macchina a stati generica governata da regole? Mi pare così, interessante è interessante ma non ho mai visto la cosa dalla parte opposta, cioè prima scrivo le regole le carico nel gestore delle regole e avvio la macchina a stati.

Ciao.

questo pensavo un realizzatore di macchine che basta caricare un array di regole e accendere magari invece di un array un file su scheda, si potrà? prossimo passo

AmericanDreamer:
guarda, non mi ci metto nemmeno

Oh, ma non lo pretendevo mica, era un esempio. Comunque descritto a parole è molto più semplice:

  • al primo giro si impostano le variabili per la modifica, si aggiorna il display, si “resetta” il tempo attività ‘t’
  • se passa 1 minuto senza attività encoder (variabili onQualchecosa prodotte da altra funzione) si esce
  • se si preme a lungo si esce
  • se si ruota l’encoder si modifica ora o minuti (sottostato gestito con variabile ‘s’)
  • se si clicca sull’encoder si passa da regolazione ora a minuti a uscita (con write RTC se avvenute modifiche)
  • ogni azione sull’encoder “resetta” il tempo e, se valida, genera un determinato beep acustico

tutto qui :slight_smile:

Questo è uno degli 8 stati della ‘appMachine’ principale che comanda il display e interagisce con l’utente.

Poi ci sono altre 7 macchine che svolgono le funzioni accessorie, come leggere l’encoder (funzione postata in questo thread) o suonare una melodia.

Come parametrizziamo a “regole” le operazioni descritte sopra?

per nulla facile visto così

Guardare la lavatrice in funzione non è male. Specie se ha qualche anno sulle spalle e ogni tanto "sbarella". Ti fa capire il concetto di "risoluzione dei problemi". un ottimo esercizio anche per una macchina a stati finiti. Un altro elettrodomestico da poter osservare senza destare sospetti, la macchina per il pane.

Sarà perché sono manutentore

Ma non mi attrae guardare una lavatrice

Ne ho abbastanza di guardare movimentatori pneumatici e avvitatori

AmericanDreamer:
per nulla facile visto così

È questo che intendevo. Un conto è una soluzione compatta, elegante, pulita, che si adatta a un tipo di problema ben preciso, un conto è elaborare qualsiasi cosa del mondo reale, compreso il realizzare macchine con più stati attivi contemporaneamente (sempre scomponibili in diverse macchine singole, come le cinque incastonate una dentro l'altra, e sincronizzate tra loro, del diagramma seguente):

Qualcosa di simile a quello che voleva fare American Dreamer lo trovate qui. La stessa libreria è installabile dal Gestore di librerie dell'IDE arduino.

Spesso qui viene chiesto come sostituire delay() con millis, lo mostro di seguito prendendo spunto da questo post di un utente: link

Non posto tutto il codice della macchina a stati che potete vedere qui

case 1:
      if (oneShotSwitch) {
          // se oneShotSwitch == true stampa "case 1"
          Serial.println("case 1");
      }
      break;
  case 'D':
      if (oneShotSwitch) {
          // se oneShotSwitch == true 
          gLed = 0;
          rLed = 0;
          bLed = 0;
          wtv020sd16p.playVoice(0);
      }

      //delay(73000); // commentato
      if (elapsedTime >= 73000) // questo codice non impegna la cpu in modo esclusivo per 73 secondi
          currentState = 'E';
      break;
  case 'E':
      wtv020sd16p.stopVoice();
      break;

Ciao.

Qui un esempio di cancello domotica su spunto di questo post

L'interfaccia comandi è banale ed è basata sul valore della variabile command, che per comodità gli viene assegnato un comando nella funzione void serialEvent().

I comandi da inviare dal monitor seriale sono i seguenti: define carattere C_APRI '1' C_CHIUDI '2' C_STOP '3'

// stati di main machine
#define CHIUSO 0
#define IN_APERTURA 1
#define STOP 2
#define APERTO 3
#define IN_CHIUSURA 4

// comandi
#define C_APRI '1'
#define C_CHIUDI '2'
#define C_STOP '3'

boolean oneShotSwitch;
uint32_t elapsedTime;
uint32_t mainTimer;
byte currentState = 0;
byte oldState = ~currentState;
byte command = 0;


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void serialEvent() {
  uint8_t d = Serial.read();
  if (d != 255)
    command = d;
}

void loop() {
  // put your main code here, to run repeatedly:
  oneShotSwitch = false;
  if (currentState != oldState) {
    oneShotSwitch = true;
    mainTimer = millis();
  }
  oldState = currentState;
  elapsedTime = millis() - mainTimer;

  switch (currentState) {
    case CHIUSO:
      if (oneShotSwitch) {

        Serial.println("Cancello chiuso");
      }

      if (command == C_APRI)
        currentState = IN_APERTURA;
      break;
    case IN_APERTURA:
      if (oneShotSwitch) {

        Serial.println("Cancello in apertura");
      }
      if (command == C_STOP)
        currentState = STOP;
      // Ci impiega 10 secondi per aprirsi e poi passa
      // allo stato APEERTO
      if (elapsedTime >= 10000)
        currentState = APERTO;
      break;
    case STOP:
      if (oneShotSwitch) {

        Serial.println("Cancello in STOP");
      }
      if (command == C_APRI)
        currentState = IN_APERTURA;
      if (command == C_CHIUDI)
        currentState = IN_CHIUSURA;
      break;

    case APERTO:
      if (oneShotSwitch) {

        Serial.println("Cancello aperto");
      }
      // Rimane aperto per 10 secondi poi passa allo stato IN_CHIUSURA
      if (elapsedTime >= 10000)
        currentState = IN_CHIUSURA;
      break;

    case IN_CHIUSURA:
      if (oneShotSwitch) {

        Serial.println("Cancello in chiusura");
      }
      if (command == C_STOP)
        currentState = STOP;
      // Impiega 10 secondi per chiudersi poi passa allo stato CHIUSO
      if (elapsedTime >= 10000)
        currentState = CHIUSO;
      break;

  }
  command = 0;
}

Sempre in merito alle macchine a stati finiti, noto che in rete come sempre si trovano informazioni che alle volte sono totalmente scorrette da risultare lampanti, altre volte portano a conclusioni errate anche coloro i quali hanno già esperienza. Per aggiungere confusione alla confusione dico che impiegare uno o più macchine a stati non equivale a realizzare un sistema multitasking, questo perché il concetto di task applicato ad una MCU priva si supporto al multitask è una forzatura. Inoltre su un sistema (o MCU con supporto al multitask) multitasking la suddivisione in stati di una applicazione è ancora utile, desiderata e fattibile per cui la macchina a stati finiti non è da confondere con il multitask.

La macchina a stati aiuta lo sviluppatore a sfruttare al meglio il tempo CPU, per fare ciò il programmatore deve evitare l'uso dell'istruzione delay, ma anche i cicli for e while possono essere deleteri. I cicli possono essere deleteri o incompatibili con l'applicazione quando impegnano in modo esclusivo la CPU per un tempo considerato troppo lungo per l'applicazione in questione.

Impegnare la CPU in un for che per essere eseguito richiede 1 secondo, comporta tempi di risposta almeno di 1 secondo. Potremmo dire che il sistema non risponde in modo sufficientemente reattivo quanto desiderato. Si risolve questo problema dividendo il processo lungo 1 secondo in tanti piccoli pezzetti, diciamo 10, 1/10=0.1s (100ms). Quindi si impegna la CPU per 100ms in modo esclusivo si prende nota di ciò che è stato fatto e si lascia la CPU libera di processare altro codice presente nel loop. Se riusciamo a fare ciò possiamo dire che il sistema (o meglio il programma) ha tempi di reazione di circa 100mS. Bastano 100mS, non bastano tutto dipende da cosa si vuole ottenere.

Il multitask in una MCU della serie ATmega è realizzabile ed è stato fatto in vari modi incluso il così desiderato time-sharing (tempo condiviso), ora si che il concetto di task è più calzante. Il principiante sarà libero di usare delay a tutto spiano ma ad un costo; aumento di complessità, minore tempo CPU a disposizione, numero limitato di task (il limite qui è la RAM). Ma ci sono anche vantaggi.

Quindi riassumendo con la macchina a stati finiti non otteniamo il multitasking, ma sfruttiamo al meglio il tempo CPU, cosa peraltro vantaggiosa anche su un sistema multitasking.

Ciao.

Maurotec:
Sempre in merito alle macchine a stati finiti, noto che in rete come sempre si trovano informazioni che alle volte sono totalmente scorrette da risultare lampanti, altre volte portano a conclusioni errate anche coloro i quali hanno già esperienza. Per aggiungere confusione alla confusione dico che impiegare uno o più macchine a stati non equivale a realizzare un sistema multitasking, questo perché il concetto di task applicato ad una MCU priva si supporto al multitask è una forzatura.

Forse questa tua precisazione non è un caso, visto che proprio stamattina ho consigliato un articolo che parla erroneamente di multitasking! Personalmente quella parte dell’articolo non l’ho presa in considerazione, ma onestamente non ho pensato che per un principiante potesse essere fuorviante, e mi dispiace, quindi modificherò il post.

Grazie
Federico

Maurotec: Quindi riassumendo con la macchina a stati finiti non otteniamo il multitasking, ma sfruttiamo al meglio il tempo CPU, cosa peraltro vantaggiosa anche su un sistema multitasking.

... un sistema a "stati finiti" e spesso usato anche in ambienti "multitask" (che, ti assicuro, girano molto bene su AVR di fascia superiore ... 1284P o 2560 ... meglio il primo, ha il doppio della SRAM).

Come hai ben detto, le due cose sono totalmente distinte e non vanno assolutamente confuse tra loro.

Guglielmo

Il multitasking (più lavori in parallelo) può essere:

  • reale (più core o processori, uno per ogni processo)
  • virtuale (preempitivo o cooperativo, suddivisione del tempo)

Le macchine a stati di cui si parla qui, quando usate per portare avanti più compiti in parallelo rientrano nel mt. virtuale cooperativo (lo switch tra i task è realizzato via software scrivendo codice non bloccante).

Le ISR (routine di servizio interrupt) rientrano invece nel mt. virtuale preempitivo (lo switch tra i task è realizzato via hardware con interrupt, timer ecc).

I task poi possono essere:

  • concorrenti (mt.reale, virtuale preempitivo, ISR, hanno problemi di sincronizzazione intrinsechi)
  • non concorrenti (mt. cooperativo, nessun problema di sincronizzazione intrinseco... a parte quelli introdotti per errore dal programmatore ad un livello più alto)

Riassumendo: c'è parallelismo (multitasking) quando vi è più di un compito portato avanti (in modo reale su più core o a fettine di tempo su una sola CPU), e vi è anche concorrenza quando più task possono accedere asincronamente alle stesse risorse (ad esempio alle stesse variabili condivise).

Una singola macchina a stati non è in se stessa multitasking, ma è il modo più semplice per realizzarlo su un piccolo micro con pochissime risorse, e per di più evita la concorrenza a basso livello tra processi del vero mt. preempitivo / reale.