Apr 042015
 

Nello scorso articolo abbiamo visto come utilizzare il sensore SCT 013-030 per monitorare in maniera un po’ grossolana il consumo energetico. Se l’avete letto sapete che abbiamo terminato l’articolo con un sistema rudimentale, ma funzionante, sul quale affermavo fossero possibili notevoli miglioramenti, oggi vediamo quali.

Oversampling del segnale

Prima di parlare del sovracampionamento del segnale è forse meglio ribadire alcuni punti visti in precedenza. Se ricordate dicevamo che a causa di un’oscillazione di soli 2V del segnale fornito dal sensore sui 5 disponibili di Arduino, ogni singola unità misurata rappresentava, dopo le debite conversioni, un consumo di circa 32W. Per rendere più chiaro il concetto vediamo il grafico qui sotto che rappresenta l’andamento del segnale di un carico resistivo di 100W così come lo vede Arduino.

analogico128_10bitLa domanda che ci possiamo fare è quindi: c’è un modo per ridurre l’ampiezza di quei gradini per ottenere una maggior precisione? La risposta è si anche se è limitata da alcuni fattori. Per prima cosa la tecnica dell’oversampling ha bisogno di un po’ di rumore del segnale da campionare, potrebbe sembrare una contraddizione ma nel caso di un segnale eccessivamente pulito sarebbe necessario aggiungere un rumore di fondo voluto. Nel nostro caso il segnale è sufficientemente rumoroso da poter applicare la tecnica dell’oversampling. Il secondo requisito è dato dal fatto che durante i campionamenti non devono esserci grosse modificazioni del segnale. L’oversampling infatti prevede di effettuare un certo numero di campionamenti consecutivi, numero che però non è casuale e che dipende da fattori ben precisi. Prima di tutto c’è un primo limite da conoscere e che riguarda il campionamento di qualunque segnale analogico, ossia la frequenza di Nyquist che stabilisce che la frequenza di campionamento di un segnale analogico deve essere almeno doppia rispetto al segnale da campionare. Nel nostro caso abbiamo un segnale a 50Hz per cui dobbiamo campionare ad almeno 100Hz, ossia 100 campionamenti al secondo, cosa che Arduino è in grado di fare visto che, se ricordate, è in grado di campionare a 9600Hz. Si noti quell'”almeno” scritto in neretto che indica una condizione minima e non certo ideale: resta sottointeso che più è alta la frequenza di campionamento e migliore è il risultato finale.

L’oversampling del segnale impone però un secondo limite, ossia la frequenza di oversampling deve essere 4n*fn, ossia 4 elevato alla n la frequenza di Nyquist. Manca un dettaglio: cos’è n? n è il numero di bit che vogliamo aggiungere al campionamento. Ad esempio il nostro Arduino UNO campiona i segnali a 10 bit, se vogliamo fare un campionamento a 11 bit, ossia un bit in più, dobbiamo campionare con una frequenza minima di 4fn, ossia 400Hz. Se vogliamo passare a 12bit dobbiamo campionare ad almeno 16fn, ossia 1600Hz.

bit Campionamenti Frequenza
10 1 100
11 4 400
12 16 1600
13 64 6400
14 256 25600
15 1024 102400
16 4096 409600

Ora vediamo il medesimo campionamento di prima effettuato a 11bit, come potete vedere il miglioramento è significativo, ma balza subito all’occhio come la necessità di effettuare maggiori calcoli e campionamenti riduce molto rapidamente la “risoluzione temporale”, per cui a fronte di una maggior precisione sull’asse y abbiamo una riduzione su quello x. analogico128_11bitVediamo ora cosa succede a 12bit, si nota chiaramente che le onde sono talmente ravvicinate da essere al limite della riconoscibilità, nel particolare si avverte come i picchi siano ancora tutto sommato allineati, ma sicuramente il calcolo dell’area della curva diventerebbe inaffidabile anche se da quanto detto sopra siamo ancora ampiamente entro i limiti teorici del sovracampionamento. analogico128_12bitConcludiamo con i 13 bit dove benchè la teoria ci dica che siamo ancora entro i limiti di applicabilità dell’oversampling, si vede chiaramente come c’è una totale perdita dell’originaria forma d’onda, con alterazione sia dei valori di picco che dell’area sottesa dalle curve, oramai non più delineabile.

analogico128_13bitCome si procede per fare l’oversampling del segnale? Non dobbiamo far altro che sommare 4n campionamenti consecutivi e poi dividere il risultato per 2n ricordandoci che n rappresenta il numero di bit aggiuntivi rispetto quelli base. Vi faccio notare che parlo sempre di “bit rispetto a quelli base” che su Arduino UNO corrispondono a 10, ma ci sono boards che campionano i segnali in modo diverso, ad esempio Arduino DUE effettua campionamenti a 12bit. Ora verrebbe quasi spontaneo pensare che abbiamo aumentato la risoluzione sull’asse y ma ci sarebbe comodo riuscire a farlo anche sull’asse x, aumentanto la velocità di campionamento.

Il prescaler

Per capire cosè il fattore di prescaler vediamo qualè la situazione base di Arduino UNO. Sappiamo che funziona a 16Mhz, ossia esegue 16 milioni di operazioni elementari al secondo. Il convertitore analogico-digitale è settato con un fattore di prescaler base di 128. Cosa significa? Significa che la sua velocità è settata ad un 128° del clock principale. Quindi se dividiamo 16MHz per 128 otteniamo 125kHz. Siccome la conversione analogico-digitale richiede 13 cicli di clock, 125/13=9615Hz, ossia i famosi 9,6kHz che avevamo detto essere la velocità del convertitore analogico-digitale. Come avrete già campito però, il prescaler è modificabile, in particolare lo possiamo ridurre per aumentare la velocità dell’ADC (convertitore analogico-digitale). Cosa succede ad esempio se lo impostiamo a 32? Otteniamo 16MHz/32=500kHz che diviso per 13 ci ritorna 38,461kHz. Funziona veramente?

analogico32_11bitGuardate voi stessi, quello qui sopra è sempre il campionamento degli esempi precedenti, fatto con oversampling a 11bit e prescaler settato a 32. Di fatto abbiamo una risoluzione temporale simile a quella del primissimo esempio, ma con un’aumentata risoluzione sull’asse y grazie all’oversampling. C’è da aggiungere però che la riduzione del prescaler porta ad una riduzione della precisione del campionamento. In rete ho trovato un grafico che riporta la qualità della conversione, non ho peò trovato una sicura ufficialità di questi dati, comunque vi riporto qui vicino il grafico.enob_vs_freq Ho fatto tutta una serie di prove per cui mi sento di poter affermare che al di sotto del 32 si comincia a perdere precisione a livello dei picchi, in particolare a 11bit si tende ad avere una linea piatta più che una curva appuntita, in quel caso risulta necessario passare a 12bit per riprendere risoluzione, almeno parzialmente, sui picchi. Nella realtà la piena risoluzione a 10bit dell’ADC è garantita fino ad un massimo di 200kHz per cui solo il prescaler a 128 garantisce la massima precisione. Per chi si chiedesse il senso del prescaler se poi la massima risoluzione è solo con l’impostazione a 128, si faccia presente che anche se Arduino esce di fabbrica con un clock a 16MHz non è per nulla scontato che non lo si possa modificare, portandolo ad esempio a 12MHz.

A titolo di esempio vi riporto qui sotto il solito campionamento fatto a 12bit con prescaler a 16, che comunque rispetto a quanto ottenuto con il prescaler a 128 è un gran passo avanti.

analogico16_12bitDopo tutta una serie di prove ho deciso, per il mio software, di utilizzare 11bit con prescaler a 32. Ritengo però che la scelta dipenda molto dal segnale che si vuole campionare per cui non si prenda questa mia scelta come una verità assoluta e valida per ogni tipo di segnale.

Ora devo spiegarvi come modificare il prescaler il che è semplice da fare, non dobbiamo far altro che settare i bits ADPS nel registro ADCSRA. Tranquilli, non importa se non avete capito, nella pratica è più semplice di quel che pensate. Per capirci meglio vediamo la prossima tabella:

ADPS2 ADPS1 ADPS0 Prescaler
0 0 0 2
0 0 1 2
0 1 0 4
0 1 1 8
1 0 0 16
1 0 1 32
1 1 0 64
1 1 1 128

Come vedete non è nulla di particolarmente complesso, semplice notazione binaria. Per fare il tutto usiamo dei #define che poi possiamo sfruttare più facilmente. Se fate ad esempio riferimento al prescaler=128, vedete che i tre bit ADPS sono tutti settati a 1 formando il numero binario 111 che corrisponde a 7. Fate però attenzione che NON potete settare il registro ADCSRA direttamente a 7 perchè quel registro contiene anche altri bit con un loro specifico significato e rischiereste di alterarli. Vi riporto la tabella con i restanti bit, non li approfondiamo, è solo per comprendere che non dovete andare ad alterare i restanti.

Registro ADCSRA
bit 7 6 5 4 3 2 1 0
nome ADEN ADSC ADATE ADIF ADIE ADPS2 ADPS1 ADPS0
Vi segnalo che con il prescaler a 2 non sono riuscito ad ottenere nulla (segnale piatto), e settato a 4 ho ottenuto un risultato pessimo, tale da non riconoscere la forma d’onda, anche aumentando i bit di sovracampionamento.
Per modificare il prescaler non dobbiamo far altro, per comodità, che definirci alcuni #define:
#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))
A questo punto per settare uno specifico prescaler, ad esempio il 32, non dobbiamo far altro che scrivere:
ADCSRA &= ~PS_128;
ADCSRA |= PS_32;
Si noti che non vi ho fornito i prescaler inferiori a 8 per i pessimi risultati ottenuti, e l’8 stesso ve lo sconsiglio.

Il punto zero

Ricordate la scorsa puntata quando parlavamo della necessità di trovare il punto zero? Avevamo usato uno sketch per rilevare il rumore di fondo e trovare il “punto zero”. Avevamo detto che la tolleranza dei resistori, la temperatura, la tensione non esattamente a 5V possono portare ad uno spostamento del punto zero sull’asse verticale. Nel mio prototipo attuale ho fatto una cosa molto semplice per stabilire questo punto, ossia dall’uscita del partitore di tensione, dove ci sono i teorici 2.5V, ho portato un cavo ad un secondo ingresso analogico di Arduino, nello specifico ho usato A5, in questo modo posso fare direttamente la misura, cosa che posso ripetere anche durante il funzionamento del programma. Più precisamente ho utilizzato la media matematica di 150 campionamenti consecutivi, infatti la semplice media è un ottimo sistema per attenuare il rumore di fondo.

La media quadratica

La scorsa volta dicevamo anche che anzichè usare il valore di picco e basta, è più corretto utilizzare la media quadratica dei campionamenti. Farla non è particolarmente difficile. Per ogni campionamento viene sottratto il “punto zero” ed il risultato viene elevato al quadrato. Questa operazione ha anche il vantaggio di rendere tutti i numeri positivi, anche per i campionamenti della semionda negativa. I quadrati vengono di volta in volta sommati e poi, come per ogni media, divisi per il numero di campionamenti. Il risultato viene posto sotto radice quadrata ed otterrete il valore medio, quello che viene comunemente chiamato RMS (Root mean square).

Calibrazione

Come detto in origine non teniamo conto del fatto che la tensione non è detto essere di 220V, inoltre possono esserci tutta una serie di motivazioni per le quali il sistema possa avere delle imprecisioni di misurazione. Per questo motivo ho deciso di utilizzare anche una costante di calibrazione. Più nello specifico ho utilizzato il noto carico di 100W, ho valutato il risultato ottenuto e dallo scostamento ho calcolato la costante di calibrazione che nel mio caso è di 1.02. Questo piccolo accorgimento mi ha permesso di ottenere un ulteriore miglioramento della precisione del sistema. Per quanto riguarda la calibrazione avrete notato che nel precedente articolo avevo parlato sia di carichi da 100W che 300W. In realtà ho usato sempre la stessa lampadina di riferimento, l’unica modifica che ho fatto è triplicare l’avvolgimento del cavo nella pinza del sensore, se infatti create delle spirali e fate passare più volte il cavo nel foro del sensore, il valore letto sarà doppio, tiplo, quadruplo etc. Potete usare questo trucco anche semplicemente per trasformare il vostro sensore da uno di 30A ad uno di 15A, in questo modo raddoppiate la risoluzione del sensore in un colpo solo, è sufficiente infatti modificare una costante sul software per ottenere la giusta lettura.

Onde e media

A differenza del precedente articolo, qui le misure vengono effettuate esattamente per blocchi di 0.02s, ossia la durata di un periodo completo. In questo modo abbiamo la certezza di coprire un’intera semionda positiva ed una negativa. In realtà non aspettiamo di passare per lo zero per il semplice motivo che se cominciamo ad esempio da mezza onda positiva, l’altra mezza verrà campionata alla fine del ciclo. risultatomisureanalogicheconsumoCerto se il carico cambia proprio in quell’istante la misura potrà essere poco precisa, ma credo sia la miglior soluzione fra complessità del software e precisione di misura. Attualmente il software è tarato per campionare tre periodi consecutivi in modo da limitare eventuali errori di misura, per cui la media quadratica viene fatta su campionamenti sui 0.06s. Inoltre ogni treno di misure viene ripetuto tre volte e se ne ricava un valore medio per cui di fatto ogni misurazione tiene conto di 9 periodi completi.
 
Nella figura qui vicino vi ho copiato una delle tante schermate di prova con un carico di 200W giusto per mostrarvi i risultati ottenuti. Come potete vedere la precisione è notevolmente migliore rispetto a quella ottenuta nel precedente articolo. A dirla tutta sono un po’ deluso sul “valore zero” in quanto ottengo risultati senza alcun carico che possono arrivare anche a 30W. Certo ci sono ancora molte migliorie che si possono fare ma ad ogni miglioria che aggiungo aumenta la complessità del sistema per cui, per questo articolo, ho preferito fermarmi a questo punto in quanto mi sono accorto che il tutto diventava un circolo vizioso nel cercare nuove migliorie, implementarle e descriverle.

Sketch

Lo sketch che vi propongo oggi non è particolarmente curato, è ancora una versione in sviluppo che avrò modo di modificare profondamente, anche perchè pensavo di spostare parte del codice in un’apposita libreria per la gestione del campionamento dei segnali analogici. C’è un utilizzo decisamente eccessivo di variabili globali, ma durante le fasi di sviluppo fanno spesso comodo per cui in questa fase dovrete accontentarvi di un software perfezionabile.
C’è una prima fase in cui vengono definiti i #define per il prescaler, molte variabili globali che possono essere modificate in base al tipo di sensore e di board usata. La costante di taratura la lascio a 1 in quanto se decidete di usarla deve essere tarata sul vostro sistema.
Nel setup() vengono fatte alcune impostazioni preliminari fra cui il prescaler ed effettuate alcune misurazioni fra cui il punto zero oltre che misurare i tempi di misura, usati a puro scopo di debug oltre che di pura curiosità.
Le funzioni sono abbastanza semplici. La SampleAnalogAvg effettua n campionamenti su un certo ingresso ritornando la media dei campionamenti. La OverSampleAnalogRead viene utilizzata al posto della basilare analogRead ed in base alle impostazioni fatte inizialmente effettua misurazione con i bit di base o con l’oversampling. La GetWaveValue campiona un certo numero di periodi (riferiti ai 50Hz) utilizzando la media quadratica. Si noti che ad ogni misura viene rifatta la misurazione del punto zero (eventualmente da modificare se non avete collegato il partitore ad un secondo ingresso analogico).
Il loop fornisce le varie misurazioni, nulla di più.
 
 
// Define various ADC prescaler
#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))

const int sensorPin=A1;

//voltage range of sensor
#define sensorvoltagerange 1
//how many ampere the sensor reads (RMS)
#define sensorrange 30
//arduino adc range (5V, 3.3V or href value)
#define arduinorange 5
//numero ripetizione misure
#define misure 5
// 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 taratura 1

//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
 
  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
 
  ADCSRA &= ~PS_128;  //Set up the ADC: remove bits set by Arduino library
  ADCSRA |= PS_32;    // set our own prescaler to XX
    
  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)
   {
    sensore=OversampledAnalogRead(pin);
    sensore-=puntozero;
    
    somma+=(sensore*sensore);
    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<20) 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.println(" W");

}

Bene, anche questo lungo articolo è terminato. Sono convinto che si possano fare ancora dei miglioramenti, in particolare per ridurre il rumore sui bassi. La via più semplice  è impostare una soglia di cut-off, vi ho già impostato la riga di codice, basta eliminare il commento per tagliare i valori al di sotto di 20W. Ho altre idee per migliorare il sistema, ma come detto non volevo esagerare con questo articolo per cui questa puntata si conclude qui. Magari quando deciderò di aggiungere un sistema per misurare anche la tensione e l’angolo di sfasamento scriverò un nuovo articolo.