Skip to content

Feat: multi-sample averaging and 50Hz mains filtering #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 15, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions adafruit_max31856.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
_MAX31856_CR0_CJ = const(0x08)
_MAX31856_CR0_FAULT = const(0x04)
_MAX31856_CR0_FAULTCLR = const(0x02)
_MAX31856_CR0_50HZ = const(0x01)

_MAX31856_CR1_REG = const(0x01)
_MAX31856_MASK_REG = const(0x02)
Expand Down Expand Up @@ -76,6 +77,8 @@
_MAX31856_FAULT_OVUV = const(0x02)
_MAX31856_FAULT_OPEN = const(0x01)

_AVGSEL_CONSTS = {1: 0x00, 2: 0x10, 4: 0x20, 8: 0x30, 16: 0x40}


class ThermocoupleType: # pylint: disable=too-few-public-methods
"""An enum-like class representing the different types of thermocouples that the MAX31856 can
Expand Down Expand Up @@ -113,6 +116,8 @@ class MAX31856:
:param ~microcontroller.Pin cs: The pin used for the CS signal.
:param ~adafruit_max31856.ThermocoupleType thermocouple_type: The type of thermocouple.\
Default is Type K.
:param ~int sampling: Number of samples to be averaged [1,2,4,8,16]
:param ~bool filter_50hz: Filter 50Hz mains frequency instead of 60Hz

**Quickstart: Importing and using the MAX31856**

Expand Down Expand Up @@ -155,13 +160,64 @@ def __init__(self, spi, cs, thermocouple_type=ThermocoupleType.K):
self._write_u8(_MAX31856_CR0_REG, _MAX31856_CR0_OCFAULT0)

# set thermocouple type
self._set_thermocouple_type(thermocouple_type)

def _set_thermocouple_type(self, thermocouple_type: ThermocoupleType):
# get current value of CR1 Reg
conf_reg_1 = self._read_register(_MAX31856_CR1_REG, 1)[0]
conf_reg_1 &= 0xF0 # mask off bottom 4 bits
# add the new value for the TC type
conf_reg_1 |= int(thermocouple_type) & 0x0F
self._write_u8(_MAX31856_CR1_REG, conf_reg_1)

@property
def averaging(self) -> int:
"""Number of samples averaged together in each result.
Must be 1, 2, 4, 8, or 16. Default is 1 (no averaging).
"""
conf_reg_1 = self._read_register(_MAX31856_CR1_REG, 1)[0]
avgsel = conf_reg_1 & ~0b10001111 # clear bits other than 4-6
# check which byte this corresponds to
for key, value in _AVGSEL_CONSTS.items():
if value == avgsel:
return key
raise KeyError(f"AVGSEL bit pattern was not recognised ({avgsel:>08b})")

@averaging.setter
def averaging(self, num_samples: int):
# This option is set in bits 4-6 of register CR1.
if num_samples not in _AVGSEL_CONSTS:
raise ValueError("Num_samples must be one of 1,2,4,8,16")
avgsel = _AVGSEL_CONSTS[num_samples]
# get current value of CR1 Reg
conf_reg_1 = self._read_register(_MAX31856_CR1_REG, 1)[0]
conf_reg_1 &= 0b10001111 # clear bits 4-6
# OR the AVGSEL bits (4-6)
conf_reg_1 |= avgsel
self._write_u8(_MAX31856_CR1_REG, conf_reg_1)

@property
def noise_rejection(self) -> int:
"""
The frequency (Hz) to be used by the noise rejection filter.
Must be 50 or 60. Default is 60."""
# this value is stored in bit 0 of register CR0.
conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0]
if conf_reg_0 & _MAX31856_CR0_50HZ:
return 50
return 60

@noise_rejection.setter
def noise_rejection(self, frequency: int):
conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0]
if frequency == 50:
conf_reg_0 |= _MAX31856_CR0_50HZ # set the 50hz bit
elif frequency == 60:
conf_reg_0 &= ~_MAX31856_CR0_50HZ # clear the 50hz bit
else:
raise ValueError("Frequency must be 50 or 60")
self._write_u8(_MAX31856_CR0_REG, conf_reg_0)

@property
def temperature(self):
"""Measure the temperature of the sensor and wait for the result.
Expand All @@ -170,7 +226,7 @@ def temperature(self):
return self.unpack_temperature()

def unpack_temperature(self) -> float:
'''Reads the probe temperature from the register'''
"""Reads the probe temperature from the register"""
# unpack the 3-byte temperature as 4 bytes
raw_temp = unpack(
">i", self._read_register(_MAX31856_LTCBH_REG, 3) + bytes([0])
Expand All @@ -184,16 +240,14 @@ def unpack_temperature(self) -> float:

return temp_float



@property
def reference_temperature(self):
"""Wait to retreive temperature of the cold junction in degrees Celsius. (read-only)"""
"""Wait to retrieve temperature of the cold junction in degrees Celsius. (read-only)"""
self._perform_one_shot_measurement()
return self.unpack_reference_temperature()

def unpack_reference_temperature(self) -> float:
'''Reads the reference temperature from the register'''
"""Reads the reference temperature from the register"""
raw_read = unpack(">h", self._read_register(_MAX31856_CJTH_REG, 2))[0]

# effectively shift raw_read >> 8 to convert pseudo-float
Expand Down Expand Up @@ -277,13 +331,12 @@ def _perform_one_shot_measurement(self):
# wait for the measurement to complete
self._wait_for_oneshot()


def initiate_one_shot_measurement(self):
'''Starts a one-shot measurement and returns immediately.
"""Starts a one-shot measurement and returns immediately.
A measurement takes approximately 160ms.
Check the status of the measurement with `oneshot_pending`; when it is false,
the measurement is complete and the value can be read with `unpack_temperature`.
'''
"""
# read the current value of the first config register
conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0]

Expand All @@ -295,20 +348,20 @@ def initiate_one_shot_measurement(self):
# write it back with the new values, prompting the sensor to perform a measurement
self._write_u8(_MAX31856_CR0_REG, conf_reg_0)


@property
def oneshot_pending(self) -> bool:
'''A boolean indicating the status of the one-shot flag.
A True value means the measurement is still ongoing.
A False value means measurement is complete.'''
oneshot_flag = self._read_register(_MAX31856_CR0_REG, 1)[0] & _MAX31856_CR0_1SHOT
"""A boolean indicating the status of the one-shot flag.
A True value means the measurement is still ongoing.
A False value means measurement is complete."""
oneshot_flag = (
self._read_register(_MAX31856_CR0_REG, 1)[0] & _MAX31856_CR0_1SHOT
)
return bool(oneshot_flag)

def _wait_for_oneshot(self):
while self.oneshot_pending:
sleep(0.01)


def _read_register(self, address, length):
# pylint: disable=no-member
# Read a 16-bit BE unsigned value from the specified 8-bit address.
Expand Down