Il problema: un sistema composto da fasi
Una macchina a stati diventa utile quando un sistema non deve soltanto reagire a un ingresso, ma deve seguire un processo composto da fasi differenti.
Una lavatrice, per esempio, non esegue sempre la stessa azione. Durante un ciclo può trovarsi in condizioni come:
- ferma;
- carico dell’acqua;
- lavaggio;
- scarico;
- centrifuga.
Queste condizioni non sono semplici nomi assegnati alle uscite. Rappresentano la fase raggiunta dal processo e determinano il significato degli ingressi, dei sensori e del tempo trascorso.
Quando la macchina è ferma, l’assenza di acqua è normale. Durante il carico, la stessa assenza di acqua significa che la valvola deve rimanere aperta. Durante il lavaggio può invece indicare un’anomalia. Durante lo scarico può significare che la pompa ha completato il proprio compito.
L’informazione acqua assente non contiene quindi, da sola, la decisione corretta. Per decidere serve anche sapere in quale fase si trova il sistema.
Lo stato è la memoria della fase raggiunta
Lo stato è una variabile interna che riassume la parte del passato necessaria per decidere il comportamento presente.
Se il programma conserva:
statoAttuale = STATO_SCARICO;
sta ricordando che il processo ha già attraversato le fasi precedenti ed è arrivato allo scarico. Da quel momento gli ingressi e le temporizzazioni vengono interpretati secondo le regole dello stato STATO_SCARICO.
In una macchina a stati semplice il sistema occupa un solo stato alla volta. Non può essere contemporaneamente in carico, lavaggio e scarico.
Questa unicità evita combinazioni contraddittorie come:
caricoAttivo = true;
lavaggioAttivo = true;
scaricoAttivo = true;
Con una sola variabile di stato, la fase corrente è invece inequivocabile.
Perché non è un semplice if
Un’istruzione if controlla se una condizione è vera:
if (pulsantePremuto)
{
// esegui un'azione
}
Il problema è stabilire quale azione debba essere eseguita. Lo stesso pulsante può avere significati differenti:
- in
FERMAavvia il ciclo; - in
CARICO_ACQUAannulla il ciclo e avvia lo scarico; - in
LAVAGGIOinterrompe il lavaggio e avvia lo scarico; - in
SCARICOnon arresta la pompa, perché il sistema deve completare lo svuotamento.
Gli if rimangono presenti nel programma, ma vengono valutati all’interno di uno stato preciso. La decisione completa assume la forma:
stato attuale + evento o condizione
→ azione + eventuale nuovo stato
La macchina a stati non sostituisce le istruzioni condizionali. Organizza le condizioni in modo che ogni evento venga interpretato secondo la fase corrente del processo.
Stati, eventi, transizioni e azioni
Una macchina a stati è descritta da quattro elementi.
Stati
Sono le condizioni possibili del sistema. Nel programma finale sono:
STATO_FERMA;STATO_CARICO_ACQUA;STATO_LAVAGGIO;STATO_SCARICO.
Eventi e condizioni
Sono i fatti che possono modificare il comportamento: pressione del pulsante oppure termine di una temporizzazione.
Transizioni
Sono i passaggi da uno stato a un altro. Per esempio:
STATO_FERMA + pulsante
→ STATO_CARICO_ACQUA
Azioni
Sono le operazioni associate allo stato. Nel programma i tre LED rappresentano tre attuatori:
- LED valvola acceso durante il carico;
- LED motore acceso durante il lavaggio;
- LED pompa acceso durante lo scarico.
Diagramma degli stati
Il diagramma mostra gli stati come rettangoli e le transizioni come frecce. Le scritte sulle frecce indicano l’evento o la condizione che provoca il passaggio.
Tabella delle transizioni
Lo stesso comportamento può essere descritto senza codice mediante una tabella.
FERMA + pulsante → CARICO_ACQUA
CARICO_ACQUA + 5 secondi → LAVAGGIO
CARICO_ACQUA + pulsante → SCARICO
LAVAGGIO + 10 secondi → SCARICO
LAVAGGIO + pulsante → SCARICO
SCARICO + 5 secondi → FERMA
La tabella definisce quali passaggi sono ammessi. Non esiste, per esempio, una transizione diretta da CARICO_ACQUA a FERMA: anche in caso di annullamento il sistema passa prima allo scarico.
Che cosa fa il programma
Il programma simula un ciclo automatico semplificato mediante un pulsante e tre LED.
- In
FERMAtutti i LED sono spenti. - La pressione del pulsante avvia
CARICO_ACQUAe accende il LED della valvola. - Dopo 5 secondi il sistema passa a
LAVAGGIOe accende il LED del motore. - Dopo 10 secondi passa a
SCARICOe accende il LED della pompa. - Dopo 5 secondi di scarico torna a
FERMA.
Se il pulsante viene premuto durante il carico o il lavaggio, il ciclo viene annullato ma il sistema passa comunque a SCARICO. Durante lo scarico il pulsante viene ignorato e la pompa simulata rimane attiva fino al termine dei 5 secondi.
Questo comportamento rende evidente che il significato del pulsante dipende dallo stato corrente.
Collegamenti
Il programma utilizza:
- pulsante tra il pin 2 e GND;
- LED della valvola sul pin 8 attraverso una resistenza da 330 Ω;
- LED del motore sul pin 9 attraverso una resistenza da 330 Ω;
- LED della pompa sul pin 10 attraverso una resistenza da 330 Ω;
- catodi dei tre LED collegati a GND.
Il pulsante utilizza INPUT_PULLUP. A pulsante rilasciato il pin legge HIGH; quando il pulsante viene premuto e collega il pin a GND, la lettura diventa LOW.
I LED rappresentano soltanto gli attuatori. Un’elettrovalvola, un motore o una pompa reali non possono essere alimentati direttamente dai pin dell’ATmega328P e richiedono circuiti di potenza e protezioni adeguati.
Definizione degli stati con enum
Gli stati vengono definiti come un insieme chiuso di valori:
enum StatoSistema
{
STATO_FERMA,
STATO_CARICO_ACQUA,
STATO_LAVAGGIO,
STATO_SCARICO
};
La variabile:
StatoSistema statoAttuale = STATO_FERMA;
può contenere uno solo dei valori dichiarati. In questo modo il programma conserva esplicitamente la fase corrente.
enum non crea da solo una macchina a stati. Serve soltanto a rappresentare gli stati con nomi leggibili invece di utilizzare numeri privi di significato.
Una sola funzione per cambiare stato
Ogni transizione passa attraverso la funzione:
void cambiaStato(
StatoSistema nuovoStato,
unsigned long tempoCorrenteMs
)
La funzione esegue quattro operazioni:
- stampa sul monitor seriale lo stato precedente e quello nuovo;
- aggiorna
statoAttuale; - registra il momento di ingresso nel nuovo stato;
- imposta le uscite corrispondenti.
Concentrare queste operazioni in una sola funzione impedisce di cambiare stato dimenticando di aggiornare la temporizzazione oppure le uscite.
Le uscite dipendono dallo stato
La funzione impostaUscite() spegne prima tutti i LED e poi accende soltanto quello associato allo stato corrente.
La relazione è:
STATO_FERMA → tutti i LED spenti
STATO_CARICO_ACQUA → LED valvola acceso
STATO_LAVAGGIO → LED motore acceso
STATO_SCARICO → LED pompa acceso
I LED non sono gli stati. Sono le uscite prodotte dagli stati. La macchina continuerebbe ad avere la stessa struttura anche sostituendo i LED con relè, transistor, messaggi seriali o indicazioni su un display.
La logica centrale con switch
Nel loop() viene eseguita soltanto la parte relativa allo stato corrente:
switch (statoAttuale)
{
case STATO_FERMA:
// attende il pulsante
break;
case STATO_CARICO_ACQUA:
// controlla pulsante e tempo di carico
break;
case STATO_LAVAGGIO:
// controlla pulsante e tempo di lavaggio
break;
case STATO_SCARICO:
// controlla il tempo di scarico
break;
}
switch non è la macchina a stati. È soltanto una forma ordinata per selezionare la logica associata allo stato corrente.
La macchina a stati è costituita dagli stati definiti, dagli eventi riconosciuti, dalle transizioni ammesse e dalle azioni eseguite.
Temporizzazioni senza delay()
Quando il sistema entra in un nuovo stato, viene memorizzato il valore di millis():
inizioStatoMs = tempoCorrenteMs;
Nel loop() il tempo trascorso viene calcolato con:
unsigned long tempoNelloStatoMs =
tempoCorrenteMs - inizioStatoMs;
La transizione avviene quando il tempo raggiunge la durata prevista.
Il programma non si blocca durante l’attesa. Può continuare a leggere il pulsante e reagire all’annullamento mentre il carico o il lavaggio sono in corso.
Debounce del pulsante
Un contatto meccanico non passa istantaneamente e in modo pulito da aperto a chiuso. Per alcuni millisecondi può produrre più commutazioni elettriche.
La funzione pressionePulsanteRilevata() considera valida una variazione soltanto quando la nuova lettura rimane stabile per almeno 40 millisecondi.
La funzione restituisce true una sola volta quando viene riconosciuta la transizione stabile da pulsante rilasciato a pulsante premuto. Tenere premuto il pulsante non genera quindi eventi ripetuti a ogni esecuzione del loop().
Programma completo
Download: macchina_stati.ino
/*
Macchina a stati: ciclo automatico semplificato.
Hardware:
- pulsante tra pin 2 e GND;
- LED valvola sul pin 8 tramite resistenza;
- LED motore sul pin 9 tramite resistenza;
- LED pompa sul pin 10 tramite resistenza.
Il pulsante utilizza INPUT_PULLUP:
rilasciato = HIGH
premuto = LOW
Sequenza:
FERMA -> CARICO_ACQUA -> LAVAGGIO -> SCARICO -> FERMA
Premendo il pulsante durante CARICO_ACQUA o LAVAGGIO,
il ciclo viene annullato e passa a SCARICO.
*/
const uint8_t PIN_PULSANTE = 2;
const uint8_t PIN_LED_VALVOLA = 8;
const uint8_t PIN_LED_MOTORE = 9;
const uint8_t PIN_LED_POMPA = 10;
const unsigned long DURATA_CARICO_MS = 5000UL;
const unsigned long DURATA_LAVAGGIO_MS = 10000UL;
const unsigned long DURATA_SCARICO_MS = 5000UL;
const unsigned long DEBOUNCE_MS = 40UL;
enum StatoSistema
{
STATO_FERMA,
STATO_CARICO_ACQUA,
STATO_LAVAGGIO,
STATO_SCARICO
};
StatoSistema statoAttuale = STATO_FERMA;
unsigned long inizioStatoMs = 0;
unsigned long ultimoCambioLetturaMs = 0;
bool letturaPrecedentePulsante = HIGH;
bool statoStabilePulsante = HIGH;
void stampaNomeStato(StatoSistema stato)
{
switch (stato)
{
case STATO_FERMA:
Serial.print(F("FERMA"));
break;
case STATO_CARICO_ACQUA:
Serial.print(F("CARICO_ACQUA"));
break;
case STATO_LAVAGGIO:
Serial.print(F("LAVAGGIO"));
break;
case STATO_SCARICO:
Serial.print(F("SCARICO"));
break;
}
}
void impostaUscite(StatoSistema stato)
{
digitalWrite(PIN_LED_VALVOLA, LOW);
digitalWrite(PIN_LED_MOTORE, LOW);
digitalWrite(PIN_LED_POMPA, LOW);
switch (stato)
{
case STATO_FERMA:
break;
case STATO_CARICO_ACQUA:
digitalWrite(PIN_LED_VALVOLA, HIGH);
break;
case STATO_LAVAGGIO:
digitalWrite(PIN_LED_MOTORE, HIGH);
break;
case STATO_SCARICO:
digitalWrite(PIN_LED_POMPA, HIGH);
break;
}
}
void cambiaStato(
StatoSistema nuovoStato,
unsigned long tempoCorrenteMs
)
{
Serial.print(F("Transizione: "));
stampaNomeStato(statoAttuale);
Serial.print(F(" -> "));
stampaNomeStato(nuovoStato);
Serial.println();
statoAttuale = nuovoStato;
inizioStatoMs = tempoCorrenteMs;
impostaUscite(statoAttuale);
}
bool pressionePulsanteRilevata(
unsigned long tempoCorrenteMs
)
{
bool letturaAttuale = digitalRead(PIN_PULSANTE);
if (letturaAttuale != letturaPrecedentePulsante)
{
letturaPrecedentePulsante = letturaAttuale;
ultimoCambioLetturaMs = tempoCorrenteMs;
}
if (
tempoCorrenteMs - ultimoCambioLetturaMs
>= DEBOUNCE_MS
)
{
if (letturaAttuale != statoStabilePulsante)
{
statoStabilePulsante = letturaAttuale;
if (statoStabilePulsante == LOW)
{
return true;
}
}
}
return false;
}
void setup()
{
pinMode(PIN_PULSANTE, INPUT_PULLUP);
pinMode(PIN_LED_VALVOLA, OUTPUT);
pinMode(PIN_LED_MOTORE, OUTPUT);
pinMode(PIN_LED_POMPA, OUTPUT);
Serial.begin(115200);
statoStabilePulsante = digitalRead(PIN_PULSANTE);
letturaPrecedentePulsante = statoStabilePulsante;
inizioStatoMs = millis();
impostaUscite(statoAttuale);
Serial.println(F("Sistema avviato"));
Serial.print(F("Stato iniziale: "));
stampaNomeStato(statoAttuale);
Serial.println();
}
void loop()
{
unsigned long tempoCorrenteMs = millis();
bool pulsantePremuto =
pressionePulsanteRilevata(tempoCorrenteMs);
unsigned long tempoNelloStatoMs =
tempoCorrenteMs - inizioStatoMs;
switch (statoAttuale)
{
case STATO_FERMA:
if (pulsantePremuto)
{
cambiaStato(
STATO_CARICO_ACQUA,
tempoCorrenteMs
);
}
break;
case STATO_CARICO_ACQUA:
if (pulsantePremuto)
{
cambiaStato(
STATO_SCARICO,
tempoCorrenteMs
);
}
else if (
tempoNelloStatoMs >= DURATA_CARICO_MS
)
{
cambiaStato(
STATO_LAVAGGIO,
tempoCorrenteMs
);
}
break;
case STATO_LAVAGGIO:
if (
pulsantePremuto
|| tempoNelloStatoMs >= DURATA_LAVAGGIO_MS
)
{
cambiaStato(
STATO_SCARICO,
tempoCorrenteMs
);
}
break;
case STATO_SCARICO:
if (
tempoNelloStatoMs >= DURATA_SCARICO_MS
)
{
cambiaStato(
STATO_FERMA,
tempoCorrenteMs
);
}
break;
}
}
Perché questo programma è una macchina a stati
Il programma non è una macchina a stati semplicemente perché contiene enum e switch.
Lo è perché:
- possiede un insieme definito di fasi;
- conserva una sola fase corrente;
- interpreta gli eventi in funzione di quella fase;
- ammette soltanto transizioni stabilite in precedenza;
- associa a ogni stato determinate uscite;
- registra il momento di ingresso nello stato per gestirne la durata.
Il pulsante fornisce l’esempio più evidente. In FERMA avvia il ciclo; in CARICO_ACQUA e LAVAGGIO lo annulla; in SCARICO non produce alcuna transizione.
Dal modello semplificato a un sistema reale
Il codice utilizza tempi fissi e LED per rendere visibile la sequenza. Un sistema reale introdurrebbe ulteriori stati e condizioni, per esempio:
- controllo della porta;
- livello dell’acqua;
- temperatura;
- errore di riempimento;
- errore di scarico;
- arresto di sicurezza;
- centrifuga;
- fine ciclo.
La struttura rimarrebbe la stessa: ogni nuovo comportamento dovrebbe essere definito come stato, evento, transizione oppure azione, evitando combinazioni implicite di flag e condizioni sparse.
Conclusioni
Una macchina a stati organizza il comportamento di un sistema che attraversa fasi differenti nel tempo.
Lo stato rappresenta la memoria della fase raggiunta. Gli eventi e le condizioni vengono interpretati in funzione di quella memoria; le transizioni stabiliscono quali passaggi sono consentiti; le azioni determinano le uscite associate a ogni fase.
Le istruzioni if continuano a essere utilizzate, ma non rimangono isolate. Ogni condizione viene valutata all’interno dello stato al quale appartiene.
Nel programma finale il ciclo passa da fermo a carico, lavaggio e scarico. La pressione dello stesso pulsante produce risultati differenti secondo lo stato corrente. È proprio questa dipendenza dalla fase memorizzata a distinguere una macchina a stati da una semplice reazione a un ingresso.
Fonti principali
David Harel — Statecharts: A Visual Formalism for Complex Systems, Science of Computer Programming, 1987.
Microchip Technology — ATmega328P: 8-bit AVR Microcontroller with 32K Bytes In-System Programmable Flash, datasheet.
Arduino — documentazione di millis(), switch, digitalRead(), digitalWrite() e INPUT_PULLUP.