domenica 24 novembre 2013

Self Balancing Robot - parte 4 - Controllare e calibrare i motori

Per il controllo dei motori la mia scelta è andata su una economica scheda della Microbot (http://www.microbot.it/product/60/Dual-DC-Motor-Shield.html). Questa scheda dispone di 2 canali, ovvero può controllare fino a 2 motori in corrente continua. La scheda è la classica shield che si monta a castello sopra la scheda arduino. La fonte di alimentazione, a scelta tramite apposito jumper, può essere scelta tra i 5V interni provenienti dalla scheda arduino (sconsigliato), oppure da una fonte esterna.
Scegliendo la seconda, è possibile pilotare motori con tensioni ben superiori ai 5V, come è il caso dei motori Pololu scelti che funzionano a 12V.
Il controllo dei motori è davvero facile: per ogni motore sono necessarie 3 uscite di arduino, due in digitale per determinare la direzione di rotazione oppure lo stato di stop ("freno a mano"), uno in analogico (pwm) per determinare la velocità di rotazione.
Come già descritto in uno dei post precedenti, uno dei problemi è proprio quello di determinare il vero "zero" del motore, ovvero il valore di segnale minimo fino al quale il motore non si muove. Il secondo problema è la differenza di velocità tra un motore e l'altro.
Nel mio caso, ma forse è un problema della scheda, ho notato differenze significative sulla tensione in uscita nei due canali della scheda a parità di segnale pwm in ingresso.
Prima di iniziare a lavorare con logiche PID e algoritmi di filtraggio è bene predisporre al meglio l'hardware su cui lavoreremo, per essere sicuri di avere una risposta il più possibile lineare, compensando in modo software i problemi dovuti ad attrito interno nei motori, di peso del robot e di differenza di velocità.

Per fare questo ho predisposto un semplice sketch arduino che fa ruotare entrambi i motori a velocità costante, incrementando la velocità ogni 5 secondi, e misurando tramite gli encoder la velocità reale delle ruote. In output, sulla console seriale, viene mostrato per ogni valore pwm in ingresso, il rapporto tra la velocità dei motori, in particolare il rapporto di velocità tra il primo motore e il secondo.
Lo scopo è raccogliere questi valori (256 valori) e utilizzarli per creare una funzione che restituisca il corretto valore da applicare al 2° motore (quello più lento nel mio caso), in modo che il valore diverso pwm applicato provochi una velocità di rotazione identica (o il più possibile simile) ad entrambi i motori.

Il main dello sketch è davvero semplice:
void loop()
{
  for (int i=0; i < 256 ; i++)
  {
   setMotor0(i,1);
   setMotor1(i,1);
   delay(5000);
   currentVelocity0=getVelocity0();
   currentVelocity1=getVelocity1();
   Serial.print (" case ");
   Serial.print  (i);
   Serial.print  (": return (int) ( (float) t ");
   Serial.print (" * (float) ");
   Serial.print  ((float)currentVelocity0/(float)currentVelocity1);
   Serial.println (" ); break;");
  }
}
la funzione setMotor0(i,1) invia al motore il comando di girare alla velocità determinata dalla variabile i. Dopo aver applicato il comando ai motori, si attendono 5 secondi per stabilizzare la velocità, e tramite le funzione getVelocitity si ottiene dagli encoder la lettura della velocità reale di rotazione.
A questo punto si ottiene il rapporto tra le 2 velocità o lo si stampa in output tramite Serial.print.
L'output è formattato in modo furbo in modo da ottenere già delle istruzioni inseribili in uno case statement di tipo switch.
Sull'encoder tornerò in modo approfondito nei prossimi post, per ora mi limito unicamente a descrivere il controllo dei motori.
La funzione setMotor0() è così composta:
void setMotor0(int power,int dir)
{
 if (dir == -1)
 {  
  digitalWrite(motor0PinA  , HIGH);      
  digitalWrite(motor0PinB  , LOW);   
 }
 else
 {  
  digitalWrite(motor0PinA  , LOW);      
  digitalWrite(motor0PinB  , HIGH);    
 }
 analogWrite(motor0PinPwm , power);      
 currentPower0=power;
 currentDirection0=dir;
}
il funzionamento è davvero semplice: la variabile dir descrive il senso di rotazione (-1, 1) mentre power descrive la potenza applicata al motore (0, 255) ovvero il valore pwm inviato al controller dei motori, dove 255 corrisponde al 100% della velocità del motore.
Il senso di rotazione viene indicato al controller dei motori utilizzando le diverse combinazioni di 2 uscite arduino: su 4 combinazioni possibili , 2 descrivono lo stato di quiete, 2 invece i due sensi di rotazione.
motor0PinA e le altre variabili sono di fatto costanti che definiscono i pin utilizzati.
#define motor0PinA 9
#define motor0PinB 10
#define motor0PinPwm 11  //pin 3 della scheda dc motor routtato sull'11 di arduino

#define motor1PinA 8  //pin 2 della scheda dc motor routtato sull'8 di arduino
#define motor1PinB 6 
#define motor1PinPwm 5

In questi casi è bene documentarsi con i datasheet della scheda controller, perchè ogni scheda utilizza il suo set di I/O e quindi non è possibile utilizzare il codice sopra in modo universale.
Questo ad esempio l'estratto dal datasheet 
Noterete a questo punto che nelle #define sopra mostrate c'è una discrepanza rispetto al pinout richiesto nel datasheet, cioè che al posto dei pin 2 e 3 sono stati utilizzati l'8 e l'11.
Il problema è dovuto al fatto che la scheda controller (shield) è montata a castello sopra la scheda arduino e quindi utilizza in modo fisso le porte richieste. 
Per sfortuna, chi ha progettato questa scheda, non ha tenuto conto del fatto che per utilizzare degli encoder digitali bisogna utilizzare in modo obbligato gli attachInterrupt di arduino, che su una scheda arduino Uno come quella da me utilizzata sono utilizzabili solo sulle porte 2 e 3, purtroppo qui usate proprio dal controller motori.
Per gestire la cosa, ho dovuto fare un routing forzato di tipo hardware, escludendo il collegamento dei pin 2-3 tra il controller e la scheda arduino e sfruttare altri 2 pin, l'8 e l'11 per inviare alla scheda controller quanto richiesto sui pin 2-3. 
In questo modo è stato possibile collegare gli encoder ai pin 2-3 di Arduino, e pilotare i motori tramite i pin 8-11 in sostituzione degli originali 2-3, il tutto con un collegamento hardware come visibile in figura.
Attenzione che la scelta dei pin 8 e 11 non è casuale: Arduino Uno permette di pilotare le uscite in modalità pwm solo sulle porte 3-5-6-9-10 e 11. Facciamo due conti: la porta 3 la usiamo per l'encoder, la 5-6-9-10 sono già usate dal controller motori, resta la 11 per sostituire la 3, scelta obbligata quindi.
Per la sostituzione della 2 invece non serve una porta pwm, in quanto sono richiesti solo gli stati high/low: in questo caso possiamo usare la porta 8 che è libera.
Dalla foto vedete inoltre che per evitare di fare saldature direttamente sulle shield, ho applicato in cima al castello una protoboard con  un pinout compatibile alle shield standard, sulla quale poi sono andato liberamente a saldare.
Infine, un ultimo consiglio e poi andiamo ad applicare ed eseguire il nostro sketch: se possibile utilizzate una fonte di alimentazione esterna (alimentatore) che fornisca una tensione pari a quella delle batterie completamente cariche. Dato che la risposta del motore cambia in funzione della tensione, è bene fare delle prove in un contesto stabile. Per fare questo sul mio robot ho applicato uno switch che mi permette di passare dall'alimentazione esterna (morsettiera) a quella a batterie, senza dover ogni volta risaldare cavi. Inoltre ho saldato due pin volanti ai quali fissare il caricabatterie che è dotato di due comode pinze.
Assicuriamoci che le ruote non tocchino a terra (giratelo a testa in su) e mandatelo in esecuzione. Il risultato sarà probabilmente una cosa simile:
Si nota che fino a un certo valore le ruote non girano. Probabilmente prima si avvierà una ruota e in seguito l'altra: solo in questo momento lo sketch rileverà due velocità diverse da zero e potrà quindi restituire un valore diverso da nan (not a number) o da inf (infinito, ovvero divisione per zero). 
Questo ci permette di determinare il famoso "zero" dei motori, ovvero il valore pwm oltre il quale i motori iniziano a muoversi.
Per il resto il risultato è così interpretabile:
case 55: return (int) ( (float) t  * (float) 1.53 ); break;
se pwd=55 , allora restituisci valore pwd * 1.53
A questo punto la nostra funzione è praticamente fatta. Completiamo la parte di codice che manca e avremo la nostra funzione che data l'intensità pwm del primo motore, restituirà l'intensità pwm del secondo motore necessaria ad avere la stessa velocità di rotazione.
int fixMotor1(int t)
{
 switch (t)
 {
 case 0: 
 case 1: 
 case 2: 
 case 3: 
 case 4: 
 case 5: 
 case 6: 
 case 7: 
 case 8: 
 case 9: 
 case 10: 
 case 11: 
 case 12: 
 case 13: 
 case 14: 
 case 15: 
 case 16: 
 case 17: 
 case 18: 
 case 19: 
 case 20: 
 case 21: 
 case 22: 
 case 23: 
 case 24: 
 case 25: 
 case 26: 
 case 27: 
 case 28: 
 case 29: 
 case 30: 
 case 31: 
 case 32: 
 case 33: 
 case 34: 
 case 35: 
 case 36: 
 case 37: 
 case 38: 
 case 39: 
 case 40: return (int) ( (float) t  * (float) 2 ); break;
 case 41: return (int) ( (float) t  * (float) 2 ); break;
 case 42: return (int) ( (float) t  * (float) 2 ); break;
 case 43: return (int) ( (float) t  * (float) 2.00 ); break;
 case 44: return (int) ( (float) t  * (float) 1.98 ); break;
 case 45: return (int) ( (float) t  * (float) 1.95 ); break;
 case 46: return (int) ( (float) t  * (float) 1.93 ); break;
 case 47: return (int) ( (float) t  * (float) 1.89 ); break;
 case 48: return (int) ( (float) t  * (float) 1.86 ); break;
 case 49: return (int) ( (float) t  * (float) 1.83 ); break;
 case 50: return (int) ( (float) t  * (float) 1.81 ); break;
 case 51: return (int) ( (float) t  * (float) 1.78 ); break;
 case 52: return (int) ( (float) t  * (float) 1.76 ); break;
 case 53: return (int) ( (float) t  * (float) 1.75 ); break;
 case 54: return (int) ( (float) t  * (float) 1.74 ); break;
 case 55: return (int) ( (float) t  * (float) 1.72 ); break;
 case 56: return (int) ( (float) t  * (float) 1.71 ); break;
 case 57: return (int) ( (float) t  * (float) 1.70 ); break;
 case 58: return (int) ( (float) t  * (float) 1.69 ); break;
 case 59: return (int) ( (float) t  * (float) 1.67 ); break;
......
......
......
 case 253: return (int) ( (float) t  * (float) 1.12 ); break;
 case 254: return (int) ( (float) t  * (float) 1.12 ); break;
 case 255: return (int) ( (float) t  * (float) 1.12 ); break;
 }
}

Nel prossimo post: come usare gli encoder

Nessun commento:

Posta un commento