Accurate Voltage Measurement
I’ve been trying to accurately measure voltage for my open-source charge controller project. This project relies upon accurate measurement of voltage as this is used to control the regulator.
I have been having some difficulty getting reliable and accurate voltage measurement, so I wanted to figure out why that was.
I am using a simple potential divider to measure the voltage. This is being read by the 10-bit accuracy analog to digital converter (ADC).
In this post I’m going to run through the options available to improve measurement accuracy. I have covered some of this in my voltage measurement post, but this post mainly relates to using the options within the microcontroller to improve accuracy.
I’ve also included some Arduino code to automatically calibrate the Vref value and store it in EEPROM for use in other programs.
Voltage measurement
I’m using the following circuit to measure voltage. This gives me some smoothing via the capacitor, over-voltage protection via the 5.1V zener diode and the resistors themselves reduce the input voltage to a useful range.
Lets look into how we can improve the accuracy of the circuit.
Resistor tolerance
The resistors used for the potential divider have a specified tolerance, typically 1% or 5%. This means that a 1000 ohm resistor with a 5% tolerance, could have a value from 950 ohms to 1050 ohms.
If the input is 10V then we should expect to see 10V *100k / (680k+100k) = 1.282V at the output.
In the worst case for this potential divider, using 5% accuracy resistors then the 100k resistor could be 95k and the 680k resistor could be 714k.
In this situation the output will be 10V x 95k / (95k + 714k) = 1.174V, which is 91.5% of the expected value, so out by 8.5%!
For this circuit I am using the slightly more expensive 1% tolerance devices. 0.1% tolerance devices are available, but their cost is prohibitive in this case.
Another item relating to resistance, which I have just read in the data sheet, is the input impedance of the ADC input. This is designed to be optimal for around 10k input impedance. With a combined impedance of the 680k & 100k resistor network this will be much higher than the 10k suggested. This will mean that the capacitor used to do the ADC will take longer to fill and hence the ADC conversion time will be longer than optimal.
ADC resolution
This relates to the fact that we are converting an analogue signal into a digital signal. (This is sometimes called quantization). Digital signals can either be on or off. Analogue signals can be anything at all and can vary infinitely within their range. We are trying to get this varying analogue data into our digital device (the micro-controller). In order to do this we use the concept of levels (also called quanta).
Say we have a sine wave signal varying from 0 to 5V. Lets say we only have one bit resolution. The digital representation can only be on or off. If we set the level at 2.5V then we will see the digital signal to be 0 then 1 then 0 then 1 as the waveform goes above and below 2.5V. You can see that we do not get much detail from the signal. This might be enough data to do what we want, but generally we use more levels. The more levels, the higher the resolution and hence the better data accuracy.
Adding an extra bit to the resolution increases the resolution by a factor of 2. Hence 1 bit = 2 levels, 2bits – 4 levels, 3 bits = 8 levels. This quickly multiplies up and at 8 bits we have 256 levels or at 10bits we have 1024 levels. A typical micro-controller (such as a PIC 18 series, or the Atmel chip in an Arduino Uno) has 10-bit accuracy. Some have 12, 14 or even 16 bits. If you go to higher numbers of bits this puts more work onto the microprocessor, so you can also use special analogue to digital chips (ADC) which process the data and sends it to the micro-controller.
In this case we do not want to add any additional components (due to cost and complexity), so we must make the most of the 1024 steps available to us in a 10 bit ADC.
AnalogReference()
The micro-controller ADC uses a reference voltage in order to make a comparison and hence perform and analog to digital comparison. In the case of the ATMEL microcontrollers I have been using (ATmega328 and ATTiny85) this reference voltage can be obtained in a number of places. These are listed here in the Arduino analogueReference pages:
- DEFAULT: the default analog reference of 5 volts (on 5V Arduino boards) or 3.3 volts (on 3.3V Arduino boards)
- INTERNAL: an built-in reference, equal to 1.1 volts on the ATmega168 or ATmega328 and 2.56 volts on the ATmega8 (not available on the Arduino Mega)
- INTERNAL1V1: a built-in 1.1V reference (Arduino Mega only)
- INTERNAL2V56: a built-in 2.56V reference (Arduino Mega only)
- EXTERNAL: the voltage applied to the AREF pin (0 to 5V only) is used as the reference.
Power supply reference
In default the ATmega328 will utilise the Aref, external reference, but the ATTiny25/45/85 will use the power supply line as the ADC reference voltage.
The power supply voltage will be variable depending upon the load applied. This can cause some issues with accuracy. Generally it is best to have a precision reference, rather than a voltage which can vary with load applied.
External reference
An external reference can also be used, applied to the Aref pin. This is standard for the ATmega328, but needs to be specified for the ATTiny25/45/85 and is applied to pin 5.
Using an external reference will require the use of an extra external pin. This is not suitable for this design, but might be a way of performing accurate conversions.
Internal reference
An internal reference of either 1.1V or 2.56V is available for both the ATmega328 and the ATtiny25/45/85. This seems like the most suitable way of performing an accurate voltage conversion.
The main issue with the internal reference is the variation in the reference voltage. This value will be relatively stable, but there is variation between each IC due to manufacturing techniques. The datasheet for the ATTiny25/45/85 states that the 1.1V reference can vary between 1.0 and 1.2V and the 2.56V reference can vary between 2.3 and 2.8V. This will cause issues unless this variation can be cancelled.
Calculating the internal reference
In order to use the internal reference we must have some form of calibration procedure performed for each IC. To do this I needed to calculate the actual Vref for each device.
We know that the integer reading (Vint) be proportional to the Vref value and the Vinput value. The calculation is:
Vint = (Vinput x 1024) / (Vref)
If we apply a know Vinput and can record Vint, then we can calculate Vref. This can be rearranged to give:
Vref = (Vinput x 1024) / Vint
To calibrate a device we perform the following function:
- Apply a known accurate and stable reference voltage to Vinput (in this case 1V or 1000mV)
- Read a number of sample ins (in this case 100 samples)
- Average the Vint
- Perform the calculation to calculate Vref in milliVolts
- Store the Vref value in EEPROM – This allows it to be used in other code
I decided to store the data in EEPROM in places 126 and 127. Two locations are required as each location can only hold one byte each. These locations were chosen as they are available for each form of ATTiny IC, as the ATTiny25 only has 128 bytes of EEPROM.
The Arudino code is below:
/* ATTiny Calibrate Vref Overview: This code is for calibrating the internal reference in an ATTiny25/45/85 When using the internal reference there is a wide variation in the reference tolerance For 1.1V reference this can be from 1.1 to 1.3V For 2.56V reference this can be from 2.3 to 2.8V. A constant and accurate reference is applied to pin 7 (A1). This code is designed to take in 100 samples of the analog input. This will give us an averaged reading (Vint). We can use these known values (the reading (Vint) and the input voltage (Vinput)) to find the reference (Vref). Vinput / (Vref /1024) = Vint Rearrange to give: Vref = (Vinput x 1024) / Vint This value is then stored (as a millivolt reading) in EEPROM, for use by other code. ATtiny25/45/85 have different amounts of EEPROM. (128/256/512 respecively). The EEPROM can only hold a byte (256) hence we must use 2 EEPROM locations We will store this number into EEPROM locations 126 and 127. This code is designed to run on the ATTiny 25/45/85 The serial output only works with the larger ATTiny85 IC The connections to the ATTiny are as follows: ATTiny Arduino Info Pin 1 - 5 RESET / Rx (Not receiving any data) Pin 2 - 3 Tx for serial conenction Pin 3 - 4 FET driver (PWM) Pin 4 - GND Pin 5 - 0 RED LED (PWM) Pin 6 - 1 GREEN LED Pin 7 - 2 / A1 Vsensor (Analog) Pin 8 - +Vcc See www.re-innovation.co.uk for more details including flow code 14/8/13 by Matt Little Updated: This example code is in the public domain. */ #include <stdlib.h> #include <EEPROM.h> // Only use Serial if using ATTiny85 // Serial output connections: #include <SoftwareSerial.h> #define rxPin 5 // We use a non-existant pin as we are not interested in receiving data #define txPin 3 SoftwareSerial serial(rxPin, txPin); #define INTERNAL2V56NC (5) int deviceType = 85; // This specifies if it is ATTiny25/45/85 // LED output pins: int redled = 0; // Red LED attached to here (0, IC pin 5) int greenled = 1; // Green LED attached to here (1, IC pin 6) // MOSFET Driver output int FETdriver = 4; // Analog sensing pin int VsensePin = A1; // Reads in the analogue number of voltage unsigned long int Vint = 0; // Hold the Vint value unsigned long int Vinput = 1000; // This is the input voltage in millivolts unsigned long int Vref = 0; // This holds the Vref value in millivolts // Varibales for writing to EEPROM int hiByte; // These are used to store longer variables into EERPRPROM int loByte; // the setup routine runs once when you press reset: void setup() { pinMode(FETdriver, OUTPUT); digitalWrite(FETdriver, LOW); // Switch the FET OFF // Set up IO pins pinMode(rxPin, INPUT); pinMode(txPin, OUTPUT); pinMode(redled, OUTPUT); pinMode(greenled, OUTPUT); if(deviceType=85) { // Start the serial output string - Only for ATTiny85 Version serial.begin(4800); delay(100); serial.println("Calibrate Device....."); } analogReference(INTERNAL2V56NC); // This sets the internal ref to be 2.56V (or close to this) digitalWrite(redled, HIGH); // Set the RED LED ON digitalWrite(greenled, LOW); // Read in the Voltage Set-point hiByte = EEPROM.read(126); loByte = EEPROM.read(127); Vref = (hiByte << 8)+loByte; // Get the sensor calibrate value if(deviceType=85) { // Output the data (if ATTiny85) serial.println("Vref from EEPROM:"); serial.println(Vref); } delay(1000); // Have a delay while the device settles // We need to read in 100 samples for(int i=0;i<100;i++) { // Analogue read. We get 100 readings to average things Vint = Vint + analogRead(VsensePin); // Read the analogue voltage delay(10); // Short delay to slow things down } // Average the value Vint = Vint /100; // Calculate the value of Vref: Vref = (Vinput*1024)/(Vint); digitalWrite(redled, LOW); // Set the GREEN LED ON digitalWrite(greenled, HIGH); // Store the data to the EEPROM EEPROM.write(126, Vref >> 8); // Do this seperately EEPROM.write(127, Vref & 0xff); if(deviceType=85) { // Output the data (if ATTiny85) serial.println("Vint: "); serial.println(Vint); serial.println("Vref (mV): "); serial.println(Vref); } } void loop() { // This loop is not used. }
This code worked well for me. I used a 1V input measured by a Fluke multimeter, which is pretty accurate. This gave me a reading of Vref = 2473. This meas that the actual Vref in my IC was 2.473V, rather than the 2.56V I had been using, although this is within the specification of the ATTiny.
The voltage readings from this device were not accurate and I think this might be the first place to start. I will calibrate all the ICs and the use the Vref value in my main code.
EDIT: 15/8/13
When using this code I noticed that a red LED attached to pin 5 (0 on the arduino list) was always lit but very dim. I have since looked into why this is.
From this post, it turns out that the modes for the ATTiny are:
- DEFAULT = Vcc used as voltage reference
- EXTERNAL = Voltage at Aref used as reference
- INTERNAL1V1 = Internal 1.1V reference used
- INTERNAL2V56 = Internal 2.56V reference used, with external bypass capacitor on Aref
Note the last part – for the internal 2.56V reference, it is implied that you use an external bypass capacitor on Aref, hence the LED was ‘seeing’ the Vref , which should have had a bypass/smoothing capacitor used.
According to the data sheet the bypass capacitor is optional, so we should not need to use a I/O pin to do this.
As eventually found within the readme file of this github repository, I found that the definitions “DEFAULT”, INTERNAL2V56″ etc just relate to a number sent to a register. For example the “DEFAULT” sends binary 000 = decimal 0 to the register. “INTERNAL2V56” sends binary 111 = decimal 7 which switches ON the bypass capacitor. Sending binary 101 = decimal 5 sets to internal voltage reference of 2.56V, but with no external bypass capacitor.
This meant defining a new value for the analogReference. I used:
#define INTERNAL2V56NC (5)
and then called the analogReference with this value in the set-up script:
analogReference(INTERNAL2V56NC); (5)
I have updated the code above to include this.
Regarding impedances. Shouldnt divider output impedance be sigificantly lower than ADC input impedance? Otherwise DAC input impedance affects (sags) dividers voltage thus readings are affected.
I noticed the statement “Sending binary 101 = decimal 6 sets to internal voltage reference of 2.56V, but with no external bypass capacitor.” is incorrect since binary 101 equals decimal 5 and binary 110 = decimal 6. could this be the reason the absence of bypass capacitor was causing troubles? I’m not sure but it appears to me that #define INTERNAL2V56NC (6) is using the (6) to effect the selection for with no bypass capacitor. If so, the correct statement should be #define INTERNAL2V56NC (5).
Please confirm.
Hi. Thanks for the comment and well spotted.
Yes – this could definitely be the case.
It was code I wrote in 2013, so I’m a bit hazy about the details.
I’ve updated the page to reflect your comments, so use #define INTERNAL2V56NC (5), but I have not confirmed that this works, as I dont have the hardware set up at the moment.
Regards, Matt
I think it should be 9 instead of 5. Arduino IDE already defines INTERNAL2V56 as 9, WITHOUT external capacitor as described in attiny85 technical doc section 17.13.1. Value with capacitor should be 13.
Thanks you – very useful information!
Hi and thank you for this post! I’m very new to all of this and I have no idea what the FET driver is and how it should be connected is all of this. If you have a minute to clue me in I’d very much appreciate it! I’d like to reliably use the internal reference voltage (1.1V) for some temperature readings using a TMP37. Thank you!
I think you can ignore the FET as well as the LEDs if you have the attiny85 which can hook up to the Arduino board to display the results on screen.