ADC Voltmeter Linearity Problems (SOLVED)


I’m having a bit of trouble on getting accurate readings from my ADC. Basically what this project does is take a current and voltage reading, then sends it to another Arduino via Xbee. The Xbee connection is working 100% okay and the current reading seems to be fine, but I’m having trouble with the voltage readings.

For example, even though I set the voltage to 0V, the Arduino’s ADC will read something like 187 and output a voltage value of 2.45V or something like that.

The Arduino doing the measurements is a Pro-Mini 5V 328p 16Hz.

The Arduino actually measures the voltage across a 1.2Mohm resistor which is in series with a 3.8MOhm resistor, this is to protect it so I can measure up to 20V without frying the Arduino.

Here are some values that I get:

0V (correct voltage), ~110-140 from ADC, `2-3V measured
3.4V correct, ~251 on ADC, 5.25V measured
17.9V correct, ~ 741 on ADC, 15.5V measured

For some reason it seems to follow a nonlinear pattern, despite the fact that the ADC should in theory be somewhat linear. For the most part, the ADC output seems to be consistent with its respective voltage.

On some very rare occasions I’ll get a completely wrong reading of something like 12V when I only have 2V on there, but this is usually fixed when I reset the Arduino.

The only solution I can think of off the top of my head is to measure values one by one, record the ADC output and the corresponding “correct” voltage, then use Microsoft Excel to create an approximate equation.

Any and all help is welcome

int voltagePin=0;
int currentPin=3;
int V_0=10;
int I_0=12;
int command=0;
int V0_state=LOW;
int I0_state=HIGH;
double voltage_estimate=0.0;
double current_estimate=0.0;
int currentADC=0;
int voltageADC=0;

char vmsg[6]="05.00";
char vadc[5]="0000";
char cadc[5]="0512";
char cmsg[6]="01.00";
// "Voltage: 40.38V

//Mini consumes approximately 75mW
void setup() {

void loop(){
  if (Serial.available()>0){;
  if (command == 49){ //Reads integer of 1 (ASCII 49)
    command = 0;
  if (command == 57){ //Reads ASCII 57, integer 9, Gets voltage measurement
    if (voltageADC <0) voltageADC=0;
    if (voltageADC >1024) voltageADC=1023;
    voltage_estimate=((long)voltageADC*5.0/1023)*(5.01/1.17); //Convert from ADC
    if (currentADC < 0) currentADC=0;
    if (currentADC > 1023) currentADC=1023;
    current_estimate=((long)currentADC*0.0259); //Convert from ADC


The analog circuit (for the Arduino analog input) should have an impedance of 10k or less. That is because the ADC needs some current during the conversion.

Do you have this : 20V — 3M8 — 1M2 — GND
And you measure the voltage over the 1M2, with the 5V as reference ?

The analogRead() returns a value from 0 up to 1023. It will never be below zero, and never 1024 or higher.

Could you have a look at this attached file :

The clamping diodes will protect the input, and the 1k to the input will protect the internal protection diodes. The 10nF capacitor will allow the ADC to do a conversion without a big change of the input voltage during the conversion.

The ATmega328P chip has internal protection diodes for every pin. It is possible push/pull 1mA into a pin. That means that if your 1M2 resistor does not connect ground (because of some fault), the 3M8 will reach the 1mA at 3805V. That is extra protection for free :stuck_out_tongue:

May I do a suggestion ? Could you use the 1.1V internal voltage reference ? It is a better reference than the 5V.
If you really want to use the 3M8, then the other resistor could be 220k. Put a 1nF to 100nF capacitor parallel next to the 220k. The clamping diodes and the 1k protection resistor are not needed. The 220k causes less problems then the 1M2, and the capacitor smoothes the ADC conversion.

Your code could use some improvements. I see a bad calculation with ‘double’, ‘float’ and ‘int’ in a single line, and array sizes that are so tight they could easily go wrong, and unnecessary use of Serial.write() where a Serial.print() would be easier.


Turns out that there's a trick, when using more than one analog input, read each input twice with a 10ms delay like so:

readAnalog(A0) delay(10); readAnalog(A0); delay(10);

The first ADC call switches the multiplexer, the delay allows the voltage time to stabilize, and this results in the second ADC call having a much more accurate and stable result.

I did this and now my system works exactly as it should!