Overlander Monitor - varying power sources - ADC readings unreliable

Hi all,
I've got an Arduino Uno V3 equivalent running a 20x4 I2C LCD, and am gradually building a vehicle monitor for my 4WD, starting with monitoring my two batteries and my solar panels, plus two DS18B20 temp sensors, before I move on to greater things on my Arduino Mega 2560 and KS0108B screen.

I've had these 2 years but in the past fortnight have really been getting in to wanting to finalise something and get it mounted in my car. Below is a schematic of my current circuit, and my code. I'm a Mechanical & Mechatronic Engineer, so covered much of this in my university education, but we never really got in to circuit design, and am coming a bit unstuck. I finally got a couple DC-DC buck converters (LM2596) to run off my car batteries, and I've got it running off my auxiliary battery (to avoid alternator and starter motor spikes), which should vary from 11.5 to 14.5v with the Buck outputting 9v. The trouble is, I'm finding that with the Buck converter, my voltages are down by approximately 3-4% compared to my Digital Multimeter, so the batteries show about 0.4v lower when the car is off. If I connect the USB cable for power and disconnect the Buck, the voltages are much more accurate, within about 0.5% of my DMM, which is fine with me. If I disconnect the car inputs but leave the USB and the Buck, the Buck converter seems to backfeed and show a battery voltage of 3.6V (which is why I've tentatively put Diodes on the positive lines of it in my schematic. If i disconnect the USB, and instead use a 9v cell battery (reading 8.4v OCV), with the car inputs connected, they are about 3-4% over what my DMM reads.

I've tried searching for similar experiences of others and haven't been able to find what I'm looking for. I found some code for checking Vcc and calculating based on that, so I've implemented that in my code, and it's a bit better than it was before, but still not right. I don't want the USB powering the unit, so trying to work out if it's a software or hardware issue. any advice would be so appreciated, hope I've covered all bases.

The code included the start of implementing a pushbutton and current measurement, but that's sidelined til I re-learn op amps. It's sort of been hacked together from a bunch of other examples, but I hope it's succinct enough. Thank you!

//load libraries
#include <Wire.h>
#include <OneWire.h>
#include <LCD.h>
#include <LiquidCrystal_I2C.h>
#include <DallasTemperature.h>


//Define variables 

//LCD I2C
#define I2C_ADDR          0x27        //Define I2C Address where the PCF8574A is
#define BACKLIGHT_PIN      3
#define En_pin             2
#define Rw_pin             1
#define Rs_pin             0
#define D4_pin             4
#define D5_pin             5
#define D6_pin             6
#define D7_pin             7

//Temp Sensors DS18B20
#define ONE_WIRE_BUS       7
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

//Initialise the LCD
LiquidCrystal_I2C      lcd(I2C_ADDR, En_pin,Rw_pin,Rs_pin,D4_pin,D5_pin,D6_pin,D7_pin);

//Pushbutton
int button = 0; //button off state
int oldButton = 0; //last state read

//Voltages
int batt_voltage_alarm = 11; //min voltage before alarm is tripped
int pri_battPin = 0; // Input from primary Battery
int aux_battPin = 1; // Input from auxiliaryy Battery
int solar_Pin = 2; // Input from solar Battery
int primaryVal = 0; //temporarily store the primary input
int auxVal = 0; //temporarily store the secondary input
int solarVal = 0; //temporarily store the solar input
float pri_pinVolt = 0; // variable to hold the calculated voltage
float aux_pinVolt = 0; // variable to hold the calculated voltage
float solar_pinVoltage = 0; // variable to hold the calculated voltage
float priBattVolt = 0; //Temporarily store the primary Voltage
float auxBattVolt = 0; //Temporarily store the secondary Voltage
float solar_chargeVoltage = 0; //Temporarily store the secondary Voltage

float ratio = 5.5334; // The multiplier depending on which resistors used
// I used 68K and 15K 1/2 watt resistors.


//Screen Update
long previousMillis = 0;
long interval = 500;

//Temperatures
float tempC1 = 0;
float tempC2 = 0;


//reading Vcc for accurate ADC values
long readVcc() {
  long result;
  // Read 1.1V reference against AVcc
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Convert
  while (bit_is_set(ADCSRA,ADSC));
  result = ADCL;
  result |= ADCH<<8;
  result = 1125300L / result; // Back-calculate AVcc in mV
  return result;
}


void setup()
 {
    //Define the LCD as 20 column by 4 rows 
    lcd.begin (20,4);
    sensors.begin();
    
    //Switch on the backlight
    lcd.setBacklightPin(BACKLIGHT_PIN,POSITIVE);
    lcd.setBacklight(HIGH);

    pinMode(2, INPUT); //pushbutton input

    pinMode(3, OUTPUT);
    analogWrite(3, 0); //pwm output to pin, duty cycle out of 255
    
    lcd.setCursor(3, 0);// Place the Cursor on the Fist line first character position on the LCD screen
    lcd.print("WELCOME  LEWIS"); // First Line 
    lcd.setCursor(3, 2);// Place the Cursor on the Fist line first character position on the LCD screen
    lcd.print("NISSAN PATROL"); // First Line 
    lcd.setCursor(4, 3);// Place the Cursor on the Fist line first character position on the LCD screen
    lcd.print("GUIV TD42TI"); // First Line 
    delay(2000); //Pause for two seconds 
    lcd.clear();
 }
 
 
void loop()
{ 
  unsigned long currentMillis = millis();
  double Vcc = readVcc()/1000.0;
  float ADCratio = Vcc / 1024;
  

  //VOLTAGES
  primaryVal = analogRead(pri_battPin); //Read the primary Battery input per loop
  auxVal = analogRead(aux_battPin); //Read the secondary Battery input per loop
  solarVal = analogRead(solar_Pin); //Read the primary Battery input per loop
  pri_pinVolt = primaryVal * ADCratio; //ADCratiomV per 1024 steps for 5 Volts (ie; 5 / 1024 = ADCratio)
  aux_pinVolt = auxVal * ADCratio; 
  solar_pinVoltage = solarVal * ADCratio; //ADCratiomV per 1024 steps for 5 Volts (ie; 5 / 1024 = ADCratio)
  priBattVolt = pri_pinVolt * ratio; //grab the calculated under 5Volt input and multiply it by the ratio to ..........
  priBattVolt = round(priBattVolt * 10)/10.0;
  auxBattVolt = aux_pinVolt * ratio; // .............take it back to the 11~12~13Volt Range.
  auxBattVolt = round(auxBattVolt*10)/10.0;
  solar_chargeVoltage = solar_pinVoltage * ratio; //grab the calculated under 5Volt input and multiply it by the ratio to ..........
  solar_chargeVoltage = round(solar_chargeVoltage*10)/10.0;


  //TEMPS
  sensors.requestTemperatures();
  tempC1 = sensors.getTempCByIndex(0);
  tempC2 = sensors.getTempCByIndex(1);

if (digitalRead(2) == HIGH){ //If Pin 2 is high (at 5V)
  button = 1 - button; //inverts value
}

if(button == 1 && oldButton ==0){
  //enter functions for with button on, such as swap backlight
  delay(500);
}

if(button == 0 && oldButton ==1){
  //enter functions for with button off, such as swap backlight
  delay(500);
}
  
if (currentMillis - previousMillis > interval)
{
  //lcd.clear();
  lcd.setCursor(0, 0);// Place the Cursor on the Fist line first character position on the LCD screen
  lcd.print("MAIN:"); // First Line 
  if (priBattVolt < 10.0){
    lcd.print(" ");
  }
  lcd.print(priBattVolt,1); //The Voltage of Primary Battery.. Still on the first Line
  lcd.print("V"); // First Line
  lcd.setCursor(11, 0);
  lcd.print("AUX:"); // Second Line 
    if (auxBattVolt < 10.0){
    lcd.print(" ");
  }
  lcd.print(auxBattVolt,1); //The Voltage of Secondary Battery..
  lcd.print("V"); 
  lcd.setCursor(4, 1);
  lcd.print("SOLAR:"); // 2nd Line 
    if (solar_chargeVoltage < 10.0){
    lcd.print(" ");
  }
  lcd.print(solar_chargeVoltage,1);
  lcd.print("V"); 
  lcd.setCursor(1, 3);
  lcd.print("IN:"); 
    if (tempC1 < 10.0){
    lcd.print(" ");
  }
  lcd.print(tempC1,0);
  lcd.print("'C");   
  lcd.setCursor(11, 3);
  lcd.print("OUT:");
    if (tempC2 < 10.0){
    lcd.print(" ");
  }
  lcd.print(tempC2,0);
  lcd.print("'C");
}  

oldButton = button; //data is now old

}

Hi,
Welcome to the forum.

The Arduino uses the 5V supply as the Vref for the AtoD, so if your 5V is different with different supplies then that would explain it.

You can use the internal 1.1V ref or attach an external reference.
If you use the 1.1V you will need to change your potential dividers, because 0 to 1.1V will be 0 to 1023.
An external reference maybe the answer.

Tom... :slight_smile:

Thanks Tom :slight_smile:
Yeah I'd read on the 1.1v reference and heard that can vary between 1 & 1.2 too. I thought I'd heard of people getting better than 4% accuracy just with the 5v Vref, so I hoped with the implementation of that code lump that calculates Vcc that I'd get an acceptable accuracy. Seems my 5v supply is more relioable than the readings I get out of the system. Might have to look more at the 1.1v or those LM4040 4.096v ones.

1.1volt Aref is the easy answer. Just one line of code in the setup.
The Arduino uses one A/D and a muxer to switch between analogue inputs.
If you use more than one analogue input, it's wise to use a dummy read before the real reading.
That "bleeds" the ghost charge from the previous analogue input reading.
Or use multiple A/D readings for averaging.
Add 100n caps from the analogue inputs to ground.
I have added some code to try.
Leo..

/*
  0 - ~17volt voltmeter
  works with 3.3volt and 5volt Arduinos
  uses the stable internal 1.1volt reference
  10k resistor from A0 to ground, and 150k resistor from A0 to +batt
  (1k8:27k or 2k2:33k are also valid 1:15 ratios)
  100n capacitor from A0 to ground for stable readings
*/

unsigned int total; // holds readings
float voltage; // converted to volt

void setup() {
  analogReference(INTERNAL); // use the internal ~1.1volt reference | change (INTERNAL) to (INTERNAL1V1) for a Mega
  Serial.begin(9600); // ---set serial monitor to this value---
}

void loop() {
  total = 0; // reset
  analogRead(A0); // one unused reading to clear any ghost charge
  for (int x = 0; x < 64; x++) { // 64 analogue readings for averaging
    total = total + analogRead(A0); // add each value
  }
  voltage = total * 0.0002567; // convert readings to volt | ---calibrate by changing the last three digits---

  Serial.print("The battery is ");
  Serial.print(voltage);
  Serial.println(" volt");
  delay(1000); // use a non-blocking delay when used with other code
}

Thanks so much, both of you! Looking forward to implementing and testing it out.

My solar panels are up to 22v, so I suppose it would be alright to run 220k for R1 and 10k for R2? I've read a few times lately about confirming input impedance is within spec, but I've forgotten what that is, so I'm refreshing my memory.

Yes, 10k:220k could read >=22volt.
You could still have 150k:10k for the batteries.
Just use two (or three) voltage variables and two/three different maths lines.
Three is better, because the 150:10 resistor dividers could be slightly off too.

Using 1.1volt Aref and those ratios has the added bonus of overvoltage protection.
Just calculate how much input voltage you need to reach 5volt on the divider tap.

Input impedance is taken care off by the 100n cap.
That cap has a "solid" voltage on it for when the A/D comes past to sample.
With a "sample" cap, divider resistors of >1Megohm are possible.
Leo..

Excellent. I remembered seeing similar code previously and found where you'd posted it previously to better understand the breakdown of your ratio 0.0002567. I noticed you previously had an Aref constant in your code, but of I understand correctly, now you're just calibrating that ratio when you test the ADC against a voltmeter? I've put my refined math code below. resRatio is (220k + 10k)/10k = 23, and I've used your Aref of 1.075. And the overvoltage you mention, with a ratio of 23 would mean that anything from 25.3vdc to 115vdc would be safe, but the ADC will just see the maximum value.

https://forum.arduino.cc/index.php?topic=442778.0

  total = 0; //reset total
  analogRead(pri_battPin);//one unused reading to clear any ghost charge
  for(int x = 0; x < 64; x++){
      total = total + analogRead(pri_battPin); //Add each value
     // pri_pinVolt = primaryVal * ADCratio; //ADCratiomV per 1024 steps for 5 Volts (ie; 5 / 1024 = ADCratio)
      //priBattVolt = pri_pinVolt * ratio; //grab the calculated under 5Volt input and multiply it by the ratio to ..........
      //priBattVolt = round(priBattVolt * 10)/10.0;
  }
  priBattVolt = (total / 64) * resRatio * Aref / 1024;

  
  total = 0; //reset total
  analogRead(aux_battPin);//one unused reading to clear any ghost charge
  for(int x = 0; x < 64; x++){
      total = total + analogRead(aux_battPin); //Add each value
  }
  auxBattVolt = (total / 64) * resRatio * Aref / 1024 ;
  

  total = 0; //reset total
  analogRead(solar_Pin);//one unused reading to clear any ghost charge
  for(int x = 0; x < 64; x++){
      total = total + analogRead(solar_Pin); //Add each value
  }
  solar_chargeVoltage = (total / 64) * resRatio * Aref / 1024;

As this is an automotive environment you want your analog inputs to be protected from noise,
so 100nF to ground on each of them might be wise. All your cabling to remote sensors should
ideally be shielded and grounded only at the Arduino to avoid ground loops through the chassis.

Avoid anywhere near the ignition for a petrol engine if at all possible, spark plug leads put out
fearsome interference (unless of the shielded type).

Good luck.

flewis:
Excellent. I remembered seeing similar code previously and found where you'd posted it previously to better understand the breakdown of your ratio 0.0002567.

It's just the pre-calculated constants of resistor divider and Aref.
Those constants are rather stable, but not very precise.
Resistors could be off by 1%, and Aref could be 1.0-1.2volt.
One single calibration number now fixes it all.

Don't think you can fix this fully with an external reference voltage.
You still have the inaccuracy of the voltage divider, the reference voltage, and maybe the quirks of the A/D.
Calibration is always needed.
Leo..

Thank you MarkT, I've only got 10nF on me, so I'll go buy 100nF caps along with the other bits i need this afternoon. It's a mechanical diesel, so I'm fairly safe for my own implementation, but I'd love to take this to the point of manufacturing, though that's a way off yet. Plenty of learning to do first!

Thank you Leo. I've got a far greater understanding of it all now!

I've implemented the changes to hardware and software (besides the addition of the capacitors, will buy them this afternoon), and the results seem to be less accurate than before. I also added diodes to the +in and +out of the dc-dc buck converter. I found in changing power supplies that Aref varied between 1.07 and 1.14, which would giver me a greater error than my 5v varying from 4.88 to 5.14 the way I see it? Since my battery can vary between 11.5 and 14.5 depending on charge or discharge, I want to give myself a pretty reliable reference voltage. Am I doing something wrong? I'll adjust my calibration and I'll get caps this afternoon and re-check.

You must be doing something wrong (wiring/grounding).
Aref is not the same for every board, but it should be stable and supply independent.

Another way to calibrate (for production) is to add e.g a 1k 10-turn pot to the 10k fixed resistor (in series)
Leo..

I think you might be right, I recorded the following values at the analog inputs versus the Arduino GND, and then approximate calculated values, but I know they were down around 20% from where they should be. I just checked resistances from supply to analog input and analog input to GND and they were all around 222+-1K ohms for the high side and 9.95+-0.05K ohms on the low side, so sounds like I may have a dodgy connection to my DC-DC charger :S I'll have to scope it out tomorrow.

A0 = 0.456v -> on LCD 10.5 approx
A1 = 0.446v -> on LCD 10.4 approx
A2 = 0.301v -> on LCD 7 approx

Watch out for ground-loops - you should be careful not to use a current-carrying ground wire as part of
the ground path to a sensor, as the IR and LdI/dt voltages get imposed on top of the sensor reading
that way. Separate low-current ground wire to your sensor...

Thanks MarkT, I've investigated ground loops with the charging circuit of my car before, but still don't have a complete understanding of them. I'll check over my circuit again, but my DS18B20 sensors have not been playing up noticeably.

FYI, my DC-DC charger (Ctek D250s Dual) has 4 terminals on it, GND (common), Solar +, Start battery +, Aux Battery +, so I've picked up these 4 points and fed them to the Arduino for power and readings. I re-measured this morning, between the charger negative and A0 (start battery) and got 0.58v. Now 0.58 / 1.07(Aref) * 25 (max voltage) is 13.55v which was confirmed by my DMM, but the LCD was displaying 9.5v, so I'll turn my head to the code when I get a chance.

I've got a protoshield I'm picking up this morning, and I'll be able to get some decent soldered joins instead of breadboard, so that should be a better starting point. I checked all my known resistancesfrom Input and Ground across the voltage divider and everything was accurate to 1% it seemed

No idea what was causing me dramas, but i started with a blank canvas and rewrote the code afresh, now it's working reliably! Thanks all for the help! Next up is bluetooth reporting to an app, and current measurement.