Un esempio pratico: inseguitore di luce
Nell'articolo precedente abbiamo visto la funzione millis() ed il suo utilizzo.
In questo post si vuole evidenziare con un esempio i passaggi da seguire per passare da uno sketch con il delay() ad uno con il millis().
Lo scenario
Per cercare di spiegare meglio il tutto partiamo con un esempio pratico.
Si immagini di avere due sensori di luce a distanza di pochi centimetri e posizionati a 90 gradi.
In mezzo a loro due servo motori che ruotano rispetto ciascuno di essi a seconda che la luce sia puntata verso un sensore o verso l'altro.
Questo sistema simula, in piccolo, il funzionamento di un inseguitore solare di pannelli fotovoltaici...
Primo sketch con delay...
Il primo esperimento è stato effettuato con il metodo classico usando il delay().
Prima di rappresentare il codice ricapitoliamo le esigenze:
- Leggere il sensore 1 e il sensore 2
- Muovere a seconda della luce i due motori in direzione della luce stessa
- Stampare a video le posizioni dei motori e l'intensità di luce rilevata
Alcune precisazioni sono d'obbligo per comprendere appieno il codice:
- I motore, per come è progettato, tra un comando e l'altro, desidera circa 15ms di ritardo
- Una persona, per leggere correttamente, deve avere un output circa ogni 500-1000ms
- Nell'articolo si da per scontato che il lettore conosca arduino, i fondamenti della sua programmazione e il collegamento di sensori analogici e componenti di base
Procediamo quindi con lo sketch:
#include
#define SENSORELUCE1 0
#define SENSORELUCE2 5
int letturaluce1=0;
int letturaluce2=0;
Servo servo1; //crea l'oggetto per il controllo del primo servo
Servo servo2; //crea l'oggetto per il controllo del secondo servo
int posservo1; //immagazzina la posizione del primo servo
int posservo2; //immagazzina la posizione del secondo servo
void setup(){
pinMode (SENSORELUCE1, INPUT);
pinMode (SENSORELUCE2, INPUT);
servo1.attach(9); //attaches the first servo on pin 9 to the servo object
servo2.attach(10); //attaches the second servo on pin 10 to the servo object
Serial.begin(9600);
}
void loop(){
letturaluce1=analogRead(SENSORELUCE1);
posservo1= analogRead(letturaluce1); // reads the value of the light sensor 1 (value between 0 and 1023)
posservo1=map(posservo1,0,1023,0,179); // scale it to use it with the servo (value between 0 and 180)
servo1.write(posservo1); // sets the servo position according to the scaled value
delay(15); // waits for the servo to get there
letturaluce2=analogRead(SENSORELUCE2);
posservo2= analogRead(letturaluce2); // reads the value of the light sensor 2 (value between 0 and 1023)
posservo2=map(posservo2,0,1023,0,179); // scale it to use it with the servo (value between 0 and 180)
servo2.write(posservo2); // sets the servo position according to the scaled value
delay(15); // waits for the servo to get there
Serial.print("Sensore 1: ");
Serial.println(letturaluce1,DEC);
Serial.print("Posizione motore 1: ");
Serial.println(posservo1,DEC);
Serial.print("Sensore 2: ");
Serial.println(letturaluce2,DEC);
Serial.print("Posizione motore 2: ");
Serial.println(posservo2,DEC);
Serial.print("Differenza: ");
Serial.println(letturaluce1-letturaluce2);
delay(1000); //solo per facilitare la lattura dei dati
}
Lo sketch funziona correttamente ma a causa dei continui delay() inserit che, abbiamo visto nelle precisazioni precedenti sono indispensabili, rende il programma macchinoso.
In particolare si riscontrano i seguenti problemi:
- Se la luce cambia posizione velocemente, i servomotori non riescono a seguirla. Questo avviene perchè non aspettano solo 15ms tra un comando e il successivo bensì 1030ms (1000ms per la lettura e 15+15 per ciascun motore); si pensi se si inserissero altri elementi... il tempo si dilaterebbe aumentando questo probelma in maniera esponenziale.
- Il movimento dei motori risulta a scatti ed estremamente impreciso. Ciò avviene perchè tra un comando e il successivo non passano 15ms (necessari al corretto funzionamento del motore ed impercettibili per l'occhio umano) bensì, come abbiamo visto al punto prima 1030ms (che per l'occhio umano risultano ben identificabili)
- Un altro problema importante è che la lettura del sensore non è sempre molto accurata e può variare anche sensibilmente da una rilevazione alla successiva. In questo modo si genera un movimento "confuso" dei motori che rilevano valori anche molto discordanti da una volta all'altra
Il sistema risulta, nel suo complesso, lento a reagire ed estremamente impreciso oltre a dare una sensazione di scatto continuo.
Provate per credere :-)
Da queste "anomalie" nasce la necessità di modificare lo sketch e rendere il sistema più preciso e reattivo.
In particolare i primi due problemi verranno eliminati utilizzando la funzione millis() al posto del delay().
Il terzo problema al contrario con una media di rilevazioni come spiegato di seguito.
Vediamo quindi il codice seguente:
#include
#define SENSORELUCE1 0
#define SENSORELUCE2 5
int letturaluce1=0;
int letturaluce2=0;
int luce1;
int luce2;
int luce1temp;
int luce2temp;
Servo servo1; //crea l'oggetto per il controllo del primo servo
Servo servo2; //crea l'oggetto per il controllo del secondo servo
int posservo1;
int posservo2;
Fino a questo punto il programma non subisce alcuna modifica rispetto la versione precedente.
Le variabili prima dichiarate continuano a presentarsi anche in questo caso.
Successivamente, però, si dichiarano le
variabili-tempo presentate nel precedente articolo.
Ad ogni
variabile-tempo viene associato un evento.
int k;
unsigned long time;
unsigned long servotime1;
unsigned long servotime2;
unsigned long letturadati;
unsigned long letturaluce_time;
time è la variabile che contiene "lo scorrere" del tempo; servotime1 e 2 rappresentano gli eventi associati al movimento dei motori; letturadati e lettura_luce rappresentano gli eventi associati alla lettura dei due sensori di luce.
La variabile k servirà come contatore per evitare le letture dei sensori "sballate".
Questo problema è stato anticipato prima; l'algoritmo che ci viene in aiuto, infatti, sarà semplicemente fare "x" letture consecutive e farne la media.
Considerando che arduino fa 16 milioni di operazioni al secondo, anche facendo 20 letture a distanza di 5ms, ogni frazione di secondo avremmo una rilevazione molto accurata del valore di luce circostante ed il calcolo della media smorza eventuali picchi anomali nei rilevamenti.
void setup(){
pinMode (SENSORELUCE1, INPUT);
pinMode (SENSORELUCE2, INPUT);
servo1.attach(9);
servo2.attach(10);
Serial.begin(9600);
time=millis();
servotime1=millis();
servotime2=millis();
letturadati=millis();
letturaluce_time=millis();
k=0;
luce1=0;
luce2=0;
luce1temp=0;
luce2temp=0;
}
void loop(){
time=millis();
if(time>letturaluce_time+5){
luce1=analogRead(SENSORELUCE1);
luce2=analogRead(SENSORELUCE2);
k=k+1;
luce1temp=luce1temp+luce1;
luce2temp=luce2temp+luce2;
}
Come spiegato nell'articolo precedente, si associa l'evento ad un if.
Ogni 5ms si effettua una lettura di entrambi i sensori ma nel frattempo il programma continua a "girare" senza nessun impedimento.
if(k=20){
k=0;
letturaluce1=luce1temp/20;
letturaluce2=luce2temp/20;
luce1temp=0;
luce2temp=0;
}
Questo if risolve il problema della poca accuratezza nella misura.
Come detto due paragrafi fa, si effettuano k letture (in tal caso 20 ma nessuno vieta di aumentare o diminuire questo valore) al termine delle quali si effettua una media e si azzera il valore di k.
I valori di riferimento saranno sempre letturaluce1 e letturaluce2 che varieranno il proprio valore ogni 20 misure.
if(time>servotime1+15){
posservo1= letturaluce1;
posservo1=map(posservo1,0,1023,0,179); //scala il valore da usare tra 0 e 180.
servo1.write(posservo1); //imposta la posizione del servo1
servotime1=millis();
}
if(time>servotime2+15){
posservo2= letturaluce2;
posservo2=map(posservo2,0,1023,0,179);//scala il valore da 0 a 180
servo2.write(posservo2); // imposta la posizione del servo2.
servotime2=millis();
}
Queste istruzioni dovrebbero essere chiare dopo la lettura del precedente articolo.
In entrambi i casi si rileva la intensità della luce, si posiziona il servo corrispondente rimappando il valore con la funzione map e si "azzerano" le
variabile-tempo servotime1 e
servotime2 assegnando loro il numero di millisecondi correnti.
if(time>letturadati+3000){
Serial.print("Sensore 1: ");
Serial.println(letturaluce1,DEC);
Serial.print("Posizione motore 1: ");
Serial.println(posservo1,DEC);
Serial.print("Sensore 2: ");
Serial.print(letturaluce2,DEC);
Serial.print("Posizione motore 2 : ");
Serial.println(posservo2,DEC);
Serial.print("Differenza: ");
Serial.println(letturaluce1-letturaluce2);
letturadati=millis();
}
}
Anche queste istruzioni seguono lo stesso ragionamento precedente.
In questo caso, addirittura, possiamo permetterci il lusso di "aspettare" 3000ms e non solo 1000ms tra una scrittura e la successiva (rendendo il tutto più leggibile all'utente finale) perchè il programma non si blocca ma continua a girare ed eseguire le azioni.
Tutto questo rende il movimento dei servo motori estremamente fluido.
L'osservatore non vedrà alcuno scatto e noterà anche una dinamica del sistema estremamente veloce con un "inseguimento" della luce decisamente reattivo.
Il programma nel suo complesso è decisamente utilizzabile.
Tuttavia presenta ancora un piccolo problemino: è lungo...
La lunghezza del codice proposto si può risolvere, come vedremo nel prossimo articolo con la programmazione a stati finiti.
Argomento precedente: "Multitasking Arduino: millis() -- PARTE 1"
Riporto il codice dell'intero sketch visto in questo articolo per facilitare il lettore che volesse testarlo:
#include
#define SENSORELUCE1 0
#define SENSORELUCE2 5
int letturaluce1=0;
int letturaluce2=0;
int luce1;
int luce2;
int luce1temp;
int luce2temp;
Servo servo1;
Servo servo2;
int posservo1;
int posservo2;
int k;
unsigned long time;
unsigned long servotime1;
unsigned long servotime2;
unsigned long letturadati;
unsigned long letturaluce_time;
void setup(){
pinMode (SENSORELUCE1, INPUT);
pinMode (SENSORELUCE2, INPUT);
servo1.attach(9);
servo2.attach(10);
Serial.begin(9600);
time=millis();
servotime1=millis();
servotime2=millis();
letturadati=millis();
letturaluce_time=millis();
k=0;
luce1=0;
luce2=0;
luce1temp=0;
luce2temp=0;
}
void loop(){
time=millis();
if(time>letturaluce_time+5){
luce1=analogRead(SENSORELUCE1);
luce2=analogRead(SENSORELUCE2);
k=k+1;
luce1temp=luce1temp+luce1;
luce2temp=luce2temp+luce2;
letturaluce_time=millis();
}
if(k=20){
k=0;
letturaluce1=luce1temp/20;
letturaluce2=luce2temp/20;
luce1temp=0;
luce2temp=0;
}
if(time>servotime1+15){
posservo1= analogRead(letturaluce1); // reads the value of the light sensor 1 (value between 0 and 1023)
posservo1=map(posservo1,0,1023,0,179); // scale it to use it with the servo (value between 0 and 180)
servo1.write(posservo1); // sets the servo position according to the scaled value
servotime1=millis();
}
if(time>servotime2+15){
posservo2= analogRead(letturaluce2); // reads the value of the light sensor 2 (value between 0 and 1023)
posservo2=map(posservo2,0,1023,0,179); // scale it to use it with the servo (value between 0 and 180)
servo2.write(posservo2); // sets the servo position according to the scaled value
servotime2=millis();
}
if(time>letturadati+3000){
Serial.print("Sensore 1: ");
Serial.println(letturaluce1,DEC);
Serial.print("Posizione motore 1: ");
Serial.println(posservo1,DEC);
Serial.print("Sensore 2: ");
Serial.println(letturaluce2,DEC);
Serial.print("Posizione motore 2: ");
Serial.println(posservo2,DEC);
Serial.print("Differenza: ");
Serial.println(letturaluce1-letturaluce2);
letturadati=millis();
}
}
Argomento precedente: "Multitasking Arduino: millis() -- PARTE 1"
Argomento successivo: "Ardino e programmare a stati finiti"