A wireless non-contact thermometer

So far I showed that NTC’s measure the contact temperature and that you can easily integrate them into an Moteino project. In this blog I want to show that temperatures can also be measured with a non-contact device, called a thermopile. A thermopile is a sensor that converts heat into a tiny voltage, the measurement is based on the Seebeck effect that is documented here.

The main advantage of non-contact thermometers is that one measures an integrated temperature directly, and that no contact is required between the detector and the object. The detector has an opening angle, for this project I’ve chosen a low cost thermopile with an angle of 90 degrees. More expensive thermopiles include optics consisting of Germanium lenses so that the detector area can be reduced down to as little as 5 degrees.

The relation between temperature and long-wave infrared radiation from an object is described by the Stephan Boltzmann law which says that the heat energy received is proportional to detector area times sigma times T to the power four where T is the temperature of black body, and where sigma is a fundamental constant. When the flux is measured you can back out T but only if one compensates for the self-temperature of the detector and shape factors.

Although the latency of the thermopile is short, the actual latency of the measurement is affected by the NTC which measures the detector self-temperature. As a result it can take a couple of minutes when there are extreme changes in the ambient temperature of the detector. IR thermometers are best used away from heat components, and in case of changes in the ambient temperature you should allow some time for them to adjust.

Melexis makes a low-cost IC called the 90614 which does all this for you, and it comes in a TO-39 case. It can either run as an I2C device or a PWM device. I have chosen for the first option and ordered a couple of them through futurelectronics. They are easy to integrate in a project, the 90614 is calibrated in front of a black body radiator and assumes an emissivity of 1.

The schematic below shows how you can integrate the thermopile in your microprocessor project. The detector including the 100nF and two 4k7 resistors are best placed on a separate PCB in my opinion. The window of the detector should not be covered or touched; in fact, it should stay as clean as possible. If you decide to protect the detector with a piece of thin plastic then a calibration procedure should be followed to determine an emissivity correction factor, which you either upload in the 90614’s eeprom, or you apply it in your own software. See also the attached source code.

The only drawback so far I’ve encountered so far is that it is hard to put the 90614 in sleep mode, so there is a constant current drain of about 1,5 milliamp. This is the driving factor for the battery consumption at this moment, at 1,5 mA the 800mAh battery live for about a month, maybe I should revise this at some point in the future.

inside-box

Temperature record in the living room over the last three days.
Temperature record in the living room over the last three days.

schematic

sensorhead

to39

Source code

I gathered the arduino code from various sources on the internet, the CRC calculation is messy but it works I believe. The only reason to use i2c master is that it allows for repeated starts which you can’t do with the wire library. It is all explained on the Arduino forum. Still it took me a weekend or so to comprehend this all.

// reads out the MLX90614 infrared thermometer

#include <Arduino.h>
#define SDA_PORT PORTC   // this maps to analog ports
#define SDA_PIN 4
#define SCL_PORT PORTC   // this maps to analog ports
#define SCL_PIN 5
#include <SoftI2CMaster.h>
#define DEVICE (0x5A<<1)
#include <RFM69.h>
#include <SPI.h>
#include <SPIFlash.h>
#include <Wire.h>
#include <LowPower.h>
//
#define NODEID        94                 // unique for each node on same network
#define NETWORKID     50                 // the same on all nodes that talk to each other
#define GATEWAYID     1
#define FREQUENCY     RF69_868MHZ
#define ENCRYPTKEY    "thisIsEncryptKey" // exactly the same 16 characters/bytes on all nodes!
#define ACK_TIME      30                 // max # of ms to wait for an ack
//
// Leave this in here
//
#ifdef __AVR_ATmega1284P__
  #define LED           15 // Moteino MEGAs have LEDs on D15
  #define FLASH_SS      23 // and FLASH SS on D23
#else
  #define LED           9 // Moteinos have LEDs on D9
  #define FLASH_SS      8 // and FLASH SS on D8
#endif

#define SERIAL_BAUD   115200

#define BEEPER 3  // give a short beep after power on

char buff[61];
byte sendSize=0;
boolean requestACK = false;
SPIFlash flash(FLASH_SS, 0xEF30); //EF30 for 4mbit  Windbond chip (W25X40CL)
RFM69 radio;

//-----------------------------------------------------------------------------------------------------------------------

long int cc;
float numerator,denominator,minimum,maximum,average;
const int nsamples0 = 30;  // take  30 samples every 2 seconds, then we radio the data back (so every minute)
const int nsamples1 = 150; // take 150 samples every 2 seconds, then we radio the data back (every 5 minutes)
const long int maxsec = 300;
const int stepsize = 0;    // not relevant at this (see code)
unsigned long int ctimer,ptimer;
long int sec;
float celsius;
int nsamples;

//----------------------------------------------------------------------------------------------------------------------
//
// http://stackoverflow.com/questions/20554869/implementing-crc8-on-arduino-to-write-to-mlx90614
//
//----------------------------------------------------------------------------------------------------------------------

static const uint8_t crc_table[] = {
    0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31,
    0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65,
    0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9,
    0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,
    0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1,
    0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2,
    0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe,
    0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a,
    0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16,
    0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42,
    0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80,
    0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,
    0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8,
    0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c,
    0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10,
    0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34,
    0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f,
    0x6a, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b,
    0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7,
    0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83,
    0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef,
    0xfa, 0xfd, 0xf4, 0xf3
};

//----------------------------------------------------------------------------------------------------------------------

uint8_t crc8(uint8_t *p, uint8_t len)
{
   uint16_t i;
   uint16_t crc = 0x0;
   while (len--) {
     i = (crc ^ *p++) & 0xFF;
     crc = (crc_table[i] ^ (crc << 8)) & 0xFF;
   }
   return crc;
}

//----------------------------------------------------------------------------------------------------------------------

uint8_t CRC8(const uint8_t *data, uint8_t len)         // computes crc value
{
  int i,j;
  int c;
  int CRC=0;
  int genPoly = 0x07;
  for (j=0; j<len; j++)
  {
    c = data[j];
    CRC ^= c;
    for(i = 0; i<8; i++)
        if(CRC & 0x80 )
          CRC = (CRC << 1) ^ genPoly;
        else
          CRC <<= 1;
    CRC &= 0xff;
  }
  return CRC;
}

//----------------------------------------------------------------------------------------------------------------------

void Blink(byte PIN, int DELAY_MS)
{
  pinMode(PIN, OUTPUT);
  digitalWrite(PIN,HIGH);
  delay(DELAY_MS);
  digitalWrite(PIN,LOW);
}

uint8_t ResetEmissivity() 
{
  uint8_t pec0; 
  //
  {
    uint8_t writeArray0[] = {DEVICE + I2C_WRITE, 0x20 + 0x04, 0, 0}; 
    i2c_start_wait(writeArray0[0]);
    i2c_write(writeArray0[1]); //Register Address to write to
    i2c_write(writeArray0[2]); //Erase low byte (write 0)
    i2c_write(writeArray0[3]); //Erase high byte (write 0)
    i2c_write(0xE8); //Send PEC
    i2c_stop(); 
  } delay(100); {
    uint8_t writeArray0[] = {DEVICE + I2C_WRITE, 0x20 + 0x04, 0xff, 0xff}; 
    pec0 = CRC8(writeArray0, 4);
    i2c_start_wait(writeArray0[0]);
    i2c_write(writeArray0[1]); //Register Address to write to
    i2c_write(writeArray0[2]); //Erase low byte (write 0)
    i2c_write(writeArray0[3]); //Erase high byte (write 0)
    i2c_write(0xCC); //Send PEC
    i2c_stop(); 
  } delay(100); 
}

//----------------------------------------------------------------------------------------------------------------------

uint8_t SetEmissivity( uint16_t setting, uint8_t *buf )
{
  uint8_t data_low = setting;
  uint8_t data_high = setting >> 8;
  buf[0] = 0x24; buf[1] = data_low; buf[2] = data_high; 
  uint8_t data_pec1 = CRC8(buf,3); // just avoid some space, in doubt compare crcr8 and CRC8
  uint8_t data_pec2 = CRC8(buf,3);
  //  
  buf[0] = data_low; buf[1] = data_high; buf[2] = data_pec1;  buf[3] = data_pec2;
  //
  // first erase it 
  //
  uint8_t writeArray0[4] = {DEVICE + I2C_WRITE, 0x20 + 0x04, 0, 0};
  uint8_t pec0 = CRC8(writeArray0, 4); 
  i2c_start_wait(writeArray0[0]);
  i2c_write(writeArray0[1]); //Register Address to write to
  i2c_write(writeArray0[2]); //Erase low byte (write 0)
  i2c_write(writeArray0[3]); //Erase high byte (write 0)
  i2c_write(pec0); //Send PEC
  i2c_stop(); 
  //
  // then write it
  //
  uint8_t writeArray1[4] = {DEVICE + I2C_WRITE, 0x20 + 0x04, data_low, data_high};
  uint8_t pec1 = CRC8(writeArray1, 4); 
  i2c_start_wait(writeArray1[0]);
  i2c_write(writeArray1[1]); //Register Address to write to
  i2c_write(writeArray1[2]); //Erase low byte (write 0)
  i2c_write(writeArray1[3]); //Erase high byte (write 0)
  i2c_write(pec1); //Send PEC
  i2c_stop();   
  return true;
}

//----------------------------------------------------------------------------------------------------------------------

uint8_t GetEmissivity( uint16_t &result, uint8_t *buf )
{
  uint8_t data_low = 0;
  uint8_t data_high = 0;
  uint8_t data_pec = 0;
  //
  if (!i2c_start(DEVICE+I2C_WRITE)) return false;
  i2c_write(0x24);                // emissivity correction stored in EEPROM
  i2c_rep_start(DEVICE+I2C_READ);
  data_low = i2c_read(false);     // write low byte and then send ack
  data_high = i2c_read(false);    // write high byte and then send ack
  data_pec = i2c_read(true);          // write pec byte and terminate
  i2c_stop();
  result = ((data_high << 8) + data_low);
  buf[0] = data_low; buf[1] = data_high; buf[2] = data_pec;
  return true;
}

//----------------------------------------------------------------------------------------------------------------------

void setup()
{
  Serial.begin(115200);
  i2c_init();
  //
  // initialize the radiomodem, and just say hello to the gateway
  //
  radio.initialize(FREQUENCY,NODEID,NETWORKID);
  radio.encrypt(ENCRYPTKEY);
  sprintf(buff, "\nTransmitting at %d Mhz...", FREQUENCY==RF69_433MHZ ? 433 : FREQUENCY==RF69_868MHZ ? 868 : 915);
  Serial.println(buff);
  if (flash.initialize()) {
    Serial.print("SPI Flash Init OK ... UniqueID (MAC): ");
    flash.readUniqueId();
    for (byte i=0;i<8;i++)
    {
      Serial.print(flash.UNIQUEID[i], HEX);
      Serial.print(' ');
    }
    Serial.println();
  } else {
    Serial.println("SPI Flash Init FAIL! (is chip present?)");
  }
  sprintf(buff,"IR thermometer");
  radio.sendWithRetry( GATEWAYID,buff,strlen(buff) );
  uint16_t value1,value2; uint8_t buf[10];
  //
  // If you need to reset/set/verify the emissivity correctionthen do that here
  //
  //Serial.println("Reset emissivity correction");
  //ResetEmissivity();
  //
  //value1 = 0xffff;
  //SetEmissivity(value1,buf);
  //
  // get the emissivity 
  //
  if (GetEmissivity(value1,buf)) {
    Serial.print("E1 = "); Serial.print(value1,HEX);
    Serial.print(" : ");  Serial.print(buf[0],HEX);
    Serial.print(" ");    Serial.print(buf[1],HEX);
    Serial.print(" ");    Serial.println(buf[2],HEX);
    sprintf(buff,"EMISSIVITY %4.4X %2.2X",value1,buf[2]);
      radio.sendWithRetry( GATEWAYID,buff,strlen(buff) );
  }
  // set the emissivity (becomes effective when you do a hard restart)
  /*
  double emissivity = 0.85; emissivity = emissivity * 65535.0;
  value2 = uint16_t(emissivity);
  if (SetEmissivity(value2,buf)) {
    Serial.print("E2 = "); Serial.print(value2,HEX);
    Serial.print(" : ");   Serial.print(buf[0],HEX);
    Serial.print(" ");     Serial.print(buf[1],HEX);
    Serial.print(" ");     Serial.print(buf[2],HEX);
    Serial.print(" (");    Serial.print(buf[3],HEX);  Serial.println(")");
  }
  */
  cc = 0;
  //
  // and beep that we are ready to start
  //
  pinMode(BEEPER, OUTPUT);
  digitalWrite(BEEPER,HIGH);
  delay(30);
  digitalWrite(BEEPER,LOW);
  //
  float celsius;
  long major1,minor1,major2,minor2;
  celsius = GetTemp(0); d2i( celsius,major1,minor1 );
  celsius = GetTemp(1); d2i( celsius,major2,minor2 );
  sprintf(buff,"TO %ld.%2.2ld TA %ld.%2.2ld LOOPS %ds/%ds",major1,minor1,major2,minor2,nsamples0*2,nsamples1*2);
  if (radio.sendWithRetry( GATEWAYID,buff,strlen(buff) ) ) {
    delay(60);
    digitalWrite(BEEPER,HIGH);
    delay(30);
    digitalWrite(BEEPER,LOW);
  }
}

//----------------------------------------------------------------------------------------------------------------------

void d2i( float CC, long &major0, long &minor0 ) 
{
  float copy = CC; copy = copy * 100.0; copy = abs(copy); long icopy = copy;
  major0 = icopy / 100L; 
  minor0 = icopy % 100L;
  if (CC < 0.0) major0 = -major0;
  // Serial.print(major0); Serial.print(" ");  Serial.println(minor0); 
}

//----------------------------------------------------------------------------------------------------------------------

float GetTemp(uint8_t which)
{
  //
  // Get the object temperature here
  //
  int dev = DEVICE;
  int data_low = 0;
  int data_high = 0;
  int pec = 0;    
  i2c_start(dev+I2C_WRITE);
  if (which == 0) i2c_write(0x07); // read (for the object temperature)
  if (which == 1) i2c_write(0x06); // read (for the die temperature)
  i2c_rep_start(dev+I2C_READ);
  data_low = i2c_read(false); //Read 1 byte and then send ack
  data_high = i2c_read(false); //Read 1 byte and then send ack
  pec = i2c_read(true);
  i2c_stop();
  //This converts high and low bytes together and processes temperature, MSB is a error bit and is ignored for temps
  double tempFactor = 0.02; // 0.02 degrees per LSB (measurement resolution of the MLX90614)
  double tempData = 0x0000; // zero out the data
  int frac; // data past the decimal point
  // This masks off the error bit of the high byte, then moves it left 8 bits and adds the low byte.
  tempData = (double)(((data_high & 0x007F) << 8) + data_low); 
  tempData = (tempData * tempFactor)-0.01;
  float celsius = tempData - 273.15;
  return celsius;
}

//----------------------------------------------------------------------------------------------------------------------
    
void loop()
{   
  sec = cc * 2;
  if (sec < maxsec) {
    nsamples = nsamples0;
  } else {
    nsamples = nsamples1;
  }
  celsius = GetTemp(0); 
  if ((celsius > -20.0) && (celsius < +70.0)) { // cancel the most obvious errors
    if ((cc % nsamples) == 0 ) { 
      numerator = 0; denominator = 0; minimum = 99999; maximum = -minimum;
    }
    numerator += celsius; denominator += 1.0; 
    average = numerator / denominator; 
    if (celsius < minimum) minimum = celsius;  
    if (celsius > maximum) maximum = celsius;  
    if ((cc++ % nsamples) == (nsamples-1)) { 
      Blink( LED,10 ); 
      float myavera = average;
      float dietemp = GetTemp(1);
      long number1 = round(myavera * 100.0);
      long number2 = round(minimum * 100.0);
      long number3 = round(maximum * 100.0);
      long number4 = round(dietemp * 100.0);
      sprintf(buff,"TEMP %5ld %5ld %5ld %5ld %ld",number1,number2,number3,number4,sec);
      Serial.println(buff);
      radio.sendWithRetry( GATEWAYID,buff,strlen(buff) );
      ptimer = ctimer;
     }
  }
  LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF); 
}

Last update: 13-oct-2014

Advertisements

One thought on “A wireless non-contact thermometer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s