Basic voltage input gauge

Hello,

as part of my still ongoing CarDuino project, I would like to measure certain thermistor voltages (oil and coolant temperature) on my car, so I can later use that data with the actual CarDuino to calculate those temperatures.

For this purpose, I've built a "rough and ready" voltage meter on a breadboard which I want to connect to my car to get that data. I've got a laptop with OBD software which will show me the actual temperatures, so I will just have to draw a graph with my measured voltages and the corresponding temperatures shown by the OBD software.

Note: My car has OBD I, so I have to measure most gauges out myself and can't simply tap into the on-board electronics via OBDII to get those temperature values. The car's OBD I protocol is Rover/MG specific, so even if I was able to read out the hex values from the OBD, I wouldn't know what they meant, because very little about Rover's OBD I protocol is known.

Anyway, here's the schematic. Note that for the time being, I've hooked this circuit up to a rudimentary 12V switchboard with which I can control two channels with adjustable voltages.

|500x281

And here's my sketch so far:

#define input1 A3
#define input2 A1
#define led 9
#define refVoltage A0

// defining input variables

int input1Value;
int input2Value;
int aRefValue;

// defining voltage variables

float input1Voltage;
float input2Voltage;
float aRefVoltage;

// Turning analog inputs into voltages
float makeVoltage(int inputValue) {

  // calculation formula to turn analog value 0-1023 into voltage
  // based on using 22K / 10K voltage dividers
  float doMakeVoltage = (float) ((160 * inputValue / 1023) / 10);
  return doMakeVoltage;
}

void setup() {

  // With external reference, 
  // no readings on serial at all.
  // Therefore, commented out at the moment.
  
  // analogReference(EXTERNAL);
  Serial.begin(115200);
  pinMode(input1, INPUT);
  pinMode(input2, INPUT);
  pinMode(led, OUTPUT);
  pinMode(refVoltage, INPUT);
}

void loop() {

  // Reading analog input and calculating values

  // input channel 1
  input1Value = analogRead(input1);
  input1Voltage = makeVoltage(input1Value);

  // input channel 2
  input2Value = analogRead(input2);
  input2Voltage = makeVoltage(input2Value);
  
  // ... and the car's current operating voltage
  aRefValue = analogRead(refVoltage);
  aRefVoltage = makeVoltage(refVoltage);

  // Blink status LED
  digitalWrite(led, HIGH);
  delay(100);
  digitalWrite(led, LOW);


  // Printing out the readings

  Serial.println("Input channel\t\tvalue\tvolts\n");

  Serial.print("Input 1:\t\t");
  Serial.print(input1Value);
  Serial.print("\t");
  Serial.println(input1Voltage);

  Serial.print("Input 2:\t\t");
  Serial.print(input2Value);
  Serial.print("\t");
  Serial.println(input2Voltage);

  Serial.print("ext. voltage:\t\t\t");
  Serial.println(aRefVoltage);
  Serial.print("\n\n\n");


  // wait 2 seconds before reading inputs again
  delay(2000);

}

The problem is that right now, it's not exactly giving me what I want. I get weird readings on serial, and I also can't show the car's current operating voltage.

What I want the sketch to do is show me the value of the analog reading (0-1023) as well as the calculated voltage that those readings represent.

I'm not sure yet if it's best to measure the voltages against the Arduino's 5 volts, or against the car's current operating voltage using analogReference(EXTERNAL). What I'm really after is the changing resistance of the thermistors, but that can only be measured for my purposes as a change of voltage, can't it... but then the problem is that at a given resistance, I will get different voltages between when the engine is off, and when the engine is on with the alternator running.

Any help is appreciated.

  • carguy

EDIT: The first schematic I posted had an error in it with the voltage dividers. I've updated that now.

Hi carguy. I haven't look at your code in too much detail, but one error stands out immediately.

In the line: float doMakeVoltage = (float) ((160 * inputValue / 1023) / 10);

The (float) where you've put it on the right hand side doesn't actually do anything! That is to say, it doesn't do anything that wouldn't have been done automatically anyway, and that is to convert the RHS into float at the very last step of the calculation. In other words, all of those calculations that you wanted performed in floating point are actually done in integer. The result is probable overflow and unexpected results.

This is basically a common "trap" in C. You need make at least one of the RHS operands a float. For example either

float doMakeVoltage = (160.0 * inputValue / 1023) / 10);

or

float doMakeVoltage = (160 * (float) inputValue / 1023) / 10);

will work.

You have not explained what readings you get versus what readings you expect. You need to understand the difference in C/C++ between integer and floating point arithmetic. This keyboard makes it too hard to explain.

You also need to provide hardware protection for excessive voltage to avoid destroying your ATMega328p.

Since it is the resistance of each thermister which you need to know (not necessarily the voltage on it), supply 7 to 9 volts to the barrel connector on an arduino or v_in on a nano and you get whatever you get near 5 Volts. You don't actually need to know that reference voltage in order to interpret a/d inputs from 0 to 1023 as indication of resistance of a thermister. Next do some working out in a spreadsheet to make a function from the thermister datasheet values of resistance to thermister temperature. Next set up calibrations at at least max and min temperatures spanning the range in which you need to be accurate. I suspect that a theory fit to get the shape of the function and an empirical cal to get an offset tweak and a slope tweak are enough. You do know how to set up a function in C ?

int a0;
float i0, T0;

i0 = 0.50*float(a0);
T0 = calT0(i0);


float calT0(float i0)
{
  return T0 = 2.0*i0;
}

What resistance are you using in series with your thermister ? Have you got the datasheet for your thermister ? Your schematic might seem to show 10k and 22k below a 0.25k thermister, which won't do anything much which depends on temperature. It would provide an almost fixed voltage near 10/32.25 x 12 so near 4 Volts to a0 and a1. I'd use about 10k above a "50kOhm" thermister which might have about 10k resistance at "interesting" temperature, and provide 5V to those from the arduino in order to be self-referencing; with no dependence on the car 12V nor upon the 5V reference voltage.

Before I connect the inputs to my car, I plan to put in chokes to at least filter out transient overvoltages.

What I expect from my gauge circuit is first of all to give me the analog readings between 0 and 1023 that are present at the oil and coolant thermistors. So that I can therefore make a graph that represents the relation between analog input value and the temperature that is measured by the OBD laptop.

I've just changed my makeVoltage() function as suggested by stuart0:

float doMakeVoltage = ((160 * (float) inputValue / 1023) / 10);

Right now, what I get when I connect my circuit exactly as shown in the schematic is this:

Input channel value volts

Input 1: 937 14.65
Input 2: 646 10.10
ext. voltage: 0.22

Somehow, weirdly, input 2 now gives me a correct reading, while input 1 doesn't. I've just measured the voltage at switchboard channel 2 (which is connected to input 1 and input 2) against GND, and my multimeter shows exactly the same as what I see on the serial monitor for input 2 when I dial the pot on the 12V switchboard up or down.

@ad2049q:

The 12V switchboard is only for testing and debugging purposes. I will later connect this breadboard circuit to my car.

If what I have read about my car's built-in oil and coolant thermistors (which are the ones I want to tap into) is correct, then their resistance is somewhere in the lower kiloohm range.

And yes, what I plan to do is make a chart with two dimensional coordinates and then calculate a mathematical regression function from the analog reading on the Arduino and the temperature values as given out by the OBD laptop.

ok it's working now. For some weird reason, my newly bought breadboard has some bad connections. I've simply put the voltage resistor pins into different holes on it, and now it's producing accurate voltage readings.

Here's a slightly corrected version of my code...

#define input1 A3
#define input2 A1
#define led 9
#define voltage A0

// defining input variables

int input1Value;
int input2Value;
int voltValue;

// defining voltage variables

float input1Voltage;
float input2Voltage;
float voltVoltage;

// Turning analog inputs into voltages
float makeVoltage(int inputValue) {

  // calculation formula to turn analog value 0-1023 into voltage
  // based on using 22K / 10K voltage dividers
  float doMakeVoltage = ((160 * (float) inputValue / 1023) / 10);
  return doMakeVoltage;
}

void setup() {

  // With external reference, 
  // no readings on serial at all.
  // Therefore, commented out at the moment.
  
  // analogReference(EXTERNAL);
  Serial.begin(115200);
  pinMode(input1, INPUT);
  pinMode(input2, INPUT);
  pinMode(led, OUTPUT);
  pinMode(voltage, INPUT);
}

void loop() {

  // Reading analog input and calculating values

  // input channel 1
  input1Value = analogRead(input1);
  input1Voltage = makeVoltage(input1Value);

  // input channel 2
  input2Value = analogRead(input2);
  input2Voltage = makeVoltage(input2Value);
  
  // ... and the car's current operating voltage
  voltValue = analogRead(voltage);
  voltVoltage = makeVoltage(voltValue);

  // Blink status LED
  digitalWrite(led, HIGH);
  delay(100);
  digitalWrite(led, LOW);


  // Printing out the readings

  Serial.println("Input channel\t\tvalue\tvolts\n");

  Serial.print("Input 1:\t\t");
  Serial.print(input1Value);
  Serial.print("\t");
  Serial.println(input1Voltage);

  Serial.print("Input 2:\t\t");
  Serial.print(input2Value);
  Serial.print("\t");
  Serial.println(input2Voltage);

  Serial.print("ext. voltage:\t\t");
  Serial.print(voltValue);
  Serial.print("\t");
  Serial.println(voltVoltage);
  Serial.print("\n\n\n");


  // wait 2 seconds before reading inputs again
  delay(2000);

}

It now gives me accurate readings on all three inputs.

One question about how to measure the thermistors:

So suppose the car is standing still and there's 12V at the battery. I connect my Arduino to the thermistor and it gives me a certain value between 0 and 1023. Now when I start the car and the alternator generates something around 14.5 volts or whatever, wouldn't that mean that my reading on the thermistor also goes up, while the temperature that the reading is supposed to represent has stayed the same?

What would be the best way to solve that problem?

carguy: For some weird reason, my newly bought breadboard has some bad connections. I've simply put the voltage resistor pins into different holes on it, and now it's producing accurate voltage readings.

I had the same problem a while ago and assumed the error was in my code or perhaps the circuit under test. I substituted new parts for virtually everything in the circuit. I spent two days chasing both avenues for the problem. In desperation, I finally moved the circuit to a new section of the breadboard and it worked perfectly. Like they say: "The solution is always in the last place you look." Well...duh...but I learned a valuable lesson from the experience.

carguy: One question about how to measure the thermistors:

So suppose the car is standing still and there's 12V at the battery. I connect my Arduino to the thermistor and it gives me a certain value between 0 and 1023. Now when I start the car and the alternator generates something around 14.5 volts or whatever, wouldn't that mean that my reading on the thermistor also goes up, while the temperature that the reading is supposed to represent has stayed the same?

What would be the best way to solve that problem?

One way would be to use the existing 5V reference, but to also measure the car's operating voltage on another channel. That way you could correct (in software) for the varying system voltage.

The other way is what you mentioned previously. To use an external analog reference derived from a voltage divider off the cars battery voltage. That way the reference increases in proportion with the sensor voltages (presumably also as voltage dividers formed with the sensor resistances) and the variations due to the cars "12V" system cancels out. If you do it this way, just be careful to choose your voltage dividers so that the external reference is never higher that 5 volts.

stuart0: The other way is what you mentioned previously. To use an external analog reference derived from a voltage divider off the cars battery voltage. That way the reference increases in proportion with the sensor voltages (presumably also as voltage dividers formed with the sensor resistances) and the variations due to the cars "12V" system cancels out.

Right, that was my initial idea. So that later on in the finished CarDuino, I would be able to show the correct oil or coolant temperature both with the engine off and running.

Would it be enough to just feed my operating voltage (behind a voltage divider) into AREF and declare

analogReference(EXTERNAL)

in the setup? And could I then still accurately measure the operating voltage itself on the third channel?

ok I have just done some research, and apparently you can switch the analog reference in mid-code. Which was something that I was not aware of.

So the idea is to now display both the 0...1023 values of the gauges measured against the car's operating voltage, for which I need the external reference, and the absolute voltages measured against the Arduino's internal 5V reference voltage.

I've amended my code in the loop section to try to get that to work:

#define input1 A2
#define input2 A1
#define led 9
#define voltage A0

// defining input variables

int input1ExtValue;
int input2ExtValue;

int input1IntValue;
int input2IntValue;

int voltValue;

// defining voltage variables

float input1Voltage;
float input2Voltage;
float voltVoltage;

// Turning analog inputs into voltages
float makeVoltage(int inputValue) {

  // calculation formula to turn analog value 0-1023 into voltage
  // based on using 22K / 10K voltage dividers
  float doMakeVoltage = ((160 * (float) inputValue / 1023) / 10);
  return doMakeVoltage;
}

void setup() {

  Serial.begin(115200);
  pinMode(input1, INPUT);
  pinMode(input2, INPUT);
  pinMode(led, OUTPUT);
  pinMode(voltage, INPUT);
}

void loop() {

  // Reading analog input and calculating values

  // Gathering values between 0 and 1023 for the gauges, using external reference
  // so that they will be the same regardless of the car's current operating voltage

  analogReference(EXTERNAL);

  analogRead(input1);
  delay(100);
  
  input1ExtValue = analogRead(input1);
  input2ExtValue = analogRead(input2);


  // Switching to internal 5V reference to get absolute voltages
  delay(1000);
  analogReference(INTERNAL);

  analogRead(input1);
  delay(100);
  
  input1IntValue = analogRead(input1);
  input2IntValue = analogRead(input2);
  voltValue = analogRead(voltage);

  input1Voltage = makeVoltage(input1IntValue);
  input2Voltage = makeVoltage(input2IntValue);
  voltVoltage = makeVoltage(voltValue);

  // Blink status LED
  digitalWrite(led, HIGH);
  delay(100);
  digitalWrite(led, LOW);


  // Printing out the readings

  Serial.println("Input channel\t\trel. value\tabs. volts\n");

  Serial.print("Input 1:\t\t");
  Serial.print(input1ExtValue);
  Serial.print("\t\t");
  Serial.println(input1Voltage);

  Serial.print("Input 2:\t\t");
  Serial.print(input2ExtValue);
  Serial.print("\t\t");
  Serial.println(input2Voltage);

  Serial.print("ext. voltage:\t\t\t\t");
  Serial.println(voltVoltage);
  Serial.print("\n\n\n");


  // wait 2 seconds before reading inputs again
  delay(2000);

}

But somehow, this only gives me the following on the serial monitor:

Input channel       rel. value  abs. volts

Input 1:        877     13.72
Input 2:        880     13.76
ext. voltage:               16.00

16 volts is the possible maximum using my makeVoltage() function, for inputValue = 1023.

Somehow, I guess I can't get it to switch to the internal reference, so the voltages are still calculated in relation to the external reference. :( Which would explain why the external voltage is displayed as 16 volts. In reality, according to my multimeter, I've only got 11.8V on my switchboard.

According to the ATMega328P data sheet:

23.5.2 ADC Voltage Reference

. . .

If the user has a fixed voltage source connected to the AREF pin, the user may not use the other reference voltage options in the application, as they will be shorted to the external voltage.

. . .

In other words, you cannot use both the internal reference and an external reference unless you have a way of disconnecting the external reference.

yep, I saw that as well in the Atmega specs. :confused:

I've tried having the Atmega switch a BC548C transistor high and low, with the base connected to a digital pin and thus passing the current through or restricting it from the external voltage source to Aref. That hasn't really worked either. Somehow when the transistor switches off, I still have 1V at the Aref pin. That just made everything worse... :(

This is really just a quick-and-dirty circuit to gather data for my CarDuino project. It's no problem if it's not going to be possible, because what I really need is the actual integer values from 0 to 1023 on the analog input pins, not the corresponding voltage. Absolute voltage readings would just be "nice to have", to see a bit more what is going on. But I am already getting the required analog input values as it is...

carguy: I've tried having the Atmega switch a BC548C transistor high and low, with the base connected to a digital pin and thus passing the current through or restricting it from the external voltage source to Aref.

A BJT makes a very poor analog switch for many reasons. Saturation voltage offset in the on state, too much interaction between base and emitter (that is, poor base emitter isolation), low base emitter breakdown voltage can also hamper design. You need a fairly high base resistance to limit base/emitter interaction, but even if you did everything right you'll still get a poor solution.

That hasn't really worked either. Somehow when the transistor switches off, I still have 1V at the Aref pin. That just made everything worse... :(

Well even if Aref is disconnected (externally) it still has either AVcc or Vref connected to it (internally), so you'd expect that, particularly if the internal Vref (1.1 volt) is selected.

Just go back to the other plan and account for the variation in the cars supply voltage using software.

The sensor values (voltages) that you read are just the car's supply voltage multiplied by the sensor voltage divider ratio. All of the information that you want is tied up with the voltage divider ratio, so just divide by the measured car supply voltage and you are left with just the ratio that you need.

For example (based loosely on your code's terminology):

unsigned sensorValue = analogRead(input1);
float sensorVoltage = makeVoltage(sensorValue);
unsigned supplyValue = analogRead(input2);
float supplyVoltage = makeVoltage(supplyValue);

float sensorDividerRatio = sensorVoltage / supplyVoltage    // Will map to physical sensor reading.

Not wanting to let this turn into too much of a project of its own, because it really is just means to an end, I had the idea today of adding an Attiny85 to the circuit, and then letting the Attiny measure the difference between 5V and the external reference voltage on the Atmega328. The Attiny could then send that data to the Atmega via I2C, and the Atmega would then send its calculations to my Uno board via serial.

That way, I could have my cake and eat it, i.e. have analog input readings relative to an external Aref, as well as be able to express those readings as absolute voltages by multiplying them with the quotient of Aref and 5V.

A fair bit of work just to get a little more info out of my analog inputs, which are again just raw data for another project, but maybe once I've built a circuit like that, it could come in handy for future projects... :)

carguy: Not wanting to let this turn into too much of a project of its own, because it really is just means to an end, I had the idea today of adding an Attiny85 to the circuit, and then letting the Attiny measure the difference between 5V and the external reference voltage on the Atmega328. The Attiny could then send that data to the Atmega via I2C, and the Atmega would then send its calculations to my Uno board via serial.

That way, I could have my cake and eat it, i.e. have analog input readings relative to an external Aref, as well as be able to express those readings as absolute voltages by multiplying them with the quotient of Aref and 5V.

A fair bit of work just to get a little more info out of my analog inputs, which are again just raw data for another project, but maybe once I've built a circuit like that, it could come in handy for future projects... :)

All of that to get around a single floating point divide? :confused:

stuart0: All of that to get around a single floating point divide? :confused:

Oh boy... I get it now... you're right... :) I am kind of feeling stupid now...

What you're essentially saying is to measure the two inputs AND the battery voltage (all of which go through respective 22K/10K voltage dividers) against 5V internal, and then use the difference between Vcc and battery voltage as a quotient to work out analog input readings of input 1 and 2, relative to the battery voltage...

That is admittedly many times simpler, and many times less daft than my idea of using an additional Attiny85... ;)

carguy: What you're essentially saying is to measure the two inputs AND the battery voltage (all of which go through respective 22K/10K voltage dividers) against 5V internal, and then use the difference between Vcc and battery voltage as a quotient to work out analog input readings of input 1 and 2, relative to the battery voltage...

Basically what I'm saying is that you should calibrate (or map) your sensor inputs (input1 and input2) on a "per volt of supply" basis. So that you effectively get a normalized reading that is independent of the actual battery voltage for the sensor inputs.

I would think the temp sensors (thermistors?) are already fed by a regulated, stable voltage (probably 5v), have you checked that? How many wires are running to each sensor? If you connect a voltmeter from vehicle ground to the sensor wire(s), what kind of voltage do you see, cold and hot?