SPI Digital Thermometer

I want to be able to measure temperature of stuff for a small project, but calibrating thermistors seems like far too much hassle.

I've gone for a MAX6662 - A 12 bit Temperature sensors with SPI Compatible interface. the headline specs are :

  • 12-Bit + Sign, 0.0625°C Resolution
  • Accuracy
    • ±1°C max (+30°C to +50°C)
    • ±1.6°C max (0°C to +70°C)
    • ±2.5°C max (-20°C to +85°C)
    • ±2.5 typ (+150°C)
  • Extended Temperature Measurement Range -55°C to +150°C
  • Low Average Supply Current, 125µA (typ)
  • Shutdown Mode to Minimize Power Consumption in Portable Applications
  • Two Programmable Temperature Fault Outputs
  • SOIC-8 package

The full data sheet is here : MAX6662 Datasheet.

Results so far

So far it works a treat. If you want to run the Hello World, program for yourself, there binary is here :

The source code is here :

#include "mbed.h"
#include "MAX6662.h"

MAX6662 st(p5, p6, p7, p8);
Serial pc(USBTX,USBRX);

int main() {
    while(1) {
         wait(1.0);
         pc.printf("Temp now =  %f ^c \n",st.read());
    }
}

Hardware

Form Factor

The samples I got from Maxim arrive as SOIC-8 devices in a tube. Not much good for breadboarding, so I'll start by breaking one out onto a bit of veroboard. My method for doing this is :

  • Cut a bit of vero board the right size and shape to make an suitable DIP package.
  • Put two rows of pins that will become the DIP pins, into a piece of solderless breadboard. This holds them nice and steady
  • Place veroboard copper side up onto the pins, and solder them in place. The veroboard now has mounted pins.
  • Put a blob of hot melt glue onto the middle of the veroboard and while it is hot, stick the device into it, ensuring the glue doesnt cover the pins of the device.
  • Solder small stubbs of wire to the pins, with the stubs of wire towards to the middle of the board, ensuring they are long enough to reach the intended pin.
  • The stubs are now mechanically sound, as is the device. Trim the stubs to the right length with wire cutters
  • Use a pair of tweezers to bend the stubs into position so that you can easily solder to the pins

...I'm off to do the stuff above now. I'll take some pictures for illustration as I go, maybe i'll make a Breakout Page.

source:/SPIThermometer/doc/beforesoldering.jpg source:/SPIThermometer/doc/aftersoldering.jpg
Before Soldering Finished

Hooking it up

When I went to hook it up I realised the "3-wire SPI" didnt mean SCK,MISO,MOSI plus the ever present tedious nCS. What it mean was SCLK,SIO,nCS. Bugger! Bidirectional data.

After a bit of a chat about it, Simon hacked the SPI class to create a new class SPI3. This pretty much works the same, but you wire MOSI and MISO together. When you do a read it sets MOSI to high-impedance, allow the slave to drive it's data back.

So, time to wire it all up :

Signal MAX6662 pin mbed Pin
SCLK 1 p7
nCS 2 p8
SIO 3 p5,p6
GND 4 Gnd
nOT 5 -
nALERT 6 p9*
NC 7 -
Vcc 8 Vo

* Not actually used initially. Only when I started looking at the setting thresholds and triggering alarms.

source:/SPIThermometer/doc/hookedup_small.jpg
All hooked up

* A huge (2Mb) Picture in good detail

Hello World!

Using Simon's hacked SPI3 library this was the first working program :

#include "mbed.h"
#include "SPI3.h"

SPI3 spi(p5, p6, p7);
DigitalOut _cs (p8);
Serial pc (USBTX,USBRX);

int main() {

    _cs = 1;
    spi.frequency(100000);
    spi.format(8,0);

    wait (1.0);
    _cs=0;

    spi.write(0xc1); // command for temp register read

    int x = ((spi.read() << 8) & 0xff00); // read upper byte first
    x |= (spi.read() & 0xff);             // OR in the lower byte

    _cs=1;

    x = (x >> 3); // chop off the 3 flag bits
    pc.printf("Temp =  %f ^c\n",((float)x*0.0625));

}

I know it works (mostly) because I got this :
source:/SPIThermometer/doc/helloworld1.jpg

Hello World (again)

Okay, so I got something sensible out of the device, but I am not convinced that it really is 26.4375 degrees in the office. How? Well, anyone who knows me will know how I whine and complain when the room temperature gets above 23 C. These SPI parts are super accurate (according to the spec sheet) so I suspect my arithmetic is wrong when interpreting the value I read from the register. So, back to the data sheet.

This is the conversion table from the datasheet :

TEMPERATURE (°C) BINARY CODE
+150.0000 0100 1011 0000 0xxx
+125.0000 0011 1110 1000 0xxx
+25.0000 0000 1100 1000 0xxx
+0.0625 0000 0000 0000 1xxx
0.0000 0000 0000 0000 0xxx
-0.0625 1111 1111 1111 1xxx
-25.0000 1111 0011 0111 0xxx
-55.0000 1111 1100 0111 0xxx

An extract from the datasheet

The MSB of the Temperature register is the sign bit. The next 12 bits 
are temperature data. The digital temperature data for the most recent 
temperature conversion is stored in the Temperature register and is 
in °C, using a two’s-complement format with 1 LSB corresponding to 
0.0625°C (Table 3). The three least-significant bits (LSBs) are 
temperature status (flag) bits. The Temperature register is read only. 
Set the command byte to C1h for reading the Temperature register.

Okay, so I need to read out the raw binary value that I am getting to see if I am doing the conversion wrong somehow.

I add a 0x%x to my printf staement, and also print out the flags in the 3 LSB's before I chop them off. this is what I get :

Temp =  24.625000 ^c  (0x18a), flags = 0x0

Okay, so none of the flags are set, and we have 0x18a = 394d. 394 * 0.0625 = 24.625, which is what we were seeing printed out. Any it seems to be fairly close to accurate, well, close enough for now.

Use cases

So I want to wrap this up into class so I can just pull the temperature from it as a float. I also want to be able to set the Thigh and Tlow registers so that ALERT is driven low if they are exceeded. The API I want will look something like this :

void  MAX6662 (int mosi, int miso, int clk, int ncs); // constructor where we specify the pin mapping
float read (void);   // Reads current temperature
void  Thigh (float); // Sets the Thigh threshold
void  Tlow (float);  // Sets the Tlow threshold

I'd also want to use the "=" operator overloading so that I can just assign the current temperature to a float.

Building a class

The first thing I do is look at another device that uses SPI, such as the MobileLCD which can be found at http://mbed.org/projects/cookbook/svn/MobileLCD/trunk

The results can be imported into your compiler projects from http://mbed.org/projects/cookbook/svn/SPIThermometer/trunk

Setting thresholds

The next thing I wanted to add was the ability to set the Tlow and Thigh thresholds, for triggering an ALARM event. I've already included them in the class declaration, I just need to go make them work.

The biggest puzzler at the moment is how I convert the float input of temperature that goes into the methods into data that can be poked into the register.

As it turns out, the number poked into the register is in 1 degree resolution, and occupies register bits 14:7, with 15 being the sign bit.

So I write a quick program to make sure the maths works as I think it does.

#include "mbed.h"

Serial pc(USBTX,USBRX);

float temp = -23.512; // this is the input temperature

int main() {

   short x = temp;
   pc.printf("\n%f = %d steps = 0x%x\n",temp,x,x);
}

Output : 23.511999 = 23 steps = 0x17

When I run the program again with a negative number I get : "-23.511999 = -23steps = 0xffffffe9"

As I am doing this, the first bug report comes in. It is inthe read() function that fetches the value from the temperature register, and returns the float. basically, I am using int in (32 bits) where the top bits are all zero. When I right shift the 16 bit sample 3 places to chop off the flags, i'm not sign extending - so this will only work above 0 degrees C :-)

The fix - instead of using an int, use a signed short (16 bits). When the shift takes place, the sign extension happens automagically. Great!. Job done.

Back to the threshold stuff.

So my little test program has proven that the sums work, I just need to add it into the code.

The functions therefore simply need to cast the float number to a signed short and write it to the register. Although bits [6:0] of the register are unsused, the Datasheets says that writes to these bits are ignored, so it is a simple write.

Of course, I also need to add methods that let me query what the current value of the register is. I'll add that while I'm there.

Getting thresholds

It is really clunky I know, but for now, I am going to use void setTlow(float) and float getTlow(void), to set and get values. I'd much rather be able to assign to and from it using "=" but I am happy to do it the long way for now.

So, to get the thresholds, all I need to do is read the two bytes of the register, compositing them into a signed short, then shift it right 7 places, cast it to a float and return it. Lets see what happens with this :

float MAX6662::getThigh(void) {   
    _select();
    _spi.write(0xcb); // command to read to the Thigh register
    short x = (_spi.read() << 8); // read the top byte first
    x |= _spi.read();             // now read the bottom byte
    _deselect();
    float temp = (x >> 7);
    return(temp);    
}  

My main program now looks like this (notice that I have already add in input for the alert signal, and an LED to show the alert signal status!)

#include "mbed.h"
#include "MAX6662.h"

MAX6662 st(p5, p6, p7, p8);
DigitalOut led1(LED1);
DigitalIn  alert(p9);
Serial pc (USBTX,USBRX);

int main() {

        pc.printf("Thigh =  %f ^c \n",st.getThigh());
        pc.printf("Setting Thigh = 28.0^c \n");
        st.setThigh(28.0);
        pc.printf("Thigh now =  %f ^c \n",st.getThigh());
    

    while(1) {
        wait(1.0);
        pc.printf("Temp =  %f ^c \n",st.read());
    }
}

The output I get is :

Thigh =  64.000000 ^c
Setting Thigh = 28.0^c
Thigh now =  0.000000 ^c

Bum. I know for fact that the power on reset (POR) condition of Thigh is +80c, and I am reading it wrong before I even do anything to it. Lets go back to basics.

The value I know should be there is :

Oh, hang on... I was reading the wrong bit of the datasheet. Tmax is +80c, Thigh is +64c. Yay! I am at least reaidng it correctly before obilterating it :-). Attention turns to the write function...

I am writing 28.0, which 28d = 0x1c. But remember that the bottom 7 bits are dont cares. Lets take another look at setThigh(float)

/* ***************************************
*************************************** */
void MAX6662::setThigh(float temp) {   
    short x = temp;
    _select();
    _spi.write(0x8b); // command to write to the Thigh register
    _spi.write(x>>8); // write the top byte of x first
    _spi.write(x);    // now write the bottom byte
    _deselect();
    
}  

Okay, so there is the problem. I blame it on being preoccupied with pizza when I wrote it. I've just written in the value, I've not shifted it at all.

The first byte has to be bit the original value shifted right one place. The second byte is the original value shifted left 7 places.

Original data (numbered bits) : fedcba9876543210
How it must appear : 876543210xxxxxxx

I now have :

void MAX6662::setThigh(float temp) {   
    short x = temp;
    _select();
    _spi.write(0x8b); // command to write to the Thigh register
    _spi.write(x>>1); // write the top byte of x first
    _spi.write(x<<7); // now write the bottom byte
    _deselect();
    
}  

And it gives me the following output :

Thigh =  64.000000 ^c
Setting Thigh = 28.0^c
Thigh now =  28.000000 ^c

Perfect!. Now I need to apply the same change to setTlow(float);

Triggering Alerts

The first thing I need to do is read how the nALERT signal works - I might have to change the constructor so set it up to trigger.

Right. Done.

Firstly, I want to set it to "Interrupt Mode". This means ALERT is asserted when either threshold is crossed, and it remains that way until the temperature register is read at which point is it cleared. the temperature needs to them go through a cycle of hysteresis before it triggers again.

Also, ALERT is open drain active low. It can be configured active high, but it still requires an external pull up resistor. This fits nicely with a conversation we had recently about DigitalIn defaulting to pull down. I have a 2k7 with me, so no need to open the internal resistor can-o-worms.

I also have to write to bit 9 of the configuration register to enable interrupt mode. I'll do that inthe constructor.

Done.

I have noticed that room temperature is 22.625, but when i put my finger on the MAX6662 it very quickly rises to more than 28. So in my main, i will now set a threshold at 28C, and in the while loop that polls temperature, I'll read the ALERT pin and assign it to LED1. Dont forget the pullup!

My program now looks like this :

#include "mbed.h"
#include "MAX6662.h"

MAX6662 st(p5, p6, p7, p8);
DigitalOut led1(LED1);
DigitalIn  alert(p9);
Serial pc (USBTX,USBRX);

int main() {

        pc.printf("Thigh =  %f ^c \n",st.getThigh());
        pc.printf("Setting Thigh = 28.0^c \n");
        st.setThigh(28.0);
        pc.printf("Thigh now =  %f ^c \n",st.getThigh());
    

    while(1) {
        led1 = alert;
        wait(1.0);
        pc.printf("Temp =  %f ^c \n",st.read());
    }
}

Drum roll please..... Phut!

Okay, schoolboy error in the code to enable the ALERT function. I wrote to bit 8 of the config register instead of bit 9. Modifyt the code... it now works! Hurrah!

With a threshold of 27.0, I dont actually see the ALERT go until 28.xxx, i.e. even at 27.0625, the alert hasnt triggered. When I remove my the heat source (my finger), the temperature drops, and as I am constantly reading the temperature register, I notice that ALERT has cleared when the temperature drops below 26.0, i.e. at 25.9375.

All that seems to be perfectly in order.

Using interrupts, properly

In my last experiment, I'm going to do the following

  • Set up the thresholds, and then just go into a while(1) loop
  • Set up a function that reads the temperature register and prints it to the screen
  • attach the function above to the falling edge of the alert pin.
  • now I should have a system that loops in main doing nothing until the temperature goes outside of range, at which point it will print to the screen.

Need to read up more about the interrupts.

Right, my code now looks like this :

#include "mbed.h"
#include "MAX6662.h"

MAX6662 st(p5, p6, p7, p8);
DigitalOut led1(LED1);
DigitalIn  alert(p9);
Serial pc(USBTX,USBRX);

float th=27.0;
float tl=21.0;

void thandler(void){
        pc.printf("Outside the range %.2f to %.2f at %f ^c \n",tl,th,st.read());
}

int main() {

        alert.fall(&thandler);
        pc.printf("Thigh =  %f ^c \n",st.getThigh());
        pc.printf("Setting Thigh = %.2f^c \n",th);
        st.setThigh(th);
        pc.printf("Thigh now =  %f ^c \n",st.getThigh());
    
        pc.printf("Tlow =  %f ^c \n",st.getTlow());
        pc.printf("Setting Tlow = %.2f^c \n",tl);
        st.setTlow(tl);
        pc.printf("Tlow now =  %f ^c \n",st.getTlow());

    while(1) {
        wait(1.0);
    }
}

I now alternate between a frozen pea and my finger and get the following output :

Thigh =  27.000000 ^c
Setting Thigh = 27.00^c
Thigh now =  27.000000 ^c
Tlow =  10.000000 ^c
Setting Tlow = 21.00^c
Tlow now =  20.000000 ^c
Outside the range 21.00 to 27.00 at 20.812500 ^c
Outside the range 21.00 to 27.00 at 28.000000 ^c
Outside the range 21.00 to 27.00 at 20.000000 ^c
Outside the range 21.00 to 27.00 at 28.062500 ^c

I believe I am now done for the night :-)