2017-12-01 10:38:25 +01:00
|
|
|
# The MIT License (MIT)
|
|
|
|
#
|
|
|
|
# Copyright (c) 2017 Tony DiCola for Adafruit Industries
|
|
|
|
#
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
|
|
# in the Software without restriction, including without limitation the rights
|
|
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
#
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
#
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
# THE SOFTWARE.
|
|
|
|
"""
|
|
|
|
`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
|
|
|
|
"""
|
|
|
|
import math
|
|
|
|
import time
|
|
|
|
|
2017-12-07 21:24:08 +01:00
|
|
|
from micropython import const
|
2017-12-01 10:38:25 +01:00
|
|
|
|
2017-12-15 23:59:22 +01:00
|
|
|
import adafruit_bus_device.spi_device as spi_device
|
|
|
|
|
|
|
|
|
|
|
|
#pylint: disable=bad-whitespace
|
2017-12-01 10:38:25 +01:00
|
|
|
# Register and other constant values:
|
2017-12-15 23:59:22 +01:00
|
|
|
_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)
|
2017-12-07 21:24:08 +01:00
|
|
|
_RTD_A = 3.9083e-3
|
|
|
|
_RTD_B = -5.775e-7
|
2017-12-15 23:59:22 +01:00
|
|
|
#pylint: enable=bad-whitespace
|
2017-12-01 10:38:25 +01:00
|
|
|
|
|
|
|
|
|
|
|
class MAX31865:
|
2017-12-07 21:24:08 +01:00
|
|
|
"""Driver for the MAX31865 thermocouple amplifier."""
|
2017-12-01 10:38:25 +01:00
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
2017-12-07 21:24:08 +01:00
|
|
|
def __init__(self, spi, cs, *, rtd_nominal=100, ref_resistor=430.0, wires=2):
|
2017-12-01 10:38:25 +01:00
|
|
|
self.rtd_nominal = rtd_nominal
|
2017-12-02 00:52:35 +01:00
|
|
|
self.ref_resistor = ref_resistor
|
2017-12-07 03:13:48 +01:00
|
|
|
self._device = spi_device.SPIDevice(spi, cs, baudrate=500000,
|
|
|
|
polarity=0, phase=1)
|
2017-12-01 10:38:25 +01:00
|
|
|
# Set wire config register based on the number of wires specified.
|
2017-12-02 00:52:35 +01:00
|
|
|
if wires not in (2, 3, 4):
|
2017-12-01 10:38:25 +01:00
|
|
|
raise ValueError('Wires must be a value of 2, 3, or 4!')
|
2017-12-07 21:24:08 +01:00
|
|
|
config = self._read_u8(_MAX31865_CONFIG_REG)
|
2017-12-01 10:38:25 +01:00
|
|
|
if wires == 3:
|
2017-12-07 21:24:08 +01:00
|
|
|
config |= _MAX31865_CONFIG_3WIRE
|
2017-12-01 10:38:25 +01:00
|
|
|
else:
|
|
|
|
# 2 or 4 wire
|
2017-12-07 21:24:08 +01:00
|
|
|
config &= ~_MAX31865_CONFIG_3WIRE
|
|
|
|
self._write_u8(_MAX31865_CONFIG_REG, config)
|
2017-12-01 10:38:25 +01:00
|
|
|
# Default to no bias and no auto conversion.
|
|
|
|
self.bias = False
|
|
|
|
self.auto_convert = False
|
|
|
|
|
|
|
|
def _read_u8(self, address):
|
|
|
|
# Read an 8-bit unsigned value from the specified 8-bit address.
|
|
|
|
with self._device as device:
|
2017-12-02 00:52:35 +01:00
|
|
|
self._BUFFER[0] = address & 0x7F
|
2017-12-01 10:38:25 +01:00
|
|
|
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:
|
2017-12-07 03:13:48 +01:00
|
|
|
self._BUFFER[0] = (address | 0x80) & 0xFF
|
2017-12-01 10:38:25 +01:00
|
|
|
self._BUFFER[1] = val & 0xFF
|
2017-12-02 00:52:35 +01:00
|
|
|
device.write(self._BUFFER, end=2)
|
2017-12-01 10:38:25 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def bias(self):
|
2017-12-07 21:24:08 +01:00
|
|
|
"""True when the sensor's bias voltage is on"""
|
2017-12-01 10:38:25 +01:00
|
|
|
return bool(self._read_u8(_MAX31865_CONFIG_REG) & _MAX31865_CONFIG_BIAS)
|
|
|
|
|
|
|
|
@bias.setter
|
|
|
|
def bias(self, val):
|
2017-12-07 21:24:08 +01:00
|
|
|
config = self._read_u8(_MAX31865_CONFIG_REG)
|
2017-12-01 10:38:25 +01:00
|
|
|
if val:
|
2017-12-07 21:24:08 +01:00
|
|
|
config |= _MAX31865_CONFIG_BIAS # Enable bias.
|
2017-12-01 10:38:25 +01:00
|
|
|
else:
|
2017-12-07 21:24:08 +01:00
|
|
|
config &= ~_MAX31865_CONFIG_BIAS # Disable bias.
|
|
|
|
self._write_u8(_MAX31865_CONFIG_REG, config)
|
2017-12-01 10:38:25 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def auto_convert(self):
|
2017-12-07 21:24:08 +01:00
|
|
|
"""True when the sensor automatically does conversions."""
|
2017-12-01 10:38:25 +01:00
|
|
|
return bool(self._read_u8(_MAX31865_CONFIG_REG) & _MAX31865_CONFIG_MODEAUTO)
|
|
|
|
|
|
|
|
@auto_convert.setter
|
|
|
|
def auto_convert(self, val):
|
2017-12-07 21:24:08 +01:00
|
|
|
config = self._read_u8(_MAX31865_CONFIG_REG)
|
2017-12-01 10:38:25 +01:00
|
|
|
if val:
|
2017-12-07 21:24:08 +01:00
|
|
|
config |= _MAX31865_CONFIG_MODEAUTO # Enable auto convert.
|
2017-12-01 10:38:25 +01:00
|
|
|
else:
|
2017-12-07 21:24:08 +01:00
|
|
|
config &= ~_MAX31865_CONFIG_MODEAUTO # Disable auto convert.
|
|
|
|
self._write_u8(_MAX31865_CONFIG_REG, config)
|
2017-12-01 10:38:25 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def fault(self):
|
2017-12-07 21:24:08 +01:00
|
|
|
"""The fault state of the sensor. Use `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
|
2017-12-01 10:38:25 +01:00
|
|
|
"""
|
|
|
|
faults = self._read_u8(_MAX31865_FAULTSTAT_REG)
|
2017-12-15 23:59:22 +01:00
|
|
|
#pylint: disable=bad-whitespace
|
2017-12-01 10:38:25 +01:00
|
|
|
highthresh = bool(faults & _MAX31865_FAULT_HIGHTHRESH)
|
2017-12-15 23:59:22 +01:00
|
|
|
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)
|
|
|
|
#pylint: enable=bad-whitespace
|
2017-12-01 10:38:25 +01:00
|
|
|
return (highthresh, lowthresh, refinlow, refinhigh, rtdinlow, ovuv)
|
|
|
|
|
|
|
|
def clear_faults(self):
|
|
|
|
"""Clear any fault state previously detected by the sensor."""
|
2017-12-07 21:24:08 +01:00
|
|
|
config = self._read_u8(_MAX31865_CONFIG_REG)
|
|
|
|
config &= ~0x2C
|
|
|
|
config |= _MAX31865_CONFIG_FAULTSTAT
|
|
|
|
self._write_u8(_MAX31865_CONFIG_REG, config)
|
2017-12-01 10:38:25 +01:00
|
|
|
|
|
|
|
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
|
2017-12-07 21:24:08 +01:00
|
|
|
nominal value of the resistance-to-digital conversion and some math. If you just want
|
|
|
|
temperature use the temperature property instead.
|
2017-12-01 10:38:25 +01:00
|
|
|
"""
|
|
|
|
self.clear_faults()
|
|
|
|
self.bias = True
|
|
|
|
time.sleep(0.01)
|
2017-12-07 21:24:08 +01:00
|
|
|
config = self._read_u8(_MAX31865_CONFIG_REG)
|
|
|
|
config |= _MAX31865_CONFIG_1SHOT
|
|
|
|
self._write_u8(_MAX31865_CONFIG_REG, config)
|
2017-12-01 10:38:25 +01:00
|
|
|
time.sleep(0.065)
|
|
|
|
rtd = self._read_u16(_MAX31865_RTDMSB_REG)
|
|
|
|
# Remove fault bit.
|
|
|
|
rtd >>= 1
|
|
|
|
return rtd
|
|
|
|
|
|
|
|
@property
|
|
|
|
def temperature(self):
|
2017-12-07 21:24:08 +01:00
|
|
|
"""The temperature of the sensor in degrees Celsius."""
|
|
|
|
raw_reading = self.read_rtd()
|
|
|
|
raw_reading /= 32768
|
|
|
|
raw_reading *= self.ref_resistor
|
|
|
|
|
|
|
|
# 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
|
2017-12-01 10:38:25 +01:00
|
|
|
Z1 = -_RTD_A
|
2017-12-07 03:13:48 +01:00
|
|
|
Z2 = _RTD_A * _RTD_A - (4 * _RTD_B)
|
|
|
|
Z3 = (4 * _RTD_B) / self.rtd_nominal
|
2017-12-01 10:38:25 +01:00
|
|
|
Z4 = 2 * _RTD_B
|
2017-12-07 21:24:08 +01:00
|
|
|
temp = Z2 + (Z3 * raw_reading)
|
2017-12-01 10:38:25 +01:00
|
|
|
temp = (math.sqrt(temp) + Z1) / Z4
|
|
|
|
if temp >= 0:
|
|
|
|
return temp
|
2017-12-07 21:24:08 +01:00
|
|
|
rpoly = raw_reading
|
2017-12-01 10:38:25 +01:00
|
|
|
temp = -242.02
|
|
|
|
temp += 2.2228 * rpoly
|
2017-12-07 21:24:08 +01:00
|
|
|
rpoly *= raw_reading # square
|
2017-12-01 10:38:25 +01:00
|
|
|
temp += 2.5859e-3 * rpoly
|
2017-12-07 21:24:08 +01:00
|
|
|
rpoly *= raw_reading # ^3
|
2017-12-01 10:38:25 +01:00
|
|
|
temp -= 4.8260e-6 * rpoly
|
2017-12-07 21:24:08 +01:00
|
|
|
rpoly *= raw_reading # ^4
|
2017-12-01 10:38:25 +01:00
|
|
|
temp -= 2.8183e-8 * rpoly
|
2017-12-07 21:24:08 +01:00
|
|
|
rpoly *= raw_reading # ^5
|
2017-12-01 10:38:25 +01:00
|
|
|
temp += 1.5243e-10 * rpoly
|
|
|
|
return temp
|