Arduino Secret Voltmeter Explanation?

By now I’m sure lots of folks knwo about the secret Vin Voltmeter Secret Arduino Voltmeter – Measure Battery Voltage

long readVcc() {
  // Read 1.1V reference against AVcc
  // set the reference to Vcc and the measurement to the internal 1.1V reference
  #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  

  delay(2); // Wait for Vref to settle
  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

  long result = (high<<8) | low;

  result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
  return result; // Vcc in millivolts
}

Since I’m using the ATMega328, I confused on the line

ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);

What is that line doing and how is it doing the comment “set the reference to Vcc and the measurement to the internal 1.1V reference”?

How does setting the reference to Vcc and measuring the 1.1V reference result in returning a value for Vcc?

P.S. I’m interested in understanding this in depth as I use the interrupt method for ADC and need to be able to convert this, if it applies to what I’m trying to measure.

adwsystems: Since I'm using the ATMega328, I confused on the line

ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);

What is that line doing and how is it doing the comment "set the reference to Vcc and the measurement to the internal 1.1V reference"?

It is setting specific bits in the ADMUX register. Have a look at the datasheet (page 248) for an explanation of the bits and it might help understand what they are doing.

How does setting the reference to Vcc and measuring the 1.1V reference result in returning a value for Vcc?

The website you linked specifically explains this...

Read the '23. Analog-to-Digital Converter' section in the 328 data sheet.

LarryD: Read the '23. Analog-to-Digital Converter' section in the 328 data sheet.

My datasheet has it in section 28, but any who. It's a little tricky to follow the 1.1V reference through the document to realize you can read it through the ADMUX since they use three different names for it.

saximus: The website you linked specifically explains this...

I see no explanation of the correlation between Vcc and 1.1V internal reference on that website. I'm not seeing anything in the datasheet either. My hope would have been the 1.1V reference is independent of the Vcc supply to the chip. If I can't justify this, then my plan is shot.

You measure the 5v with an accurate DVM then calculate the real value of 1.1v.

.

how is it doing the comment "set the reference to Vcc and the measurement to the internal 1.1V reference"?

The ADC is measuring the 1.1v internal band gap voltage against Vcc as reference. See Wikipedia for this explanatiuon of band gap voltage. It is independent of supply voltage. https://en.wikipedia.org/wiki/Bandgap_voltage_reference

With 5volts Vcc, the ADC measurement of the 1.1v band gap reference should be 1023* 1.1/5 =225 counts. If that is not the value, then the Vcc value is corrected by the formula.

For example if the 1.1v reference returns an ADC reading of 230. Then 1125300/230 = 4.89V Vcc.

adwsystems: My hope would have been the 1.1V reference is independent of the Vcc supply to the chip.

Cattledog's answer is awesome, as usual. So I won't try to improve on it but it is independent. You're probably right that it's not entirely obvious but if you can understand what the ADMUX register bits do when they are set the way he sets them, it makes more sense.

In any case, if your measurement is super critical, you might be better off using an external reference?

While I'm working on re-re-reading the datasheet, I came across something that may answer another question.

The question is from the outside of the chip and program, is it possible to tell which reference is being used?

In the ADC Voltage Reference section it reads

If the user has a fixed voltage source connected to the AREF pin, the user may not use the other reference voltage options in the application, as they will be shorted to the external voltage. If no external voltage is applied to the AREF pin, the user may switch between AVCC and 1.1V as reference selection.

Does this mean the AREF pin is equal to the internal 1.1V reference or Vcc, which ever is selected?

In any case, if your measurement is super critical, you might be better off using an external reference?

That's probably correct. The band gap voltage is stable, but it is specified to be between 1.0 and 1.2 v. Using it to correct Vcc is good for repeatability and stability but not accuracy. It depends on what you are trying to do with your ADC readings.

Does this mean the AREF pin is equal to the internal 1.1V reference or Vcc, which ever is selected?

Yes, but using Aref as an output is most likely not a good idea.

If it was obvious, then it wouldn't be a secret voltmeter, would it?

I really wish he would update his site. The correct divisor is 1024. And, that is not the correct way to read the ADC value. And, the value is truncated instead of rounded. Ugh.

@adwsystems, this may help... https://forum.arduino.cc/index.php?topic=38119.0

cattledog: Yes, but using Aref as an output is most likely not a good idea.

I was trying to use as an output to drive anything. I just wanted to put a (real) voltmeter on it to make sure the program changed/set the reference correctly.

PaulMurrayCbr: If it was obvious, then it wouldn't be a secret voltmeter, would it?

That made me laugh.

[quote author=Coding Badly link=msg=3194475 date=1490575947] I really wish he would update his site. The correct divisor is 1024. And, that is not the correct way to read the ADC value. And, the value is truncated instead of rounded. Ugh.

@adwsystems, this may help... https://forum.arduino.cc/index.php?topic=38119.0 [/quote] I agree on the 1024. I updated the long number in the code accordingly. Why do you say the number is truncated instead of rounded?

adwsystems: Why do you say the number is truncated instead of rounded?

Integer division is always {blank}. Guess what fills in the blank?

Compare the two versions. The bit of code responsible for rounding sticks out like a sore thumb. @retrolefty even commented on it.

Yes. I see where you are adding some random value (5) to the actual and then dividing by 10. That result is x.xxV, losing a full digit of resolution after having added 5mv (that doesn't exist) and rounding, so 1.005V will become 1.01V. Whereas the division would result in 1.004-1.006V keeping all 3 decimals resolution and having an error within +/- 1mV.

The simple rule of thumb is to work with as big a number as you can as early as you can. Multiply and add then subtract and divide (so much for the order of operations from grade school. LOL)

(error based on math, not on measurement)

[quote author=Coding Badly link=msg=3194475 date=1490575947] And, that is not the correct way to read the ADC value.

[/quote] Now you've got me curious. Why is that not the correct way? He is basically following the way analogRead() is written.

saximus: Now you've got me curious. Why is that not the correct way?

By splitting the read into two lines of code the optimizer is free to reverse the order.

In other words, the optimizer is free to generate code like this...

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

...instead of what the developer intended because, from the optimizer's perspective, there is no relationship between those two lines.

What gives the optimizer the “option” to re-arrange the lines of code? The fact it is declared and assigned at the same time (see above)? That I can actually understand and is why I was told never to do that.

Because if I found out the optimizer is rearranging my code:

ISR (ADC_vect)
{
  byte low, high;

  low = ADCL;
  high = ADCH;

  adcReading = (high << 8) | low;
  adcDone = true;
}  // end of ADC_vect

I’m likely to go ballistic.

There is NO REASON for the optimizer to NOT execute the instructions in the order programmed. If it is rearranging the order, then how am I supposed to be able to program anything if I don’t know what order things are going to happen?

adwsystems: What gives the optimizer the "option" to re-arrange the lines of code?

You have knowledge that the order is important. How did you come by that knowledge?

[quote author=Coding Badly link=msg=3195681 date=1490638872] You have knowledge that the order is important. How did you come by that knowledge? [/quote]

Of what aspect of knowledge are you asking?

In general, 30+ years of programming tells me in what order to write lines of code to execute.

Related to the ADC, been there done that. It is in the datasheet to read one byte and then the other.

Related to optimizing, been there done that too.

Once upon a time, programming an 80C51 I was trying to read a 12 bit parallel DAC. At first the high byte was always equal to the low byte. The problem was the byte came from the same external memory address. I had written the code something like:

LowByte=ADC_byte; HighByte=ADC_byte;

Why was Highbyte-Lowbyte?

Reviewing the ASM code it was optimized: MOV ADC_Byte Scratchpad MOV Scratchpad LowByte MOV Scratchpad HighByte

It did the slower external memory read and stored it locally, then used the local value to assign to the two variables. 20 clock cycles (12+4+4) instead of 24, but very different results.

Moving the read to a function forced the compiler to perform two external reads. byte read_adc(void) { return ADC_byte; } then LowByte= readadc(); HighByte=readadc();

I took it on faith the library code written for the Arduino ATMega worked. So far it has. When I was working with the 8051s there was no library of code. Heck there was no internet.

In the case of the secret voltmeter code, the combination of declaring the variable and assigning the value can be performed in either order. According to the optimizer it is just an assignment of one value to a variable. If they are performed as two separate functions (declare vs assign), the variable memory will be allocated (in some order), then the values assigned in the order written. The code should be written in three lines instead of two. This is not subject to the same problem as my 8051 because in the ATMega the high and low bytes are at different memory addresses.

But we have gotten sidetracked...rounded versus truncated?

adwsystems: Of what aspect of knowledge are you asking?

Related to the ADC, been there done that. It is in the datasheet to read one byte and then the other.

Correct. The answer is "I read it in the datasheet." Without reading that in the datasheet you would have no idea that a relationship exists between ADCL and ADCH.

The compile cannot read English. So it cannot get that knowledge from the datasheet.

There is nothing in the C(++) language to tell the compiler that such a relationship exists. (There is in the GCC compiler. But that something incurs a significant cost.)

In the case of the secret voltmeter code, the combination of declaring the variable and assigning the value can be performed in either order.

Half correct. The declaration is irrelevant.

According to the optimizer it is just an assignment of one value to a variable.

Exactly. "Just an assignment." More specifically "two independent assignments" which means the order is irrelevant. Which means the optimizer is free to perform them in any order or even move them to different points in the function. The only requirement is that the two values have to be available when the values are combined by the bitwise-or.