/ 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