/ adafruit_max31856.py
adafruit_max31856.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2018 Bryan Siepert 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 `MAX31856` 24 ==================================================== 25 26 CircuitPython module for the MAX31856 Universal Thermocouple Amplifier. See 27 examples/simpletest.py for an example of the usage. 28 29 * Author(s): Bryan Siepert 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 **Software and Dependencies:** 40 41 * Adafruit CircuitPython firmware for the supported boards: 42 https://github.com/adafruit/circuitpython/releases 43 44 * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice 45 """ 46 47 from time import sleep 48 from micropython import const 49 from adafruit_bus_device.spi_device import SPIDevice 50 51 try: 52 from struct import unpack 53 except ImportError: 54 from ustruct import unpack 55 56 __version__ = "0.0.0-auto.0" 57 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MAX31856.git" 58 59 # Register constants 60 _MAX31856_CR0_REG = const(0x00) 61 _MAX31856_CR0_AUTOCONVERT = const(0x80) 62 _MAX31856_CR0_1SHOT = const(0x40) 63 _MAX31856_CR0_OCFAULT1 = const(0x20) 64 _MAX31856_CR0_OCFAULT0 = const(0x10) 65 _MAX31856_CR0_CJ = const(0x08) 66 _MAX31856_CR0_FAULT = const(0x04) 67 _MAX31856_CR0_FAULTCLR = const(0x02) 68 69 _MAX31856_CR1_REG = const(0x01) 70 _MAX31856_MASK_REG = const(0x02) 71 _MAX31856_CJHF_REG = const(0x03) 72 _MAX31856_CJLF_REG = const(0x04) 73 _MAX31856_LTHFTH_REG = const(0x05) 74 _MAX31856_LTHFTL_REG = const(0x06) 75 _MAX31856_LTLFTH_REG = const(0x07) 76 _MAX31856_LTLFTL_REG = const(0x08) 77 _MAX31856_CJTO_REG = const(0x09) 78 _MAX31856_CJTH_REG = const(0x0A) 79 _MAX31856_CJTL_REG = const(0x0B) 80 _MAX31856_LTCBH_REG = const(0x0C) 81 _MAX31856_LTCBM_REG = const(0x0D) 82 _MAX31856_LTCBL_REG = const(0x0E) 83 _MAX31856_SR_REG = const(0x0F) 84 85 # fault types 86 _MAX31856_FAULT_CJRANGE = const(0x80) 87 _MAX31856_FAULT_TCRANGE = const(0x40) 88 _MAX31856_FAULT_CJHIGH = const(0x20) 89 _MAX31856_FAULT_CJLOW = const(0x10) 90 _MAX31856_FAULT_TCHIGH = const(0x08) 91 _MAX31856_FAULT_TCLOW = const(0x04) 92 _MAX31856_FAULT_OVUV = const(0x02) 93 _MAX31856_FAULT_OPEN = const(0x01) 94 95 96 class ThermocoupleType: # pylint: disable=too-few-public-methods 97 """An enum-like class representing the different types of thermocouples that the MAX31856 can 98 use. The values can be referenced like ``ThermocoupleType.K`` or ``ThermocoupleType.S`` 99 Possible values are 100 101 - ``ThermocoupleType.B`` 102 - ``ThermocoupleType.E`` 103 - ``ThermocoupleType.J`` 104 - ``ThermocoupleType.K`` 105 - ``ThermocoupleType.N`` 106 - ``ThermocoupleType.R`` 107 - ``ThermocoupleType.S`` 108 - ``ThermocoupleType.T`` 109 110 """ 111 112 # pylint: disable=invalid-name 113 B = 0b0000 114 E = 0b0001 115 J = 0b0010 116 K = 0b0011 117 N = 0b0100 118 R = 0b0101 119 S = 0b0110 120 T = 0b0111 121 G8 = 0b1000 122 G32 = 0b1100 123 124 125 class MAX31856: 126 """Driver for the MAX31856 Universal Thermocouple Amplifier 127 128 :param ~busio.SPI spi_bus: The SPI bus the MAX31856 is connected to. 129 :param ~microcontroller.Pin cs: The pin used for the CS signal. 130 :param ~adafruit_max31856.ThermocoupleType thermocouple_type: The type of thermocouple.\ 131 Default is Type K. 132 133 """ 134 135 # A class level buffer to reduce allocations for reading and writing. 136 # Tony says this isn't re-entrant or thread safe! 137 _BUFFER = bytearray(4) 138 139 def __init__(self, spi, cs, thermocouple_type=ThermocoupleType.K): 140 self._device = SPIDevice(spi, cs, baudrate=500000, polarity=0, phase=1) 141 142 # assert on any fault 143 self._write_u8(_MAX31856_MASK_REG, 0x0) 144 # configure open circuit faults 145 self._write_u8(_MAX31856_CR0_REG, _MAX31856_CR0_OCFAULT0) 146 147 # set thermocouple type 148 # get current value of CR1 Reg 149 conf_reg_1 = self._read_register(_MAX31856_CR1_REG, 1)[0] 150 conf_reg_1 &= 0xF0 # mask off bottom 4 bits 151 # add the new value for the TC type 152 conf_reg_1 |= int(thermocouple_type) & 0x0F 153 self._write_u8(_MAX31856_CR1_REG, conf_reg_1) 154 155 @property 156 def temperature(self): 157 """The temperature of the sensor and return its value in degrees celsius. (read-only)""" 158 self._perform_one_shot_measurement() 159 160 # unpack the 3-byte temperature as 4 bytes 161 raw_temp = unpack( 162 ">i", self._read_register(_MAX31856_LTCBH_REG, 3) + bytes([0]) 163 )[0] 164 165 # shift to remove extra byte from unpack needing 4 bytes 166 raw_temp >>= 8 167 168 # effectively shift raw_read >> 12 to convert pseudo-float 169 temp_float = raw_temp / 4096.0 170 171 return temp_float 172 173 @property 174 def reference_temperature(self): 175 """The temperature of the cold junction in degrees celsius. (read-only)""" 176 self._perform_one_shot_measurement() 177 178 raw_read = unpack(">h", self._read_register(_MAX31856_CJTH_REG, 2))[0] 179 180 # effectively shift raw_read >> 8 to convert pseudo-float 181 cold_junction_temp = raw_read / 256.0 182 183 return cold_junction_temp 184 185 @property 186 def temperature_thresholds(self): 187 """The thermocouple's low and high temperature thresholds 188 as a ``(low_temp, high_temp)`` tuple 189 """ 190 191 raw_low = unpack(">h", self._read_register(_MAX31856_LTLFTH_REG, 2)) 192 raw_high = unpack(">h", self._read_register(_MAX31856_LTHFTH_REG, 2)) 193 194 return (round(raw_low[0] / 16.0, 1), round(raw_high[0] / 16.0, 1)) 195 196 @temperature_thresholds.setter 197 def temperature_thresholds(self, val): 198 199 int_low = int(val[0] * 16) 200 int_high = int(val[1] * 16) 201 202 self._write_u8(_MAX31856_LTHFTH_REG, int_high >> 8) 203 self._write_u8(_MAX31856_LTHFTL_REG, int_high) 204 205 self._write_u8(_MAX31856_LTLFTH_REG, int_low >> 8) 206 self._write_u8(_MAX31856_LTLFTL_REG, int_low) 207 208 @property 209 def reference_temperature_thresholds(self): # pylint: disable=invalid-name 210 """The cold junction's low and high temperature thresholds 211 as a ``(low_temp, high_temp)`` tuple 212 """ 213 return ( 214 float(unpack("b", self._read_register(_MAX31856_CJLF_REG, 1))[0]), 215 float(unpack("b", self._read_register(_MAX31856_CJHF_REG, 1))[0]), 216 ) 217 218 @reference_temperature_thresholds.setter 219 def reference_temperature_thresholds(self, val): # pylint: disable=invalid-name 220 221 self._write_u8(_MAX31856_CJLF_REG, int(val[0])) 222 self._write_u8(_MAX31856_CJHF_REG, int(val[1])) 223 224 @property 225 def fault(self): 226 """A dictionary with the status of each fault type where the key is the fault type and the 227 value is a bool if the fault is currently active 228 229 =================== ================================= 230 Key Fault type 231 =================== ================================= 232 "cj_range" Cold junction range fault 233 "tc_range" Thermocouple range fault 234 "cj_high" Cold junction high threshold fault 235 "cj_low" Cold junction low threshold fault 236 "tc_high" Thermocouple high threshold fault 237 "tc_low" Thermocouple low threshold fault 238 "voltage" Over/under voltage fault 239 "open_tc" Thermocouple open circuit fault 240 =================== ================================= 241 242 """ 243 faults = self._read_register(_MAX31856_SR_REG, 1)[0] 244 245 return { 246 "cj_range": bool(faults & _MAX31856_FAULT_CJRANGE), 247 "tc_range": bool(faults & _MAX31856_FAULT_TCRANGE), 248 "cj_high": bool(faults & _MAX31856_FAULT_CJHIGH), 249 "cj_low": bool(faults & _MAX31856_FAULT_CJLOW), 250 "tc_high": bool(faults & _MAX31856_FAULT_TCHIGH), 251 "tc_low": bool(faults & _MAX31856_FAULT_TCLOW), 252 "voltage": bool(faults & _MAX31856_FAULT_OVUV), 253 "open_tc": bool(faults & _MAX31856_FAULT_OPEN), 254 } 255 256 def _perform_one_shot_measurement(self): 257 258 self._write_u8(_MAX31856_CJTO_REG, 0x0) 259 # read the current value of the first config register 260 conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0] 261 262 # and the complement to guarantee the autoconvert bit is unset 263 conf_reg_0 &= ~_MAX31856_CR0_AUTOCONVERT 264 # or the oneshot bit to ensure it is set 265 conf_reg_0 |= _MAX31856_CR0_1SHOT 266 267 # write it back with the new values, prompting the sensor to perform a measurement 268 self._write_u8(_MAX31856_CR0_REG, conf_reg_0) 269 270 sleep(0.250) 271 272 def _read_register(self, address, length): 273 # pylint: disable=no-member 274 # Read a 16-bit BE unsigned value from the specified 8-bit address. 275 with self._device as device: 276 self._BUFFER[0] = address & 0x7F 277 device.write(self._BUFFER, end=1) 278 device.readinto(self._BUFFER, end=length) 279 return self._BUFFER[:length] 280 281 def _write_u8(self, address, val): 282 # Write an 8-bit unsigned value to the specified 8-bit address. 283 with self._device as device: 284 self._BUFFER[0] = (address | 0x80) & 0xFF 285 self._BUFFER[1] = val & 0xFF 286 device.write(self._BUFFER, end=2) # pylint: disable=no-member