Addendum
I have noticed a few more problems and inconsistencies with your program.
Powering the thermistor from the internal reference
In setup()
, you write
analogReference(INTERNAL); //use the interval votlage of 1.1V for the ADC resolution
and then, in a comment further down you have the schematic
+Vref---[Thermistor]---+--[1.8K]---GND
This suggests you are powering the voltage divider from the internal voltage reference of the MCU. You should not do that, as the datasheet states: "Note that VREF is a high impedance source, and only a capacitive load should be connected in a system."
Relationship between ADC reading and thermistor resistance
In the same comment as above, you wrote:
ADC = 1023*10000/(Rtherm+10000)
Actually, the factor is 1024, not 1023. Again, see the datasheet. And
the number 10000
should be 1800
, i.e. the resistance you have put
between the ADC input and GND. I understand that you may have simply
changed the value and forgotten to update one of the comments. But you
should consider a misleading comment as a bug, especially if it's not
consistent with the actual code.
For this kind of measurement, you get the best precision if the pulldown resistor has a value close to the resistance of the thermistor. The comment at the beginning of the program says your thermistor has 10 kΩ @ 25°C. This means that, if you are mostly interested in the range around 25°C, a 10 kΩ pulldown is better than 1.8 kΩ.
Relationship between array index and temperature
The comment before the LUT says:
The array index starts at zero, which corresponds to a temperature of +6^C
Then, the very first line of the LUT has the comment
//7^C to 7.9^C
And later on, the implementation of getTempFloat()
assumes
index 0 means 13°C. You should sort this out and make everything
consistent.
BTW, your LUT values go all the way up to 1023. A reading of 1023 would mean the ADC voltage is higher than ×ばつVREF. Which in turn would imply the thermistor has a resistance lower than 14.6 Ω (assuming a 10 kΩ pulldown). I find this dubious.
Removal of the LUT
Since your program is meant to run very slowly (constDelay = 3000
),
you can afford doing complex computations in it. You can get the same
precision with a way smaller LUT if you use a higher order
interpolation. For a function sampled at constant steps, the
Catmull–Rom
spline
is easy to implement and is way better than linear interpolation.
Or you can remove the LUT altogether and use an analytical expression instead. If you derived the LUT from such an expression, why not use it in the program? If the LUT derives from experimental data, you can try to do an empirical fit, i.e. an arbitrary function that closely fits the data. The most obvious (though maybe not the best) empirical function would be a polynomial. Below is an example of such a function that reproduces you LUT with good accuracy using a 6th degree polynomial. It assumes the first LUT entry is for a temperature of 7°C:
float getTempFloat(int pin)
{
const float T0 = 7; // temperature at beginning of range
int adc = analogRead(pin);
float x = (adc-223.) / (1023.-223.) * 2 - 1; // scale to [-1:1]
if (x < -1 || x > 1) return NAN; // out of range
float y = 1.474 - x*(0.533 - x*(0.732 - x*0.458));
return T0 + 26.107 + x*(19.287 - x*(3.596 - x*y));
}
Actually this may be even more accurate than your LUT-based function, as that LUT is made of integer values whereas the calibration curve should obviously be continuous.
Addendum
I have noticed a few more problems and inconsistencies with your program.
Powering the thermistor from the internal reference
In setup()
, you write
analogReference(INTERNAL); //use the interval votlage of 1.1V for the ADC resolution
and then, in a comment further down you have the schematic
+Vref---[Thermistor]---+--[1.8K]---GND
This suggests you are powering the voltage divider from the internal voltage reference of the MCU. You should not do that, as the datasheet states: "Note that VREF is a high impedance source, and only a capacitive load should be connected in a system."
Relationship between ADC reading and thermistor resistance
In the same comment as above, you wrote:
ADC = 1023*10000/(Rtherm+10000)
Actually, the factor is 1024, not 1023. Again, see the datasheet. And
the number 10000
should be 1800
, i.e. the resistance you have put
between the ADC input and GND. I understand that you may have simply
changed the value and forgotten to update one of the comments. But you
should consider a misleading comment as a bug, especially if it's not
consistent with the actual code.
For this kind of measurement, you get the best precision if the pulldown resistor has a value close to the resistance of the thermistor. The comment at the beginning of the program says your thermistor has 10 kΩ @ 25°C. This means that, if you are mostly interested in the range around 25°C, a 10 kΩ pulldown is better than 1.8 kΩ.
Relationship between array index and temperature
The comment before the LUT says:
The array index starts at zero, which corresponds to a temperature of +6^C
Then, the very first line of the LUT has the comment
//7^C to 7.9^C
And later on, the implementation of getTempFloat()
assumes
index 0 means 13°C. You should sort this out and make everything
consistent.
BTW, your LUT values go all the way up to 1023. A reading of 1023 would mean the ADC voltage is higher than ×ばつVREF. Which in turn would imply the thermistor has a resistance lower than 14.6 Ω (assuming a 10 kΩ pulldown). I find this dubious.
Removal of the LUT
Since your program is meant to run very slowly (constDelay = 3000
),
you can afford doing complex computations in it. You can get the same
precision with a way smaller LUT if you use a higher order
interpolation. For a function sampled at constant steps, the
Catmull–Rom
spline
is easy to implement and is way better than linear interpolation.
Or you can remove the LUT altogether and use an analytical expression instead. If you derived the LUT from such an expression, why not use it in the program? If the LUT derives from experimental data, you can try to do an empirical fit, i.e. an arbitrary function that closely fits the data. The most obvious (though maybe not the best) empirical function would be a polynomial. Below is an example of such a function that reproduces you LUT with good accuracy using a 6th degree polynomial. It assumes the first LUT entry is for a temperature of 7°C:
float getTempFloat(int pin)
{
const float T0 = 7; // temperature at beginning of range
int adc = analogRead(pin);
float x = (adc-223.) / (1023.-223.) * 2 - 1; // scale to [-1:1]
if (x < -1 || x > 1) return NAN; // out of range
float y = 1.474 - x*(0.533 - x*(0.732 - x*0.458));
return T0 + 26.107 + x*(19.287 - x*(3.596 - x*y));
}
Actually this may be even more accurate than your LUT-based function, as that LUT is made of integer values whereas the calibration curve should obviously be continuous.
I went through your program and found a few bugs you may want to fix. I think only one of these (wastage of RAM) is really related to your problem, but anyway, here it goes:
Fist, there are two issues with LPF()
. The description states that
this is a rolling average low-pass filter. This is erroneous: a rolling
average would take a single reading, then report the average of the
last n readings. This function, in contrast, takes n readings and
reports their average. This makes a big difference: a rolling average
would need to store the last n values in static memory, while your
function does this for no good reason. You are just wasting
300 bytes of RAM.
Here is a reimplementation of LPF()
that does the same thing as yours
without wasting RAM. I changed its name to be more consistent with it's
real purpose:
/*
* Take 'count' temperature readings from 'pin' and return their average.
* Returns NaN (not a number) if too many readings where in error.
*/
float getAvgTemp(int pin, int count)
{
const int MAXERRORS = 5; // max number of errors that can occur
float temp; // current temperature reading
float tempSum = 0; // sum of temperatures
int errCounter = 0; // number of erroneous readings
for (int i = 0; i < count; i++) {
// Try to get a valid reading.
do {
temp = getTempFloat(pin);
if (isnan(temp))
errCounter++;
delay(25); // allow the ADC to settle
} while (isnan(temp) && errCounter <= MAXERRORS);
// Too many errors: return an error.
if (errCounter > MAXERRORS)
return NAN;
tempSum += temp;
}
return tempSum / count;
}
Here I use NaN (not a number) as an error indicator, as it is semantically clearer than a random out-of-range value.
There are also a few errors in getTempFloat()
:
pgm_read_word(LUT_Therm[constLUTArraySize-1])
andpgm_read_word(LUT_Therm[i])
will not work: you have to passpgm_read_word()
the address where to read.float(i/10)
does not do what you want: it computesi/10
as an integer division (i.e. discarding the fractional part), and then it converts the result to a float. If you want a floating point division, you should make sure that at least one of the arguments is a float. The usual idiom isi/10.0
.
Here is a version of getTempFloat()
with these problems fixed, and
also somewhat simplified:
float getTempFloat(int pin)
{
int i, lutval, prev_lutval;
// Take an analog reading.
analogRead(pin); // dummy reading to settle the MUX
delay(10);
int adc = analogRead(pin);
Serial.print(F("ADC: "));
Serial.println(adc);
// Find i such that adc lies between LUT[i-1] and LUT[i].
for (i = 0; i < constLUTArraySize; i++) {
prev_lutval = lutval;
lutval = pgm_read_word(&LUT_Therm[i]);
if (lutval >= adc) break;
}
// Special case: adc == LUT[0].
if (i == 0 && adc == lutval)
return 13.0;
// Report an error if out of range.
if (i == 0 || i == constLUTArraySize)
return NAN;
// Return interpolated temperature.
float fraction = float(adc - prev_lutval) / (lutval - prev_lutval);
return 13.0 + (i-1 + fraction) / 10.0;
}