Go Down

### Topic: Measurement of Bandgap voltage (Read 53482 times)previous topic - next topic

#### retrolefty

##### Jan 08, 2011, 10:20 amLast Edit: Jan 08, 2011, 05:26 pm by retrolefty Reason: 1
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:

Quote
Table 23-4. Input Channel Selections
MUX3..0 Single Ended Input
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
##### Jan 08, 2011, 10:26 pm

Nice!  This is exactly what I need!

Quote
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.

#### retrolefty

#2
##### Jan 09, 2011, 02:52 amLast Edit: Jan 09, 2011, 03:01 am by retrolefty Reason: 1
Quote
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.

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.

Lefty

#3
##### Jan 09, 2011, 02:59 amLast Edit: Apr 08, 2014, 09:11 pm by Coding Badly Reason: 1

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

Determine the internal reference voltage...

2. Disconnect power from the board

3. Connect a 0.1 uF capacitor from AREF to ground

4. Connect power to the board

Code: [Select]
`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 );}`

6. Wait for a few readings to be displayed in Serial Monitor

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

Use AREF to measure the USB voltage...

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.

Code: [Select]
`const long InternalReferenceVoltage = 1083L;  // <<<<<<<<<< Change this to the reading from your internal voltage referencevoid 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 );}`

6. Wait for a few readings to be displayed in Serial Monitor

7. value is the voltage at AREF *100

Quote
So no need for external parts.

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

Quote
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.

Quote
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.

Quote
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.

Code: [Select]
`  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.   8-)

#### retrolefty

#4
##### Jan 09, 2011, 03:37 amLast Edit: Jan 09, 2011, 03:39 am by retrolefty Reason: 1
Nice work. I will play around with your procedure tonight and find out what my bandgap voltage actually is.

Quote
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:

Code: [Select]
`#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.   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

#5
##### Jan 09, 2011, 06:06 am
Quote
Nice work.

Thanks.

Quote
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.

Quote
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.

Quote
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.

Quote
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.

Quote
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?

Quote
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.

#### retrolefty

#6
##### Jan 09, 2011, 08:29 amLast Edit: Jan 09, 2011, 04:24 pm by retrolefty Reason: 1
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:

Code: [Select]
` // 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:

Code: [Select]
`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!

Lefty

#### retrolefty

#7
##### Jan 10, 2011, 07:44 amLast Edit: Apr 08, 2014, 08:55 pm by Coding Badly Reason: 1
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.

Code: [Select]
`// 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, etcvoid 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

#### retrolefty

#8
##### Jan 13, 2011, 11:27 amLast Edit: Apr 08, 2014, 08:55 pm by Coding Badly Reason: 1
Updated to support both 168/328 and mega boards.

Code: [Select]
`// 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 = 500void 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

#### cjands40

#9
##### Jan 14, 2011, 10:16 pm
Quote
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!

#### retrolefty

#10
##### Jan 14, 2011, 11:09 pm
Quote
I'd call this a bug!

Lefty

#11
##### Jan 14, 2011, 11:24 pmLast Edit: Jan 15, 2011, 08:20 pm by bcook Reason: 1

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

#### retrolefty

#12
##### Jan 15, 2011, 12:51 amLast Edit: Apr 08, 2014, 09:17 pm by Coding Badly Reason: 1
Quote
Interested?

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

I'm still trying to figure out

Code: [Select]
`results = (((InternalReferenceVoltage * 1024L) / ADC) + 5L) / 10L;`

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

Lefty

#13
##### Jan 15, 2011, 07:26 amLast Edit: May 15, 2014, 07:08 am by Coding Badly Reason: 1
Quote
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.

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?

#14
##### Jan 15, 2011, 08:42 pmLast Edit: Jan 15, 2011, 08:47 pm by bcook Reason: 1
Quote
Sure, lay it on us. Has comments I hope.

Add this to the top of the Sketch...
Code: [Select]
`#include <avr/sleep.h>`

Put this anywhere in the Sketch...
Code: [Select]
`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.

Code: [Select]
`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 );}`

Go Up