At first glance, measuring a voltage with an Arduino UNO seems to be a simple task. You only need to use the builtin A/D converter and the analogRead() function. But if you want to do this and get good results, you need to take some precautions and understand what you are doing !

Here my experience, and at the end i got very good results !

As every beginner should do,  i have read a lot of articles and posts. But doing this, i also have seen a LOT of nonsense...

The first one is that most examples are using a wrong ADC step number. Some write it's 1023, other 1024... There are 1024 steps going from 0 to 1023. BUT, the reference voltage does NOT give a precise value, but a range of value. For example 5V/1024 gives a range from 0.00488 V, but an ADC value of 0 is a range(V) from 0 to 0.00488 V !

Another common mistake is to trust in the 5V voltage reference.  It is only a very theoretical value and it would be a bad guess to use this value without some precautions. The USB port supplies the UNO with a very USB approximative 5V voltage, he regulator tolerance is quite large. Furthermore, this voltage is very "noisy" because of the spikes and noise coming from all supplied parts connected on this 5V line.

To partly solve this problem, ATMEL is providing a reference voltage through a AVR pin. The value of this voltage has a thoetical value of 1,100 V. BUT the ATMEL datasheet gives a 10% precision ! It's almost even worse than the 5V regulated supply voltage !

The "rich" solution is to use a high precision, low noise external reference. I will not describe this solution as i didn't use it for simplicity and cost.

My solution is to use the internal reference after measuring it. Note that this voltage is unfortunately temperature dependent !

I used a small sketch i've found that is using the internal reference and allows to measure this reference on the AREF pin. Testing different chips, i've measured values from 1.066V to 1.111V..

// put a 0.1uF cap on pin AREF
// run the script and measure the reference voltage

const uint8_t PinLED = 13;

void setup( void ){

Serial.begin( 38400 );
Serial.println( "\r\n\r\n" );
pinMode( PinLED, OUTPUT );
digitalWrite( PinLED, LOW );
delay( 1000 );
analogReference( INTERNAL );
	}

void loop( void ) {
Serial.println( analogRead( 0 ) );
digitalWrite( PinLED, HIGH );
;delay( 1000 );
}

Another problem is that, when using the inernal reference, you can only measure a maximum of 1.1V. So you need to scale down the input voltage with a voltage divider.

ATMEL recommends to choose the resistors so you have a 10 kOhms impedance at the ADC input. To be able to measure a maximum voltage of 15V, we will need a divider by 15/1.1 = 13.63. We select 13 to get normalized values for the resistorss. We choose 120000 and 10000 Ohms. These resistors will consume around 10 uA.

When thinking of very low power consumption, these 10uA are a LOT !! So, i choosed to drop this consumption to 1uA and multiplied the values by 10, which gives me 1.2 M and 100k Ohms.

Another solution would be to keep the low resistor values in order to keep the noise as low as possible and switch the divider off when not in use. This could be done by using a Mos transistor. (to be done)
 
In all cases, it is needed to use precision resistors (1% or better) just to be sure that we have the right ratio and also for stability reasons. 0.1uF capacitors between ground and the AREF and analog input pins are also required to keep the noise as low as possible.
 
Next problem is the ADC behaviour. The ATMEL documentation says that after the converter call, the first measure "may be" wrong. With my tests, i came to the conclusion that even the first 3 are wrong...
Just to be sure, i wake up the ADC, pause for a while and make 4 measures and keep only the last one. I could also smooth the 4 measures.

 

Once you get the analog value of the ADC in the range of 0-1023, you must convert it in a voltage value in Volts. You must take care of the true value of the reference voltage and the true voltage divider ratio.

 

2 methods could be used :
  • The traditionnal one using the step calculation
  • The more original one using the map() function wich makes a value translation

For the moment, i use the first one. It allows to easily correct the divider imprecision by adjusting the divider ratio and the reference voltage dispersion.

const float seuilBatterie = 11.5;                   // battery low level alarm
boolean alerteBatterie;                             // alarm fag

void readBatterie() {
  int analogBatt = analogRead(0);                   // ADC wake up on pin A0
  delay(100);                                       // wait 100ms to stablilize 
  
  for (byte i=0; i <= 4; i++) {
      analogBatt = analogRead(A0);                  // make 4 measures keep the last one
    }
  tensionBatterie = analogBatt * (1.100 / 1024);    // 1.100 = AREF
  tensionBatterie *= 11.275 ;                       // 11.275 = voltage divider ratio (to ba adjusted)
  
  if ( tensionBatterie < seuilBatterie ) {
      alerteBatterie = 1;                           // low battery flag used elsewhere

#if defined(DEVMODE)                                // compiler option to save memory while in production
      Serial.println(F("BATTERY ALARM"));
#endif      
  }
}