/ 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