Arduino Uno 3..3V as voltage reference: help needed f a collaborative experiment

Playing with the Uno I noticed there is a 3.3V pin. I suppose its intended to provide power for external 3.3V devices; but I wondered if it could be used as a voltage reference. So I wrote this simple sketch to check the idea.

How it works:

The 3v3 output is connected to A0 with a jumper.

The sketch measures Vcc using the "secret voltmeter", then using the accurate value of Vcc as a reference measures the 3.3V output. ( Vcc and V3v3 are calculated froma set of 16 measurements and the results averaged to reduce noise.)

These readings are repeated 256 times. To get statistics readings are stored in an array and limited ram space only allows 256 values.

The results are displayed as you can see in the image below.

I'd be grateful if you would repeat my experiment and show your results in a reply;
ALL YOU NEED IS AN ARDUINO UNO AND A JUMPER.

For consistency I've used "termite" as my serial monitor, you can get it here

  • its a portable app so runs without needing to be installed; of course you could just use the IDE's serial monitor, but if you try termite I think you will like it as much as I do.
    You can right click inside the app to save the results.

secretvoltmeterplus3v3_v2.ino (4.43 KB)

Actually it looks good however there is no reference to compare against. You are using the 5V of the arduino as the reference for the A/D and the output of the 3.3V regulator which is powered from the 5V regulator. The 3V3 and 5V pins will supply a small amount of current (< 50mA for the 3v3) but best treat them as a reference. You would be better off using a known accurate volt meter at least 10X accuracy then what you want then do your measurements.

Which Arduino we're talking about.
The Uno/Mega has a fully independent 5-pin 3.3volt/150mA (not 50mA) regulator, powered from the 5volt rail.
That supply is not used by anything else on the board, and should be very stable if not used for anything else.
Older big-boards could have the 3.3volt extracted from the USB chip (the uncorrected 50mA rating), and so do most smaller 5volt boards (Nano). Stability could depend on sleeping/active USB.
3.3volt boards (Due, and most newer boards) could have a very 'dirty' 3.3volt supply.

Read the datasheet notes about the dangers of using an external Aref.
Must switch to external if you do.
A resistor between 3.3volt and Aref could make this 'safer', and give you an option to lower (adjust) Aref as well.
Leo..

@gilshultz Thanks for contributing; if you looked at the sketch (code below) you would see that Vcc is first measured against the INTERNAL 1.1V "reference" on the Uno; then knowing Vcc we can use it as a reference to measure V3v3.

Thanks Leo; I've just edited to make it clear this experiment is just for the Uno.

Yes, it says if the external Vref pin is used the "EXTERNAL" reference must be chosen, or another reference will be shorted to it. I havent tried THAT experiment but I dont see how you can do that before you make the connection! However a decent value R (say 10k) would protect it.

Hadnt realized this could be tried on the nano too but I cant get mine to program just now so I'll try it later.

Any chance you could give it a try Leo to confirm results? I've posted the code here.

/*
   Sketch to measure the 3.3V level on an arduino uno for possible use as a voltage reference.
   Requires only 1 jumper from the 3v3 pin to A0
   Author J Errington September 2020
*/
const int nsamples = 16;  //number of samples to aggregate to reduce noise
int Vcc;  //our measured value of Vcc against the internal reference
int V3v3;  //our measured value of 3v3 against Vcc
const int analogPin = A0;
const long cal = 1115702L;  //calibration constant for particular board - just leave as set.

//statistics
const int count = 256; //number of samples to take to get the stats
int x[count];
float sum, mean, squareval, sumsqs, variance, stDev, stErr;
int max = 0, min = 5000;

//function prototypes
long readVcc();  //measure Vcc using 1.1V reference and "secret voltmeter" trick
long readV3v3(long vcc);  //measure V3v3 by comparison with nown value of Vcc

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
  sum = 0; //we use this to find the mean
  //  *** take a set of measurements ***
  for (int i = 0 ; i < count; i++) {
    //readVcc() reads the current value of Vcc by comparison with the internal 1.1V reference
    V3v3 = readV3v3(readVcc()); //read the value of V3v3 using the known value of Vcc (DEFAULT) as a reference
    x[i] = V3v3; //fill array with readings
    sum += V3v3; //aggregate the sum of readings to find the mean;
    if (V3v3 > max) max = V3v3;
    if (V3v3 < min) min = V3v3;
  }
  // *** calculate the mean  ***
  mean = sum / count;  // sum needs to be a float to get a correct float value for the mean.
    Serial.print("3V3 mean is ");
    Serial.print(mean);
    Serial.print(" mV");
    Serial.print("; max is ");
    Serial.print(max);
    Serial.print("; min is ");
    Serial.println(min);
    
  // *** now calculate variance and standard deviation from the mean ***
  sumsqs = 0;
  for (int i = 0 ; i < count; i++) {
    x[i] = x[i] - mean; //find the differences
    squareval = x[i] * x[i];
    sumsqs += squareval; //add them up
  }
  variance = sumsqs / (count-1);
  Serial.print("3V3: variance is ");
  Serial.print(variance);
  Serial.print(" ");
  stDev = sqrt(variance);
  Serial.print("  stDev is ");
  Serial.print(stDev);
  stErr = stDev/sqrt(count);
  Serial.print("mV;   stError is ");
  Serial.print(stErr);
  Serial.println(" mV");
  Serial.println();
}

long readV3v3(long vcc) {
  long result, val = 0;
  analogReference(DEFAULT);  //use Vcc to measure V3v3
  val = analogRead(analogPin); //dummy read
  val = 0;
  for (int i = 0; i < nsamples; i++) {
    val += analogRead(analogPin);
    delay(3);
  }
  result = vcc * val;
  result = result / nsamples; //divide by nsamples to get our number representing  3.3V
  V3v3 = result >> 10; // divide by 1024
  return (V3v3);
}

// code for secret voltmeter adapted from a program by JRemington
long readVcc() {
  long result;
  // Read 1.1V reference against AVcc
  // set the reference to Vcc and the measurement to the internal 1.1V reference
  //choose the right register settings for the processor in use
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
  ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
  ADMUX = _BV(MUX3) | _BV(MUX2);
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

  int agg = 0; // reset aggregate ready for next reading
  for (int count = 0; count < nsamples; count++) {
    delay(2); // Wait for Vref to settle
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA, ADSC)); // measuring
    // read it a second time
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA, ADSC)); // measuring

    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
    uint8_t high = ADCH; // unlocks both

    result = (high << 8) | low;
    agg = agg + result;  //collect and add up nsamples readings
    delay(3);
  }
  // we dont find the average here, it gives better resolution to multiply by the calibration constant first
  result = cal * nsamples / agg; // 1115702 = 1.0896 * 1024 * 1000  - calibration constant, different for each processor
  return result; // Vcc in millivolts
}

johnerrington:
However a decent value R (say 10k) would protect it.

5K is recommended on this page.
Leo..

Thanks for that link Leo, hadnt realised it was a (relatively) low impedance;

a bit ridiculous since as you have pointed out the internal components "COULD" be damaged by connecting an external reference and not setting the ref to "EXTERNAL" before doing a read.

However if you DO connect an external reference through a protextion resistor the value will be changed. And it doesnt say how accurate or precise the internal resistance is.

johnerrington:
And it doesnt say how accurate or precise the internal resistance is.

As always, you need to calibrate if you want accurate returns. That 32k is close from experience, and a single resistor is an easy way to get an Aref of 1-3.3volt if you need that.
Leo..

No-one seems interested to try this so I ran it again myself on a Nano for comparison.
Here are the results

3V3 mean is 3347.96 mV; max is 3351; min is 3342
3V3: variance is 1.40   stDev is 1.18mV;   stError is 0.07 mV

3V3 mean is 3347.67 mV; max is 3352; min is 3342
3V3: variance is 1.65   stDev is 1.28mV;   stError is 0.08 mV

3V3 mean is 3347.37 mV; max is 3352; min is 3342
3V3: variance is 1.49   stDev is 1.22mV;   stError is 0.08 mV

3V3 mean is 3346.76 mV; max is 3352; min is 3336
3V3: variance is 3.29   stDev is 1.81mV;   stError is 0.11 mV

3V3 mean is 3346.80 mV; max is 3355; min is 3336
3V3: variance is 5.16   stDev is 2.27mV;   stError is 0.14 mV

3V3 mean is 3348.07 mV; max is 3355; min is 3336
3V3: variance is 1.63   stDev is 1.28mV;   stError is 0.08 mV

3V3 mean is 3347.66 mV; max is 3355; min is 3336
3V3: variance is 0.96   stDev is 0.98mV;   stError is 0.06 mV

3V3 mean is 3347.56 mV; max is 3355; min is 3336
3V3: variance is 1.15   stDev is 1.07mV;   stError is 0.07 mV

3V3 mean is 3347.52 mV; max is 3355; min is 3336
3V3: variance is 1.20   stDev is 1.10mV;   stError is 0.07 mV

3V3 mean is 3347.65 mV; max is 3355; min is 3336
3V3: variance is 0.69   stDev is 0.83mV;   stError is 0.05 mV

3V3 mean is 3347.71 mV; max is 3355; min is 3336
3V3: variance is 0.93   stDev is 0.97mV;   stError is 0.06 mV

3V3 mean is 3347.40 mV; max is 3355; min is 3336
3V3: variance is 0.89   stDev is 0.94mV;   stError is 0.06 mV

3V3 mean is 3347.62 mV; max is 3355; min is 3336
3V3: variance is 1.74   stDev is 1.32mV;   stError is 0.08 mV

3V3 mean is 3347.67 mV; max is 3355; min is 3336
3V3: variance is 1.38   stDev is 1.17mV;   stError is 0.07 mV

3V3 mean is 3347.65 mV; max is 3355; min is 3336
3V3: variance is 0.89   stDev is 0.94mV;   stError is 0.06 mV

3V3 mean is 3347.64 mV; max is 3355; min is 3336
3V3: variance is 1.25   stDev is 1.12mV;   stError is 0.07 mV

3V3 mean is 3347.67 mV; max is 3355; min is 3336
3V3: variance is 0.95   stDev is 0.98mV;   stError is 0.06 mV

3V3 mean is 3347.68 mV; max is 3355; min is 3336
3V3: variance is 0.81   stDev is 0.90mV;   stError is 0.06 mV

3V3 mean is 3347.74 mV; max is 3355; min is 3336
3V3: variance is 0.91   stDev is 0.95mV;   stError is 0.06 mV

3V3 mean is 3347.78 mV; max is 3355; min is 3336
3V3: variance is 0.88   stDev is 0.94mV;   stError is 0.06 mV

3V3 mean is 3347.70 mV; max is 3355; min is 3336
3V3: variance is 1.13   stDev is 1.06mV;   stError is 0.07 mV

3V3 mean is 3347.82 mV; max is 3355; min is 3336
3V3: variance is 1.29   stDev is 1.14mV;   stError is 0.07 mV

3V3 mean is 3348.14 mV; max is 3355; min is 3336
3V3: variance is 1.41   stDev is 1.19mV;   stError is 0.07 mV

3V3 mean is 3347.71 mV; max is 3355; min is 3336
3V3: variance is 1.13   stDev is 1.06mV;   stError is 0.07 mV

3V3 mean is 3347.08 mV; max is 3355; min is 3336
3V3: variance is 4.72   stDev is 2.17mV;   stError is 0.14 mV

3V3 mean is 3347.66 mV; max is 3355; min is 3336
3V3: variance is 1.49   stDev is 1.22mV;   stError is 0.08 mV

Leo: To calculate stDev I need to store readings in an array; (see code in previous post).
I'd LIKE to run the experiment over a longer period and get more data but I run out of space.
do you have any suggestions if I can do the calculations WITHOUT storing all the readings?

Not sure what you're trying to do.
Measuring the 3.3volt supply against 1.1volt Aref?
I guess both are somewhat 5volt supply dependent,
so not a good way to test one against the other (the tide rises all boats).

Try two 1.5volt batteries in series (~3volt), and connect them through a ~4k7 resistor to an analogue pin (assuming you don't have an other stable voltage source).
Then measure constantly (3.3volt Aref), and store min and max A/D values in two variables, to find deviation.
Could send each measurement to the SerialPlotter too.

Or, make an array of about 25 elements, centred around the expected A/D value,
and store the number of hits of that A/D value in the array belonging to that value.
That could give you a deviation graph.

Keep it real.
3347.37 (334737 values) is about 300 times 'better' than a 10-bit A/D.
Leo..

Just comparing the stability of the 3v3 against the 1.1 and it seems good.

Three standard deviations (95% confidence interval) is about 5mV - which is 1 LSB on the ADC.

3347.37 (334737 values) is about 300 times 'better' than a 10-bit A/D.

Thats just the arithmetic mean Leo, not the number of samples.

I found a better algorithm that doesnt require storing values

variance = (sum of squares)/n - square of mean

void loop() {
  sum = 0; //we use this to find the mean
  sumsqs=0;
  //  *** take a set of measurements ***
  for (int i = 0 ; i < count; i++) {
    //readVcc() reads the current value of Vcc by comparison with the internal 1.1V reference
    V3v3 = readV3v3(readVcc()); //read the value of V3v3 using the known value of Vcc (DEFAULT) as a reference
    sum += V3v3; //aggregate the sum of readings to find the mean;
    fV3v3 = (float) V3v3;  //needs to be a float to find the square correctly
    squareval = sq(fV3v3);
    sumsqs += squareval; //aggreagate the sum of the squares to find variance

but still limited to 256 values (never liked decimal, I can count on my fingers to 1023 in binary!)
as the results go wrong with larger counts as you see here. Not sure why, maybe a number getting too big.

3V3 mean of 16 samples is 3347.44 mV; max is 3349; min is 3344
3V3: variance is 3.00 stDev is 1.73mV; stError is 0.43 mV

3V3 mean of 64 samples is 3347.45 mV; max is 3350; min is 3343
3V3: variance is 3.00 stDev is 1.73mV; stError is 0.22 mV

3V3 mean of 256 samples is 3347.66 mV; max is 3352; min is 3344
3V3: variance is 3.00 stDev is 1.73mV; stError is 0.11 mV

3V3 mean of 512 samples is 3347.92 mV; max is 3350; min is 3343
3V3: variance is 39.00 stDev is 6.24mV; stError is 0.28 mV

I've put the full code below for context

/*
   Sketch to measure the 3.3V level on an arduino uno for possible use as a voltage reference.
   Requires only 1 jumper from the 3v3 pin to A0
   Author J Errington September 2020
*/
const int nsamples = 16;  //number of samples to aggregate to reduce noise
int Vcc;  //our measured value of Vcc against the internal reference
int V3v3;  //our measured value of 3v3 against Vcc
const int analogPin = A0;
const long cal = 1115702L;  //calibration constant for particular board - just leave as set.

//statistics
const int count = 512; //*** change this as required - number of samples to take to get the stats
float fV3v3, sum, mean, squareval, sumsqs, variance, stDev, stErr;
int max = 0, min = 5000;

//function prototypes
long readVcc();  //measure Vcc using 1.1V reference and "secret voltmeter" trick
int readV3v3(long vcc);  //measure V3v3 by comparison with known value of Vcc

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
  sum = 0; //we use this to find the mean
  sumsqs=0;
  //  *** take a set of measurements ***
  for (int i = 0 ; i < count; i++) {
    //readVcc() reads the current value of Vcc by comparison with the internal 1.1V reference
    V3v3 = readV3v3(readVcc()); //read the value of V3v3 using the known value of Vcc (DEFAULT) as a reference
    sum += V3v3; //aggregate the sum of readings to find the mean;
    fV3v3 = (float) V3v3;  //needs to be a float to find the square correctly
    squareval = sq(fV3v3);
    sumsqs += squareval; //aggreagate the sum of the squares to find variance
    if (V3v3 > max) max = V3v3;
    if (V3v3 < min) min = V3v3;
  }
  // *** calculate the mean  ***
  mean = sum / count;  // sum needs to be a float to get a correct float value for the mean.
    Serial.print("3V3 mean of ");
    Serial.print(count);
    Serial.print(" samples is ");
    Serial.print(mean);
    Serial.print(" mV");
    Serial.print("; max is ");
    Serial.print(max);
    Serial.print("; min is ");
    Serial.println(min);

  // *** now calculate variance and standard deviation from the mean ***
  variance = sumsqs / count;
  variance = variance - sq(mean);

  Serial.print("3V3: variance is ");
  Serial.print(variance);
  Serial.print(" ");
  stDev = sqrt(variance);
  Serial.print("  stDev is ");
  Serial.print(stDev);
  stErr = stDev/sqrt(count);
  Serial.print("mV;   stError is ");
  Serial.print(stErr);
  Serial.println(" mV");
  Serial.println();
}

int readV3v3(long vcc) {
  long vTemp, val = 0;
  int voltage;
  analogReference(DEFAULT);  //use Vcc to measure V3v3
  val = analogRead(analogPin); //dummy read
  val = 0;
  for (int i = 0; i < nsamples; i++) {
    val += analogRead(analogPin);
    delay(3);
  }
  vTemp = vcc * val;
  vTemp = vTemp / nsamples; //divide by nsamples to get our number representing  3.3V
  voltage = vTemp >> 10; // divide by 1024
  return (voltage);
}

// code for secret voltmeter adapted from a program by JRemington
long readVcc() {
  long result;
  // Read 1.1V reference against AVcc
  // set the reference to Vcc and the measurement to the internal 1.1V reference
  //choose the right register settings for the processor in use
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
  ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
  ADMUX = _BV(MUX3) | _BV(MUX2);
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif

  int agg = 0; // reset aggregate ready for next reading
  for (int count = 0; count < nsamples; count++) {
    delay(2); // Wait for Vref to settle
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA, ADSC)); // measuring
    // read it a second time
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA, ADSC)); // measuring

    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
    uint8_t high = ADCH; // unlocks both

    result = (high << 8) | low;
    agg = agg + result;  //collect and add up nsamples readings
    delay(3);
  }
  // we dont find the average here, it gives better resolution to multiply by the calibration constant first
  result = cal * nsamples / agg; // 1115702 = 1.0896 * 1024 * 1000  - calibration constant, different for each processor
  return result; // Vcc in millivolts
}