Go Down

Topic: Macchina a stati finiti (FSM) basata su switch case  (Read 681 times) previous topic - next topic

Maurotec

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.

AmericanDreamer

#16
Oct 27, 2019, 07:27 pm Last Edit: Oct 27, 2019, 07:35 pm by AmericanDreamer
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

Code: [Select]


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...

AmericanDreamer

.....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


Maurotec

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.

AmericanDreamer

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

Claudio_FF

#20
Oct 27, 2019, 08:00 pm Last Edit: Oct 27, 2019, 08:29 pm by Claudio_FF
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 :)

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?
********* IF e SWITCH non sono cicli ! *********
**** Una domanda ben posta è già mezza risposta ****
*** La corrente si misura in mA, la quantità di carica in mAh ***

AmericanDreamer


speedyant

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.

AmericanDreamer

Sarà perché sono manutentore

Ma non mi attrae guardare una lavatrice

Ne ho abbastanza di guardare movimentatori pneumatici e avvitatori

Claudio_FF

#24
Oct 28, 2019, 09:15 am Last Edit: Oct 28, 2019, 09:35 am by Claudio_FF
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):

********* IF e SWITCH non sono cicli ! *********
**** Una domanda ben posta è già mezza risposta ****
*** La corrente si misura in mA, la quantità di carica in mAh ***

Maurotec

#25
Nov 15, 2019, 02:03 am Last Edit: Nov 15, 2019, 02:29 am by Maurotec
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

Code: [Select]

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.


Maurotec

#26
Nov 24, 2019, 10:53 am Last Edit: Nov 24, 2019, 11:05 am by Maurotec
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'

Code: [Select]

// 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;
}

Maurotec

#27
Nov 29, 2019, 02:54 pm Last Edit: Nov 29, 2019, 02:58 pm by 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. 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.



Federico66

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
"La logica vi porterà da A a B. L'immaginazione vi porterà dappertutto." A. Einstein

gpb01

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
Search is Your friend ... or I am Your enemy !

Go Up