Measurement of Bandgap voltage

I was surfing the postings on AVRfreaks site and came across an interesting topic where someone asked how and if they could measure the battery voltage that was powering their AVR chip.

There were the normal suggestions of using a voltage divider across the battery and changing the A/D reference to the internal 1.1 band gap option. But also mentioned was a way I hadn't heard of or had known about before.

It seems with the proper analog mux channel selection made, one can directly measure the 1.1 bandgap voltage while using the normal default Avcc voltage reference. As the 1.1 bandgap is a constant voltage but Avcc will change as the battery voltage decreases, one will get a A/D count reading that will change with the battery's actual voltage.

So no need for external parts. That sounds useful. However the existing analogRead() allows for no selection of the mux channel 14 (for the 328 chip, channel 30 for the 1280 chip).

So is there a way to allow a sketch to read mux channel 14? Would a new whole function be required to bypass analogRead(), or could the core library for analogRead() be modified?

328 datasheet shows the following analog mux channels available:

Table 23-4. Input Channel Selections
MUX3..0 Single Ended Input
0000 ADC0
0001 ADC1
0010 ADC2
0011 ADC3
0100 ADC4
0101 ADC5
0110 ADC6
0111 ADC7
1000 ADC8(1)
1001 (reserved)
1010 (reserved)
1011 (reserved)
1100 (reserved)
1101 (reserved)
1110 1.1V (VBG)
1111 0V (GND

I believe that channel 8 is the internal temp sensor recently posted about.

Lefty

1 Like

Nice! This is exactly what I need!

So is there a way to allow a sketch to read mux channel 14?

Yes. I'll get back to you this evening with how to do it and what my testing reveals.

Yes. I'll get back to you this evening with how to do it and what my testing reveals.

That would be great. You know having this capability would go well beyond just being able to measure a battery that is powering the processor. One could easily come up with a 'dynamic calibration' function that would make A/D conversions more accurate and consistence between if the board was being powered by the USB Vs on-board +5vdc regulator or even direct +5vdc applied to the Arduino +5vdc pin.

However this may require a little initial measurement procedure before calibration as even the bandgap voltage has a tolerance value between chip to chip, so to get best result one might have to power their chip with a accurately measured adjusted to exactly +5 Vcc, then find out the specific bandgap voltage for that specific chip. Of course there could be a temperature variation component, but I'm sure it would still better then the > .5vdc variation seen between USB and regulators often. Anyway, probably getting ahead of the goal, as if it's not easy to actually get and use the bandgap voltage value then it is just day dreaming away. ;D

There just needs to be detected the 'magic number +/- offset value' or more likely a remapping function that represents the count difference between a perfect +5vdc and whatever specific voltage the chip is actually being fed with. Being able to read the bandgap while powered and referenced with Vcc is the key function needed.

Good luck with your attempt.

Lefty

Good news! It works! This is what I did...


Determine the internal reference voltage...

  1. Upload an empty Sketch

  2. Disconnect power from the board

  3. Connect a 0.1 uF capacitor from AREF to ground

  4. Connect power to the board

  5. Upload the following Sketch...

const uint8_t PinLED = 13;

void setup( void )
{
  Serial.begin( 38400 );
  Serial.println( "\r\n\r\n" );

  pinMode( PinLED, OUTPUT );
  digitalWrite( PinLED, LOW );
  delay( 1000 );

  analogReference( INTERNAL );
}

void loop( void )
{
  Serial.println( analogRead( 0 ) );
  digitalWrite( PinLED, HIGH );
  delay( 1000 );
}
  1. Wait for a few readings to be displayed in Serial Monitor

  2. Measure and record the voltage across the AREF capacitor. In my case the voltage is 1.083.


Use AREF to measure the USB voltage...

  1. Upload an empty Sketch

  2. Disconnect power from the board

  3. Connect a jumper from AREF to +5V

  4. Connect power to the board

  5. Modify the following Sketch to use your reading from the internal voltage reference and then upload the Sketch. Note that the value is *1000 with no decimal.

const long InternalReferenceVoltage = 1083L;  // <<<<<<<<<< Change this to the reading from your internal voltage reference

void setup( void )
{
  Serial.begin( 38400 );
  Serial.println( "\r\n\r\n" );

  // REFS1 REFS0          --> 0 0 AREF, Internal Vref turned off
  // MUX3 MUX2 MUX1 MUX0  --> 1110 1.1V (VBG)
  ADMUX = (0<<REFS1) | (0<<REFS0) | (0<<ADLAR) | (1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (0<<MUX0);
}

void loop( void )
{
  int value;

  // Start a conversion  
  ADCSRA |= _BV( ADSC );
  
  // Wait for it to complete
  while( ( (ADCSRA & (1<<ADSC)) != 0 ) );

  // Scale the value
  value = (((InternalReferenceVoltage * 1024L) / ADC) + 5L) / 10L;

  Serial.println( value );
  delay( 1000 );
}
  1. Wait for a few readings to be displayed in Serial Monitor

  2. value is the voltage at AREF *100 :slight_smile:


So no need for external parts.

And, the whole thing can be turned off to reduce power consumption.

So is there a way to allow a sketch to read mux channel 14?

Not directly. The core restricts the value to 0 through 7 for the 328 processor.

Would a new whole function be required to bypass analogRead(),

Switching the analog reference caused problems for me. In my case, switching to AREF and then restoring to the previous value just didn't work. The readings were all over the place. So, in my opinion, a seperate function is needed to ensure a stable reading is returned.

or could the core library for analogRead() be modified?

The modification for the 328 processor is trivial (see below) but I really think something like this should be a separate function.

  ADMUX = (analog_reference << 6) | (pin & 0x0F);

One final note: With the Sketch above, my readings have a one bit jitter. I was able to eliminate the jitter and make more accurate readings using noise reduction sleep mode. This has the added benefit of conserving power while the battery is measured. :sunglasses:

Nice work. I will play around with your procedure tonight and find out what my bandgap voltage actually is. :wink:

The modification for the 328 processor is trivial (see below) but I really think something like this should be a separate function.

Code:
ADMUX = (analog_reference << 6) | (pin & 0x07);

I saw that line in wiring_analog.c, also and thought just change to a 4 bit and mask = 0x0f , but then I saw this earlier in the file:

#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
      if (pin >= 54) pin -= 54; // allow for channel or pin numbers
#else
      if (pin >= 14) pin -= 14; // allow for channel or pin numbers
#endif

So seems it won't let a pin number of 14 pass through? Not sure why they have to do that, but it seems to get in the way. :wink: Could the test value just be changed to 15?

Now back to reading the battery voltage while running a normal sketch using default Avcc reference and standard analogRead() function, how would one tie all this together? Say your sketch wanted to activate an alarm when the battery voltage dropped to a set value?

Thanks for taking an interest in this topic.

Lefty

Nice work.

Thanks.

I will play around with your procedure tonight and find out what my bandgap voltage actually is.

I'll be playing around with the same 328 on 2 AA batteries. I'll let you know how that goes.

So seems it won't let a pin number of 14 pass through?

Dang it. You're absolutely correct. That if is also a problem.

Not sure why they have to do that

It's the adjustment for the A* constants (A0 for analog input zero, A1 for analog input one, etcetera). If you are not using the A* constants, you can comment-out the if.

Could the test value just be changed to 15?

The safest change is to comment-out the if and don't use the A* constants; use a number instead.

Now back to reading the battery voltage while running a normal sketch using default Avcc reference and standard analogRead() function, how would one tie all this together?

I don't follow. Are you asking how the normal analog inputs could be used in conjunction with this battery testing method?

Say your sketch wanted to activate an alarm when the battery voltage dropped to a set value?

I'll put together a more concrete example while I'm experimenting with the 328 ala batteries.

Well I have been having tons of fun with your bandgap measurement method. I took your code and changed it into a function and also changed the reference back to the normal internal Avcc as below:

 // REFS1 REFS0          --> 0 0 AREF, Internal Vref turned off, --> 0 1, AVcc ref.
        // MUX3 MUX2 MUX1 MUX0  --> 1110 1.1V (VBG)
        ADMUX = (0<<REFS1) | (1<<REFS0) | (0<<ADLAR) | (1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (0<<MUX0);

I used your battery voltage X 100 value and called in battVolts.
I then added in the loop of the sketch this partial portion:

Serial.print("1.888v = ");
  int corrected;
  corrected = map(analogRead(0), 0, 1023, 0, battVolts); 
  Serial.println(corrected); // prints pin volts X 100   
  delay(600);

So to see if along with being able to detemine the actual Vcc voltage value that you solved in your procedure, I wanted to see if I could use that information for correcting the standard analogRead() function to give more consistant readings with variable chip Vcc voltages. First I needed a stable voltage source, so I soldered a short jumper from the anode lead of the power LED and plugged it into A0. I at the time only had two Vcc voltage sources to play with on my rs-232 arduino clone, one is the on-board voltage regulator that measures 4.98vdc on my Fluke model 45 DVM. The other power source is a regulated cell phone charger that puts out 5.29vdc. My led voltage source changed only .5% between the two voltage sources. The two voltage sources represent -.4% and +5.8% of ideal 5.00.

Results:

Battery voltage reported in under 1% error for either of my two voltage sources. The LED Vf voltage drop reference wired to analog pin 0 was read within .1% when switching between the two power sources.

So this isn't an exastive examination, as I would like a larger stable variable voltage to power the board with, and possible more reference voltages or another variable voltage to wire to a analog input pin to see if the error size stays linear or not. All in all this is pretty incouraging considering the rather wide accuracy spec that Atmel spec's for the chip (+/- 2 LSB) for the A/D convertor.

Tonight was another fun Arduino night! :wink:

Lefty

I've finished up with this little experiment for now. Below is a simple sketch that displays the chip's actual Vcc voltage as well as displaying any reference value wired to analog pin 0 to show that it stays a consistent value even with changing Vcc voltage.

// Function created to obtain chip's actual Vcc voltage value, using internal bandgap reference
// This demonstrates ability to read processors Vcc voltage and the ability to maintain A/D calibration with changing Vcc
// For 328 chip only, mod needed for 1280/2560 chip
// Thanks to "Coding Badly" for direct register control for A/D mux
// 1/9/10 "retrolefty"

int battVolts;   // made global for wider avaliblity throughout a sketch if needed, example a low voltage alarm, etc

void setup(void)
    {
     Serial.begin(38400);
     Serial.print("volts X 100");
     Serial.println( "\r\n\r\n" );
     delay(100);
    }
    
void loop(void)
    {
     for (int i=0; i <= 2; i++) battVolts=getBandgap();  //3 readings seem required for stable value?
     Serial.print("Battery Vcc volts =  ");
     Serial.println(battVolts);
     Serial.print("Analog pin 0 voltage reference of 1.888v = ");
     Serial.println(map(analogRead(0), 0, 1023, 0, battVolts));
     Serial.println();    
     delay(1000);
}
 
int getBandgap(void) 
      {
        const long InternalReferenceVoltage = 1050L;  // Adust this value to your specific internal BG voltage x1000
        // REFS1 REFS0          --> 0 1, AVcc internal ref.
        // MUX3 MUX2 MUX1 MUX0  --> 1110 1.1V (VBG)
        ADMUX = (0<<REFS1) | (1<<REFS0) | (0<<ADLAR) | (1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (0<<MUX0);
        // Start a conversion  
        ADCSRA |= _BV( ADSC );
        // Wait for it to complete
        while( ( (ADCSRA & (1<<ADSC)) != 0 ) );
        // Scale the value
        int results = (((InternalReferenceVoltage * 1024L) / ADC) + 5L) / 10L;
        return results;
       }

Lefty

Updated to support both 168/328 and mega boards.

// Function created to obtain chip's actual Vcc voltage value, using internal bandgap reference
// This demonstrates ability to read processors Vcc voltage and the ability to maintain A/D calibration with changing Vcc
// Results printed to serial monitor @ 38400 baud are volt X 100, i.e 5 volts = 500
// Now works for 168/328 and mega boards.
// Thanks to "Coding Badly" for direct register control for A/D mux
// 1/13/10 "retrolefty"

int battVolts;   // made global for wider avaliblity throughout a sketch if needed, example for a low voltage alarm, etc
                 // value is volts X 100, 5 vdc = 500

void setup(void)
    {
     Serial.begin(38400);
     Serial.print("volts X 100");
     Serial.println( "\r\n\r\n" );
     delay(100);
    }
    
void loop(void)
    {
     for (int i=0; i <= 3; i++) battVolts=getBandgap();  //4 readings required for best stable value?
     Serial.print("Battery Vcc volts =  ");
     Serial.println(battVolts);
     Serial.print("Analog pin 0 voltage = ");
     Serial.println(map(analogRead(0), 0, 1023, 0, battVolts));
     Serial.println();    
     delay(1000);
    }

int getBandgap(void)
    {
        
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
     // For mega boards
     const long InternalReferenceVoltage = 1115L;  // Adust this value to your boards specific internal BG voltage x1000
        // REFS1 REFS0          --> 0 1, AVcc internal ref.
        // MUX4 MUX3 MUX2 MUX1 MUX0  --> 11110 1.1V (VBG)
     ADMUX = (0<<REFS1) | (1<<REFS0) | (0<<ADLAR) | (1<<MUX4) | (1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (0<<MUX0); 
#else
     // For 168/328 boards
     const long InternalReferenceVoltage = 1050L;  // Adust this value to your boards specific internal BG voltage x1000
        // REFS1 REFS0          --> 0 1, AVcc internal ref.
        // MUX3 MUX2 MUX1 MUX0  --> 1110 1.1V (VBG)
     ADMUX = (0<<REFS1) | (1<<REFS0) | (0<<ADLAR) | (1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (0<<MUX0);      
#endif
        // Start a conversion  
     ADCSRA |= _BV( ADSC );
        // Wait for it to complete
     while( ( (ADCSRA & (1<<ADSC)) != 0 ) );
        // Scale the value
     int results = (((InternalReferenceVoltage * 1024L) / ADC) + 5L) / 10L;
     return results;
    }

Lefty

So is there a way to allow a sketch to read mux channel 14?

Well that stinks - there used to be! I used analogRead(14) in an old sketch just fine but it was also built using an earlier version of the IDE. It looks like this would have worked up to version -0017 but -0018 changed the mask value from 0x0f to 0x07. A later version added the if statements. I'd call this a bug!

I'd call this a bug!

Nah, they call it a feature now. Hey, they made it so you can address analog input pins with Ax. :wink:

Lefty

Lefty, I have a variation that uses Noise Reduction Sleep Mode. Interested?

Interested?

Sure, lay it on us. Has comments I hope. :wink:

I'm still trying to figure out

results = (((InternalReferenceVoltage * 1024L) / ADC) + 5L) / 10L;

It works, but I don't have a good feel for the magic involved. ;D

Lefty

I'm still trying to figure out
results = (((InternalReferenceVoltage * 1024L) / ADC) + 5L) / 10L;
It works, but I don't have a good feel for the magic involved.

That does look strange. Let me see if I can figure out how it works. :o ;D

The goal is to scale the ADC value to a voltage. The scaling is close enough to linear (according to the datasheet) that we can consider it linear so we will. This is the equation used to perform linear scaling (it's just the equation for a line)...

Y = mX + b

I'm going to use A for the ADC value and V for the calculated voltage...

V = mA + b

But, we can simplify the equation because a zero ADC value is also zero volts. That allows us to drop the b term...

V = mA

The m term is a constant. That means we could write our equation like either of these...

m = V0 / A0

m = V1 / A1

That also means we could write our equation like this...

V0 / A0 = V1 / A1

Two of the values are constant... The bandgap voltage (we measured it and Atmel guarantees it to be constant) and the ADC value is always 1023 when the measured voltage is the same as AREF. Because of the way successive approximation converters work, we use 1024. Now we have...

Vbandgap / A0 = V1 / 1024

The code we have gives us a value for A0. We now have everything needed to calculate the voltage on AREF (V1)...

V1 = Vbandgap / A0 * 1024

VAREF = Vbandgap / Ameasured * 1024

We really want to avoid floating-point math. To avoid a loss of precision, we need to perform multiplication before division. But, we also have to ensure that the multiplication does not overflow the data-type. We use the bandgap * 1000. Atmel states the bandgap can be up to 1.2 volts. So, the maximum value from the multiplication is...

VAREF = (Vbandgap * 1024) / Ameasured
(Vbandgap * 1024)
(1200 * 1024)
1,228,800

Well within the range of a long. We don't need to worry about overflow.

We want our voltage to be rounded instead of truncated. The typical way to round is to add 0.5. We can't do that. We're using integer math. What is 0.5? It can also be 5/10. If our value is already multiplied by 10, adding 5 and then dividing by 10 is the same as adding 0.5. What good fortune! Our value is already multiplied by 10*100 because we multiplied the bandgap voltage by 1000. That gives us...

VAREF = trunc( (((Vbandgap * 1024) / Ameasured) + 5) / 10 )

Vbandgap has been multiplied by 1000. The equation divides by 10. This means VAREF is the volts at AREF * 100.

Make sense?

Sure, lay it on us. Has comments I hope.

Comments? We don't need no stinkin' comments!

Add this to the top of the Sketch...

#include <avr/sleep.h>

Put this anywhere in the Sketch...

ISR(ADC_vect) 
{
}

If we don't provide an interrupt handler, the compiler / run-time library provides one for us. I think the default handler resets the application. Certainly not what we want so we have to provide a handler. There's no code in the handler because the interrupt is only used to wake the processor.


This performs an A/D conversion using the current ADMUX settings. You must set ADMUX before calling this function.

int rawAnalogReadWithSleep( void )
{
  // Generate an interrupt when the conversion is finished
  ADCSRA |= _BV( ADIE );

  // Enable Noise Reduction Sleep Mode
  set_sleep_mode( SLEEP_MODE_ADC );
  sleep_enable();

  // Any interrupt will wake the processor including the millis interrupt so we have to...
  // Loop until the conversion is finished
  do
  {
    // The following line of code is only important on the second pass.  For the first pass it has no effect.
    // Ensure interrupts are enabled before sleeping
    sei();
    // Sleep (MUST be called immediately after sei)
    sleep_cpu();
    // Checking the conversion status has to be done with interrupts disabled to avoid a race condition
    // Disable interrupts so the while below is performed without interruption
    cli();
  }
  // Conversion finished?  If not, loop.
  while( ( (ADCSRA & (1<<ADSC)) != 0 ) );

  // No more sleeping
  sleep_disable();
  // Enable interrupts
  sei();

  // The Arduino core does not expect an interrupt when a conversion completes so turn interrupts off
  ADCSRA &= ~ _BV( ADIE );

  // Return the conversion result
  return( ADC );
}

Cool, something to play with tonight.

So is the purpose of this so that the A/D conversion is performed in a quieter condition then normal non-sleeping condition? I have noticed that on the code I've posted that if I perform just one read of the bandgap voltage it has not settled down to a stable count value. Three reads is OK, within a count or two, and four has the same stable count reading that stays the same no matter how long the function continues. I recall reading in the datasheet about having to perform multiple A/D conversions before obtaining a stable reading, but I thought that was only when changing voltage references, which I'm not doing?

Again thanks for posting the code.

Lefty

So is the purpose of this so that the A/D conversion is performed in a quieter condition then normal non-sleeping condition?

Exactly. I've notice some difference on the ATmega processors. There seems to be less jitter in the least-significant bit. I've noticed a huge difference on the ATtiny processors. The values are rather erratic in "polling mode" but rock solid in "noise reduction mode".

I have noticed that on the code I've posted that if I perform just one read of the bandgap voltage it has not settled down to a stable count value.

My 168 and 328 processors behave the same way. The first reading after switching ADMUX to "measure the bandgap" is garbage.

Three reads is OK, within a count or two, and four has the same stable count reading that stays the same no matter how long the function continues.

About the same here. I usually have a short time delay between readings so, in my case, the readings are stable by the third one.