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.
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);
}
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:
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);
}
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.
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.
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.
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.
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.
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:
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;
}
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
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.
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.
Thank You for the guidance for septilion and pointing out the major discrepancies. The modified script:
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:
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.
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.
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?
Why do you keep changing the analog reference?
Do you NEED to measure something against the DEFAULT?
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.
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];
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 !
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:
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.
The ACS712 requires indeed the DEFAULT voltage reference of 5V, because it does not output a voltage, it outputs a certain fraction of the 5V.
steger, those ACS712 modules are very inaccurate. Assume they are 20% inaccurate as a start. Maybe it can be improved later.
Thanks @Koepel, I didnt spot that.
In which case would it be sensible to calibrate the default against the internal reference using the "magic voltmeter" and do everything with the default reference - assuming Vcc is stable?
johnerrington:
In which case would it be sensible to calibrate the default against the internal reference using the "magic voltmeter" and do everything with the default reference - assuming Vcc is stable?
That is possible, but changing the reference voltage is made for this situation. Since there is a 100nF at AREF, some time is needed and the first ADC value might not be valid.
The 5V is probably not really stable. It will be less stable than the internal reference.
With a Arduino Mega 2560 board + Ethernet shield I switch the reference voltage all the time, and I also run the "magic voltmeter" all the time. Returning to normal after the "magic voltmeter" took more effort. I had to increase the delay to 20ms and do more than one dummy analogRead(). I made a mistake in my project. I put everything on a prototype shield and now the current to the Mega+Ethernet causes a offset to my voltage measurements. That is because R2 of the voltage divider is connected to the GND on the prototype shield. The offset is only 6mV, but it is noticeable.
@steger Again, did you notice the easy calibrate option
float averageRead(int pin)
Why is that float so damn tempting Keep it integer
#define BatVolt_pin = 0; //Anlaoge pin # A0
What's wrong with a type ave integer!?
I would still wait a bit for the ref to settle after changing it. Now you include the settling into the averaging.
byte BattVoltage() {
[...]
return percentage;
Seems rather confusing to me
Also, try
to
keep
the
indentation
in check
And about percentage. Yeah, you can take temperature and current all in consideration, but do you really need to now it that accurate in that many situation? I can tell you, most consumer electronics does not bother. And are you actually downing that much current? And do you expect it to set in the freezing cold?
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
const byte BatVoltPin = A0; //Analog pin # A0
const unsigned int NrSamples = 1000; // Usually from 5 to 10000
byte batteryPercentage() {
analogReference(INTERNAL2V56); // set the reference voltage level from 5V to 2.56V
delay(2); //I would still wait for it to settle in
// 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(BatVoltPin); // read the average of the divided battery voltage on analog pin # 0
analogReference(DEFAULT); //switch back for later use
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 [%]: "); //<= statement ends, next line please!
Serial.println(percentage);
return percentage;
}
unsigned int averageRead(byte pin){
unsigned long total = 0;
for(unsigned int i = 0; i < NrSamples; i++)
{
total += analogRead(pin);
}
total += nSamples / 2; // add half a bit
return (total / NrSamples);
}
I prefer calculations in the source code with numbers that are in real units (liters, meters, volt, ampere, kPa, kg, and so on).
With float calculations there is no trouble with overflow or divisions, it is just as fast (maybe 50% slower, that's peanuts) and most important: the source code is more readable.
Using the 10-bit ADC value for a calculation with 32-bit single precision float is just fine. Nothing bad will happen.
septillion, my averageRead() function is supposed to return a float. That is the whole point of that function. It uses noise to get a little more information than the 10-bit from the ADC.
Don't sink, stay float
steger, when Arduino users disagree and both solutions are good, then you can pick what seems best for you