Multitasking: Arduino Automa a stati finiti

Multitasking e l’automa a stati finiti, chi si diletta con Arduino sicuramente avrà visto e provato lo sketch del lampeggio del led, il famoso Blink…

Blink Sketch:

int led = 13;

void setup()
{
  pinMode(led, OUTPUT);
}


void loop()
{
  digitalWrite(led, HIGH);
  delay(1000); // attendo 1 secondo
  digitalWrite(led, LOW);
  delay(1000);    // attendo 1 secondo
}

Credo che di fatto è il primo sketch che si vada ad eseguire su Arduino, il classico “Hello world!” diciamo, anche grazie al fatto di poter sfruttare direttamente il led integrato in Arduino, quello collegato al pin 13 per intenderci.

Un’occhiata al codice e si intuisce subito che il lampeggio è determinato dalla funzione delay() che mette in attesa Arduino come un pazzo per N millisecondi a far niente, per esempio con N=500 avremmo un lampeggio al secondo perché rimarrebbe 500 millisecondi spento e 500 millisecondi accesso , con N=1000 ogni due secondi e così via…

Dopo averci giocato un pò variando la frequenza del lampeggio, ad un certo punto potremmo però volerci collegare un altro led, dovendo solo decidere se farlo lampeggiare insieme all’altro cioè farli accendere e spegnere insieme nello stesso istante, oppure farli accendere e spegnere in modo alternato, quando si spegne uno si accende l’altro e viceversa.
Volendo scegliere il primo caso, il codice diventerebbe così:

int led_uno = 13; 
int led_due = 12
 
 
void setup()
{
pinMode(led_uno, OUTPUT);
pinMode(led_due, OUTPUT);
}
 

void loop() 
{ 
digitalWrite(led_uno, HIGH);
digitalWrite(led_due, HIGH);
 
delay(1000); // attendo 1 secondo
 
digitalWrite(led_uno, LOW);
digitalWrite(led_due, LOW);
 
delay(1000);    // attendo 1 secondo
}

Abbastanza facile no???
Ma se invece volessimo far lampeggiare il secondo led con una frequenza maggiore??
Dovremmo variare l’argomento da passare alla funzione delay(), invece di 1000 millisecondi aumentiamo la frequenza diminuendo il tempo a 250 millisecondi, ma questo non basterebbe, perché come è facile intuire in questo modo andremmo a variare anche la frequenza del primo led.
E quindi la cosa da fare è scervellarsi in modo da far “incastrare” tra di loro diverse chiamate della funzione delay() … poco ortodosso e poco elegante!

Dobbiamo pensare a qualcosa … c’è bisogno di una logica per separare le cose, in modo da far lavorare i led nel modo più indipendente possibile l’uno dall’altro.

Per fare questo esiste già un modello logico, e come da titolo si tratta dell’automa a stati finiti.
Passeremo quindi da un modello imperativo ad un modello a eventi, evitando quindi quanto più possibile che nessuna parte del codice rimanga in attesa di non si sa che cosa.

Tutto si scompone in quattro punti fondamentali:

Stati:
parti di codice che si ricordano di qualcosa ad esempio aver premuto un pulsante, semplice concetto di memoria… anche un istante di tempo può essere uno stato, ad esempio, aver acceso un led dopo 5 secondi dall’avvio di Arduino può essere uno stato, quindi salvo in memoria che sono trascorsi 5 secondi dall’avvio di Arduino fino all’accensione del led.

Eventi:
sono le cose che succedono, come l’esser trascorso un certo tempo, ad esempio, un if che controlli se dall’avvio di Arduino sono trascorsi 10 secondi oppure no.

Transizioni:
le azioni che vengono eseguite dopo un evento, ad esempio, sono trascorsi 2 secondi dall’avvio di Arduino? Allora accendo un led.

Stato iniziale:
si tratta della configurazione inziale degli stati di memoria, che vengono decisi nel modo più oppurtuno perché necessari per il corretto avvio e funzionamento del sistema.

A questo punto vediamo come si scompone il problema del lampeggio dei led adottando questa logica nel nostro sketch.

#define led_uno 12
#define led_due 13

// led uno
unsigned long istante_ultimo_cambiamento_led_uno = 0;      // qui salvo l'istante di tempo nel quale è cambiato lo stato del led uno
int tempo_led_uno = 1000; // millisecondi                  // qui salvo e decido quanto tempo deve restare off/on il led uno
boolean led_uno_off_on = false;                            // qui salvo l'ultimo stato del led uno.... l'ultima volta era off oppure on ?


// led due
unsigned long istante_ultimo_cambiamento_led_due = 0;
int tempo_led_due = 250; // millisecondi
boolean led_due_off_on = false;


void setup()
{
  pinMode(led_uno, OUTPUT);
  pinMode(led_due, OUTPUT);
}


void loop()
{
  func_led_uno();
  func_led_due();
}


void func_led_uno()
{
  // da quando ho cambiato lo stato del led, è trascorso il tempo deciso per fare di nuovo uno scambio di stato ?
  if (millis() - istante_ultimo_cambiamento_led_uno >= tempo_led_uno) {

    // se prima era spento inverto lo stato in modo da accenderlo o viceversa
    led_uno_off_on = ! led_uno_off_on;

    // salvo l'istante in cui vado a cambiare di stato il led in modo da calcolare successivamente se sia trascorso il tempo necessario per effettuare di nuovo lo scambio
    istante_ultimo_cambiamento_led_uno = millis();

    // accendo o spengo il led in base allo stato precedente, che mantengo in memoria grazie alla variabile led_uno_off_on
    digitalWrite(led_uno, led_uno_off_on);
  }
}


void func_led_due()
{
  if (millis() - istante_ultimo_cambiamento_led_due >= tempo_led_due) {
    led_due_off_on = ! led_due_off_on;
    istante_ultimo_cambiamento_led_due = millis();
    digitalWrite(led_due, led_due_off_on);
  }
}

A questo punto per chi non è proprio alle prime armi dovrebbe essere più o meno chiaro come applicare questo modello logico per simulare il multitasking, se avete letto lo sketch anche grazie ai vari commenti ed ai nomi che ho usato per le variabili dovrebbe essere abbastanza intuitivo.

E’ anche evidente come i due led siano stati separati tra di loro, rispettivamente in func_led_uno() e func_led_due().
Ma andiamo ad analizzare ancora qualcosa in più per chiarirci meglio le idee.

La funzione millis(); questa funzione non fà altro che restituire il tempo in millisecondi trascorso dall’avvio di Arduino,
che a noi sinceramente poco importa, ma ciò che a noi interessa è la sua funzione da contatore, perché il tempo che scorre dall’avvio di Arduino non è altro che un contatore a tutti gli effetti, inoltre sappiamo che si incrementa di 1 ogni millisecondo perché si tratta di un registro che non viene influenzato dall’esecuzione dello sketch, ne consegue che se in un dato istante il contatore cioè millis() restituisce 1000 ed alla successiva lettura ci restituisce 2000 viene da se che tra la prima lettura e la successiva è trascorso 1 secondo…. e perché ci interessa questa cosa???
Perché avendo un riferimento temporale abbiamo la possibilità di scandire in modo preciso determinate azioni senza bloccare lo sketch!

Generalizziamo il nostro concetto di multitasking traducendolo in codice per Arduino:

unsigned long tempo_di_azione = 2000;
unsigned long millis_ultima_azione = 0;


void Setup()
{
  // ...
}


void Loop()
{
  if ( millis() - millis_ultima_azione >= tempo_di_azione) {
    millis_ultima_azione = millis();


    // leggi sensore temperatura
    // oppure ...
    // scrive su micro SD
    // oppure ...
    // inverti stato del pin X
    // etc etc...
  }
}

Cosa determinano le variabili..?

tempo_di_azione determinerà il tempo che dovrà trascorrere tra un’azione e la successiva, o se vogliamo possiamo anche dire che si tratta del tempo che dovrà trascorrere per rendere VERA di volta in volta la condizione all’interno dell’IF.
Nel caso pratico del led determina appunto il tempo ACCESO/SPENTO

millis_ultima_azione questa variabile serve per ricordare ad Arduino quanto “segnava” in termine di tempo millis() l’ultima volta che ha eseguito l’azione, in modo da poter successivamente eseguire una semplice sottrazione ottenendo il tempo trascorso ogni volta che andrà a domandarsi tramite l’IF: “è trascorso il tempo minimo richiesto per compiere una nuova azione?” …. in più con questa variabile possiamo anche determinare dopo quanto tempo far eseguire la prima azione dall’avvio di Arduino.
Ad esempio, invece di inizializzarla a 0 millisecondi la si inizializza a 10000, in questo modo è chiaro come la prima azione non sarà più eseguita dopo 2000 millisecondi ma dopo 12000.
Perché se prima bastava che millis() raggiungesse un valore pari a 2000 per ottenere il tempo minimo che noi abbiamo deciso debba trascorrere tra un’azione e la successiva, ottenendo quindi 2000 – 0 = 2000 nel caso di 2000 -10000 = -8000 non otteniamo più un valore uguale o maggiore al nostro tempo di azione perché -8000 non è maggiore di 2000, di conseguenza dovrà trascorrere un tempo minimo totale di 12000 millisecondi in modo da ottenere 12000 – 10000 = 2000 che coincide con il nostro tempo di azione stabilito .. così Arduino potrà tranquillamente eseguire l’azione dentro l’IF per la prima volta.

Ovviamente dentro l’IF non dobbiamo per forza cambiare lo stato di un’uscita cioè accendere e spegnere due led con tempi diversi, ma come avrete già intuito dai commenti dello sketch possiamo ad esempio leggere un sensore di temperatura, oppure scrivere su una memoria SD, e chi più ne ha più ne metta!

Capire la logica dell’Automa a stati finiti per simulare il multitasking è abbastanza importante, altrimenti non è che si può poi andare tanto lontani dall’accendere e spegnere un led.

Immaginate se invece del led ci fosse collegato un sensore al quale non è possibile permettersi dei ritardi sulla lettura, tipo le fotocellule che troviamo nei cancelli automatici …. Arduino è fermo a girare come un pazzo con un delay(90000) per chiudere un cancello, qualcuno nello stesso tempo prova ad attraversare e viene colpito perché Arduino in quel momento non stava leggendo il segnale dalle fotocellule… non è certo una cosa accettabile.

E qui possiamo vedere un esempio dei led in funzione:

Arrivati alla fine dell’articolo vi lascio con un link di un vecchio articolo in cui viene spiegato come creare una centralina per serrande o cancelli con Arduino utilizzando la funzione millis() invece di delay()…

Come al solito vi auguro buon divertimento e alla prossima ;)