Temperature is one of the most frequently measured physical quantities, and features prominently in many of our projects, from weather stations to 3D printers. Most commonly we’ll see thermistors, thermocouples, infrared sensors, or a dedicated IC used to measure temperature. It’s even possible to use only an ordinary diode, leading to some interesting techniques.
Often we only need to know the temperature within a degree Celsius or two, and any of these tools are fine. Until fairly recently, when we needed to know the temperature precisely, reliably, and over a wide range we used mercury thermometers. The devices themselves were marvels of instrumentation, but mercury is a hazardous substance, and since 2011 NIST will no longer calibrate mercury thermometers.
Luckily, resistance temperature detectors (RTDs) are an excellent alternative. These usually consist of very thin wires of pure platinum, and are identified by their resistance at 0 °C. For example, a Pt100 RTD has a resistance of 100 Ω at 0 °C.
An accuracy of +/- 0.15 °C at 0 °C is typical, but accuracies down to +/- 0.03 °C are available. The functional temperature range is typically quite high, with -70 °C to 200 °C being common, with some specialized probes working well over 900 °C.
It’s not uncommon for the lead wires on these probes to be a meter or more in length, and this can be a significant source of error. To account for this, you will see that RTD probes are sold in two, three, and four wire configurations. Two-wire configurations do not account for lead wire resistance, three-wire probes account for lead resistance but assume all lead wires have the same resistance, and four-wire configurations are most effective at eliminating this error.
In this article we’ll be using a 3-wire probe as it’s a good balance between cost, space, and accuracy. I found this detailed treatment of the differences between probe types useful in making this decision.
Circuits for Reading a Resistance Temperature Detector
Unfortunately, RTDs come with a few difficulties. Mainly, they are a resistive sensor and subject to self-heating with anything but the smallest of currents, requiring a constant-current supply. You might build this out of operational amplifiers and a wheatstone bridge.
Not all of us enjoy analog circuit design, and there is a decent IC that will handle it for you: the MAX31865 RTD to digital converter. It works by comparing the RTD probe resistance to a reference resistor, and is fairly easy to design a board around (the chip is available in both SSOP and TQFN form factors). This chip is readily available in pre-assembled Arduino-compatible modules, and is supported by an Arduino library. However, that’s not for everyone, and today we will cover how to interact with this useful chip over SPI.
Regardless of your platform, most MAX31865 modules require some initial setup. Some areas of the board will have to be bridged or have traces cut to set what type of probe you are using.
A Test Run with ESP8266 and Lua
In this example, I’ll be using Lua on an ESP8266 microcontroller running NodeMCU, but similar steps should work on a platform of your choice. Our first step will be to configure SPI. The MAX31865 datasheet (PDF) states that only SPI modes 1 and 3 with 8 data bits, and a maximum clock of 5 MHz are supported. A quick search on Wikipedia returned a table with the SPI settings for each mode.
I chose SPI mode 1 and a hefty clock divider of 256. I also set up an output pin to use as the Chip Select pin. This pin needs to be driven low to initiate reads and writes, then driven high once the operation is completed. The resulting code is:
pin = 1 spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_HIGH, 8, 256); gpio.mode(pin, gpio.OUTPUT)
Next we define sending and receiving functions based on the required behavior of the Chip Select pin. The returned integer after a read will always be between 0 and 255. The ESP8266 will interpret this as an ASCII character, so we use string.byte to convert it to a decimal value:
function spi_write(target, data) gpio.write(pin, gpio.LOW) tmr.delay(20) spi.send(1, target) tmr.delay(20) spi.send(1, data) gpio.write(pin, gpio.HIGH) tmr.delay(20) end function spi_read (target) gpio.write(pin, gpio.LOW) tmr.delay(20) spi.send(1, target) tmr.delay(20) x = spi.recv(1,1) print (string.byte (x, 1)) tmr.delay(20) gpio.write(pin, gpio.HIGH) tmr.delay(20) end
Configuring MAX31865 Behavior
Using the SPI read and write functions, we can now use the MAX31865 chip. There are only 8 registers on the chip, each containing 8 bits of data. To read from a register we precede it with a ‘0’, and to write to it, an ‘8’. For example, to read from register 1 we would use spi_read(0x01), and to write the number 0xFF to it we would use spi_write(0x81,0xFF).
The main configuration register is register 0 and it works according to the following table:
Since I want to automatically read every second, I need to turn on both Vbias and set conversion mode to Auto, so that’s ‘11’. I don’t want to use 1-shot, so that gets written ‘0’. I’m using a 3-wire probe, so that’s a ‘1’. I’ll clear fault detection for now, so that’s ‘00’. I want to start with fault status clear, so that’s a ‘1’, and the electrical frequency in my area is 50Hz so that’s a ‘1’ too. Concatenating that all together, we get ‘1101 0011’ or hexidecimal 0xD3.
The chip supports alert thresholds when the temperature goes above or below a value. I won’t be using these, so I’ll set them to extreme values.
spi_write(0x83, 0x7f) spi_write(0x84, 0xff) spi_write(0x85, 0x00) spi_write(0x86, 0x00)
Reading Temperature Data
Now that it’s set up, we can finally read data. The data are 15-bit ratios between the reference resistor (in my case 427 Ω) and the RTD probe. As the temperature of the probe increases, its resistance increases in a nearly linear fashion.
According to the datasheet, the temperature can be approximated from this with the formula:
However, there’s a good reason not to do this: the error is up to 1.75 °C in the range of +/- 100 °C. Also it assumes a 400 Ω reference resistor, so you’ll likely need to correct for that. This level of accuracy is not significantly better than cheaper, easier-to-use devices, but is useful to determine if your probe is more or less reading correctly.
To improve accuracy, we have to consider the slight nonlinearity of the relationship between probe resistance and temperature. There are a few ways to do this, but the most commonly used is the Callendar-Van Dusen equation. This is typically a second-order polynomial of the form that predicts the resistance at a given temperature as:
This is accurate up to about 660 °C (more complex functions can handle higher temperatures). The probe specification states that the values for a and b are 3.90830 x 10-3 and -5.77500 x 10-7 respectively. With those constants, we can write an implementation as follows:
#Read high ADC register (8 bits) gpio.write(pin, gpio.LOW) tmr.delay(20) spi.send(1, 0x01) tmr.delay(20) x = spi.recv(1,1) x = (string.byte (x, 1)) tmr.delay(20) gpio.write(pin, gpio.HIGH) tmr.delay(20) #Read low ADC register (7 bits) gpio.write(pin, gpio.LOW) tmr.delay(20) spi.send(1, 0x02) tmr.delay(20) y = spi.recv(1,2) y = (string.byte (y, 1)) tmr.delay(20) gpio.write(pin, gpio.HIGH) #print out approximation of temperature from function in datasheet, accounting for our resistor being 427 Ω, not 400 Ω. Note that the ADC value is 15 bits long, not 16. z = tonumber(x)*127+tonumber(y) z = z*(1.0675) z = (z/32) - 256 print (z) #implement Callendar-Van Dusen equation to provide more accurate result a = 0.00390830 b = -0.0000005775 z = tonumber(x)*127+tonumber(y) z = (z/(2^15))*427 z = (-100*a+((10000*(a)^2) - 400*b*(100-z))^0.5)/(200*b) print (z)
This worked quite well, with the Callendar-Van Dusen equation providing a result of about 0.15 °C higher than the approximation at 30 °C.
If you’re still holding out and have a mercury thermometer, it might be time to take a deep breath (assuming it is intact), put aside the nostalgia, have it recycled, and move on.