Arduino / Programmazione · 24 novembre 2013 2

Programmiamo i compiti con millis()

Millis() è una funzione del core di Arduino che restituisce il numero di millisecondi trascorsi dall’istante in cui si fornisce l’alimentazione alla scheda. Millis() si basa su di un contatore che viene aggiornato continuamente da un interrupt agganciato al timer 0, e per questo motivo il suo valore cresce costantemente, non essendo influenzato dal codice dell’utente.

Possiamo usare millis() per programmare l’esecuzione di alcune azioni, come se fosse un timer che dà la sveglia al nostro codice senza per questo bloccare l’esecuzione del medesimo in attesa che arrivi il momento giusto.


Iniziamo con un semplice test, il lampeggio di un LED. L’IDE di Arduino fornisce un esempio famosissimo, chiamato “Blink”. Quella che segue è la versione quasi originale:

const byte led = 13;
void setup() {
  pinMode(led, OUTPUT); //initialize the digital pin as an output.
}
void loop() {
  digitalWrite(led, HIGH);   //turn the LED on (HIGH is the voltage level)
  delay(1000);               //wait for a second
  digitalWrite(led, LOW);    //turn the LED off by making the voltage LOW
  delay(1000);               //wait for a second
}

Questo codice non fa altro che mettere su HIGH il pin del LED integrato, attendere 1000 ms (1 secondo), metterlo a LOW, attendere 1000 ms e poi ripartire dall’inizio.
Questo sketch funziona ma ha un grosso limite: durante l’attesa del delay(1000) il microcontrollore non può fare altro. Se modificate lo sketch inserendo un Serial.print dopo il secondo delay vedrete che la scritta compare sul monitor seriale dopo ben 2 secondi:

const byte led = 13;
void setup() {
  Serial.begin(19200);
  pinMode(led, OUTPUT); //initialize the digital pin as an output.
} 

void loop() {
  digitalWrite(led, HIGH); //turn the LED on (HIGH is the voltage level)
  delay(1000); // wait for a second
  digitalWrite(led, LOW); //turn the LED off by making the voltage LOW
  delay(1000); // wait for a second
  Serial.println("Test...");
}

Un modo per aggirare il problema ci viene dall’esempio BlinkWithoutDelay sempre dell’IDE. La versione qui sotto l’ho rielaborata per renderla più compatta:

const int ledPin =  13; // the number of the LED pin
int ledState = 0; // ledState used to set the LED
unsigned long previousMillis = 0; //will store last time LED was updated
unsigned long interval = 1000; //interval at which to blink (milliseconds)
void setup() {
  pinMode(ledPin, OUTPUT); //set the digital pin as output:
}

void loop() {
  unsigned long currentMillis = millis();
  if(currentMillis - previousMillis > interval) {
    previousMillis = currentMillis; //save the last time you blinked the LED
    //if the LED is off turn it on and vice-versa:
    ledState ^= 1;
    digitalWrite(ledPin, ledState);
  }
}

Questo esempio fa il suo dovere senza bloccare il codice: ad ogni esecuzione del loop() controlla se la differenza fra il valore attuale di millis() e quello del precedente lampeggio è maggiore dell’intervallo impostato, nel caso cambia lo stato del LED. Se mettete il solito Serial.print alla fine del loop() vedrete che questa volta la seriale vi si riempirà di scritte senza fermarsi mai.

E se i compiti da svolgere solo più di 1? Possiamo usare due coppie di variabili per memorizzare il tempo di lampeggio. Una possibile soluzione è questa:

const byte ledPin =  13; //the number of the LED pin
byte ledState = 0; //ledState used to set the LED
unsigned long previousMillis1 = 0; //will store last time LED was updated
unsigned long interval1 = 100; //interval at which to blink (milliseconds)
unsigned long previousMillis2 = 0;
unsigned long interval2 = 1000;
unsigned int counter = 0;

void setup() {
  Serial.begin(19200);
  delay(2000);
  pinMode(ledPin, OUTPUT); //set the digital pin as output:
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis1 > interval1) {
    previousMillis1 = currentMillis; // save the last time I blinked the LED
    //if the LED is off turn it on and vice-versa:
    ledState ^= 1;
    digitalWrite(ledPin, ledState);
  }
  currentMillis = millis();
  if (currentMillis - previousMillis2 > interval2) {
    previousMillis2 = currentMillis; // save the last time I printed on the serial
    Serial.println(++counter);
  }
}

Questo programma esegue 2 compiti: il primo è il lampeggio del LED integrato ogni 100 ms, il secondo è la stampa di un contatore sulla seriale ogni 1000 ms. Ogni compito viene eseguito correttamente perché il programma, non usando delay(), non si blocca in attesa che passi il tempo prefissato per l’esecuzione di ognuno dei 2 compiti.

Se vogliamo tenere il loop() pulito e separare i 2 compiti, possiamo creare 2 funzioni indipendenti richiamate dal ciclo principale:

const byte ledPin =  13; // the number of the LED pin
byte ledState = 0; // ledState used to set the LED
unsigned long previousMillis1 = 0; //will store last time LED was updated
unsigned long interval1 = 100;  //interval at which to blink (milliseconds)
unsigned long previousMillis2 = 0;
unsigned long interval2 = 1000;
unsigned int counter = 0;
void setup() {
  Serial.begin(19200);
  delay(2000);
  pinMode(ledPin, OUTPUT); //set the digital pin as output
}

//main loop
void loop() {
  blinkLed();
  printCounter();
}

//blink the led
void blinkLed() {
  //check if it's time to blink the led
  if (millis() - previousMillis1 > interval1) {
    previousMillis1 = millis(); //save the last time I blinked the LED
    // if the LED is off turn it on and vice-versa:
    ledState ^= 1;
    digitalWrite(ledPin, ledState);
  }
}

//print on the serial the value of a counter
void printCounter() {
  //check if it's time to print the new value of the counter
  if (millis() - previousMillis2 > interval2) {
    previousMillis2 = millis(); //save the last time I printed on the serial
    Serial.println(++counter); //increment the counter and print it on the serial
  }
}

In questo modo leggendo il codice nel loop() possiamo già ad una prima occhiata capire com’è organizzato il programma: vengono chiamate 2 funzioni distinte, perennemente. E’ buona abitudine mettere un piccolo commento prima delle 2 funzioni in modo da descrivere cosa esse facciano, nonché usare nomi descrittivi dei compiti che svolgono.
Volendo migliorare la gestione dei compiti da svolgere, la soluzione è l’utilizzo di looper, una libreria che ho scritto per eseguire diversi compiti predefiniti dall’utente.