How to get best accuracy when measuring battery voltage with Arduino UNO?

Hello,

I want to measure the DC voltage supplied to my Arduino UNO R3 setup, by using the Arduino itself to find the most accurate voltage reading. I have come across a few methods during my research, but i want to avoid buying breakout boards, since i would rather make it as DIY as possible.

This is my current approach which is to use a resistive voltage divider which i built using two commonly available THT resistors:

// Define analog input
#define ANALOG_IN_PIN A4
 
// Floats for ADC voltage & Input voltage
float adc_voltage = 0.0;
float in_voltage = 0.0;
 
// Floats for resistor values in divider (in ohms)
float R1 = 30000.0;
float R2 = 7500.0; 
 
// Float for Reference Voltage
float ref_voltage = 5.0;
 
// Integer for ADC value
int adc_value = 0;
 
void setup(){
   // Setup Serial Monitor
   Serial.begin(9600);
   Serial.println("DC Voltage Test");
}
 
void loop(){
   // Read the Analog Input
   adc_value = analogRead(ANALOG_IN_PIN);
   
   // Determine voltage at ADC input
   adc_voltage  = (adc_value * ref_voltage) / 1024.0; 
   
   // Calculate voltage at divider input
   in_voltage = adc_voltage / (R2/(R1+R2)); 
   
   // Print results to Serial Monitor to 2 decimal places
  Serial.print("Input Voltage = ");
  Serial.println(in_voltage, 2);
  
  // Short delay
  delay(500);
}

But i have a doubt about the correctness of this line since i found this formula online:

adc_voltage  = (adc_value * ref_voltage) / 1024.0;

Shouldn't it be this instead, since the ADC reads from 0 to a maximum value of 1023?

adc_voltage  = (adc_value * ref_voltage) / 1023.0;

Is there another way to make this resistive voltage divider setup better or more accurate? I read about using internal precision reference but i wonder if it will be the same since the UNO is powered by the same DC voltage source.

Also, from what i understand, this sketch is using the UNO’s power supply voltage as a reference or maybe not since the following line is used:

float ref_voltage = 5.0;

When i measured with the digital voltmeter, there was a small discrepancy, so i discovered another solution while searching but it uses a breakout board which has precision resistors. Is this worth a try? Will it give significantly better precision?

There is also another method of using an external voltage reference. https://cdn-shop.adafruit.com/datasheets/lm4040-n.pdf. I could consider buying the SMD component but i don't know if it's easy to build the circuit myself?

I'd be more concerned about this

float R1 = 30000.0;
float R2 = 7500.0;

Shouldn't it be this instead, since the ADC reads from 0 to a maximum value of 1023?

No.

Use the internal reference, but make sure you calibrate

You can read the internal 1.1 volt reference voltage relative to the current Vcc and calculate Vcc from that. However, if your external power source goes through the Uno voltage regulator (Vin pin), that does not help much.

Normally, measuring voltage is more relevant to battery powered applications and a Uno, with all its power hungry accessories, is not optimal for battery based projects.

Start with pushing a 103 ceramic cap into the Aref and GND pins for starters.

Next use a multi-meter to actually measure the voltage in question so the formula can be tweaked to match.

The better way for have a precise read, imho, is using a precise reference ... power supply can float for any reason, so can do the reference ... maybe using a simple TL431B, a pair of resistors and a capacitor you can get a 2.5V reference more precise and stable of the internal ones :wink:

It’s simpler to stick with integers , and “ map” the input signal according to how it calibrates . If you are not careful you’ll lose accuracy mixing up your variable types and multiplying/dividing
Produce your output in millivolts .

Use the internal 1.1volt reference

TheMemberFormerlyKnownAsAWOL:
I'd be more concerned about this

float R1 = 30000.0;

float R2 = 7500.0;



No.

Use the internal reference, but make sure you calibrate

I have two 18650 rechargeable batteries connected to the Arduino setup. When both batteries are fully charged, it should be a maximum voltage of 4.2 x 2 = 8.4 V.

So, i chose R1 = 16 KOhms and R2 = 22 KOhms which gives an output voltage of 4.863 V at maximum voltage input from battery. I am using 1% tolerance film resistors, so i hope this configuration is good enough and not too high resistance.

Instead of the internal reference in the Arduino UNO R3 which is 1.1 V, why not use the 3.3 V? So, i could instead connect the AREF pin to the onboard 3.3 V pin on the Arduino. Would that be a better option? If not, why?

Idahowalker:
Start with pushing a 103 ceramic cap into the Aref and GND pins for starters.

Next use a multi-meter to actually measure the voltage in question so the formula can be tweaked to match.

I only have a 104 ceramic capacitor. Is that OK? So, i would need to measure the voltage across the capacitor terminals. But i'm trying to understand why not use a bigger capacitor size, like an electrolytic one? My guess is that a bigger capacitor will damage the multimeter but that shouldn't be an issue, since a multimeter can measure much higher voltages than 5 V.

DryRun:
So, i chose R1 = 16 KOhms and R2 = 22 KOhms which gives an output voltage of 4.863 V at maximum voltage input from battery. I am using 1% tolerance film resistors, so i hope this configuration is good enough and not too high resistance.

Waste of time comparing battery voltage to the 5volt supply of the Arduino.
A 5% fluctuation of the (USB) supply (not uncommon) will also give a 5% fluctuation in your battery voltage readout.

DryRun:
Instead of the internal reference in the Arduino UNO R3 which is 1.1 V, why not use the 3.3 V? So, i could instead connect the AREF pin to the onboard 3.3 V pin on the Arduino. Would that be a better option? If not, why?

The 3.3volt supply of an Uno, if not used for other things, should be rather stable/clean.
But you could burn out Aref if you connect 3.3volt to the Aref pin and forget to change Aref to EXTERNAL.
The internal 1.1volt Aref is much easier/safer to use.

Just use a voltage divider to ~1volt.
10k:68k could be ok.

A 104 (100n) ceramic cap is perfect.
Don't use electrolytic for decoupling and don't go higher in value.

Don't over-complicate your sketch.
This should work (untested).

float voltage;

void setup() {
  analogReference(INTERNAL);
  Serial.begin(9600);
}

void loop() {
  voltage = analogRead(A0) * 0.00822; // calibrate
  Serial.println(voltage);
  delay(500);
}

Leo..

So, i chose R1 = 16 KOhms and R2 = 22 KOhms

So why doesn't it say that in the code?

They're 1% resistors, but your quibbling about a 0.1% difference between 1023 and 1024?

Whatever method you use, you're still going to have to calibrate .
What if the 3.3V pin gets heavily loaded?

Wawa:
Waste of time comparing battery voltage to the 5volt supply of the Arduino.
A 5% fluctuation of the (USB) supply (not uncommon) will also give a 5% fluctuation in your battery voltage readout.
. . .
Leo..

Unless the fluctuation is wild and continuous, it would be possible to do it in two stages. First measure the Vcc against the internal voltage reference. Having got a value for Vcc, then use that as a reference for measuring the voltage divider.

Wawa:
Just use a voltage divider to ~1volt.
10k:68k could be ok.

I would like to try it for higher accuracy but currently, i don't have the necessary resistors in my toolbox. But i will consider it for the future.

So, i am using R1 = 10kOhms and R2 = 14.36 KOhms. The battery voltage source has a maximum value of 8.4 V when fully charged. The corresponding output voltage from the voltage divider is 4.952 V.

I have connected a 104 ceramic cap in between the AREF and GND pins. I measured the voltage as 1.071 V. But should i just leave the ceramic cap in place so that the voltage signal measured by the ADC will always be more accurate?

Here is my updated sketch:

// Define analog input
#define ANALOG_IN_PIN A4
 
// Floats for ADC voltage & Input voltage
float adc_voltage = 0.0;
float in_voltage = 0.0;
 
// Floats for resistor values in divider (in ohms)
const float R1 = 10000.0;
const float R2 = 14360.0;
 
// Float for Reference Voltage
const float ref_voltage = 1.071; // <- Internal Reference Voltage as measured (or 1v1 by default)
 
// Integer for ADC value
int adc_value = 0;
 
void setup(){
   // Setup Serial Monitor
   Serial.begin(9600);
   Serial.println("DC Voltage Test");
}
 
void loop(){
   // Read the Analog Input
   adc_value = analogRead(ANALOG_IN_PIN);
  
   // Determine voltage at ADC input
   adc_voltage  = (adc_value * ref_voltage) / 1024.0;
  
   // Calculate voltage at divider input
   in_voltage = adc_voltage / (R2/(R1+R2));
  
   // Print results to Serial Monitor to 2 decimal places
  Serial.print("Input Voltage = ");
  Serial.println(in_voltage, 2);
  
  // Short delay
  delay(500);
}

Here is a sample of the output in Serial Monitor, with nothing connected to the ADC pin:

Input Voltage = 0.70
Input Voltage = 0.59
Input Voltage = 0.45
Input Voltage = 0.39
Input Voltage = 0.38
Input Voltage = 0.48
Input Voltage = 0.64
Input Voltage = 0.67

Why is there a voltage measured? Should i include this in the calculation as an offset for a more accurate battery voltage?

I connected the DC battery which has a measured voltage of 8.3 V via the potential divider but in the Serial Monitor, it says a constant value of 1.75 V ??

I did some research and i found some code for a similar application but i can't understand why the 0.5 was added for rounding up. So, i'm wondering if there is a way to make my results more accurate with a similar approach?

// Float normally reduces precion but works OK here. Add 0.5 for rounding not truncating.
  float results = InternalReferenceVoltage / float (ADC + 0.5) * 1024.0;

DryRun:
I have connected a 104 ceramic cap in between the AREF and GND pins.

Why?
The Uno has AFAIK already a cap there.
A 100n cap from the analogue pin (not the Aref pin) to ground is sometimes needed if the total impedance of the voltage divider is > 10k.

DryRun:
I measured the voltage as 1.071 V.

Confused. So you did switch Aref to INTERNAL?
Your code shows default Aref (~5volt).

DryRun:
Here is a sample of the output in Serial Monitor, with nothing connected to the ADC pin:
Input Voltage = 0.70

With nothing connected to the analogue pin (floating pin), there will be random noise and a random voltage.
With the voltage divider connected, but not the battery, there should be zero volt.

You seem to confuse accuracy (fixed with calibration) with stability (bad Aref).
The example code I posted uses the internal ~1.1volt Aref for stability, and a common calibration factor to take care of Aref and voltage divider accuracy.
Leo..

Wawa:
Why?
The Uno has AFAIK already a cap there.
A 100n cap from the analogue pin (not the Aref pin) to ground is sometimes needed if the total impedance of the voltage divider is > 10k.

I did it based on this suggestion above:

Idahowalker:
Start with pushing a 103 ceramic cap into the Aref and GND pins for starters.

Next use a multi-meter to actually measure the voltage in question so the formula can be tweaked to match.

Wawa:
Confused. So you did switch Aref to INTERNAL?
Your code shows default Aref (~5volt).
With nothing connected to the analogue pin (floating pin), there will be random noise and a random voltage.
With the voltage divider connected, but not the battery, there should be zero volt.

I added analogReference(INTERNAL); to the code and with no battery connected it now shows 0.00 V on the Serial Monitor. But the voltage measured with the battery connected is now 1.82 V which is wrong as it should be 8.30 V.

// Define analog input
#define ANALOG_IN_PIN A4
 
// ADC voltage and Input voltage
float adc_voltage = 0.0;
float in_voltage = 0.0;
 
// Voltage divider resistor values (in ohms)
const float R1 = 10000.0;
const float R2 = 14360.0;
 
// Bandgap voltage reference
const float bandgap_voltage = 1.071;  // Internal Reference Voltage as measured (or 1v1 by default)
 
// ADC value
int adc_value = 0;
 
void setup(){
   // Configure the reference voltage used for analog input
   analogReference(INTERNAL);  // INTERNAL: a built-in reference, equal to about 1.1 volts on the ATmega328P
   // Setup Serial Monitor
   Serial.begin(9600);
   Serial.println("Battery Voltage");
}
 
void loop(){
   // Read the Analog Input
   adc_value = analogRead(ANALOG_IN_PIN);
  
   // Determine voltage at ADC input
   adc_voltage  = (adc_value * bandgap_voltage) / 1024.0;
  
   // Calculate voltage at divider input
   in_voltage = adc_voltage / (R2/(R1+R2));

   // Print results to Serial Monitor, by default to 2 decimal places
  Serial.print("Input Voltage (V) = ");
  Serial.println(in_voltage);
  
  // Short delay
  delay(500);
}

Wawa:
You seem to confuse accuracy (fixed with calibration) with stability (bad Aref).
The example code I posted uses the internal ~1.1volt Aref for stability, and a common calibration factor to take care of Aref and voltage divider accuracy.
Leo..

I tried to adapt your code into my initial sketch to make it more detailed. But i'm not sure what i'm missing.

DryRun:
I tried to adapt your code into my initial sketch to make it more detailed.

But i'm not sure what i'm missing.

More complicated you mean.

You have the wrong resistor ratio for 1.1volt Aref.
10k:68k computes to 10/(10+68)= ~0.1282
1023 * 1.071 / 1024 / 0.1282 = ~8.3volt.

Better stick with
float voltage = analogRead(A0) * 0.00822; // nothing else needed
Leo..

Edit: 10k:68k might not quite get you to 8.4volt with 1.071volt Aref.
Maybe only to 8.3volt. 10k:69k will.
Could fix that by adding an extra 1k resistor.

The theory behind it...

You should calculate the voltage divider for maximum A/D resolution,
meaning 8.4volt should divide down to 1.071volt, or close to that.

That means that an A/D value of 1023 should print as ~8.4volt.

8.4volt / 1023 = 0.008211 (close to the factor I gave you).
Leo..

Wawa:
The theory behind it...

You should calculate the voltage divider for maximum A/D resolution,
meaning 8.4volt should divide down to 1.071volt, or close to that.

That means that an A/D value of 1023 should print as ~8.4volt.

8.4volt / 1023 = 0.008211 (close to the factor I gave you).
Leo..

Thanks for clarifying. I was getting a bit lost... So, i managed to get the required resistors. R1 = 68.5 K Ohms and R2 = 9.99 K Ohms. Since maximum output to the ADC is 1.071 V, then the maximum battery voltage is 8.415 V.

This is now my serial output:

Battery Voltage (V) = 8.34
Battery Voltage (V) = 8.34
Battery Voltage (V) = 8.41
Battery Voltage (V) = 8.32
Battery Voltage (V) = 8.33

The problem is that it fluctuates too much in the decimals, even for one decimal place it bounces between 3 and 4. Is there a way to make it more stable?

Did you build it on a breadboard. Could be picking up noise.
Did you use a 100n cap from the analogue pin to ground.
I assume there is nothing else currently connected to the battery.
A common way to reduce fluctuations is to average multiple readings.
Leo..

Explanation here as to why averaging multiple readings helps!

For best accuracy you need an external reference as described here; you can use a 2.5V reference for calibration, and the 3.3V supply as the "EXTERNAL" reference, and tailor your battery voltage with a divider (as you already have done) to fit nicely into the 3.3V range.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.