Measuring temperature

The LM385 voltage reference is well under way of becoming my favourite element. Last time I ordered a bunch of electronics, I also included a couple of KTY81-121 temperature sensors, and when trying to make sense of the values, the LM385 proved to be incredibly useful again: I'm powering the Arduino circuit via the FTDI cable using my notebook's power while developing the sketch. However, the notebook's voltage varies heavily, which is one of the reasons why I couldn't get a sensible reading.

VCC

My original intention behind buying the LM385 went in quite a different direction. I'm planning to deploy Arduino-based sensors around the house, and at least some of them will have to run on battery. Batteries run out after a while, so I wanted the Arduino to be able to report its VCC so that I can react before the sensor stops working.

But since the KTY81 is used as a voltage divider and the datasheet lists the resistance for various temperatures, I need to use the voltage divider formula again:

Vout = (R2 / (R1 + R2)) * Vin

Vin is VCC, so we need its value anyway. We can just copy the code for that from the power supply sketch:

unsigned long vref =  1200; // mV of reference voltage
unsigned long vcc  =     0; // current VCC (calculated from vref)

unsigned int pinref = A3;   // pin to read vref from

void loop(){
    vcc = vref * 1024 / analogRead(pinref);
    Sensor.println(vcc);
}

While checking if this part works correctly, I noticed VCC being unstable enough that even my multimeter would notice the jitter. Putting in a 220µF capacitor helped, but even then the voltage went from 4.7V to 5.1V and back. Hence, hardcoding a value for VCC wouldn't even work on my notebook. When the circuit is powered using batteries, VCC might be more stable in the short run, but will ultimately drop as the batteries empty. This means I can in no way rely on VCC, like I could have done with the power supply. I have to measure it. And the LM385 does a marvellous job for that.

Temperature

Calculating the actual temperature from the reading we get from the Arduino's analog input is a bit tricky as well. The KTY81's datasheet provides the resistance in Ohms for various temperatures, so I started off by copying that list into two arrays:

int           centigrade[] = {-40, -30, -20, -10,   0,  10,  20,  25,   30,   40,   50};
unsigned long resistance[] = {562, 617, 677, 740, 807, 877, 951, 990, 1029, 1111, 1196};
unsigned int  num_steps    = sizeof(centigrade)/sizeof(int);

The input is a value measured by the ADC, which can be converted to a voltage pretty easily:

float toCentigrade(unsigned long x){
    // Convert ADC units to a voltage
    unsigned long vin = vcc * x / 1024;
}

Now, in order to get a temperature, we first need to find the correct interval, and then interpolate between the edges of the interval to get an exact value. But since we can't compare resistance and volts directly, we could either calculate the sensor's current resistance, or convert the edge resistance to Volts and compare that to the input voltage. This is what I chose to do.

unsigned long rvcc = 1000;

float toCentigrade(unsigned long x){
    // Convert ADC units to a voltage
    unsigned long vin = vcc * x / 1024;

    // Find the interval we're in
    unsigned long vupper;
    unsigned long vlower;
    unsigned long rgnd;
    int idx;
    for( idx = 1; idx < num_steps; idx++ ){
        // vupper is the voltage at the upper boundary, vlower the one at the lower boundary.
        rgnd = resistance[idx];
        vupper = vcc * rgnd / (rvcc + rgnd);

        rgnd = resistance[idx - 1];
        vlower = vcc * rgnd / (rvcc + rgnd);

        if(vlower <= vin && vin < vupper){
            break;
        }
    }
}

Looks simple enough, right?

Well, at first the Arduino told me I had a room temperature of about 60°C. After a bit of digging I found that I had actually swapped the resistors in my calculations. Meh, but this was easily fixed.

Unfortunately, then it only worked for the sensor I had hung outside the window, while it was about 7°C outside — the sensor mounted directly on the board reported a room temperature of 25-30°C while I had somewhere around 20°C in the room.

Finding this bug nearly drove me nuts. I quadruple-checked the algorithm, modified it to deliver a result more in line with reality, and decided that this can't be right because the algorithm just doesn't make any sense anymore. I changed data types of variables to something bigger and made sure all values were in range. I eagle-eyed the Serial monitor, trying to find a line where the < operator would have failed. Needless to say, no such line arrived. I re-re-re-read the KTY81 datasheet, making sure I had used the correct table and not mistyped any of the values. Everything looked ok, yet the values were off.

Then I noticed a little line popping up in various places in the datasheet that says \(I_{sen(cont)} = 1 mA\). So far, I had been using a 1kΩ resistor going to VCC, so at 5V, the current would be 5mA. Out of desperation, I replaced the 1kΩ resistor with two 10kΩ resistors in parallel which make a total of 5kΩ, which at 5V gives a current of 1mA. To my own surprise, it worked — the room temperature went down to 20-25°C, which sounds all right.

So we can go ahead and use interpolation to find the exact value:

unsigned long rvcc = 5000;

float toCentigrade(unsigned long x){
    // Convert ADC units to a voltage
    unsigned long vin = vcc * x / 1024;

    // Find the interval we're in
    unsigned long vupper;
    unsigned long vlower;
    // snip for( idx = 1; idx < num_steps; idx++ ) loop

    // We're in [idx - 1:idx]. Interpolate where exactly
    float partial = (vin - vlower) / (float)(vupper - vlower);
    int   cupper = centigrade[idx];
    int   clower = centigrade[idx - 1];
    return clower + (partial * (cupper - clower));
}

This gives me a room temperature of around 23°C. (I uploaded the complete sketch to my misc repo.)

Dynamic range

The KTY81 is specified for temperatures between -55°C and +150°C. Since I'm using it to measure ambient temperatures and not planning to deploy the Arduino in a Sauna, I don't quite need such a vast dynamic range — but I'm quite unhappy with the fact that currently, one ADC unit amounts to pretty much 1°C. With VCC being as unstable as it is, I'd like this thing to be far more accurate. The voltage divider outputs around 0.8V now, so I guess I can just add a transistor that ramps 1V up to 5V or something and then divide the measured voltage by 5 to get a more accurate reading.

For that to work, the voltage that pops out of the voltage divider would have to be less than 1V for the highest temperature I'd like to be able to read correctly. If we choose 50°C tops, that would amount to a resistance of 1196Ω. Let's see if that would work, assuming 5V VCC:

>>> r1 = 5000
>>> r2 = 1196
>>> 5 * r2 / (r1 + r2)
0.9651387992253067

Nice. I guess I'm up for another blog post then.