PrettyPrint

domenica 17 dicembre 2023

Arduino UNO R3: "correggere" la tensione di riferimento interna

L'idea di questa procedura mi è nata quando ho dovuto effettuare la correzione software della tensione di riferimento generata dal bandgap entro contenuto nell'ATmega328P, per l'uso con il sensore AD590 senza stadi attivi di condizionamento. 

La tensione di riferimento del bandgap interno è estremamente stabile, ottima per misurare range fra 0 e 1,1V,  ma ha un'elevata tolleranza di produzione (tipicamente fra 1,0 e 1,2V) che può quindi introdurre errori non trascurabili nel calcolo della tensione misurata dall'ADC.

Il suo utilizzo corretto richiede, quindi, innanzitutto una stima del suo effettivo valore (mediante la procedura di seguito descritta) ma anche lo scarto della prima lettura (che sarebbe non accurato, così come è scritto nel datasheet).

DESCRIZIONE DELLA PROCEDURA

Via software si misura una tensione di prova, VR, compresa fra 0,1 e 1V, ai capi di una resistenza di un partitore (come per esempio un potenziometro), usando nel calcolo, come tensione di riferimento il valore nominale della tensione di riferimento bandgap da 1,1V e si memorizza il valore, che chiameremo VR':

ove N è il numero restituito da analogRead()

Contemporaneamente si misura la stessa tensione mediante un multimetro digitale (con portata 2V=)  che chiameremo VR''.  Il rapporto fra questi due valori ci darà il coefficiente di "correzione" della tensione di riferimento che verrà usato nei software in cui si impiegherà la tensione di riferimento da 1,1V:


Tale valore, infine, verrà memorizzato nella memoria EEPROM della scheda e richiamato, all'occorrenza, nel software che impiega la tensione di riferimento da 1,1V.

Evidentemente, tale procedura andrà ripetuta ogni volta si cambia la scheda Arduino UNO R3 ovvero il microcontrollore ATmega328, essendo la tolleranza di fabbricazione del bandgap interno piuttosto variabile.

La procedura è stata testata solo su questo microcontrollore, ma dovrebbe funzionare anche per altri micro ATmegax che hanno un bandgap interno da 1,1V

Altri metodi reperibili in rete, usano un procedimento simile, che raggiunge lo stesso obiettivo finale di ricavare il coefficiente di correzione:  "giocando" con le configurazioni dei registri interni del micro, si misura la tensione del bandgap, usando come tensione di riferimento quella di alimentazione 5V, di default; poi, con una formula inversa di quella di ricostruzione, si calcola la tensione di riferimento che ne scaturisce (più piccola o più grande di 5,0V a seconda se la tensione bandgap effettiva è, rispettivamente, minore o maggiore del valore nominale di 1,1V). Alla fine si confronta questo valore calcolato con il valore misurato "reale" della tensione di alimentazione di Arduino, mediante il DMM, e da qui si ricava lo stesso coefficiente di correzione. 

Però, la misura di una tensione di 5V (sempre rimanendo nell'ambito dei più diffusi DMM 3,5 cifre) impone l'uso di portata minima di 20V= , con un'incertezza assoluta di misura, sulle cifre, sicuramente più alta di quando si usa una portata più piccola, come quella usata nella procedura qui descritta (una misura con P=2V infatti ha 3 cifre decimali, mentre una misura con P=20V ne ha solo 2).


HARDWARE

E' stato usato un trimmer resistivo da 10K, collegato all'ingresso analogico A0, regolandolo per una tensione intorno ai 0,7V:






MISURA TENSIONE VR'' CON DMM:


SOFTWARE

1)  BANDGAP_1V1_CAL.INO

Calcolo del fattore di correzione e salvataggio su EEPROM

/****** Problema:
 * la tensione di riferimento del bandgap interno 1,1V degli ATmegaX, sebbene molto 
 * stabile, ha una tolleranza produttiva non trascurabile. Conoscere l'effettivo valore,
 * dunque, migliora notevolmente il calcolo delle tensioni misurate via software
 * che usano come riferimento questa tensione.
 * 
 ****** Risoluzione:
 * confronta due misure della stessa tensione (compresa fra 0,1 e 1V) applicata a 
 * un ingresso analogico di Arduino, la prima fatta mediante software
 * la seconda mediante un DMM. Dal rapporto fra questi due valori, si determina un 
 * fattore di correzione, che verrà salvato in una locazione della memoria EEPROM; 
 * valore che verrà richiamato, successivamente, nel software  di lettura A/D 
 * che utilizza come VREF il bandgap interno.
 * La procedura va ovviamente ripetuta a ogni cambio di scheda Arduino/microcontrollore.
 * 
 ****** Autore: 
 * Francesco Parisi, 20231217
 */
 
#include <EEPROM.h>

const int pinR=A0;     // pin collegato al partitore
const float VREF =1.1; // tensione nominale b/gap int.
const int EEADD=0;     // ind. EEPROM dove salvare il fatt. correttivo
/*****************************************************/
float VR1;             // tensione partitore misurata via software
float VR2;             // tensione partitore misurata con il DMM
float CAL;             // fattore di correzione calcolato
/*****************************************************/ 

void setup() {
  Serial.begin(9600);
  analogReference(INTERNAL); // Vref interna =1,1V nominale
  VR1=misuraVR();  // misura ripetuta di VR, via software
  Serial.print("\nMisura tensione con bandgap interno: ");
  Serial.print( VR1, 3 );  
  Serial.println(" V");
  Serial.println(F("============================================="));
  Serial.print("Inserire misura tensione DMM in Volt (es. 0.752): ") ;
  while (Serial.available()==0)  
  { } // attende input da monitor seriale
  VR2=Serial.parseFloat();  // trasforma input in float
  Serial.println(VR2,3);
  CAL=VR2/VR1;
  Serial.print("\n\nFattore di correzione calcolato: ");
  Serial.println(CAL, 4);
  EEPROM.put(EEADD, CAL);  // scrive il val. CAL all'ind. EEADD
  Serial.println(F("============================================="));
  Serial.print("Fattore di correzione salvato all'indirizzo EEPROM: ");
  Serial.println(EEADD);
  Serial.println(F("============================================="));
  Serial.println("Programma terminato con successo!"); 
}

void loop() {
}

/* Funzione per la misura ripetuta di VR */
float misuraVR() {
  float somma=0;   // somma delle letture singole di VR
  float vmedio=0;  // media delle letture singole di VR
  float v;         // lettura singola di VR
  int n;           // numero restituito da analogRead()  
  int nlett=5;     // quante letture singole
  for (int i=0; i<nlett+1; i++) {
     n=analogRead(pinR);
     v=VREF*n/1024;
     if (i>0) // prima lettura scartata
       somma=somma+v;
     delay(50);
  }  
  vmedio=somma/nlett;  
  return (vmedio);     // ritorna la media della misure di VR
}
Caricato sulla scheda il software, si attiverà il monitor seriale. Dopo la stampa della misura software, l'utente inserirà nella casella di input dello stesso, il valore di tensione VR letto con il multimetro (0,766 nell'esempio):




Il programma procederà nell'esecuzione, calcolerà il fattore correttivo, lo stamperà e lo salverà nella locazione di memoria EEPROM (valore predefinito 0, che può essere cambiato nella sezione dichiarativa: costante EEADD):






Con il successivo sketch, si andrà a verificare l'effettiva scrittura in EEPROM del fattore di correzione.

2)  BANDGAP_1V1_TEST.INO

Stampa su monitor seriale del fattore correttivo salvato su EEPROM

/*
 * Test lettura valore CAL memorizzato nella EEPROM
 * 
 */

#include <EEPROM.h>

const int EEADD=0;     // ind. EEPROM dove è stato memorizzato il fatt. correttivo
float CAL;             // fattore di correzione memorizzato all'indirizzo EEADD

void setup() {
  Serial.begin(9600);
  Serial.print("Indirizzo EEPROM :");
  Serial.println(EEADD);
  
  EEPROM.get(EEADD, CAL);  // legge locazione EEADD e assegna valore a CAL
  
  Serial.print("CAL = ");
  Serial.println(CAL,4);
}

void loop() {

}
Attivando il monitor seriale avremo questo esempio di output:



USARE IL VALORE MEMORIZZATO NELLO SKETCH UTENTE

Nello sketch utente, ovvero lo sketch che impiegherà questo valore occorrerà innanzitutto inserire all'inizio la direttiva di inclusione del file di intestazione della libreria EEPROM:

#include <EEPROM.h>

A seguire, nella sezione globale, si defineranno la costante int EEADD e la variabile float CAL  e la costante  int VREF=1.1

Nel corpo della setup() si richiamerà la seguente funzione di assegnazione del valore letto dalla EEPROM alla variabile CAL:

EEPROM.get(EEADD, CAL);

Nella formula di ricostruzione della tensione analogica, la costante VREF andrà ovviamente moltiplicata per la costante CAL. Esempio:

 int N = analogRead(A0);

 float v = CAL*VREF*N/1024.0;

domenica 29 ottobre 2023

Un altro debounce (non bloccante) per Arduino Uno

Nell'impiego di interruttori, commutatori o pulsanti meccanici, si verifica quasi sempre un inconveniente che, nella maggioranza dei casi, costituisce causa di malfunzionamento nei circuiti digitali: l'elemento mobile che realizza il contatto, quando viene spostato da una posizione all'altra rimbalza (bouncing) diverse volte prima di stabilizzarsi al valore impostato, sia nella fase di pressione che in quella di rilascio.


Questi continui rimbalzi, che avvengono in un tempo brevissimo (generalmente fra 0,1 e 5ms), vengono percepiti dal software in esecuzione  come ripetute pressioni del pulsante e quindi erroneamente conteggiati. Il problema viene aggirato impiegando soluzioni cosiddette di anti-rimbalzo (debouncing) che possono essere circuitali o software. Una delle soluzioni software più semplici è quella di far eseguire un'istruzione di attesa, di un tempo pari, o leggermente superiore, a quello di rimbalzo, in modo che il programma ignori sicuramente queste innumerevoli commutazioni. Al termine di questo periodo, per sicurezza, si torna a leggere lo stato del pulsante: se è corrispondente a quello attivo (es. livello alto) il pulsante è stato dunque "de-rimbalzato" (debounced) e quindi può essere eseguito il codice previsto all'occorrenza della pressione del bottone.


HARDWARE                                  

Il pulsante N.A. è collegato fra il pin di ingresso (2) e la massa; il resistore di pull-up è quello interno dell'ATmegax8: quindi la pressione del pulsante comporta, a regime, la lettura sul pin 2 di un livello logico LOW, mentre il rilascio comporta, a regime,  la lettura di un livello logico HIGH. Sul pin 9 è collegato un LED (attraverso una resistenza da 220..330 Ohm), che, alla pressione del pulsante, commuterà il suo stato da acceso a spento e viceversa. 




SOFTWARE                                 

Lo sketch è imperniato sull'intercettazione della pressione del pulsante (ovvero del rilascio) attraverso un'istruzione di selezione semplice if, che va a "testare" lo stato corrente del pulsante (memorizzato nella variabile statoP1)  e quello della precedente esecuzione del loop (memorizzato nella variabile statoP1prec).  Se i due stati sono diversi vuol dire che c'è stata una variazione (da HIGH a LOW: pressione oppure da LOW a HIGH: rilascio), ma per essere sicuri che sia la prima variazione - ovvero la pressione o il rilascio - e quindi non una successiva variazione dovuta ai rimbalzi, occorre testare una terza variabile di flag che assume valore 0 solo quando non si è ancora verificata la prima variazione.

Se dunque si verifica la condizione logica (stato attuale pulsante NOT EQUAL a quello precedente AND flag=0) dalla pressione (ovvero dal rilascio) , attraverso un marker temporale (variabile t1) viene "segnato" l'istante conteggiato dalla funzione millis() e ovviamente settata a 1 la variabile flag, essendosi verificato l'evento di prima variazione.

Successivamente , a ogni loop, si andrà a leggere il valore  millis() e a confrontarlo con t1, se la differenza è MAGGIORE O UGUALE a 5ms (tempo tipico di attesa per l'esaurirsi del debounce; potrebbe essere necessario aumentarlo, se la qualità dei pulsanti è bassa) si andrà allora a testare lo stato del pulsante: se LOW, allora è una condizione di pressione e quindi si va ad eseguire il codice previsto all'occorrenza della pressione del pulsante.

Diversamente, verrà eseguito il codice restante dello sketch, cioè quello non relativo all'evento della pressione del pulsante, senza alcuna pausa. Infatti l'uso di millis()rende la gestione "non bloccante" dell'intero software, a differenza delle istruzioni di delay(): infatti il "costo" temporale delle istruzioni di check del tempo trascorso sono assolutamente trascurabili rispetto alle decine di millisecondi che avrebbe richiesto una delay.

Ad ogni loop, ovviamente, occorrerà aggiornare lo stato precedente del pulsante per il successivo loop, che ovviamente sarà uguale a quello attuale. Generalmente questa istruzione viene messa come ultima nel corpo della loop().

MIO_DEBOUNCE.INO

/*
 *  Debounce con millis() sulla pressione e sul rilascio
 *  del pulsante.
 *
*/
const int P1=2;   // pulsante 
const int L1=9;   // LED

int statoP1=LOW;       // stato P1 (premuto LOW / rilasciato HIGH)
int statoP1prec=LOW;   // stato precedente P1
int statoL1=LOW;       // stato LED (spento LOW / acceso HIGH)
int flag=0;            // 1: pressione o rilascio
int T=5;               // tempo attesa bouncing (5ms)
unsigned long t1;      // marker temporale di millis()

void setup() {
  pinMode(P1, INPUT_PULLUP);  // attiva la R pull-up interna
  pinMode(L1, OUTPUT);
  digitalWrite(L1, statoL1);
}

void loop() {

  statoP1 = digitalRead(P1);

  // rilevata una pressione o un rilascio
  if (statoP1 != statoP1prec && !flag)
  {
     flag=1;        // flag di avvenuta pressione/rilascio
     t1=millis();   // marca inizio conteggio tempo
  }

  // se si è esaurito il rimbalzo
  if ( millis()-t1 >= T && flag) 
  {
     flag=0;    // azzera flag  (per successive pressioni/rilasci)
// se è una pressione if (statoP1==LOW) { // istruzioni da eseguire alla pressione di P1: statoL1=!statoL1; digitalWrite(L1, statoL1); } } // istruzioni da eseguire al di fuori della pressione di P1: statoP1prec = statoP1; }