Go Down

Topic: 12V battery meter with Arduino MEGA (Read 1 time) previous topic - next topic

steger

Jan 06, 2021, 09:09 am Last Edit: Jan 06, 2021, 12:49 pm by steger
Hi Everybody,

I was searching for hours for proper solution and finally decided to make my version and post here.
Before blowing up my board, I would highly appreciate, if somebody can spend some time and check the goodness of the below code and schematic:

So I have a 4 cell LiFePO4 battery with max. 12.8 V charging voltage and 10.7 V cut-off, connected to MEGA Vin pin.
Goal: measure the battery charged state in % as accurate as possible.
For this purpose I'm planning a voltage divider and use the internal reference voltage for accurate measuring.

Code: [Select]

void loop() {
  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V
  for (int i=0; i<8; i++) analogRead(A0);   // just burn some ADC readings after reference change
  ADCvalue = analogRead(A0);            // read the divided battery voltage on analog pin # 0
  analogReference(DEFAULT);            // set back the reference voltage level to normal 5V
  A0volt = 2.56 * ADCvalue / 1023       // convert digital signal to voltage.
//  You might measure the Ref voltage separately, because not always 2.56V !!! (put delay(60000); 1 min. after above 'for' loop) and hard-code the reference voltage.
  R1 = 9978.9;                            // ohm (measured with DMM; near to 10k);
  R2 = 50128.2;                          // ohm (measured with DMM; near to 50k);
  Vin = A0volt * (R1 + R2) / R1;       // calculate the actual batter supply voltage
  percentage = 100 * (Vin - 10.7) / (12.8 - 10.7);
  Serial.print("Battery level [%]: "); Serial.println(percentage);
  delay(1000);
}

The wiring:



Thank You !

septillion

#1
Jan 06, 2021, 02:25 pm Last Edit: Jan 06, 2021, 02:26 pm by septillion
Three notes on accurate:

Code: [Select]

A0volt = 2.56 * ADCvalue / 1023       // convert digital signal to voltage.

Should have 1024

Just don't use floats. Just do the calculation in mV.

Battery percentage is nowhere near liniear with voltage. So the percentage calculation is flawed.

Think this should give you more than accurate enough results:
Code: [Select]

const unsigned long BatRs[] = {50128, 9979}; // as long as they have the same unit
const unsigned long BatVref = 2560; //mV
const unsigned long BatResolution = 1024;
const unsigned long BatRatio = ((BatRs[0] + BatRs[1]) * BatVref) / BatRs[1];

void setup(){
  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V
  //for (int i=0; i<8; i++) analogRead(A0);   // just burn some ADC readings after reference change
  delay(10);
}

void loop() {
  unsigned int adcValue = analogRead(A0);            // read the divided battery voltage on analog pin # 0
 
  //analogReference(DEFAULT);            // set back the reference voltage level to normal 5V
  //WHY?
 
  //A0volt = 2.56 * ADCvalue / 1023       // convert digital signal to voltage.
 
  unsigned int batVoltage = adcValue * BatRatio / BatResolution;
  Serial.print("Batery voltage: [mV]: ");
  Serial.println(batVoltage);
  byte percentage = 100 * (batVoltage - 10.7) / (12.8 - 10.7);
  Serial.print("Battery level [%]: ");
  Serial.println(percentage);
  delay(1000);
}


Although I didn't test it so may contain a bug
Use fricking code tags!!!!
I want x => I would like x, I need help => I would like help, Need fast => Go and pay someone to do the job...

NEW Library to make fading leds a piece of cake
https://github.com/septillion-git/FadeLed

Koepel

#2
Jan 06, 2021, 07:39 pm Last Edit: Jan 06, 2021, 07:39 pm by Koepel
The resistor values of 10k and 50k are good.
You have to be careful when you have a loose wire with 12V. If that wire drops on the Arduino board, then the board is broken. If you accidentally shortcut the 50k resistor, then the board is broken.

Do you really need to switch between 2.56V and 5V reference ?
Can you just select the 2.56 reference in setup() ?

Switching the voltage reference might influence the first read value. I would use a delay(10) and single analogRead().

After setting the 2.56 voltage reference, you could measure the voltage off the AREF pin. Use that value in the sketch instead of 2.56. You might need a little more tuning in the sketch.

If you keep the board always at room temperature, then the 2.56V voltage reference is good. If you put the board in freezing cold, then it changes.

There is always some noise. When using the average of a few samples, it is possible to get rid of the noise.
It is even possible to use the noise to increase the accuracy.
I have a different opinion as septillion about integers and float. I use the analogRead() as integer, but once I calculate the result with a possible higher accuracy then I use float: averageRead.ino.


septillion

Ow, one thing I forgot, add a cap (100nF ceramic will do) to the output of the voltage divider. Arduino doesn't like to measure high impedance (top of my head >50k). Cap will add impedance without draining the battery.
Use fricking code tags!!!!
I want x => I would like x, I need help => I would like help, Need fast => Go and pay someone to do the job...

NEW Library to make fading leds a piece of cake
https://github.com/septillion-git/FadeLed

steger

#4
Jan 07, 2021, 07:25 am Last Edit: Jan 07, 2021, 09:03 am by steger
Dear All,

first of all thank You very much for your time and continuous help spent in this topic.
I summarized the feedbacks for future use/other users (lesson learned :-).

I made a function from the scrip, so it can be called when the robot in operating. (If it will be below limit (let say 10%) the rover can stop or search for better solar conditions (solar panel is on the roof of the rover).)
In the script I set back the REF voltage from 2.56V to 5.0 V, because it will cause later issues in other part of the full program (not listed here).
I used standard resistor 50k -> 51k.
Code: [Select]
const unsigned long BatRs[] = {51028, 9979};    // as long as they have the same unit
const unsigned long BatVref = 2560;            //mV ; this must be measured on the AREF pin accurately with DMM
//after initiating 'analogReference(INTERNAL2V56);' and change here.
const unsigned long BatResolution = 1024;
const unsigned long BatRatio = ((BatRs[0] + BatRs[1]) * BatVref) / BatRs[1];
const float batVolt_Max = 12.8;
const float batVolt_Min = 10.7;
#define BatVolt_pin = 0;     //Anlaoge pin # A0


void BattVoltage() {

  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V
  for (int i=0; i<8; i++) analogRead(BatVolt_pin);   // just burn some ADC readings after reference change
  delay(10);
  // delay(60000)    // 1 min delay only one time to have time
     // to measure the voltage on AREF pin with DMM.
  unsigned int adcValue = analogRead(BatVolt_pin);      // read the divided battery voltage on analog pin # 0
 
  analogReference(DEFAULT);                 // set back the reference voltage level to normal 5V
  for (int i=0; i<8; i++) analogRead(BatVolt_pin);      // so later in the main/full program no trouble
  delay(10);
 
  unsigned int batVoltage = adcValue * BatRatio / BatResolution;
  Serial.print("Batery voltage: [mV]: ");  Serial.println(batVoltage);
  byte percentage = 100 * (batVoltage - batVolt_Min) / (batVolt_Max - batVolt_Min);
  Serial.print("Battery level [%]: ");  Serial.println(percentage);
  return percentage;
}


If there is some improvement point still left, kindly share with us.

Thank You for Your support in advance !

Koepel

I think it is okay. I prefer floats more than septillion I guess.

Did you know that the 32-bit single float precision library for the AVR boards (Arduino Uno and Mega) is one of the most optimized pieces of code there is ? It is super fast and it is fully according to the IEEE standard. Divisions are slow, but divisions with integers are also slow.

When reading an analog value in such a situation (or with a LDR and so on), then taking the average of 5 samples already gives a good improvement.

To measure the real voltage of the 2.56V, you could make an empty sketch with only setting the voltage reference.
Code: [Select]
void setup()
{
  analogReference( INTERNAL2V56);
}

void loop()
{
}


Do you have a good multimeter to tune the calculation ?

steger

#6
Jan 07, 2021, 09:33 am Last Edit: Jan 07, 2021, 10:19 am by steger
Dear Koepel,

Thank You for the quick feedback.
I didn't know.
The empty sketch and the averaging are very good ideas!  ;)
I have bench top type DMM to measure the 2.56V ref..
I updated the script:

Code: [Select]
const unsigned long BatRs[] = {51028, 9979};    // as long as they have the same unit
const unsigned long BatVref = 2560;            //mV ; this must be measured on the AREF pin accurately with DMM
//after initiating 'analogReference(INTERNAL2V56);' and change here.
const unsigned long BatResolution = 1024;
const unsigned long BatRatio = ((BatRs[0] + BatRs[1]) * BatVref) / BatRs[1];
const unsigned long batVolt_Max = 12.8;
const unsigned long batVolt_Min = 10.7;
#define BatVolt_pin = 0;            //Analog pin # A0
int batVoltage = 0;

void BattVoltage() {

  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V
  for (int i=0; i<8; i++) analogRead(BatVolt_pin);   // just burn some ADC readings after reference change
  delay(10);
 
  unsigned int adcValue = analogRead(BatVolt_pin);      // read the divided battery voltage on analog pin # 0
   
  // take the average of 5 readings:
  for (j = 0; j < 5; j++)  {
    batVoltage += = adcValue * BatRatio / BatResolution;
    delay(10);
  }
  batVoltage = batVoltage / 5;
 
  analogReference(DEFAULT);                             // set back the reference voltage level to normal 5V
  for (int i=0; i<8; i++) analogRead(BatVolt_pin);      // so later in the main/full program
  delay(10);                                 // it will not cause any trouble
   
  Serial.print("Batery voltage [mV]: ");  Serial.println(batVoltage);
  byte percentage = 100 * (batVoltage - batVolt_Min) / (batVolt_Max - batVolt_Min);
  Serial.print("Battery level [%]: ");  Serial.println(percentage);
  return percentage;
}

septillion

Another way to calibrate the system would be to:
- Apply known (aka measure it) battery voltage
- just print the raw ADC value (or do a little average)

Now the calibrated value for 'BatRatio' is simply:

BatRatio = Vbat * 1024 / adcValue
With Vbat in mV.

This will make 'BatRs' and 'BatVref' obsolete.

For the sketch, just drop
Code: [Select]

for (int i=0; i<8; i++) analogRead(BatVolt_pin);

You already have the small delay. I think this delay can even be shorter.

And especially in the average I would reduce the delay(). 50ms is already pretty blocking.

And two point about the battery percentage:

a) You have an error in the calculation. Or, at least in the values used. Vbat is in mV, so 'batVolt_Max' and 'batVolt_Min' need to be as well

b) The percentage value will not give you a real indication. Battery capacity is not at all lineair with voltage. So now it will probably drop very quick, hover around a certain value most of the time and at the end drop quickly again.

Example of a discharge curve:


And do yourself (and especially future-self) a favor, pick a single naming convention and stick with it. I use camelCase as is tradition with Arduino, and in this case I used UpperCamelCase to indicate constants. With 'batVolt_Max' you mix camelCase and underscores which makes it harder to remember how to write a variable name. Was it 'batVoltMax'? 'bat_Volt_max'? 'bat_voltMax'? If you stick to a convention you just know how to write it.
Use fricking code tags!!!!
I want x => I would like x, I need help => I would like help, Need fast => Go and pay someone to do the job...

NEW Library to make fading leds a piece of cake
https://github.com/septillion-git/FadeLed

steger

#8
Jan 07, 2021, 05:08 pm Last Edit: Jan 07, 2021, 05:45 pm by steger
Thank You for the guidance for septilion and pointing out the major discrepancies.  The modified script:
Code: [Select]
const unsigned long BatRs[] = {51028, 9979};    // as long as they have the same unit
const unsigned long BatVref = 2560;            //mV ; this must be measured on the AREF pin accurately with DMM
//after initiating 'analogReference(INTERNAL2V56);' and change here.
const unsigned long BatResolution = 1024;
const unsigned long BatRatio = ((BatRs[0] + BatRs[1]) * BatVref) / BatRs[1];
const float BatVoltMax = 12800; //mV
const float BatVoltMin = 10700; //mV
int BatVoltPin = 0;     //Anlaoge pin # A0


void battVoltage() {

  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V
  for (int i=0; i<8; i++) analogRead(BatVoltPin);   // just burn some ADC readings after reference change
  delay(2);

  unsigned int adcValue = analogRead(BatVoltPin);      // read the divided battery voltage on analog pin # 0
 
  analogReference(DEFAULT);                 // set back the reference voltage level to normal 5V
  for (int i=0; i<8; i++) analogRead(BatVoltPin);      // so later in the main/full program no trouble
  delay(2);
 
  unsigned int batVoltage = adcValue * BatRatio / BatResolution;
  Serial.print("Batery voltage: [mV]: ");  Serial.println(batVoltage);
  byte percentage = 100 * (batVoltage - BatVoltMin) / (BatVoltMax - BatVoltMin);  //to be replaced
  Serial.print("Battery level [%]: ");  Serial.println(percentage);    // with regression func.
  return percentage;
}

 I modified for reference measurement this code as well:
Code: [Select]
int value;
void setup()
{
 analogReference(INTERNAL2V56);
 value = analogRead(0);    //The effect of analogReference() call doesn't take place till the next call to analogRead()
}

void loop()
{
}

This is giving on MEGA AREF's pin with DMM 2.465 V, instead of 2.560 V.
So row#2 to be modified to: const unsigned long BatVref = 2465; //mV

Regarding point b):  This is a major obstacle and the note is absolutely correct.
I think for more accurate calculation the following steps are required:
  • I have to get the characteristic of the battery.
  • Guess the average discharging current and calculate C.
  • Split the selected curve in three section and approximate the curve with some functions.
  • Calculate the capacity (%).
  • The above is valid on a certain temperature !

steger

#9
Jan 07, 2021, 05:32 pm Last Edit: Jan 07, 2021, 05:37 pm by steger
Just on more thing regarding BatResolution = 1024;
If I connect 5V out from UNO to same UNO analog A0 and run this code:

Code: [Select]

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

void loop()
{
Serial.println(analogRead(0));
}

I got 1023, so 5V == 1023
I think const unsigned long BatResolution = 1024; to be changed to
const unsigned long BatResolution = 1023;

Koepel

#10
Jan 07, 2021, 06:20 pm Last Edit: Jan 07, 2021, 06:26 pm by Koepel
I think const unsigned long BatResolution = 1024; to be changed to
const unsigned long BatResolution = 1023;
O no, you opened a box of spiders. This has been a long discussion. The final answer by Nick Gammon is here: https://www.gammon.com.au/adc.

In short: The ADC has 1024 steps. It can measure 0V, but it can not measure 5V. The 5V would be the 1025th step. The division factor must therefor be 1024. That is the range of the ADC and the range is required for the calculation.
When you apply 5V to the analog input, then you are already one step too high for the ADC.
The voltage range is still exactly 5V.

Example: suppose the 5V is exactly 5.000000V. If you read a value of 1023, that means you measure 1023/1024 * 5V. That is correct, that is the level one step below 5V. At 5V there is one step overvoltage.

Note: When the analogReference() is set to 1.1V, then the ADC can measure between 0V and 1.1V and everything between 1.1 and 5V returns 1023. That is okay, nothing will be damaged.

Did you check my averageRead.ino in a previous post ? I already mentioned there the 1024 and the half-a-bit and I also mentioned Nick Gammon's page.

septillion

@steger Close but no sigar.

Small changes:
Code: [Select]
const unsigned long BatRs[] = {51028, 9979};    // as long as they have the same unit
const unsigned long BatVref = 2465;            //mV ; this must be measured on the AREF pin accurately with DMM
 //after initiating 'analogReference(INTERNAL2V56);' and change here.
const unsigned long BatResolution = 1024;
const unsigned long BatRatio = ((BatRs[0] + BatRs[1]) * BatVref) / BatRs[1];
const unsigned long BatVoltMax = 12800; //mV
const unsigned long BatVoltMin = 10700; //mV
const byte BatVoltPin = A0;     //Analog pin # A0


void battVoltage() {
  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V
  //for (int i=0; i<8; i++) analogRead(BatVoltPin);   // just burn some ADC readings after reference change
  delay(2);

  unsigned int adcValue = analogRead(BatVoltPin);      // read the divided battery voltage on analog pin # 0
 
  analogReference(DEFAULT);                 // set back the reference voltage level to normal 5V
  //for (int i=0; i<8; i++) analogRead(BatVoltPin);      // so later in the main/full program no trouble
  delay(2);
 
  unsigned int batVoltage = adcValue * BatRatio / BatResolution;
  Serial.print("Battery voltage: [mV]: ");  
  Serial.println(batVoltage);
  byte percentage = 100 * (batVoltage - BatVoltMin) / (BatVoltMax - BatVoltMin);  //to be replaced
  Serial.print("Battery level [%]: ");  
  Serial.println(percentage);    // with regression func.
  //return percentage; //return type is void so not allowed
}


Did you also note the easier way to calibrate it in a single measurement?

O no, you opened a box of spiders.
Hehe, my thought exactly :D
Use fricking code tags!!!!
I want x => I would like x, I need help => I would like help, Need fast => Go and pay someone to do the job...

NEW Library to make fading leds a piece of cake
https://github.com/septillion-git/FadeLed

johnerrington

@Koepel
Can of worms indeed; I tried (unsuccessfully) to explain this in a way with which no-one could argue.  Guess I underestimated arduinoers.

The "academically" correct conversion is range * (n +0.5) / 1024

but ..

the 0.5 is less than the accuracy of the converter - so lets forget it.

and .. yes lets work in millivolts AND integers - because some will take 2560mV * 7 / 1024

and get 17.500 - which is nonsense.


@steger

Why do you keep changing the analog reference?
Do you NEED to measure something against the DEFAULT?

Code: [Select]

void battVoltage() {

  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V
  analogRead(BatVoltPin);   // you only need one
  delay(2);

  unsigned int adcValue = analogRead(BatVoltPin);      // read the divided battery voltage on analog pin # 0
 
  // analogReference(DEFAULT);                 // set back the reference voltage level to normal 5V  WHY?
  for (int i=0; i<8; i++)  analogRead(BatVoltPin);     //and put the result WHERE? This is doing nothing.

  delay(2);
 
  unsigned int batVoltage = adcValue * BatRatio / BatResolution;
  Serial.print("Batery voltage: [mV]: ");  Serial.println(batVoltage);
  byte percentage = 100 * (batVoltage - BatVoltMin) / (BatVoltMax - BatVoltMin);  //to be replaced
  Serial.print("Battery level [%]: ");  Serial.println(percentage);    // with regression func.
  return percentage;
}


I dont think using an array here is productive in code, basically just confusing.
As they are constants just give them names  - no indication of what they are even in the comment.

Code: [Select]
const unsigned long BatRs[] = {51028, 9979};    // as long as they have the same unit
const unsigned long BatVref = 2465;            //mV ; this must be measured on the AREF pin accurately
const unsigned long BatResolution = 1024;
const unsigned long BatRatio = ((BatRs[0] + BatRs[1]) * BatVref) / BatRs[1];



@septillion
Code: [Select]
  //return percentage; //return type is void so not allowed
would it make more sense to declare percentage AND function type as integer?




I'm trying to help. If I find your question interesting I'll give you karma. If you find my input useful please give me karma (I need it)

steger

Dear johnerrington,


thank You very much for Your feedback.
Yes, I need the to measure something against the DEFAULT later on. (The function will be called later if some button is pressed.) I just kept it like this (no harm) and info for others, how to set back, but if somebody would like to use only this script it can be commented out of course to avoid any confusion.


I updated the script with the proposal of Koepel dated 7th of Jan. Thank You for the advise Koepel ! Highly appreciated !  ;)  

Code: [Select]
const unsigned long BatRs[] = {51028, 9979};    // as long as they have the same unit
const unsigned long BatVref = 2560;            //mV ; this must be measured on the AREF pin accurately with DMM
//after initiating 'analogReference(INTERNAL2V56);' and change here.
const unsigned long BatResolution = 1024;
const unsigned long BatRatio = ((BatRs[0] + BatRs[1]) * BatVref) / BatRs[1];
const unsigned long BatVoltMax = 13500; //mV
const unsigned long BatVoltMin = 10400; //mV
#define BatVolt_pin = 0;     //Anlaoge pin # A0
const int nSamples = 1000;   // Usually from 5 to 10000

byte BattVoltage() {
  analogReference(INTERNAL2V56);   // set the reference voltage level from 5V to 2.56V

  // unsigned int adcValue = analogRead(BatVoltPin);      // read the divided battery voltage on analog pin # 0
  // unsigned int batVoltage = (adcValue + 0.5) * BatRatio / BatResolution; // add them up
  // take the average of few readings for better accuracy:
  // https://gist.github.com/Koepel/f7d625a6e5c0481fc4c7a9c530c643ef   :
  unsigned int adcValue = averageRead(BatVolt_pin);  // read the average of the divided battery voltage on analog pin # 0
  unsigned int batVoltage = adcValue * BatRatio / BatResolution;
 
 Serial.print("Batery voltage [mV]: ");  Serial.println(batVoltage);
  byte percentage = 100 * (batVoltage - batVolt_Min) / (batVolt_Max - batVolt_Min);  // to be improved
  Serial.print("Battery level [%]: ");  Serial.println(percentage);
  return percentage;
}


float averageRead(int pin)
{
  unsigned long total = 0;
  for( int i=0; i < nSamples; i++)
  {
    total += (unsigned long) analogRead(pin);
  }
  total += nSamples / 2;   // add half a bit
  return(float(total) / float(nSamples));
}


Now bigger issue is the characteristic of the batteries, which are depending on the discharge rate[C-Rate] & temperature. So this formula to be improved::
byte percentage = 100 * (batVoltage - BatVoltMin) / (BatVoltMax - BatVoltMin);
As improvement,  I have to measure the temperature on the battery (or inside ;) ) and current supply from the battery to the full circuit and the function itself:
f% = f(batVoltage, Tbattery, Crate)
Crate = Isupply [A] / NominalCapacityBattery [Ah]
As a starter I thought this schematics:






johnerrington

Quote
I need the to measure something against the DEFAULT later on.
WHY?

It makes no sense to measure against two different references.
While the "INTERNAL" reference is not too bad, if calibrated, the "DEFAULT" is not reliable at all.

My recommendation would be to use a better EXTERNAL voltage reference as described here
for all your measurements, then you can expect consistency and accuracy.

You can choose an external reference to suit your measurement range and so get better accuracy and precision.
I'm trying to help. If I find your question interesting I'll give you karma. If you find my input useful please give me karma (I need it)

Go Up