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.
- 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 } }