# Non-linear error in ADC readings

I am measuring voltages up to 20V with my ATmega2650 MCU (10-bit ADC).

I'm using 5V precision voltage reference (LT1021 - 0.05%).

Voltage dividers are setup with 1% Panasonic resistors.

Vcc->10kOhm->Measure->3.3kkOhm->GND.

Division ratio: 3.3/13.3=0.248

What I've noticed is the following increasing errors while measuring bigger voltages:

Vmeas   ADC Err(%)
3.05    152 -0.013382929
4.09    205 -0.0075968
5.02    253 -0.002075695
6.08    308 0.003057305
7.07    359 0.0054141
8.07    410 0.00595279
9.07    461 0.00637229
10.02   510 0.007764232
11.05   563 0.008777353
12.05   615 0.01046932
13.05   665 0.008925735
14.05   717 0.010366242
15.06   769 0.010955198
16.05   820 0.011495804
17.07   872 0.011368671
18.06   923 0.011826103
19.04   973 0.011739502
19.51   998 0.01271154
19.94   1020    0.012715509


Can someone explain what is causing such non-linearity?

Any hints on math to estimate this (ADC features rather than poly-fit)? Any references to mathematical models would help.

EDIT - errors through all voltage range: The calculation methodology:

#define PSU_ANALOG_CHANNELS     3
#define PSU_ANALOG_MEASURES     5
#define PSU_ANALOG_MEASURE_DELAY 1

//apply vRef to each pin to measure post-divided ADC reading
const int psu_adc_corr[PSU_ANALOG_CHANNELS] =  {250,251,251};

int psu_volts_raw[PSU_ANALOG_CHANNELS];//stores ADC readings
float psu_volts[PSU_ANALOG_CHANNELS] = {0}; //stores final values
float mvAdc[PSU_ANALOG_CHANNELS]; //stores mV per each ADC-channel (to avoid division)

void calcMvADC(){
for (int i=0; i<PSU_ANALOG_CHANNELS; i++) {
mvAdc[i] = 5.0 / psu_adc_corr[i];
}
}

//returns averaged reading for each ADC channel
int readAnalog(int ch) {
int val = 0;
for (int i=0; i<PSU_ANALOG_MEASURES; i++) {
val += analogRead(ch);
delay(PSU_ANALOG_MEASURE_DELAY);
}
return val/PSU_ANALOG_MEASURES;
}

void readADC() {
for (int i=0; i<PSU_ANALOG_CHANNELS; i++) {
psu_volts_raw[i]=readAnalog(i);
}
}

/*
>6 <=7 : -1.1%
>7 <=9: -1.14%
>9 <=13: -1.25%
>13 -1.7%:
*/
float corrVoltage(float V) {
if (V<6) return V;
if (V>6 && V<=7) return V*0.989;
if (V>7 && V<=9) return V*0.9886;
if (V>9 && V<=13) return V*0.9875;
if (V>13) return V*0.983;
return V;
}

void calcVoltages() {
for (int i=0; i<PSU_ANALOG_CHANNELS; i++) {
psu_volts[i] = psu_volts_raw[i] * mvAdc[i];
psu_volts[i] = corrVoltage(psu_volts[i]);
}
}

void setup (){
analogReference(EXTERNAL);
calcMvADC();
Serial.begin(115200);
}

void loop (){
readADC();
calcVoltages();
for (int i=0; i<PSU_ANALOG_CHANNELS; i++) {
Serial.println(psu_volts[i]);
}
delay(500);
}

• How do you calculate the voltage from the ADC result, is it /1023 or /1024 ? – alexan_e Feb 23 '14 at 20:07
• Maybe I am missing it, but how are you calculating all of these true values for the voltage? – SomeEE Feb 23 '14 at 21:40
• 1023...or in this case 250, 251 - the reading I get after feeding vRef to apropriate analog channel – FlegmatoidZoid Feb 23 '14 at 21:40
• MathEE -> this line: psu_volts[i] = psu_volts_raw[i] * mvAdc[i]; 5.0/250 = 0.02V; 344(ADC reading) * 0.02 = 3.3V – FlegmatoidZoid Feb 23 '14 at 21:44
• psu_volts is the experimental voltage calculated from the ADC reading. How are you reading the true voltage sent to the ADC? – SomeEE Feb 23 '14 at 21:54

## 4 Answers

ADCs are naturally non-linear. Roughly the transfer function starts at 0, then increases faster than the expected linear transfer function until it reaches #bits/2 and then curves back to where it should be. I can draw a diagram if this explanation is not clear.

The main problem is that you are assuming the converter has the linear transfer function with Voltage = 5V/250*(value from ADC). It does not and the error isn't even linear, as you've already observed. Given the shape of the real transfer function, the data provided by Andy, and the way you are computing the errors at higher voltages the pattern you are seeing is as expected.

I don't think you stated, but let me make a conjecture based on this analysis: Your ADC consistently overestimates the voltages. This is because the slope (5V/250) is near the maximal of the slope of the real transfer function.

Edited to add: Maybe it was clear from your post they are being overestimated since Err % is always > 1. It is clear if Err%=(ATMega reading)/(real value)

2nd edit: Actually, what you can do easily and see how the results change: Put 20V across the voltage divider. You will get a number say $N$ then define psu_adc_corr=N and myAdc = 20/psu_adc_corr=20/N. I would be interested to see what you get.

• I've updated the measurements. The slope seems to decrease. To my big surprise I wasn't able to reach 20V (even tough theoretically I should have had 251*4=1004ADC at 20 volts) – FlegmatoidZoid Feb 24 '14 at 15:07

here's probably the best explanation I've found from the vendor

It has a section dedicated to non-linearity with the following conclusion: "Non-linearity cannot be compensated for with simple calculations. Polynomial approximations or table lookups can be used for that purpose."

So I've made int8_t array of 14 ADC error values (that accounts for 1 volt increments). I've applied those corrections on ADC reading and yay! - I do now have the voltage readings with an error of 0 to 1mV.
Furthemore, I can now use single value of mV_per_ADC_step (in respect to my previous version for which I've had dedicated mV value for each ADC channel).

The ADC in the MCU is specified as having an absolute accuracy of up to 2.5 LSbs at a sampling rate of 200kHz. This is specified at aVref and Vcc voltage of 4V but I'm assuming it's going to be very similar at 5V. If your reference is 5V, 2.5 LSbs is about 12mV. This can manifest itself positively or negatively for a particular input voltage.

I'm not sure how your values are obtained but at 0.248*7V on an input, 12 mV can be an error of 0.69% which is somewhere in the realm that your are seeing. But if you are using a differential ADC channel this can increase to about 18 LSbs at a gain of unity because there is an internal amplifier adding an error.

I'm not that familiar with the Atmel ADC converter, but I suggest that maybe the acquisition time for the S&H is not long enough. I think it should be 15usec or greater.

The maximum time constant is 100K$\Omega\cdot 14$pF or 1.4usec, so 10 time constants is about 15usec.

Alternately, do you have something like a 5.1V Zener diode on the input? That could easily cause errors in that range.

• it would probably have an effect on all readings, wouldn't it? Besides, I've tried delaying the "in-between" adc readings up to 10ms - no impact. – FlegmatoidZoid Feb 23 '14 at 21:14
• No zener? No it wouldn't affect all readings equally, from other ADCs I've worked with, I'd expect less effect around midscale. – Spehro Pefhany Feb 23 '14 at 21:18
• nope, no Zener. And yes, up to 5V the readings are so accurate I could use my setup for calibrating cheap voltmeters. – FlegmatoidZoid Feb 23 '14 at 21:38
• Have you independently measured the input voltage with a high-impedance voltmeter directly at the chip? – Spehro Pefhany Feb 23 '14 at 21:44
• correct, and this is how I calculated the Err(%) on chart - by comparing the atmega measurements with the voltmeter measurements. – FlegmatoidZoid Feb 24 '14 at 0:06