Pagine

giovedì 4 febbraio 2010

Multitasking Arduino: millis() -- PARTE 1

Premessa

Quando si iniziano a muovere i primi passi con Arduino, un ottimo punto di partenza per comprenderne il funzionamento è senza dubbio la pagina del playground.
Tramite gli spezzoni di codice presenti in questa sezione del sito, si è in grado di muovere i primi passi di programmazione sulla scheda.
Tuttavia, nel momento in cui la situazione diventa più articolata quando, cioè, si aggiungono due, tre, quattro, dieci componenti.... tutto diventa più difficile da seguire ed il grado di complicazione può diventare anche molto elevato.
In tal caso può agevolare molto il concetto di programmazione a stati finiti che andremo a vedere in articoli futuri ma anche l'utilizzo della funzione millis() che tratteremo in questo articolo e nei prossimi post e che permette una sorta di multitasking.

millis() vs delay()

Apparentemente le due funzioni non sono in contrapposizione ed effettivamente a livello logico è così.

La funzione delay(n) permette di fermare l'esecuzione del programma per n millisecondi. Tutto si blocca, la scheda è ferma e non rileva alcun input così come non invia alcun output.
Di conseguenza, se nel frattempo succede qualcosa e i sensori attaccati al sistema rilevano variazioni interessanti... non succederà nulla!
Questa opzione che può sembrare dannosa è, al contrario, estremamente importante in svariati scenari.
Se consideriamo che Arduino effettua circa 16 milioni di operazioni al secondo, sarebbe complesso leggere l'output di un comando su uno schermo se questo viene aggiornato a tale frequenza. In tal caso si usa il delay tra un avviso ed un altro aspettando almeno 500ms (mezzo secondo)...
Un altro caso è la lettura di un sensore. Si pensi ad un sensore di luce o temperatura; sono due sensori con una inerzia piuttosto elevata e per questo sarebbe inutile interrogarli 16 milioni volte in un secondo ma è sufficiente 1 volta al secondo o anche meno... Da qui l'uso di delay().

Al contrario millis() restituisce il numero di millisecondi passati dall'accensione del sistema con arduino. La funzione millis() può essere immagazzinata in una variabile unsigned long int: questo tipo di dati occupa molto spazio in memoria ma permette di arrivare a circa 50 giorni di autonomia prima di subire un azzeramento del valore.

Nei prossimi post andremo ad analizzare meglio queste differenze tra l'uso di delay() e millis().

Usare millis() in uno sketch

Il numero di millisecondi dalla accensione della scheda deve essere immagazzinato in una variabile all'inizio di loop() dopo essere stata inizializzata nel setup e quindi aggiornato ad ogni ciclo.
Tale variabile che chiameremo genericamente time e sarà il punto di riferimento per tutte le altre azioni/eventi.

A questo punto il ciclo di loop inizia a "girare" e si presenta un evento come, ad esempio, la lettura di un sensore o la scrittura su una seriale.
L'operazione viene effettuata la prima volta.
Se però, un evento differente o il medesimo evento si deve ripetere a distanza di tempo si presenta un problema.
In tal caso, infatti, si tende ad usare il delay per "bloccare" l'esecuzione dello sketch in attesa dell'evento successivo.
Il tutto funziona bene se i componenti in gioco sono pochi (2 o 3) ma in caso contrario si nota un rallentamento del sistema, un funzionamento "a scatti" o malfunzionamenti vari...

Per ovviare a questo problema si può assegnare ad ogni evento una variabile-tempo e se passano un tot di millisecondi tra l'evento ed il successivo, allora viene dato il comando di esecuzione tramite un if.

Tale spiegazione sembra piuttosto macchiavellica ma vediamo un esempio per capire meglio.
Leggete il codice seguente:
void setup(){
time=millis();
letturaluce_time=mills();
servotime=millis();
letturadati=millis();
}

void loop(){
time=millis();

if(time>letturaluce_time+5){
luce1=analogRead(SENSORELUCE1);
letturaluce_time=millis();
}

if(time>servotime1+15){
servo1.write(posservo1);
servotime1=millis();
}

if(time>letturadati+3000){
Serial.print("Sensore 1 : ");
Serial.print(letturaluce1,DEC);
Serial.print(" Posizione motore 1 : ");
Serial.println(posservo1,DEC);
Serial.print(" Differenza : ");
Serial.println(letturaluce1-letturaluce2);
letturadati=millis();
}
}

Cerchiamo di scoprire la dinamica di questo programma.
  • Nella funzione setup() vengono inizializzate le variabili time e quelle che segnano il tempo di ciascuno evento.
  • Successivamente la variabile time viene aggiornata all'inizio del loop() ad ogni ciclo
  • Si presuppongono tre eventi distinti: la lettura di un sensore (ogni 5 ms), il movimento di un servomotore (ogni 15ms) e la scrittura di dati sulla seriale (ogni 3000ms).
    Se usassimo il delay bloccheremo il programma ed è per questo che nello sketch sopra riportato si usa il millis().
Il concetto è il seguente:
  • Se sono passati x millisecondi fai ciò che è specificato nell'if ed aggiorna la variabile-tempo riferita all'evento.
Si noti infatti che ogni evento è associato ad un if(time>variabiletempo+millisecondi).
All'interno dell'if vengono svolte alcune azioni.
Al termine del if, l'ultima istruzione è l'aggiornamento della variabile-tempo corrispondente assegnandogli un nuovo valore di millis().

Questo è il concetto che sta alla base del multitasking con arduino ed il modo per eliminare il delay nei programmi sostituendolo con millis().
Nei prossimi post approfondiremo l'argomento.

Articolo successivo "Multitasking Arduino: millis() -- PARTE 2"

26 commenti:

  1. grande vitto. bellissimo articolo (molto utile)

    RispondiElimina
  2. Ottimo sono nuovo appassionato di Arduino con spiegazioni così s'impara in fretta.

    GRAZIE !!

    RispondiElimina
  3. Ciao Gianni,
    ti ringrazio per i complimenti.
    Guarda anche la seconda parte dell'articolo perchè faccio un esempio più completo.

    RispondiElimina
  4. Sono un novizio di programmazione anche se da molti anni desideravo imparare tutto ciò. MI sono deciso perché è difficile interagire con un programmatore e i giovani programmatori non capiscono i vecchi sperimentatori... Ho da poco comprato un paio di Arduino e sto cercando di raccogliere tutte le informazioni che trovo catalogandole per settore e livello di difficoltà. Mi si sta risvegliando il vecchio sperimentatore elettronico e ringrazio chi fa opera di divulgazione di base, sì da risultare comprensibile anche a me. Spero che continui a illustrare parti di programmazione pratica. Grazie.
    Cristiano

    RispondiElimina
  5. Buongiorno Cristiano, grazie per il commento.
    Effettivamente sono in cantiere altri due articoli divulgativi su Arduino.
    Purtroppo manca sempre il tempo ma questa è una costante per tutti...
    Se ti va, puoi rimanere aggiornato dei miei articoli tramite il feed RSS.
    A presto,
    Vittorio

    RispondiElimina
  6. Complimenti,
    bell'articolo.
    Grazie

    RispondiElimina
  7. Grazie, spero che il tuo codice mi aiuti con un piccolo problema arduinese...sta sera ci provo!

    RispondiElimina
  8. ciao nathanvi (vengo dal forum arduino :))

    complimenti per l'ottmo tutorial.

    vengo al dunque, devo mandare a video per tot tempo alcune info con tempi diversi, tipo la prima deve restare per 3 secondi, la seconda ci deve restare per 1 secondo.

    ho provato cosi' prendendo spunto dal tutorial ma evidentemente mi perdo da qualche parte, perche' i tempi sono sballati, all'inizio resta il messaggio per 3 secondi poi solo per uno, poi a volte per 4 secondi.

    -------------------------
    if(runTime>ORARIO+3000){
    alterna = ClockArray;
    ORARIO=millis();
    }

    if(runTime>DATA+1000){
    alterna = DateArray;
    DATA=millis();
    }

    // Display.
    DisplayNumberString( alterna );
    -----------------------------------------

    in futuro devo aggiungere anche TEMPERATURA,

    dove sbaglio ?

    grazie

    RispondiElimina
    Risposte
    1. Ciao Testato.

      Secondo me le due scritture devono essere sincronizzate con un'unica variabile tempo, in questo modo:

      -------------------------
      if(runTime>ORARIO+4000){
      alterna = ClockArray;
      ORARIO=millis();
      }

      if(runTime>ORARIO+3000){
      alterna = DateArray;
      }

      // Display.
      DisplayNumberString( alterna );
      -----------------------------------------

      Nicola

      Elimina
    2. in che variabile salvi runtime e orario?? se sono int a 16 bit è probabile che si azzera facilmente il contatore e ti risultano orari strani. prova a usare un unsigned double

      Elimina
  9. Da cosa vedo direi nulla.
    Un comportamento analogo, tuttavia, l'ho avuto anche io nella gestione del RFID. Sembra che rimandare il codice in una funzione cozzi un po' con il millis().
    Bada bene, è una regola empirica, ma la mia sensazione è questa... soprattutto, come immagino, nel DisplayNumberString c'è una "print".
    Prova ad eliminare il rimando alla funzione e vedi se così stampa correttamente.

    RispondiElimina
  10. Ottimo articolo! Era da un Po che cercavo una soluzione x il blocco del codice da parte del delay!
    Grazie

    RispondiElimina
  11. Complimenti, un ottima spiegazione sulla simulazione di MT con time sharing :)
    La userò in un progetto che ho.

    Grazie
    Danilo

    RispondiElimina
  12. Ciao Vittorio, grazie per l'articolo!
    volevo chiederti una cosa, è necessario mettere "time=millis();"
    sia dopo il setup che dopo il loop?
    grazie!

    RispondiElimina
    Risposte
    1. Si.
      Dopo il setup inizializzi il valore e ad ogni ciclo aggiorni la variabile

      Elimina
  13. grazie mille articolo fantastico mi ha aiutato molto

    RispondiElimina
    Risposte
    1. Grazie a te per il feedback! Ovviamente mi ha fatto molto piacere!

      Elimina
  14. Ciao... Mi chiamo Giovanni e mi hai fatto capire in un attimo quello che non andava nel mio approccio alla programmazione. Sono un vecchio elettronico e mi sto misurando con questa nuova sfida senza fare corsi o roba del genere. un mio amico programmatore aveva cercato di spiegarmi il funzionamento della macchna a stati ma non ci avevo capito gran che! Il bello naturalmente viene adesso...Grazie

    RispondiElimina
  15. Ciao Giovanni, molte grazie a te per i complimenti :-)

    RispondiElimina
  16. Complimenti Vittorio :), mi chiedevo questi giorni infatti.... ma quando arriva a 50 gg ( esattamente a 4.294.967.295 millis , che /60 /60 /24 fa 49,7102662037037, ecc ecc Giorni ... giusto per curiosità Utenti) , millis si ferma o si resetta ?... purtroppo non riesco a farlo durare tanto che è in continuo evoluzione sketch.
    Comunque la funzione Millis è la salvezza di Arduino. Pensate in domotica, relè attivo/disattivo per 7- 8 secondi, blocca lo sketch perdendo qualsiasi tipo di comunicazione.

    RispondiElimina
  17. Grazie mille.
    Si effettivamente il millis() è la manna per la domotica.
    Al tuo quesito purtroppo non so rispondere perchè non mi è capitato di mantenere acceso per così tanto tempo un arduino con millis ma a buon senso direi che si resetta partendo da zero.

    RispondiElimina
  18. Ciao Vittorio, ho un problema, devo imputare da seriale tot millisecondi per cui un relé deve rimanere attivo e poi spegnersi fino ad un nuovo input; ci sto perdendo la testa, mi puoi aiutare? Ti ringrazio

    RispondiElimina