/ adafruit_max31865.py
adafruit_max31865.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2017 Tony DiCola for Adafruit Industries 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_max31865` 24 ==================================================== 25 26 CircuitPython module for the MAX31865 platinum RTD temperature sensor. See 27 examples/simpletest.py for an example of the usage. 28 29 * Author(s): Tony DiCola 30 31 Implementation Notes 32 -------------------- 33 34 **Hardware:** 35 36 * Adafruit `Universal Thermocouple Amplifier MAX31856 Breakout 37 <https://www.adafruit.com/product/3263>`_ (Product ID: 3263) 38 39 * Adafruit `PT100 RTD Temperature Sensor Amplifier - MAX31865 40 <https://www.adafruit.com/product/3328>`_ (Product ID: 3328) 41 42 * Adafruit `PT1000 RTD Temperature Sensor Amplifier - MAX31865 43 <https://www.adafruit.com/product/3648>`_ (Product ID: 3648) 44 45 **Software and Dependencies:** 46 47 * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: 48 https://github.com/adafruit/circuitpython/releases 49 * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice 50 """ 51 import math 52 import time 53 54 from micropython import const 55 56 import adafruit_bus_device.spi_device as spi_device 57 58 __version__ = "0.0.0-auto.0" 59 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MAX31865.git" 60 61 # pylint: disable=bad-whitespace 62 # Register and other constant values: 63 _MAX31865_CONFIG_REG = const(0x00) 64 _MAX31865_CONFIG_BIAS = const(0x80) 65 _MAX31865_CONFIG_MODEAUTO = const(0x40) 66 _MAX31865_CONFIG_MODEOFF = const(0x00) 67 _MAX31865_CONFIG_1SHOT = const(0x20) 68 _MAX31865_CONFIG_3WIRE = const(0x10) 69 _MAX31865_CONFIG_24WIRE = const(0x00) 70 _MAX31865_CONFIG_FAULTSTAT = const(0x02) 71 _MAX31865_CONFIG_FILT50HZ = const(0x01) 72 _MAX31865_CONFIG_FILT60HZ = const(0x00) 73 _MAX31865_RTDMSB_REG = const(0x01) 74 _MAX31865_RTDLSB_REG = const(0x02) 75 _MAX31865_HFAULTMSB_REG = const(0x03) 76 _MAX31865_HFAULTLSB_REG = const(0x04) 77 _MAX31865_LFAULTMSB_REG = const(0x05) 78 _MAX31865_LFAULTLSB_REG = const(0x06) 79 _MAX31865_FAULTSTAT_REG = const(0x07) 80 _MAX31865_FAULT_HIGHTHRESH = const(0x80) 81 _MAX31865_FAULT_LOWTHRESH = const(0x40) 82 _MAX31865_FAULT_REFINLOW = const(0x20) 83 _MAX31865_FAULT_REFINHIGH = const(0x10) 84 _MAX31865_FAULT_RTDINLOW = const(0x08) 85 _MAX31865_FAULT_OVUV = const(0x04) 86 _RTD_A = 3.9083e-3 87 _RTD_B = -5.775e-7 88 # pylint: enable=bad-whitespace 89 90 91 class MAX31865: 92 """Driver for the MAX31865 thermocouple amplifier.""" 93 94 # Class-level buffer for reading and writing data with the sensor. 95 # This reduces memory allocations but means the code is not re-entrant or 96 # thread safe! 97 _BUFFER = bytearray(3) 98 99 def __init__( 100 self, 101 spi, 102 cs, 103 *, 104 rtd_nominal=100, 105 ref_resistor=430.0, 106 wires=2, 107 filter_frequency=60 108 ): 109 self.rtd_nominal = rtd_nominal 110 self.ref_resistor = ref_resistor 111 self._device = spi_device.SPIDevice( 112 spi, cs, baudrate=500000, polarity=0, phase=1 113 ) 114 # Set 50Hz or 60Hz filter. 115 if filter_frequency not in (50, 60): 116 raise ValueError("Filter_frequency must be a value of 50 or 60!") 117 config = self._read_u8(_MAX31865_CONFIG_REG) 118 if filter_frequency == 50: 119 config |= _MAX31865_CONFIG_FILT50HZ 120 else: 121 config &= ~_MAX31865_CONFIG_FILT50HZ 122 123 # Set wire config register based on the number of wires specified. 124 if wires not in (2, 3, 4): 125 raise ValueError("Wires must be a value of 2, 3, or 4!") 126 if wires == 3: 127 config |= _MAX31865_CONFIG_3WIRE 128 else: 129 # 2 or 4 wire 130 config &= ~_MAX31865_CONFIG_3WIRE 131 self._write_u8(_MAX31865_CONFIG_REG, config) 132 # Default to no bias and no auto conversion. 133 self.bias = False 134 self.auto_convert = False 135 136 # pylint: disable=no-member 137 def _read_u8(self, address): 138 # Read an 8-bit unsigned value from the specified 8-bit address. 139 with self._device as device: 140 self._BUFFER[0] = address & 0x7F 141 device.write(self._BUFFER, end=1) 142 device.readinto(self._BUFFER, end=1) 143 return self._BUFFER[0] 144 145 def _read_u16(self, address): 146 # Read a 16-bit BE unsigned value from the specified 8-bit address. 147 with self._device as device: 148 self._BUFFER[0] = address & 0x7F 149 device.write(self._BUFFER, end=1) 150 device.readinto(self._BUFFER, end=2) 151 return (self._BUFFER[0] << 8) | self._BUFFER[1] 152 153 def _write_u8(self, address, val): 154 # Write an 8-bit unsigned value to the specified 8-bit address. 155 with self._device as device: 156 self._BUFFER[0] = (address | 0x80) & 0xFF 157 self._BUFFER[1] = val & 0xFF 158 device.write(self._BUFFER, end=2) 159 160 # pylint: enable=no-member 161 162 @property 163 def bias(self): 164 """The state of the sensor's bias (True/False).""" 165 return bool(self._read_u8(_MAX31865_CONFIG_REG) & _MAX31865_CONFIG_BIAS) 166 167 @bias.setter 168 def bias(self, val): 169 config = self._read_u8(_MAX31865_CONFIG_REG) 170 if val: 171 config |= _MAX31865_CONFIG_BIAS # Enable bias. 172 else: 173 config &= ~_MAX31865_CONFIG_BIAS # Disable bias. 174 self._write_u8(_MAX31865_CONFIG_REG, config) 175 176 @property 177 def auto_convert(self): 178 """The state of the sensor's automatic conversion 179 mode (True/False). 180 """ 181 return bool(self._read_u8(_MAX31865_CONFIG_REG) & _MAX31865_CONFIG_MODEAUTO) 182 183 @auto_convert.setter 184 def auto_convert(self, val): 185 config = self._read_u8(_MAX31865_CONFIG_REG) 186 if val: 187 config |= _MAX31865_CONFIG_MODEAUTO # Enable auto convert. 188 else: 189 config &= ~_MAX31865_CONFIG_MODEAUTO # Disable auto convert. 190 self._write_u8(_MAX31865_CONFIG_REG, config) 191 192 @property 193 def fault(self): 194 """The fault state of the sensor. Use ``clear_faults()`` to clear the 195 fault state. Returns a 6-tuple of boolean values which indicate if any 196 faults are present: 197 198 - HIGHTHRESH 199 - LOWTHRESH 200 - REFINLOW 201 - REFINHIGH 202 - RTDINLOW 203 - OVUV 204 """ 205 faults = self._read_u8(_MAX31865_FAULTSTAT_REG) 206 # pylint: disable=bad-whitespace 207 highthresh = bool(faults & _MAX31865_FAULT_HIGHTHRESH) 208 lowthresh = bool(faults & _MAX31865_FAULT_LOWTHRESH) 209 refinlow = bool(faults & _MAX31865_FAULT_REFINLOW) 210 refinhigh = bool(faults & _MAX31865_FAULT_REFINHIGH) 211 rtdinlow = bool(faults & _MAX31865_FAULT_RTDINLOW) 212 ovuv = bool(faults & _MAX31865_FAULT_OVUV) 213 # pylint: enable=bad-whitespace 214 return (highthresh, lowthresh, refinlow, refinhigh, rtdinlow, ovuv) 215 216 def clear_faults(self): 217 """Clear any fault state previously detected by the sensor.""" 218 config = self._read_u8(_MAX31865_CONFIG_REG) 219 config &= ~0x2C 220 config |= _MAX31865_CONFIG_FAULTSTAT 221 self._write_u8(_MAX31865_CONFIG_REG, config) 222 223 def read_rtd(self): 224 """Perform a raw reading of the thermocouple and return its 15-bit 225 value. You'll need to manually convert this to temperature using the 226 nominal value of the resistance-to-digital conversion and some math. If you just want 227 temperature use the temperature property instead. 228 """ 229 self.clear_faults() 230 self.bias = True 231 time.sleep(0.01) 232 config = self._read_u8(_MAX31865_CONFIG_REG) 233 config |= _MAX31865_CONFIG_1SHOT 234 self._write_u8(_MAX31865_CONFIG_REG, config) 235 time.sleep(0.065) 236 rtd = self._read_u16(_MAX31865_RTDMSB_REG) 237 # Remove fault bit. 238 rtd >>= 1 239 return rtd 240 241 @property 242 def resistance(self): 243 """Read the resistance of the RTD and return its value in Ohms.""" 244 resistance = self.read_rtd() 245 resistance /= 32768 246 resistance *= self.ref_resistor 247 return resistance 248 249 @property 250 def temperature(self): 251 """Read the temperature of the sensor and return its value in degrees 252 Celsius. 253 """ 254 # This math originates from: 255 # http://www.analog.com/media/en/technical-documentation/application-notes/AN709_0.pdf 256 # To match the naming from the app note we tell lint to ignore the Z1-4 257 # naming. 258 # pylint: disable=invalid-name 259 raw_reading = self.resistance 260 Z1 = -_RTD_A 261 Z2 = _RTD_A * _RTD_A - (4 * _RTD_B) 262 Z3 = (4 * _RTD_B) / self.rtd_nominal 263 Z4 = 2 * _RTD_B 264 temp = Z2 + (Z3 * raw_reading) 265 temp = (math.sqrt(temp) + Z1) / Z4 266 if temp >= 0: 267 return temp 268 269 # For the following math to work, nominal RTD resistance must be normalized to 100 ohms 270 raw_reading /= self.rtd_nominal 271 raw_reading *= 100 272 273 rpoly = raw_reading 274 temp = -242.02 275 temp += 2.2228 * rpoly 276 rpoly *= raw_reading # square 277 temp += 2.5859e-3 * rpoly 278 rpoly *= raw_reading # ^3 279 temp -= 4.8260e-6 * rpoly 280 rpoly *= raw_reading # ^4 281 temp -= 2.8183e-8 * rpoly 282 rpoly *= raw_reading # ^5 283 temp += 1.5243e-10 * rpoly 284 return temp