Analog Voltage averaging increase resolution / bits / decimal places

ELEGOO Uno R3

So.. I've made a primitive voltmeter using various bits of code
It works..
I have a dc voltage between a0 and gnd
this voltage is under 5v (under 2v actually
It sends it over serial 100 times a second
but because of the 10 bit ADC the resolution is only 5mv
I would like 1mv
I have read that I can do this by averaging the values
This is my first arduino project, I'm so pleased I got this bit to work on my own!

I think if i'm correct all voltages from 0-5v are divided into 4.8mv chuncks

a few things I think I could do to improve the resolution.. is there a way to lower the reference voltage so that all 1024 values are referenced to 2v dc maximum?

And can i somehow add say 10 values together and divide to give me more than 1024 possible values?
I think I would need 4096 or 8192 possible values (12 or 13 bits) to give me 1mv resolution at 0-5v possible less if we can reduce the reference voltage (0-2v I think would be 2048 values equivalent to 11 bits)

I'm currently reading the values with putty and logging them to a csv file that I can both manipulate later in excel and view live with live-graph

Thanks for any help you may be able to offer

FYI I am analysing a Ni-mh Battery being charged and discharged in various chargers

I'm trying to view the method the chargers use to terminate the charge (delta peak etc
Therefore need more than 5mv of resolution

//



int analogInput = 0;
float vout = 0.0000;
float vin = 0.0000;
float R1 = 1.0; // resistance of R1 (100K) -see text!
float R2 = 1.0; // resistance of R2 (10K) - see text!
int value = 0;
void setup() {
  pinMode(analogInput, INPUT);
  Serial.begin(115200); // open the serial port at 9600 bps:
}
void loop() {
  // read the value at analog input
  value = analogRead(analogInput);
  vout = (value * 5.0000) / 1024.00000; // see text
  vin = vout ;
  if (vin < 0.09) {
    vin = 0.0; //statement to quash undesired reading !
  }

  delay(10);
  Serial.print(vin , 3);
  Serial.print(",");
  Serial.println();        // carriage return after the last label

}

Thankyou for any help you can offer me!

Sorry if I have made any mistakes or this is in the wrong place

You have to amplify with an Op-amp. Google non-inverting amplifier. But you cannot go above 2V! Also I would use a z-diode to make sure that you don’t go above 5V on the pin.

So you are trying to get 12 bits from a 10bit AD converter, right?

If you are only doing around 2 volts measurements then why not use an A:D internal reference?
do an internet search on words like "arduino uno internal reference." Gives links like: arduino uno - what is aref(analog reference) - Arduino Stack Exchange, words that can be read and contain info that is relevant to your posted desires. But getting 12 bits from a 10bit A:D, good luck.


Using 5 volts gives 1023/5 steps per volt. Using 3.3V gives 1023/3.3 steps per volt. But still does not give 12 bits of A:D info.

You can easily get 12 bits out of a 10 bit ADC, with some caveats, by using oversampling and averaging.

On the Arduino, add up 64 measurements (as unsigned int), add 8 to round up, then divide by 16 to convert the range 0-1023 to 0-4095. This gives you 1.2 mV resolution per bit. It works as described in the note below on the ATmega328 (Arduino Uno and similar), but of course this severely limits the frequency response.

See this application note from Atmel.

Note: in your posted code, this line

  vout = (value * 5.0000) / 1024.00000; // see text

should be replaced by the following, where Vcc is the accurately measured value of the Arduino Vcc.

  vout = (value * Vcc) / 1023.0; // see text

Or you could use an ESP32.

Idahowalker:
So you are trying to get 12 bits from a 10bit AD converter, right?

If you are only doing around 2 volts measurements then why not use an A:D internal reference?
do an internet search on words like "arduino uno internal reference." Gives links like: arduino uno - what is aref(analog reference) - Arduino Stack Exchange, words that can be read and contain info that is relevant to your posted desires. But getting 12 bits from a 10bit A:D, good luck.


Using 5 volts gives 1023/5 steps per volt. Using 3.3V gives 1023/3.3 steps per volt. But still does not give 12 bits of A:D info.

Thanks so much
This has helped a lot! I have set my bench psu to 2v and used that as the reference on the aref pin
and set analogReference(EXTERNAL)
results much more stable and at least double the resolution
A very good start to improving things Thankyou

//

int analogInput = 0;
float vout = 0.0000;
float vin = 0.0000;
float R1 = 1.0; // resistance of R1 (100K) -see text!
float R2 = 1.0; // resistance of R2 (10K) - see text!
int value = 0;


void setup() {
  pinMode(analogInput, INPUT);
  Serial.begin(9600); // open the serial port at 9600 bps:
  analogReference(EXTERNAL); // use AREF for reference voltage
}
void loop() {
  // read the value at analog input
  value = analogRead(analogInput);
  vout = (value * 2.000) / 1023.00000; // see text
  vin = vout ;
  if (vin < 0.09) {
    vin = 0.0; //statement to quash undesired reading !
  }

  delay(10);
  Serial.print(vin , 3);
  Serial.print(",");
  Serial.println();        // carriage return after the last label

}

jremington:
You can easily get 12 bits out of a 10 bit ADC, with some caveats, by using oversampling and averaging.

On the Arduino, add up 64 measurements (as unsigned int), add 8 to round up, then divide by 16 to convert the range 0-1023 to 0-4095. This gives you 1.2 mV resolution per bit. It works as described in the note below on the ATmega328 (Arduino Uno and similar), but of course this severely limits the frequency response.

See this application note from Atmel.

Note: in your posted code, this line

  vout = (value * 5.0000) / 1024.00000; // see text

should be replaced by the following, where Vcc is the accurately measured value of the Arduino Vcc.

  vout = (value * Vcc) / 1023.0; // see text

Thankyou have corrected these mistakes! Cannot pretend to understand the article but will look into it.. I understand how averaging results can give a higher resolution (at least 1 bit extra is relatively easy) but still don't understand how to implement

still don't understand how to implement

Try this:

unsigned int average = 0;
float Vcc = ?.? ; //put your *measured* value of Vcc here
for (int i=0; i<64; i++) average += analogRead(input_pin); //add 64 readings
average = (average+8)/16; //round up and divide
Serial.println(average);  //raw 12 bit value
Serial.println((average*Vcc)/4095.0); //convert to volts using measured value of Vcc

jremington:
Try this:

unsigned int average = 0;

float Vcc = ?.? ; //put your measured value of Vcc here
for (int i=0; i<64; i++) average += analogRead(input_pin); //add 64 readings
average = (average+8)/16; //round up and divide
Serial.println(average);  //raw 12 bit value
Serial.println((average*Vcc)/4095.0); //convert to volts using measured value of Vcc

Thankyou will try this out later today

jremington:
Try this:

unsigned int average = 0;

float Vcc = ?.? ; //put your measured value of Vcc here
for (int i=0; i<64; i++) average += analogRead(input_pin); //add 64 readings
average = (average+8)/16; //round up and divide
Serial.println(average);  //raw 12 bit value
Serial.println((average*Vcc)/4095.0); //convert to volts using measured value of Vcc

So I have used this code in a new file with some of my code
Its seems to work however the first reading is correct 1.46volts
Subsequent readings are high by about 0.1 volts
see my code..
I'm now using a regulated bench supply for the reference voltage at 2.5 volts (set on the psu and tested with a DMM)
I have set reference to external and added a delay

int analogInput = 0;
unsigned int average = 0;
float vref = 2.5 ; //put your *measured* value of Vcc here

void setup() {
  // put your setup code here, to run once:
  pinMode(analogInput, INPUT);
  Serial.begin(9600); // open the serial port at 9600 bps:
  analogReference(EXTERNAL); // use AREF for reference voltage
}

void loop() {
  // put your main code here, to run repeatedly:

  delay(10);
  for (int i = 0; i < 64; i++) average += analogRead(analogInput); //add 64 readings
  average = (average + 8) / 16; //round up and divide
  //Serial.println(average);  //raw 12 bit value
  Serial.println((average * vref) / 4095.0 , 4); //convert to volts using measured value of Vcc

}

Thanks for your help with this!

Every pass through loop(), you must set average to zero, before you accumulate readings.

If you want to try something else you can look towards the ADS1115 A/D used along with your Arduino. Simple, inexpensive and affords easily programmable gain. Just make sure you look to a reputable source as there are plenty of counterfeit modules floating around. Again, just something different to consider depending on exactly what you want or need.

Ron

@jremington

Note: in your posted code, this line
Code: [Select]

vout = (value * 5.0000) / 1024.00000; // see text

should be replaced by the following, where Vcc is the accurately measured value of the Arduino Vcc.
Code: [Select]

vout = (value * Vcc) / 1023.0; // see text

I thought we had agreed the correct conversion was

vout = ((value +0.5) * Vcc) / 1024.0;

I have a page here with an excel spreadsheet showing the process;

however lets simplify; if you take a 1 bit converter with a 4V range your reading n can be 0 or 1.

the measurement then is

v = (n +0.5) * 4/2 giving v= ( 0+0.5) *2 = 1V or v=(1+0.5) *2 = 3V.

those values lie in the centre of the quantisation intervals
and most closely represent the "true" input voltage of 0 -1.9999* and 2.0 -4.0
where 1.999* =2.0

whereas taking v = n*4/1 gives values of 0V and 4V

jremington:
Every pass through loop(), you must set average to zero, before you accumulate readings.

SO... I have changed to uint32_t otherwise i get a random negative number
This seems to work
Also added a time stamp with an rtc module

#include <Wire.h>
#include "RTClib.h"

RTC_DS1307 rtc;

//int ain = analogRead(A0);
int analogInput = 0;
//int average = 0;

//uint32_t extraBits = 0;  // use an unsigned integer or the bit shifting goes wonky
//unsigned int average = 0;
//unsigned int average2 = 0;
float vref = 2.5 ; //put your *measured* value of Vcc here
//int value[100];   // variable to store the value coming from the sensor



void setup() {
  // put your setup code here, to run once:
  pinMode(analogInput, INPUT);
  Serial.begin(9600); // open the serial port at 9600 bps:
  analogReference(EXTERNAL); // use AREF for reference voltage

  while (!Serial); // for Leonardo/Micro/Zero

  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }
  if (! rtc.isrunning()) {
    Serial.println("RTC is NOT running!");
    // following line sets the RTC to the date & time this sketch was compiled
    // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    // This line sets the RTC with an explicit date & time, for example to set
    // January 21, 2014 at 3am you would call:
    // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));


  }
}

void loop () {

  delay(500);
  DateTime now = rtc.now();
  Serial.print(now.hour(), DEC);
  Serial.print(':');
  Serial.print(now.minute(), DEC);
  Serial.print(':');
  Serial.print(now.second(), DEC);
  Serial.print(",");

  uint32_t average = 0;
  for (uint32_t i = 0; i < 64; i++) average += analogRead(A0); //add 64 readings
  average = (average + 8) / 16; //round up and divide
  Serial.print((average * vref) / 4095 , 4); //convert to volts using measured value of Vcc
  Serial.print(",");
  Serial.println();

}

Thanks!

@docustompc

JRemington is right about oversampling. I have an explanation here;

however you can only improve results by oversampling to a very limited extent, and results assume you have the right amount of truly random noise in the measurement chain.

Also dont get confused between precision and accuracy.

For measurements accurate to 1 part in 4096 you will need a reference voltage accurate to 0.025%;

you will also need an ADC that is linear, monotonic and has less than +_1 LSB offset error.

However I believe for your stated application the more important factor is precision, so oversampling will likely work as your signal should contain enough noise. (unless its decoupled).

I thought we had agreed the correct conversion was

No, not at all.

I'll stick with the basic understanding of how the ADC actually works, and how best to interpret the result, which is not changed by subsequent manipulations.

I have changed to uint32_t otherwise i get a random negative number

Then something else was wrong. You can't possibly get a negative number using unsigned arithmetic.

Also, summing 64 10 bit values (0 to 1023) won't ever overflow an unsigned, 16 bit int. But it is fine to declare average as type uint32_t.

Ron_Blain:
If you want to try something else you can look towards the ADS1115 A/D used along with your Arduino. Simple, inexpensive and affords easily programmable gain. Just make sure you look to a reputable source as there are plenty of counterfeit modules floating around. Again, just something different to consider depending on exactly what you want or need.

Ron

I have ordered a 16 bit 4 channel adc board that does 2 channel differential as well that will be useful for other projects
And an sd card module so i can record direct to a card and not serial

johnerrington:
@docustompc

JRemington is right about oversampling. I have an explanation here;

however you can only improve results by oversampling to a very limited extent, and results assume you have the right amount of truly random noise in the measurement chain.

Also dont get confused between precision and accuracy.

For measurements accurate to 1 part in 4096 you will need a reference voltage accurate to 0.025%;

you will also need an ADC that is linear, monotonic and has less than +_1 LSB offset error.

However I believe for your stated application the more important factor is precision, so oversampling will likely work as your signal should contain enough noise. (unless its decoupled).

There is a lot of noise! none of the cables are shielded.. but the bench psu is very stable especially with no load!
The results are much better
I realise they may not be 'truly' 12 bits but its good enough
And I agree they do not need to be completely accurate - relative voltages
relatively trying to visualize the delta peak drop (or lack of) when battery is fully charged (a voltage drop of anywhere between 0mv and 20mv)
I have already discovered a charger that massively overcharges, and 2 that are ok
next stage will be to fit a very low resistance shunt to measure the current simultaneously
Not sure how that will affect the charging of the batteries but I'm guessing lower resistance shunt will mean less impact on the charge but more difficult to measure
I will do that when I have the 16 bit ADC board which should make it easier
and maybe use a lower reference voltage and split the battery voltage with a divider so the current and battery voltage are similar ? may create too much noise on the battery voltage though
May just stick to 10 bits for current as it isn't as critical and just use the 16 bit board for voltage
I will also add thermistor to measure battery temperature during charge
Thanks for all the help everyone - much appreciated please see current code that is working it's not perfect but it matches the scope and dmm
Also added RTC

#include <Wire.h>
#include "RTClib.h"

RTC_DS1307 rtc;

//int ain = analogRead(A0);
int analogInput = 0;
//int average = 0;

//uint32_t extraBits = 0;  // use an unsigned integer or the bit shifting goes wonky
//unsigned int average = 0;
//unsigned int average2 = 0;
float vref = 2.5 ; //put your *measured* value of Vcc here
//int value[100];   // variable to store the value coming from the sensor



void setup() {
  // put your setup code here, to run once:
  pinMode(analogInput, INPUT);
  Serial.begin(9600); // open the serial port at 9600 bps:
  analogReference(EXTERNAL); // use AREF for reference voltage

  while (!Serial); // for Leonardo/Micro/Zero

  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }
  if (! rtc.isrunning()) {
    Serial.println("RTC is NOT running!");
    // following line sets the RTC to the date & time this sketch was compiled
    // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    // This line sets the RTC with an explicit date & time, for example to set
    // January 21, 2014 at 3am you would call:
    // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));


  }
}

void loop () {

  delay(20);
  DateTime now = rtc.now();
  Serial.print(now.hour(), DEC);
  Serial.print(':');
  Serial.print(now.minute(), DEC);
  Serial.print(':');
  Serial.print(now.second(), DEC);
  Serial.print(",");

  uint32_t average = 0;
  for (uint32_t i = 0; i < 64; i++) average += analogRead(A0); //add 64 readings
  average = (average + 8) / 16; //round up and divide
  Serial.print((average * vref) / 4095 , 4); //convert to volts using measured value of Vcc
  Serial.print(",");
  Serial.println();

}

It seems I cannot upload images yet.. I wanted to share the first graph I'm happy with to describe that charge discharge charge voltages of a single ni-mh cell