Arduino nano A2D

Subject: arduino nano A2D accuracy

Implementation:

I have a battery of 8 X 4.2V Li-ion cells wired in series

Each cell junction is connected to a potential divider the output of which drives an analogue input pin of an arduino nano

To compensate for resistor and other errors the unit is initially calibrated by providing a reference voltage to each input so that a volts per bit can be calculated and stored in EEPROM for use in later calculations. The calibrated A2D reference voltage is also stored. The reference voltage is provided in increments of 4.0V so:
4, 8, 12, 16, 20, 24, 28, 32

The run time code is currently very crude:

correctedVoltageArray[0] = (rawBinaryArray[0] * voltsPerBitArray[0]); // Do this only once

for(j=1; j <= 7; j++) // Do this 7 times
{
correctedVoltageArray[j] = ((rawBinaryArray[j] * voltsPerBitArray[j]) - (rawBinaryArray[j - 1] * voltsPerBitArray[j - 1])); // Calculate individual cell voltages
}

OK. Yes it works but if there is a 1 bit error in the top voltage reading then this will result in a 1 X voltsPerBitArray[7] final error.

Explanation:

Lets call the cell tapping voltages A, B, C, D, E, F, G, H
Lets assume each tap is 4.0V
They increase by 4 at each tap:
A = 4
B = 8
C = 12
D = 16
E = 20
F = 24
G = 28
H = 32

Using the formula (cell voltage CV = (this cell voltage - lower cell voltage)) cell H = H - G result CV = 4
If H has a +1 bit error where the volts per bit is Z then the result is CV = (4 + Z)

This is 'real' calculated data as an example:
Cell voltage 1 4.187773 Raw = 959 V/B = 0.004367
Cell voltage 2 4.195252 Raw = 963 V/B = 0.008705
Cell voltage 3 4.177235 Raw = 964 V/B = 0.013029
Cell voltage 4 4.211671 Raw = 956 V/B = 0.017544
Cell voltage 5 4.214136 Raw = 979 V/B = 0.021436
Cell voltage 6 4.220350 Raw = 982 V/B = 0.025668
Cell voltage 7 4.208635 Raw = 977 V/B = 0.030108
Cell voltage 8 4.225077 Raw = 964 V/B = 0.034896

The above cell voltages when checked with a DVM are all 4.21V

I would like to try a different approach:

CV = (thisVoltage + sum(lowerVoltages)) / cellFactor;

So:

resultA = (A)/1;
resultB = (B + A)/3;
resultC = (C + B + A)/6;
resultD = (D + C + B + A)/10;
resultE = (E + D + C + B + A)/15;
resultF = (F + E + D + C + B + A)/21;
resultF = (G + F + E + D + C + B + A)/28;
resultF = (H + G + F + E + D + C + B + A)/36;

Limitations:
I realise that this will not eliminate errors but it may give me results nearer to true values. In any event I am keen to see what the effect will be.

Problem:
I don't know how to turn the above into an algorithmic expression. Yes I could have a series of calculations but it should be possible to have only one.

Questions:

  1. Am I going off the rails here?
  2. Is there a better approach?
  3. Can anyone help to write the for loop for the above?

Finally - thank you if you get this far.

Brian

BrianDrury:
Subject: arduino nano A2D accuracy

CV = (thisVoltage + sum(lowerVoltages)) / cellFactor;

So:
resultA = (A)/1;
resultB = (B + A)/3;
resultC = (C + B + A)/6;
resultD = (D + C + B + A)/10;
resultE = (E + D + C + B + A)/15;
resultF = (F + E + D + C + B + A)/21;
resultF = (G + F + E + D + C + B + A)/28;
resultF = (H + G + F + E + D + C + B + A)/36;

  1. Can anyone help to write the for loop for the above?

If donno the answer to questions 1 and 2 but for qn 3 you could implement the following loop:

uint8_t cellfactor[8] = {1,3,6,10,15,21,28,36};
float CV;

CV = thisvoltage; //thisvoltage is your converted AD input I'm assuming

for(uint8_t i=1;i<8++i){

   CV = (thisvoltage +(CV*cellfactor[i-1]))/cellfactor[i];

}

Do I understand that you have 8 potential dividers into 8 analog inputs?

The highest voltage divider is taking a nominal 32V and dividing this to around 4v? to the analog input ?

On the lowest end (4V) you should see, say, 5V/1024 = 0.004*V/bit
At the upper end (32V) you should see, say, 40V/1024 = 0.039V/bit

Your most accurate determination of the voltage of any cell will be (this cell voltage - lower cell voltage), the worst accuracy being at the topmost cell (no better than around +-0.08V).

Your reading for: Cell voltage 1 4.187773 Raw = 959 V/B = 0.004367 is off by 0.022227V which, theoretically, is around 5 bits. ?

Are you actually calculating "Volts Per Bit" and storing that as a calibration value?
If so, you may well be running into accumulated floating point errors.

If you present 4V, take several A-D readings (to check how much noise is present) then store the A-D reading as Cal1. When you take a real reading, calculate the voltage like:

voltage= (reading1 * 4.0f) / Cal1; // all variables as double (float)

It would help a lot if you posted a circuit diagram and all your current code.

Yours,
TonyWilk

Hi,
Welcome to the forum.

Please read the first post in any forum entitled how to use this forum.
http://forum.arduino.cc/index.php/topic,148850.0.html then look down to item #7 about how to post your code.
It will be formatted in a scrolling window that makes it easier to read.

Can you please post a copy of your circuit, in CAD or a picture of a hand drawn circuit in jpg, png?

Thanks.. Tom.. :slight_smile:

Hi all

TonyWilk:
Do I understand that you have 8 potential dividers into 8 analog inputs?

*** BD Yes exactly that

The highest voltage divider is taking a nominal 32V and dividing this to around 4v? to the analog input ?

*** BD Yes

On the lowest end (4V) you should see, say, 5V/1024 = 0.004*V/bit
At the upper end (32V) you should see, say, 40V/1024 = 0.039V/bit

*** BD Yes, yes

Your most accurate determination of the voltage of any cell will be (this cell voltage - lower cell voltage), the worst accuracy being at the topmost cell (no better than around +-0.08V).

Your reading for: Cell voltage 1 4.187773 Raw = 959 V/B = 0.004367 is off by 0.022227V which, theoretically, is around 5 bits. ?

Are you actually calculating "Volts Per Bit" and storing that as a calibration value?

** BD Yes

If so, you may well be running into accumulated floating point errors.

If you present 4V, take several A-D readings (to check how much noise is present) then store the A-D reading as Cal1. When you take a real reading, calculate the voltage like:

voltage= (reading1 * 4.0f) / Cal1; // all variables as double (float)

It would help a lot if you posted a circuit diagram and all your current code.

Yours,
TonyWilk

Thank you for the various replies. I have attached the schematic and code as requested.

The forum rejected my post because it was too long so here is a link to the code:
https://1drv.ms/u/s!AtcxErupGaCfgas-o4T8FXgyFFawpQ

Please be aware that the code is very much work in progress. I know a lot of improvements are required.

It may be useful to know why I am doing this.

Hi,
Thanks for the nice schematic.

Tom... :slight_smile:

Hi,
You can now see the reason for the "flying capacitor".

You have the same resolution for each cell,.
You are measuring each cell directly, so no accumulated errors are incurred.

Another way would be to use an AtoD with higher resolution say 16bit.

Tom.. :slight_smile:

Hi,
Why do you have 3 x 9R1 resistors between the arduino GND and the BATT neg?

It won't be much of a problem if you are measuring the battery while not charging, but there will be a volt drop when you begin to charge the battery.

Tom.. :slight_smile:

Current sense for the TPS55340

Thank you gents

@ sherzaad

This is what I think the long hand solution should look like.
I don't think your suggestion will achieve that.

float CV[8];
float resultArray[8];
uint8_t cellfactor[] = {1,3,6,10,15,21,28,36};

resultArray[0] = (CV[0] / cellFactor[0]);
resultArray[1] = (CV[0] + CV[1] / cellFactor[1]);
resultArray[2] = (CV[0] + CV[1] + CV[2] / cellFactor[2]);
resultArray[3] = (CV[0] + CV[1] + CV[2] + CV[3] / cellFactor[3]);
resultArray[4] = (CV[0] + CV[1] + CV[2] + CV[3] + CV[4] / cellFactor[4]);
resultArray[5] = (CV[0] + CV[1] + CV[2] + CV[3] + CV[4] + CV[5] / cellFactor[5]);
resultArray[6] = (CV[0] + CV[1] + CV[2] + CV[3] + CV[4] + CV[5] + CV[6] / cellFactor[6]);
resultArray[7] = (CV[0] + CV[1] + CV[2] + CV[3] + CV[4] + CV[5] + CV[6] + CV[7] / cellFactor[7]);

@ TonyWilk

Your suggestion looks very good to me. I shall report back on the outcome.

voltage= (reading1 * 4.0f) / Cal1; // all variables as double (float)

@ TomGeorge

I switch off the charge current and wait a bit prior to making a set of voltage measurements
Yes, I did look at flying cap SH but decided to give this approach a try.

Brian

Tony Wilk's suggestion improves things considerably:

This unit is Calibrated
Cell voltage 1 4.162459 Raw = 948.00 callibration = 911.000000
Cell voltage 2 4.161926 Raw = 951.00 callibration = 914.000000
Cell voltage 3 4.161572 Raw = 953.00 callibration = 916.000000
Cell voltage 4 4.162996 Raw = 945.00 callibration = 908.000000
Cell voltage 5 4.172414 Raw = 968.00 callibration = 928.000000
Cell voltage 6 4.172043 Raw = 970.00 callibration = 930.000000
Cell voltage 7 4.172786 Raw = 966.00 callibration = 926.000000
Cell voltage 8 4.175438 Raw = 952.00 callibration = 912.000000

Thanks Tony

Hi,
Yes that looks really good for 4,21V (4.21 - 4.16)/ 4.21 x 100 = 0.011 x 100 == 1.1% at worst.

A great improvement.

Now its worth doing some experimental runs with different battery levels to see how well it tracks from flat to fully charged.

Tom... :slight_smile:

BrianDrury:
Tony Wilk's suggestion improves things considerably:

This unit is Calibrated
Cell voltage 1 4.162459 Raw = 948.00 callibration = 911.000000
snip

Had a look at the code and cct. diagram... it appears you are using the internal 1.1V reference for the ADC, which is good and bad:
Pros: You have a stable voltage reference, hopefully temperature compensated.
Cons: at 1v1 reference, each bit is only 1.07mV - a tiny voltage.

You can reduce noise interference and improve your readings further by:

  1. Fit, say, 0.1uf capacitor from each analog input to ground (this is an easy fix, do this anyway)
  2. Supply an external voltage reference of around 4V like a MCP1541T-I/TT
  3. Do some software filtering

I had a look at the analog inputs on the Arduino Nano I have, just a simple loop showing values from a potential divider on the 5V rail using the internal reference. I also added a filter function to see the effect.

Since you have fairly steady-state inputs, you can probably afford to filter the analog values over quite a long time (several seconds).

The result after running for a bit were:

Raw ADC values, min,max and deviation max-min, same for filtered values:

a0= 841 (min= 837, max=842, dev=5), filteredA0= 840.38 (min=839.31, max= 840.80, dev= 1.49)

Removing the 0.1uF capacitor I had added, the same program reported:

a0= 841 (min= 835, max=843, dev=8), filteredA0= 840.82 (min=839.52, max= 841.53, dev= 2.01)

Note that this is just a simple test... measuring the 5v rail running from my PC's USB, however, extrapolating to your case of calibrating A0 to 4v, then the filtered (with capacitor) value above equates to an error of approx 0.006V, the worst unfiltered (no cap.) error of 0.035V

The code is:

// analogTest. TonyWilk

void Sprint( char *fmt, ... );      //Serial.print "printf"

                                    // Exponential Moving Average filter...
#define FILTER_TIME_CONST   6.0     // time constant: time in samples to reach ~ 63% of a steady value from zero
                                    // the rise time of a signal to > 90% is approx 2.5 times this value
                                    // time to >99% is approx 30 times this value
                                    // For a value of 6, we want c. 50 samples on startup
                                    // small deviations (e.g. 5%) settle to >99.5% in approx 3 times this value    
#define FILTER_WEIGHT (FILTER_TIME_CONST/(FILTER_TIME_CONST +1.0))

// add new value to filtered value
// - returns new filtered value
//
double filter( double filteredValue, int newValue )
{
  return (FILTER_WEIGHT * filteredValue) + (1.0-FILTER_WEIGHT) * (double)newValue;  
}

// On startup read analog to get filtered starting value
//
double initial_read( int pin )
{
  int i,samples;
  double aval;
    analogRead( pin );                     // throwaway 1st convert
    aval= (double)analogRead(pin);         // prime filtered value (should be within +-4bits)
    samples= (int)FILTER_TIME_CONST * 30;  // do enough samples for filter to settle

    Sprint( "initial_read() taking %d samples...\n", samples );
    for( i=0; i<samples; i++ )             
    {
      delay(2);
      aval= filter( aval, analogRead(pin) );
    }
    return aval;
}

double filteredA0;

void setup() {
  Serial.begin(9600);
  pinMode(A0, INPUT);
  analogReference( INTERNAL );
  filteredA0= initial_read(A0);
}


void loop() {
  static double fa0min=1024.0, fa0max=0.0;
  static int a0, a0min=1024, a0max=0;

  a0= analogRead(A0);
  filteredA0= filter( filteredA0, a0 );

  if( a0 > a0max )      // min/max unfiltered
    a0max= a0;
  if( a0 < a0min )
    a0min= a0;

  if( filteredA0 > fa0max )   // min/max filtered
    fa0max= filteredA0;
  if( filteredA0 < fa0min )
    fa0min= filteredA0;

  Sprint("a0= %d (min= %d, max=%d, dev=%d), filteredA0= %f (min=%f, max= %f, dev= %f)\n", 
          a0, a0min, a0max, a0max-a0min, filteredA0, fa0min, fa0max, fa0max-fa0min);

  delay( 500 );
}

//-----------------------------
// Serial.print helper function
// - a real cut-down printf()
// TonyWilk
//
void Sprint( char *fmt, ... )
{
  char c;
  va_list args;
  va_start( args, fmt );
  while( (c=*fmt) != 0 ){
    switch( c )
    {
    case '%':
      c= *(++fmt);
      switch( c )
      {
      case 'd': Serial.print( va_arg(args,int) ); break;
      case 'f': Serial.print( va_arg(args,double) ); break;
      case 'h': Serial.print( va_arg(args,int), HEX ); break;
      case 'c': Serial.print( (char)va_arg(args,int) ); break;
      case 's': Serial.print( va_arg(args, char *) ); break;
      default:  break;
      }
      break;
    case '\\':
      c= *(++fmt);
      if( c == 'n' )
        Serial.println();
      else
        Serial.print( c );
      break; 
    default:
      Serial.print( c );
      break;
    }
    ++fmt;
  }
  va_end( args );
}

(also as attachment)

Yours,
TonyWilk

analogTest.ino (3.05 KB)

Thank you Tony for such a detailed response.

I plan to try all that you have suggested. The caps were easy and are now fitted.

In addition, it is now obvious that my calibration reference generator (4V, 8V, 12V, 16V, 20V, 24V, 32V) requires some improvement. My problem is that none of my 4 DMM's can resolve less than 10mV changes above 4V. My best bet may be a re-build and include precision potential dividers to ensure all the DMM measurement to be in the 0V - 4V range.

If I re-build the ref circuit do you think it would be better to put the voltage at the A2D mid point? So for the 4V use 2V, 8V use 4V etc.

Just now I am running some charge/discharge tests to get a ball park idea of how well my cell equaliser circuit works. Previously the measurements were so inaccurate there was no point.

Finally, my auto calibration code (see below calibrated = false;) has the option to use the previous calibration or not. Do you think it should be done with a clean slate?

void autoCalibrate()
{
  byte j = 0;
  int voltage1 = 0;
  
  // Here to auto calibrate analogue measurement
  // auto calibration requires the calibration harness to be connected to the analogue inputs
  // Eight calibration factors will be calculated and stored in EEPROM
  // The calibration data uses floats and these are 4 bytes each
  
  // Auto calibration is required because the potential dividers used to monitor the cells
  // are only 1% tolerance and they are preferred value rather than the precise value
  // required to provide exact voltage measurement.
  
  // To overcome this problem a calibration process using a precision multi-tapped voltage
  // source is connected to the nano PCB and each of the 8 voltages are read.
  // The calculated volts per bit for each analogue input is stored in EEPROM as a float
  
  // This process does not attempt to correct any offset error
  
  // The calibration data = true voltage / measured 10 bit voltage
  // The corrected voltage = voltage measured * correction data (volts per bit)
  //

//  calibrated = false; // Not sure if we should do this! Needs more thought

  readBattery(); // Make the eight corrected measurements into correctedVoltageArray[]

BrianDrury:
In addition, it is now obvious that my calibration reference generator (4V, 8V, 12V, 16V, 20V, 24V, 32V) requires some improvement. My problem is that none of my 4 DMM's can resolve less than 10mV changes above 4V. My best bet may be a re-build and include precision potential dividers to ensure all the DMM measurement to be in the 0V - 4V range.

Is your reference generator 8 separate potential dividers all running from ~ 32V ?
You still have to measure this with your DMM, although measuring these levels to 10mV should be accurate enough. Without precision resistors, just put trimmers in. If e.g. the 16V reference is, say, two 4k7 resistors, remake that as 4k7+200ohm, 4k7+470ohm trimmer pot.
If you can measure each reference level to around 10mV that should be fine.
Oh, and do this while the reference ladder is connected to the A-D input dividers!

If I re-build the ref circuit do you think it would be better to put the voltage at the A2D mid point? So for the 4V use 2V, 8V use 4V etc.

It depends how much 'over voltage' you expect when charging. Ideally you want a 4.096V reference for the Arduino's ADC then arrange the potential dividers such that "Max Voltage Ever" is divided to 4V on an analog input. This means you get the best use of A-D range at the highest "volts per bit" to try and avoid the effects of noise.

Finally, my auto calibration code (see below calibrated = false;) has the option to use the previous calibration or not. Do you think it should be done with a clean slate?

Don't use the previous calibration. Present your (hopefully tweaked to be accurate) voltage sources, read a lot of samples and filter them. Save the resulting 'filtered ADC output value' as your calibration value.
( like in the initial_read( int pin ) function in my previous example code. )

Yours,
TonyWilk

TonyWilk:
Without precision resistors, just put trimmers in. If e.g. the 16V reference is, say, two 4k7 resistors, remake that as 4k7+200ohm, 4k7+470ohm trimmer pot.

Good idea, and if you use 10 turn trimpots you will have good adjustment control.
Tom..

I should have included a schematic of the reference generator. Now attached.

It is not exactly up to date but near enough to show the idea.

The ten turn pots are a good idea.

Lots to do now!

Brian

While we are off topic I wonder if you guys have any suggestions regarding another problem.

I have attached an updated charger schematic. If you look at top right R1, R2, D10 are supposed to provide over voltage protection but this does not work as I had intended.

The idea is that if the constant current generator is enabled by the arduino nano when the battery is not connected the output through D9 will not be limited and may cause problems. Potential divider R1 R2 is supposed to forward bias D10 if the output from D9 rises above about 35V. Now the code is written to ensure this cannot happen but I crashed the arduino a few times and yes it can happen.

The problem is that the forward voltage drop in D10 is extremely low at very low operating current and it interferes with the feedback to U2 pin 9.

I tried lowering R28 and generally messed around with it for a while but concluded this is basically not going to work.

If you have suggestions for a different approach I am all ears.

Brian

Hi,
From what I can see TPS pos to R1, R2, to gnd is a potential divider,.

D10 will conduct the divided potential to the FB pin of the TPS.

At a certain output voltage the feedback from D10 will pull the FB pin high enough to make the TPS lower its output till it shuts off.

I hope that explains it.

Tom.. :slight_smile:

Yes, that is how I intended it to work but at such low current D10 'leaks' the R1/R2 voltage into U2 FB and reduces the constant current.

The problem is the Vf/If characteristic of D10 is not a flat line.

The circuit would work if D10 had a fixed Vf but as it is not a perfect diode the idea is flawed.

It needs a different approach I think.

Brian