# SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_max31865`
====================================================
CircuitPython module for the MAX31865 platinum RTD temperature sensor. See
examples/simpletest.py for an example of the usage.
* Author(s): Tony DiCola
Implementation Notes
--------------------
**Hardware:**
* Adafruit `Universal Thermocouple Amplifier MAX31856 Breakout
<https://www.adafruit.com/product/3263>`_ (Product ID: 3263)
* Adafruit `PT100 RTD Temperature Sensor Amplifier - MAX31865
<https://www.adafruit.com/product/3328>`_ (Product ID: 3328)
* Adafruit `PT1000 RTD Temperature Sensor Amplifier - MAX31865
<https://www.adafruit.com/product/3648>`_ (Product ID: 3648)
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://circuitpython.org/downloads
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
"""
import math
import time
from micropython import const
import adafruit_bus_device.spi_device as spi_device
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MAX31865.git"
# Register and other constant values:
_MAX31865_CONFIG_REG = const(0x00)
_MAX31865_CONFIG_BIAS = const(0x80)
_MAX31865_CONFIG_MODEAUTO = const(0x40)
_MAX31865_CONFIG_MODEOFF = const(0x00)
_MAX31865_CONFIG_1SHOT = const(0x20)
_MAX31865_CONFIG_3WIRE = const(0x10)
_MAX31865_CONFIG_24WIRE = const(0x00)
_MAX31865_CONFIG_FAULTSTAT = const(0x02)
_MAX31865_CONFIG_FILT50HZ = const(0x01)
_MAX31865_CONFIG_FILT60HZ = const(0x00)
_MAX31865_RTDMSB_REG = const(0x01)
_MAX31865_RTDLSB_REG = const(0x02)
_MAX31865_HFAULTMSB_REG = const(0x03)
_MAX31865_HFAULTLSB_REG = const(0x04)
_MAX31865_LFAULTMSB_REG = const(0x05)
_MAX31865_LFAULTLSB_REG = const(0x06)
_MAX31865_FAULTSTAT_REG = const(0x07)
_MAX31865_FAULT_HIGHTHRESH = const(0x80)
_MAX31865_FAULT_LOWTHRESH = const(0x40)
_MAX31865_FAULT_REFINLOW = const(0x20)
_MAX31865_FAULT_REFINHIGH = const(0x10)
_MAX31865_FAULT_RTDINLOW = const(0x08)
_MAX31865_FAULT_OVUV = const(0x04)
_RTD_A = 3.9083e-3
_RTD_B = -5.775e-7
[docs]class MAX31865:
"""Driver for the MAX31865 thermocouple amplifier.
:param ~busio.SPI spi: SPI device
:param ~digitalio.DigitalInOut cs: Chip Select
:param int rtd_nominal: RTD nominal value. Defaults to :const:`100`
:param int ref_resistor: Reference resistance. Defaults to :const:`430.0`
:param int wires: Number of wires. Defaults to :const:`2`
:param int filter_frequency: . Filter frequency. Default to :const:`60`
**Quickstart: Importing and using the MAX31865**
Here is an example of using the :class:`MAX31865` class.
First you will need to import the libraries to use the sensor
.. code-block:: python
import board
from digitalio import DigitalInOut, Direction
import adafruit_max31865
Once this is done you can define your `board.SPI` object and define your sensor object
.. code-block:: python
spi = board.SPI()
cs = digitalio.DigitalInOut(board.D5) # Chip select of the MAX31865 board.
sensor = adafruit_max31865.MAX31865(spi, cs)
Now you have access to the :attr:`temperature` attribute
.. code-block:: python
temperature = sensor.temperature
"""
# Class-level buffer for reading and writing data with the sensor.
# This reduces memory allocations but means the code is not re-entrant or
# thread safe!
_BUFFER = bytearray(3)
def __init__(
self,
spi,
cs,
*,
rtd_nominal=100,
ref_resistor=430.0,
wires=2,
filter_frequency=60
):
self.rtd_nominal = rtd_nominal
self.ref_resistor = ref_resistor
self._device = spi_device.SPIDevice(
spi, cs, baudrate=500000, polarity=0, phase=1
)
# Set 50Hz or 60Hz filter.
if filter_frequency not in (50, 60):
raise ValueError("Filter_frequency must be a value of 50 or 60!")
config = self._read_u8(_MAX31865_CONFIG_REG)
if filter_frequency == 50:
config |= _MAX31865_CONFIG_FILT50HZ
else:
config &= ~_MAX31865_CONFIG_FILT50HZ
# Set wire config register based on the number of wires specified.
if wires not in (2, 3, 4):
raise ValueError("Wires must be a value of 2, 3, or 4!")
if wires == 3:
config |= _MAX31865_CONFIG_3WIRE
else:
# 2 or 4 wire
config &= ~_MAX31865_CONFIG_3WIRE
self._write_u8(_MAX31865_CONFIG_REG, config)
# Default to no bias and no auto conversion.
self.bias = False
self.auto_convert = False
# pylint: disable=no-member
def _read_u8(self, address):
# Read an 8-bit unsigned value from the specified 8-bit address.
with self._device as device:
self._BUFFER[0] = address & 0x7F
device.write(self._BUFFER, end=1)
device.readinto(self._BUFFER, end=1)
return self._BUFFER[0]
def _read_u16(self, address):
# Read a 16-bit BE unsigned value from the specified 8-bit address.
with self._device as device:
self._BUFFER[0] = address & 0x7F
device.write(self._BUFFER, end=1)
device.readinto(self._BUFFER, end=2)
return (self._BUFFER[0] << 8) | self._BUFFER[1]
def _write_u8(self, address, val):
# Write an 8-bit unsigned value to the specified 8-bit address.
with self._device as device:
self._BUFFER[0] = (address | 0x80) & 0xFF
self._BUFFER[1] = val & 0xFF
device.write(self._BUFFER, end=2)
# pylint: enable=no-member
@property
def bias(self):
"""The state of the sensor's bias (True/False)."""
return bool(self._read_u8(_MAX31865_CONFIG_REG) & _MAX31865_CONFIG_BIAS)
@bias.setter
def bias(self, val):
config = self._read_u8(_MAX31865_CONFIG_REG)
if val:
config |= _MAX31865_CONFIG_BIAS # Enable bias.
else:
config &= ~_MAX31865_CONFIG_BIAS # Disable bias.
self._write_u8(_MAX31865_CONFIG_REG, config)
@property
def auto_convert(self):
"""The state of the sensor's automatic conversion
mode (True/False).
"""
return bool(self._read_u8(_MAX31865_CONFIG_REG) & _MAX31865_CONFIG_MODEAUTO)
@auto_convert.setter
def auto_convert(self, val):
config = self._read_u8(_MAX31865_CONFIG_REG)
if val:
config |= _MAX31865_CONFIG_MODEAUTO # Enable auto convert.
else:
config &= ~_MAX31865_CONFIG_MODEAUTO # Disable auto convert.
self._write_u8(_MAX31865_CONFIG_REG, config)
@property
def fault(self):
"""The fault state of the sensor. Use :meth:`clear_faults` to clear the
fault state. Returns a 6-tuple of boolean values which indicate if any
faults are present:
- HIGHTHRESH
- LOWTHRESH
- REFINLOW
- REFINHIGH
- RTDINLOW
- OVUV
"""
faults = self._read_u8(_MAX31865_FAULTSTAT_REG)
highthresh = bool(faults & _MAX31865_FAULT_HIGHTHRESH)
lowthresh = bool(faults & _MAX31865_FAULT_LOWTHRESH)
refinlow = bool(faults & _MAX31865_FAULT_REFINLOW)
refinhigh = bool(faults & _MAX31865_FAULT_REFINHIGH)
rtdinlow = bool(faults & _MAX31865_FAULT_RTDINLOW)
ovuv = bool(faults & _MAX31865_FAULT_OVUV)
return (highthresh, lowthresh, refinlow, refinhigh, rtdinlow, ovuv)
[docs] def clear_faults(self):
"""Clear any fault state previously detected by the sensor."""
config = self._read_u8(_MAX31865_CONFIG_REG)
config &= ~0x2C
config |= _MAX31865_CONFIG_FAULTSTAT
self._write_u8(_MAX31865_CONFIG_REG, config)
[docs] def read_rtd(self):
"""Perform a raw reading of the thermocouple and return its 15-bit
value. You'll need to manually convert this to temperature using the
nominal value of the resistance-to-digital conversion and some math. If you just want
temperature use the temperature property instead.
"""
self.clear_faults()
self.bias = True
time.sleep(0.01)
config = self._read_u8(_MAX31865_CONFIG_REG)
config |= _MAX31865_CONFIG_1SHOT
self._write_u8(_MAX31865_CONFIG_REG, config)
time.sleep(0.065)
rtd = self._read_u16(_MAX31865_RTDMSB_REG)
self.bias = False
# Remove fault bit.
rtd >>= 1
return rtd
@property
def resistance(self):
"""Read the resistance of the RTD and return its value in Ohms."""
resistance = self.read_rtd()
resistance /= 32768
resistance *= self.ref_resistor
return resistance
@property
def temperature(self):
"""Read the temperature of the sensor and return its value in degrees
Celsius.
"""
# This math originates from:
# http://www.analog.com/media/en/technical-documentation/application-notes/AN709_0.pdf
# To match the naming from the app note we tell lint to ignore the Z1-4
# naming.
# pylint: disable=invalid-name
raw_reading = self.resistance
Z1 = -_RTD_A
Z2 = _RTD_A * _RTD_A - (4 * _RTD_B)
Z3 = (4 * _RTD_B) / self.rtd_nominal
Z4 = 2 * _RTD_B
temp = Z2 + (Z3 * raw_reading)
temp = (math.sqrt(temp) + Z1) / Z4
if temp >= 0:
return temp
# For the following math to work, nominal RTD resistance must be normalized to 100 ohms
raw_reading /= self.rtd_nominal
raw_reading *= 100
rpoly = raw_reading
temp = -242.02
temp += 2.2228 * rpoly
rpoly *= raw_reading # square
temp += 2.5859e-3 * rpoly
rpoly *= raw_reading # ^3
temp -= 4.8260e-6 * rpoly
rpoly *= raw_reading # ^4
temp -= 2.8183e-8 * rpoly
rpoly *= raw_reading # ^5
temp += 1.5243e-10 * rpoly
return temp