Nov 262015
 

Di recente abbiamo visto come utilizzare il sensore sct13-030 per misurare la corrente assorbita da un dispositivo ed abbiamo poi approfondito l’argomento parlando dell’oversampling del segnale, la modifica del fattore di prescaler, etc. Ora riproponiamo le stesse cose in questo articolo ma usando Arduino DUE soffermandoci su quali sono le differenze più importanti di cui tenere conto rispetto l’Arduino UNO  usato in precedenza. Questo articolo tratterà cose semplici, ma mi sono reso conto che in rete ci sono pochi articoli su Arduino DUE e dato che le board basate sui processori Sam stanno aumentando è forse il momento di cominciare a prenderne confidenza. Vi dico la verità, a me non è che interessi più di tanto questo tipo di scheda ed il motivo è facile da spiegare. Arduino UNO ha un vantaggio innegabile, ossia me lo posso costruire con pochi componenti, anche su una breadboard e da li posso sviluppare tutto ciò che mi serve. Arduino Mega, Due, Zero, etc sono tutt’altra cosa, sono board che non si possono riprodurre su breadboard e la loro produzione richiede un genere di competenze che vanno oltre il basilare livello amatoriale. Ammetto però che l’hardware di Arduino UNO è piuttosto limitato e ci sono sicuramente casi in cui è necessaria più potenza elaborativa, ambito in cui Arduino DUE è in grado di dare una marcia in più. Diciamoci la verità, per riprodurre il sistema di monitoraggio della corrente assorbita visto nelle scorse puntate non occorre certo avere tutta questa potenza in più, ma dalle prove che ho fatto è quantomeno interessante fare dei paragoni con Arduino UNO.

Arduino DUE e i 3.3V

Arduino DUE ha alcune sostanziali differenze rispetto Arduino UNO. Una particolarmente importante è la tensione di funzionamento, infatti a differenza di Arduino UNO non utilizza i classici 5V ma i 3.3V, il che significa che gli ingressi sopporteranno al massimo 3.3V, le uscite erogheranno 3.3V, gli ingressi analogici leggeranno valori fra 0 e 3.3V, etc. Questo non è un particolare da poco in quanto l’erroneo utilizzo dei 5V usati con Arduino UNO andrebbero a danneggiare irrimediabilmente la scheda. Salta subito all’occhio che il sistema utilizzato la scorsa volta per collegare il sensore di corrente ad Arduino UNO non può essere usato senza modifiche su Arduino DUE. Se ricordate utilizzavamo un partitore di tensione che portava la 5V a 2.5V e su questo valore base il sensore aggiungeva un’oscillazione massima di ±1V, ossia valori finali compresi fra 1.5 e 3.5V. Il 3.5V sfora il tetto massimo di 3.3V per cui il rischio di danneggiare in modo irreparabile Arduino è decisamente reale. Il partitore di tensione, perciò, dovrà prelevare la tensione dalla linea a 3.3V e non dalla 5V per cui avremo una tensione base di 1.65V a cui aggiungere l’escursione del sensore che può portare la tensione in ingresso a valori compresi fra 0.65 e 2.65V. Faccio notare che l’oscillazione di 2V sui 3.3V copre una parte della scala decisamente maggiore rispetto a quanto avveniva sui 5V cosa che comporta già di per se un notevole miglioramento della risoluzione sull’asse Y.

Arduino DUE e l’ADC

Il convertitore Analogico Digitale (ADC) di Arduino DUE è decisamente più performante di quello di Arduino UNO. Innanzitutto già di base permette di ottenere campionamenti a 12bit senza nessun trucco particolare. Per questione di compatibilità, però, se non viene specificato effettua campionamenti a 10bit, ma è sufficiente usare la funzione analogReadResolution(12) per effettuare letture a 12bit senza nessuna tecnica di sovra campionamento del segnale. La seconda grossa differenza dell’ADC è la frequenza di campionamento, Arduino DUE è infatti circa 10 volte più veloce pur campionando a 12bit anziché a 10. Pensate che Arduino DUE nel corso dell’intera onda riesce a fare oltre 3850 campionamenti a 12bit, ognuno in circa 0.52 microsecondi, a differenza di Arduino UNO che di base riesce a farne circa 370 a 10bit, ogni 5microsecondi circa, restando pressoché stabile a 11bit con prescaler modificato.

Purtroppo non mi è facile mostrare dei grafici comparativi proprio per la grossa differenza di velocità: se avete presente i grafici che vi ho mostrato sinora, erano tutti basati su 400 campionamenti seriati ma con Arduino DUE 400 campionamenti rappresentano una quantità temporale troppo corta, ottengo infatti grafici quasi lineari in quanto il segnale cambia troppo poco in quel lasso di tempo. Potrei decuplicare il numero di campionamenti visto che ha un sacco di memoria RAM in più (96K anziché 2K) ma, giusto per darvi un’idea, vi propongo qui sotto la comparativa tra un’acquisizione  sovra campionata a 14bit di Arduino DUE (in alto) e quella (in basso) di Arduino UNO a 10bit con prescaler ridotto a 32. Si tratta di dati grezzi non elaborati in nessun modo. Faccio notare che ho usato il sovra campionamento non per migliorare la risoluzione che non ci serviva, quanto per aumentare il numero di operazioni e rallentare Arduino DUE rendendo i due grafici grossolanamente comparabili.UNOvsDUEADCFaccio inoltre notare che anche il prescaler di Arduino DUE è modificabile, ma non è una cosa che approfondiremo in quanto in questo esempio già così abbiamo un margine di potenza in più rispetto ad Arduino UNO tale da rendere del tutto inutili ulteriori tentativi di miglioramento.

Differenza significative?

Bene, in realtà non ci manca gran che da dire. Abbiamo detto quali sono le fondamentali differenze degli ADC delle due schede e qual’è la piccola modifica da apportare al partitore di tensione, di fatto si tratta solo di usare la linea 3.3V anziché quella a 5V. Non ci resta che fare delle prove comparative sul campo fra le due soluzioni e vedere il risultato. Il codice visto la scorsa volta deve per forza di cose essere un po’ riadattato. Non ci servono le costanti di prescaler di Arduino UNO (in realtà dell’Atmega328), inoltre dobbiamo aggiungere  la funzione analogReadResolution(12) nel setup iniziale, che istruisce Arduino DUE a fare campionamenti a 12bit sfruttando le potenzialità del suo convertitore analogico-digitale. Facendo queste modifiche però perdiamo tutto il lavoro fatto per Arduino UNO per cui ho pensato ad una strada diversa, ossia la compilazione condizionale, tecnica usata anche da me in alcune librerie distribuite su questo blog, ma di cui non ho mai parlato.

Arduino UNO e DUE hanno processori decisamente differenti fra di loro e risulta pressoché impossibile sfruttarli a fondo utilizzando il medesimo codice, cosa che rende di fatto indispensabile scrivere codici diversi per le diverse schede. Noi però non vogliamo creare due sketch diversi da usare in base alla scheda, anche perché modificare la parte comune in una delle due ci costringe a modificare a mano anche la parte comune della seconda, per questo motivo si utilizzano alcune funzioni del preprocessore C come #define, #ifdef, #endif, #ifndef.

Il funzionamento è semplice, se identifica una certa scheda compila un certo codice, se ne identifica un’altra compila una porzione differente di codice ed ignora la prima. Vediamo come funziona:

#ifdef __avr__
  compila questo codice per schede con preocessore avr
#else
    altrimenti compila questo codice
#endif

L’utilizzo come vedete è estremamente semplice, nello pseudocodice qui sopra vedete come viene controllata la costante __avr__ che è definita quando compiliamo per schede basate su questo processore, come Arduino UNO, mentre compila l’altra porzione di codice nei restanti casi. Questo esempio è un po’ primitivo, funziona bene per discriminare un Arduino UNO da un DUE, ma per schede più specifiche sarà necessario confrontare più tipologie di variabili che però per ora non prenderemo in considerazione. La costante __avr__ viene definita quando nell’ide di Arduino selezioniamo Arduino UNO quale scheda target per la compilazione, per cui di fatto siamo noi a definirla anche se inconsciamente. Il codice di oggi è in gran parte identico a quello dei precedenti articoli, ho fatto delle modifiche che seguono il concetto della compilazione condizionale ragruppando in porzioni di codice separate le inizializzazioni da farsi per Arduino UNO rispetto al DUE. Un’altra modifica che ho fatto riguarda l’acquisizione dei dati dall’intera onda, infatti ho notato che campionando due onde e un margine aggiuntivo di sicurezza del 20%, le misure erano più precise anche rispetto alla semplice media di più misurazioni consecutive motivo per cui ho allungato il periodo di campionamento.

const int sensorPin=A1;
#ifdef __AVR__
  #define PS_8 ((1 << ADPS1) | (1 << ADPS0))
  #define PS_16 (1 << ADPS2)
  #define PS_32 ((1 << ADPS2) | (1 << ADPS0))
  #define PS_64 ((1 << ADPS2) | (1 << ADPS1))
  #define PS_128 ((1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0))
 // i bit del convertitore ADC di arduino, su UNO sono 10
 #define basic_bits 10
 //i bit che vogliamo ottenere col sovravcampionamento
 #define extended_bits 11
 #define arduinorange 5
 #define G_Soglia 2.5
#else
 #define basic_bits 12
 //i bit che vogliamo ottenere col sovravcampionamento
 #define extended_bits 12
 //arduino adc range (5V, 3.3V or href value)
 #define arduinorange 3.3
 #define G_Soglia 15
#endif

//voltage range of sensor
#define sensorvoltagerange 1
//how many ampere the sensor reads (RMS)
#define sensorrange 30

//numero ripetizione misure
#define misure 3

#define taratura 1
//#define taratura 0.94562

//parametri calcolati automaticamente, NON modificare!
// bit di sovracampionamento
#define overbit extended_bits-basic_bits
//divisore oversampling
#define decimatoreoversample (pow(2,overbit))
//calcola risoluzione sensore

double sensorunit;
double sensorstep;
double puntozero;

void setup()
{
  Serial.begin(9600);
  while (!Serial) {} // wait for serial port to connect. Needed for Leonardo only
  Serial.println("STARTING READER.... ");
  sensorunit=(((int)(1<<extended_bits))*(float)sensorvoltagerange/(float)arduinorange);
  sensorstep=((float)sensorrange/(float)sensorunit);  
  puntozero=(1<<(extended_bits-1)); // da tenere se non fai la lettura analogica
 
  #if defined __AVR__
   ADCSRA &= ~PS_128;  //Set up the ADC: remove bits set by Arduino library
   ADCSRA |= PS_32;    // set our own prescaler to XX
  #else
   analogReadResolution(12);
  #endif
 
   
  double tstart,tstop,test;
  analogRead(A1);
  for(int i=0;i<5;i++)
  {
   tstart=micros();
   analogRead(A1);
   tstop=micros();
   Serial.print("Basic read time: ");
   Serial.print(tstop-tstart);
   Serial.print(" us");
   tstart=micros();
   test=OversampledAnalogRead(A1);
   tstop=micros();
   Serial.print(" Oversampled time: ");
   Serial.print(tstop-tstart);
   Serial.println(" us ");
  }
 
// cancellare se usi punto fisso
  Serial.print("Misured voltage divider (AnalogAvg) : ");
  puntozero=SampleAnalogAvg(A5,1500);
  Serial.println(puntozero,4);   

}

float SampleAnalogAvg(int localpin,unsigned char campioni)
{
  float valore=OversampledAnalogRead(localpin);
  for(int i=0;i<campioni;i++) valore=(valore+OversampledAnalogRead(localpin))/2;
  return(valore);
 
}

inline double OversampledAnalogRead(int pin)
{
 unsigned int val=0;
 unsigned int misureoversample=pow(4,overbit);
 for(unsigned char z=0;z<misureoversample;z++) val+=analogRead(pin);
 
 return(val>>overbit); //2^n
}

double GetWaveValue(int pin,char wavenumber)
{
  double sensore;
  double somma=0;
  int contatore=0;
  double tstart;
 
  double timelen=(double)wavenumber*1/50*1000*1000; //0.02sec
  //puntozero=OversampledAnalogRead(A5);
  puntozero=SampleAnalogAvg(A5,1500);
  tstart=micros();

  while(micros()-tstart<timelen*2.2)
   {
    sensore=OversampledAnalogRead(pin);
    sensore-=puntozero;
    
    somma+=(sensore*sensore);
    contatore++;
    }  
 //Serial.print("Campioni: ");
 //Serial.println(contatore);

  return(sqrt(somma/contatore));
}

void loop()
{
  double Vmax=0;
  double localval=0;
  for(int z=0;z<misure;z++)
  {
    localval=GetWaveValue(sensorPin,3);
    Vmax+=localval;
  }
  Vmax=((Vmax/misure));
  if(Vmax<G_Soglia) Vmax=0;
    
  Serial.print("Sensor Max: ");
  Serial.print(Vmax,2);
  Serial.print(" / Ampere: ");
 
  Serial.print(Vmax*sensorstep,2);  
  Serial.print(" / Consumo: ");
  Serial.print(Vmax*sensorstep*220*taratura,2);
  Serial.print(" W / zero: ");
  Serial.println(puntozero);
 
 

}

Una volta compilato lo sketch è utile fare il processo di taratura per ottenere risultati migliori . Potete usare la solita lampadina da 100W, io ho usato quella ed ho fatto 4 avvolgimenti del cavo nel sensore in quanto la calibrazione è migliore con segnali forti piuttosto che deboli dato che si riduce la percentuale di errore legata al rumore di fondo. In queste condizioni dovrei avere un carico di 400W, le letture fatte, con Arduino UNO, sono state mediamente sui 423 con un errore perciò del 5-6%. Facendo 400/423 ottengo 0.94562, valore da porre nella costante di calibrazione (nel mio caso, nel vostro potrebbe essere diverso).  Dopo aver modificato la costante e ricompilato le letture non si discostano di oltre 2W dai 400W attesi con un errore perciò inferiore all’1%. Bene, allora posso passare ad Arduino DUE. Senza modificare la costante di taratura le misurazioni si attestano a 375W anziché 400W. Riporto la costante a 1 per calcolare il nuovo valore e, sorpresa, la misura è pressoché perfetta senza alcuna modifica con un margine d’errore medio inferiore al watt. Ho perciò tenuto la costante a 1 per Arduino Due e 0.945 per Arduino UNO ed ho ripetuto diversi test con numerosi carichi. I risultati sono stati molto buoni, l’errore tende un po’ ad aumentare con i carichi ridotti, allo stesso modo tende ad aumentare la variabilità della misura. La cosa più deludente resta sempre il “carico zero”, ossia le misurazioni ottenute senza alcun carico applicato al sensore che sono state di 25-30W per Arduino UNO e 41-45W per Arduino DUE (non è un errore, con DUE l’errore è stato maggiore). L’unico modo per eliminare questa componente è usare una soglia che filtri la componente di rumore. Nel listato proposto sopra è impostata a 2.5, con Arduino UNO, a 15 su Arduino DUE, sempre utilizzando la compilazione condizionale di cui abbiamo accennato.

Conclusioni

Nonostante la stragrande supremazia prestazionale di Arduino DUE su UNO il risultato finale è tutto sommato simile, l’errore aggiuntivo di Arduino UNO rispetto a DUE è davvero irrisorio, dovrei fare un’analisi statistica per quantificarne la differenza, guardando unicamente i valori “a vista” ho provato la sensazione che l’errore e la variabilità delle misure siano leggermente maggiori, ma è appunto più una sensazione che un dato certo.  Per questo motivo mi sento di dire senza timore di smentita che nonostante le doti di campionamento di Arduino DUE siano nettamente superiori, non portano al alcun beneficio pratico in questo specifico ambito applicativo. A dirla tutta però potremmo sfruttare tutta la potenza elaborativa di Arduino DUE per creare un sistema software più sofisticato che rimuova più accuratamente il rumore di fondo, ma direi che a parte i carichi al di sotto dei 50W ove non abbiamo alcuna affidabilità, nei valori superiori otteniamo un errore che non supera l’1-2% rispetto al valore atteso per cui non ha alcun senso spendere ulteriori risorse per tentare di migliorare ancora. Tra le altre non ho nessun modo per verificare se la mia lampadina da 100W consuma realmente 100W e non magari 101 o 102.

 

  5 Responses to “Arduino DUE per il monitoraggio del consumo energetico – it”

  1.  

    Ciao Mc.
    Complimenti per il tuo blog. Mi è stato davvero d’aiuto.
    Faccio un cross-post col forum di arduino per farti una domanda:

    Il sensore che ho scelto (il mio contratto enel è da 4.5 Kw + pannelli solari e pompa di calore ti ricorda qualcosa ? ^^ ) è lo “Sct-013-030”.

    Come ho imparato studiando l’agomento sul tuo blog, l’output (alternato) varia da -1 a +1 V e sappiamo che il sensore monta una burden res. da 62 ohm….

    Ora: Io vorrei attaccarci una schedina Lolin NodeMCU, che ha una AREFdi 3.3v e quindi vorrei avere in output -1.65 +1.65 V (diciamo -1.5 +1.5 per avere un margine di sicurezza) per poter sfruttare a fondo tutta la scala di lettura giusto? (se usassi un arduino uno -2.5 / + 2.5v) per poi offresettarla con il partitore di corrente e ottenere un range per la lettura sul microcontroller da 0.15 V a 3.15 V

    dal sito openenergymonitor imparo che per dimensionare la burden, il calcolo è:

    Burden Resistor (ohms) = (AREF * CT TURNS) / (2√2 * max primary current)

    Ovvero nel mio caso (3.3 * 1500) / (2√2 * 30) = 58,33 ohm

    MA mettendo in parallelo una resistenza da 1000 ohm, per la legge di ohm sulle res. in parallelo ho:

    62*1000/(62+1000) = un res. eq di 58,38 ohm

    Secondo te il ragionamento fila ? Mettere una res da 1k agli estremi dei cavetti è un operazione banale ma secondo te funge? Si potrebbe anche pensare di sostituire la burden esistente, ma non volevo andare ad aprirlo e dissaldarci pezzi.

    •  

      Secondo me il tuo ragionamento è corretto e dovrebbe funzionare. Ti faccio però notare una piccola imprecisione. Hai detto che volevi limitare la scala a -1.5 / 1.5, quindi 3V, ma poi nel calcolo hai usato 3.3V. Facendo i calcoli con 3V vengon fuori 53.19Ohm. Ma a mio parere puoi fregartene proprio perchè il fondo scala corrisponderà comunque ai 30A che non raggiungerai mai.

  2.  

    inline double OversampledAnalogRead(int pin)
    {
    unsigned int val=0;
    unsigned int misureoversample=pow(4,overbit);
    for(unsigned char z=0;z>overbit); //2^n
    }

    Scusa posso chiederti 2 cose?
    1) inline è una istruzione assembly vero ? perchè l’hai utilizzata?
    2) Non capisco come mai , se per esempio ho un sovra campionamento +2bit, faccio 4^2 =16 misure, ma poi la funzione restituisce la somma dei valori diviso gli overbit, ovvero 2^2=4 e non 16, come mi aspettavo. Ho provato lo sketch e mi da valori corretti, ma non capisco questo passaggio matematico.

    Sei diventato il mio libro di testo Mc =)

    •  

      ma tu stai cercando di carpire i miei segreti 🙂 ….NO, inline NON è assembly ma purissimo C. Di fatto dice “ogni volta che trovi il richiamo a questa funzione non devi chiamare la funzione ma duplichi tutto il codice che c’è al suo interno”. In pratica la funzione viene riprodotta “in loco” ogni volta che la incontra. Questo ha un vantaggio, ossia non perdi tempo a richiamare la funzione e inviare i parametri / leggere il risultato, MA hai un incremento in dimensioni del codice. Se usata in modo sapiente puoi incrementare in modo significativo la velocità di calcolo, specie nelle schede con poca capacità di calcolo ma non devi abusarne altrimenti il codice finale diventa enorme ed ottieni il risultato opposto.
      Per il secondo punto, se tu dividessi per 16 otterresti una “scala” come quella originale e non aumentata come ti stai prefiggendo. Il “numero” finale dovrà essere su una scala superiore all’originale perchè hai incrementato i bit i campionamento…..non so se sia chiaro. Ma alla fine ti migliora l’accuratezza della misura?????

  3.  

    Ok, capito. Parto con dei valori da 0 ai X e ri-mappo il risultato con i bit estesi, non con i bit originari!!! Eh, ci sono arrivato! =)