diff --git a/micropython/drivers/sensor/mcp9808/manifest.py b/micropython/drivers/sensor/mcp9808/manifest.py new file mode 100644 index 000000000..b941d68ab --- /dev/null +++ b/micropython/drivers/sensor/mcp9808/manifest.py @@ -0,0 +1,9 @@ +metadata( + description="Microchip MCP9808 temperature sensor driver", + version="1.0.0", + license="MIT", + author="Marco Miano", +) + +# opt=2 so line numbers are preserved in case of exceptions +module("mcp9808.py", opt=2) diff --git a/micropython/drivers/sensor/mcp9808/mcp9808.py b/micropython/drivers/sensor/mcp9808/mcp9808.py new file mode 100644 index 000000000..205a50943 --- /dev/null +++ b/micropython/drivers/sensor/mcp9808/mcp9808.py @@ -0,0 +1,715 @@ +"""MIT License + +Copyright (c) 2024 Marco Miano + +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. + + + + +Microchip MCP9808 driver for MicroPython + +THE MCP9808 IS A COMPLEX SENSOR WITH MANY FEATURES. IS IT ADVISABLE TO READ THE DATASHEET. + +DO NOT ACCESS REGISTERS WITH ADDRESSES HIGHER THAN 0x08 AS THEY CONTAIN CALIBRATION CODES. +DOING SO MAY IRREPARABLY DAMAGE THE SENSOR. + +This driver is a comprehensive implementation of the MCP9808 sensor's features. It is designed +to be easy to use and offers a high level of abstraction from the sensor's registers. +The driver includes built-in error checking (such as type validation and bounds checking +for register access) and a debug mode to assist with development. + + + +Example usage: + +from mcp9808 import MCP9808, HYST_15, RES_0_125 +from machine import SoftI2C, Pin + +i2c = SoftI2C(scl=Pin(17), sda=Pin(16), freq=400000) +t_sensor = MCP9808(i2c) + +# Get temeperature with deafult settings +temperature: float = t_sensor.get_temperature() + +# Various settings +t_sensor.set_hysteresis_mode(hyst_mode=HYST_15) +t_sensor.set_resolution(resolution=RES_0_125) +t_sensor.set_alert_crit_limit(crit_limit=65.0) +t_sensor.set_alert_upper_limit(upper_limit=50.0) +t_sensor.set_alert_lower_limit(lower_limit=-10.0) +t_sensor.enable_alert() + +# Enable debug mode to get warnings +t_sensor._debug = True + + +# For more information, see the README file at +https://github.com/MarcoMiano/mip-mcp9808 +""" + +from machine import SoftI2C, I2C + +# Handy Constants +HYST_00 = 0b00 # Hysteresis 0°C (power-up default) +HYST_15 = 0b01 # Hysteresis 1,5°C +HYST_30 = 0b10 # Hysteresis 3,0°C +HYST_60 = 0b11 # Hysteresis 6,0°C + +RES_0_5 = 0b00 # Resolution 0.5°C +RES_0_25 = 0b01 # Resolution 0.25°C +RES_0_125 = 0b10 # Resolution 0.125°C +RES_0_0625 = 0b11 # Resolution 0.0625°C (power-up default) + + +class MCP9808(object): + """A class to interface with the Microchip MCP9808 temperature sensor over I2C. + + Attributes: + ``BASE_ADDR`` (int): The base I2C address for the MCP9808 sensor. + ``REG_CFG`` (int): Address of the configuration register. + ``REG_ATU`` (int): Address of the alert temperature upper boundary trip register. + ``REG_ATL`` (int): Address of the alert temperature lower boundary trip register. + ``REG_ATC`` (int): Address of the critical temperature trip register. + ``REG_TEM`` (int): Address of the temperature register. + ``REG_MFR`` (int): Address of the manufacturer ID register. + ``REG_DEV`` (int): Address of the device ID register. + ``REG_RES`` (int): Address of the resolution register. + """ + + BASE_ADDR = 0x18 + ####################################################### + # DON'T ACCESS REGISTER WITH ADDRESS HIGHER THAN 0X08 # + ####################################################### + REG_CFG = 0x01 # Config register + REG_ATU = 0x02 # Alert Temperature Upper boundary trip register + REG_ATL = 0x03 # Alert Temperature Lower boundary trip register + REG_ATC = 0x04 # Critical Temperature Trip register + REG_TEM = 0x05 # Temperature register + REG_MFR = 0x06 # Manufacturer ID register + REG_DEV = 0x07 # Device ID register + REG_RES = 0x08 # Resolution register + + def __init__( + self, + i2c: SoftI2C | I2C, + addr: int | None = None, + A0: bool = False, + A1: bool = False, + A2: bool = False, + debug: bool = False, + ) -> None: + """Initialize the sensor object instance. + + Args: + ``i2c`` (SoftI2C | I2C): The I2C bus instance to use for communication. + ``addr`` (int | None, optional): The I2C address of the sensor. If not provided, + the address will be calculated based on A0, A1, and A2. Defaults to None. + ``A0`` (bool, optional): The state of address pin A0. Defaults to False. + ``A1`` (bool, optional): The state of address pin A1. Defaults to False. + ``A2`` (bool, optional): The state of address pin A2. Defaults to False. + ``debug`` (bool, optional): Enable or disable debug mode. Defaults to False. + Returns: + ``None`` + """ + + self._i2c: SoftI2C | I2C = i2c + self._debug: bool = debug + if addr: + self._addr = addr + else: + self._addr: int = self.BASE_ADDR | (A2 << 2) | (A1 << 1) | A0 + self._check_device() + self._get_config() + + def _check_device(self) -> None: + """Checks the device's manufacturer ID and device ID to ensure it is the correct device. + + Raises: + ``Exception``: If the manufacturer ID does not match the expected value. + ``Exception``: If the device ID does not match the expected value. + Warns: + If the hardware revision does not match the expected value and debug mode is enabled. + Returns: + ``None`` + """ + + self._mfr_id: bytes = self._i2c.readfrom_mem(self._addr, self.REG_MFR, 2) + if self._mfr_id != b"\x00\x54": + raise Exception(f"Invalid manufacturer ID {self._mfr_id}") + self._dev_id: bytes = self._i2c.readfrom_mem(self._addr, self.REG_DEV, 2) + if self._dev_id[0] != 4: + raise Exception(f"Invalid device ID {self._dev_id[0]}") + if self._dev_id[1] != 0 and self._debug: + print( + f"[WARN] Module written for HW revision 0 but got {self._dev_id[1]}.", + ) + + def _get_config(self) -> None: + """Private method to read the configuration register from the sensor. + + This method reads 2 bytes from the configuration register of the sensor. + It then parses the bytes to update the following instance attributes: + ``_hyst_mode``: Hysteresis mode (int) + ``_shdn``: Shutdown mode (bool) + ``_crit_lock``: Critical temperature register lock (bool) + ``_alerts_lock``: Alerts temperature registers lock (bool) + ``_irq_clear_bit``: Interrupt clear bit (bool) + ``_alert``: Alert output status (bool) + ``_alert_ctrl``: Alert control (bool) + ``_alert_sel``: Alert output select (bool) + ``_alert_pol``: Alert output polarity (bool) + ``_alert_mode``: Alert output mode (bool) + Returns: + ``None`` + """ + + buf: bytes = self._i2c.readfrom_mem(self._addr, self.REG_CFG, 2) + self._hyst_mode: int = (buf[0] >> 1) & 0x03 + self._shdn = bool(buf[0] & 0x01) + self._crit_lock = bool(buf[1] & 0x80) + self._alerts_lock = bool(buf[1] & 0x40) + self.irq_clear_bit = bool(buf[1] & 0x20) + self._alert = bool(buf[1] & 0x10) + self._alert_ctrl = bool(buf[1] & 0x08) + self._alert_sel = bool(buf[1] & 0x04) + self.alert_pol = bool(buf[1] & 0x02) + self._alert_mode = bool(buf[1] & 0x01) + + def _set_config( + self, + hyst_mode: int | None = None, + shdn: bool | None = None, + crit_lock: bool | None = None, + alerts_lock: bool | None = None, + irq_clear_bit: bool = False, + alert_ctrl: bool | None = None, + alert_sel: bool | None = None, + alert_pol: bool | None = None, + alert_mode: bool | None = None, + ) -> None: + """Private method to set the configuration of the sensor. + + Parameters: + ``hyst_mode`` (int | None): Hysteresis mode. Valid values are HYST_00, HYST_15, HYST_30, HYST_60. + ``shdn (bool`` | None): Shutdown mode. + ``crit_lock`` (bool | None): Critical temperature register lock. + ``alerts_lock`` (bool | None): Alerts temperature registers lock. + ``irq_clear``_bit (bool): Interrupt clear bit. + ``alert_ctrl`` (bool | None): Alert output control. + ``alert_sel`` (bool | None): Alert output select. + ``alert_pol`` (bool | None): Alert output polarity. + ``alert_mode`` (bool | None): Alert output mode. + Raises: + ``ValueError``: If hyst_mode is not one of the valid values. + ``TypeError``: If any of the boolean parameters are not of type bool. + Returns: + ``None`` + """ + + if hyst_mode is None: + hyst_mode = self._hyst_mode + if shdn is None: + shdn = self._shdn + if crit_lock is None: + crit_lock = self._crit_lock + if alerts_lock is None: + alerts_lock = self._alerts_lock + if alert_ctrl is None: + alert_ctrl = self._alert_ctrl + if alert_sel is None: + alert_sel = self._alert_sel + if alert_pol is None: + alert_pol = self.alert_pol + if alert_mode is None: + alert_mode = self._alert_mode + + # Type/value check the parameters + if hyst_mode not in [HYST_00, HYST_15, HYST_30, HYST_60]: + raise ValueError(f"hyst_mode: {hyst_mode}. Value should be between 0 and 3 inclusive.") + if shdn.__class__ is not bool: + raise TypeError( + f"shdn: {shdn} {shdn.__class__}. Expecting a bool.", + ) + if crit_lock.__class__ is not bool: + raise TypeError( + f"crit_lock: {crit_lock} {crit_lock.__class__}. Expecting a bool.", + ) + if alerts_lock.__class__ is not bool: + raise TypeError( + f"alerts_lock: {alerts_lock} {alerts_lock.__class__}. Expecting a bool.", + ) + if irq_clear_bit.__class__ is not bool: + raise TypeError( + f"irq_clear_bit: {irq_clear_bit} {irq_clear_bit.__class__}. Expecting a bool.", + ) + if alert_ctrl.__class__ is not bool: + raise TypeError( + f"alert_ctrl: {alert_ctrl} {alert_ctrl.__class__}. Expecting a bool.", + ) + if alert_sel.__class__ is not bool: + raise TypeError( + f"alert_sel: {alert_sel} {alert_sel.__class__}. Expecting a bool.", + ) + if alert_pol.__class__ is not bool: + raise TypeError( + f"alert_pol: {alert_pol} {alert_pol.__class__}. Expecting a bool.", + ) + if alert_mode.__class__ is not bool: + raise TypeError( + f"alert_mode: {alert_mode} {alert_mode.__class__}. Expecting a bool.", + ) + + # Build the send buffer + buf = bytearray(b"\x00\x00") + buf[0] = (hyst_mode << 1) | shdn + buf[1] = ( + (crit_lock << 7) + | (alerts_lock << 6) + | (irq_clear_bit << 5) + | (alert_ctrl << 3) + | (alert_sel << 2) + | (alert_pol << 1) + | alert_mode + ) + # Write the buffer to the sensor + self._i2c.writeto_mem(self._addr, self.REG_CFG, buf) + self._get_config() + # Check if the configuration was set correctly id debug mode is enabled + if self._debug: + if self._hyst_mode != hyst_mode: + print( + f"[WARN] Failed to set hyst_mode. Set {hyst_mode} got {self._hyst_mode}", + ) + if self._shdn != shdn: + print( + f"[WARN] Failed to set shdn. Set {shdn} got {self._shdn}", + ) + if self._crit_lock != crit_lock: + print( + f"[WARN] Failed to set crit_lock. Set {crit_lock} got {self._crit_lock}", + ) + if self.irq_clear_bit: + print("[WARN] Something wrong with irq_clear_bit. Should always read False") + if self._alerts_lock != alerts_lock: + print( + f"[WARN] Failed to set alerts_lock. Set {alerts_lock} got {self._alerts_lock}", + ) + if self._alert_ctrl != alert_ctrl: + print( + f"[WARN] Failed to set alert_ctrl. Set {alert_ctrl} got {self._alert_ctrl}.", + ) + if self._alert_sel != alert_sel: + print( + f"[WARN] Failed to set alert_sel. Set {alert_sel} got {self._alert_sel}.", + ) + if self.alert_pol != alert_pol: + print( + f"[WARN] Failed to set alert_pol. Set {alert_pol} got {self.alert_pol}.", + ) + if self._alert_mode != alert_mode: + print( + f"[WARN] Failed to set alert_mode. Set {alert_mode} got {self._alert_mode}.", + ) + + def _set_alert_limit(self, limit: float, register: int) -> None: + """Private method to set the alert limit register. + + Inteded to be used by the set_alert_XXXXX_limit wrapper methods. + Args: + ``limit`` (float | int): The temperature limit to set. Must be between -128 and 127. + ``register`` (int): The register address to write the limit to. + Raises: + ``TypeError``: If the limit is not a float or int. + ``ValueError``: If the limit is out of the range [-128, 127]. + ``ValueError``: If the register address is not valid. + Debug: + - Issue a warning if the threshold is outside of the operational range. + - Issue a warning if the alert limit was not set correctly. + Returns: + ``None`` + """ + + if limit.__class__ not in [float, int]: + raise TypeError( + f"limit: {limit} {limit.__class__}. Expecting float|int.", + ) + if limit < -128 or limit > 127: + raise ValueError("Temperature out of range [-128, 127]") + if (limit < -40 or limit > 125) and self._debug: + print( + "[WARN] Temperature outside of operational range, limit won't be ever reached.", + ) + if register not in [self.REG_ATU, self.REG_ATL, self.REG_ATC]: + raise ValueError(f"Invalid register address {register}") + + buf = bytearray(b"\x00\x00") + + # If limit is negative set sign fifth bit ON otherwise leave it OFF + if limit < 0: + sign = 0x10 + else: + sign = 0x00 + + # If limit is between -1 and 0 (like -0.25) set integral to 0xFF (-0 in 2's complement) + if -1 < limit < 0: + integral: int = 0xFF + # Otherwise truncate limit to a int and keep only the rightmost byte + else: + integral: int = int(limit) & 0xFF + + # Calculate the fractional part by keeping the 2 rightmost bits of the integer division + # of 0.25 (the sensitivity) and the remainder part of the decimal part of limit + frac_normal: int = int((limit - integral) / 0.25) & 0x03 + + # Build the send buffer highest byte combining (bitwise-or) sign and the integral + # right-shifted by 4 + buf[0] = sign | (integral >> 4) + # Build the send buffer lowest byte combining (bitwise-or) the integral + # left-shifted by 4 and the fractional part left shifted by 2 (last 2 bit are 0) + buf[1] = (integral << 4) | (frac_normal << 2) + + self._i2c.writeto_mem(self._addr, register, buf) + + if self._debug: + check: bytes = self._i2c.readfrom_mem(self._addr, register, 2) + if check != buf: + print( + f"[WARN] Failed to set alert limit. Set {buf[0]:08b}-{buf[1]:08b}", + f"but got {check[0]:08b}-{check[1]:08b}", + ) + + def shutdown(self) -> None: + """Put the sensor in low power mode. + + Returns: + ``None`` + """ + self._set_config(shdn=True) + + def wake(self) -> None: + """Wake the sensor from low power mode. + + Returns: + ``None`` + """ + self._set_config(shdn=False) + + def lock_crit_limit(self) -> None: + """Locks the critical temperature limit. + + When the critical temperature limit is locked, it cannot be changed + until the sensor is power cycled. + Returns: + ``None`` + """ + self._set_config(crit_lock=True) + + def lock_alerts_limit(self) -> None: + """Locks the alerts limits. + + When the alerts limits are locked, they cannot be changed + until the sensor is power cycled. + Returns: + ``None`` + """ + self._set_config(alerts_lock=True) + + def irq_clear(self) -> None: + """Clears the interrupt output. + + This method clears the interrupt output. + Returns: + ``None`` + """ + self._set_config(irq_clear_bit=True) + + def get_alert_status(self) -> bool: + """Get the alert status. + + This method reads the alert status from the sensor. + Returns: + ``bool``: The alert status. + """ + self._get_config() + return self.alert + + def enable_alert(self) -> None: + """Enable the alert output. + + Returns: + ``None`` + """ + self._set_config(alert_ctrl=True) + + def disable_alert(self) -> None: + """Disable the alert output. + + Returns: + ``None`` + """ + self._set_config(alert_ctrl=False) + + def set_alert_threshold(self, only_crit=False) -> None: + """Set the alert output select. + + Select if the alert output should be activated only by the critical limit or both critical + and upper/lower limits. + Args: + ``only_crit`` (bool, optional): Set the alert output to only critical. Defaults to False. + Returns: + ``None`` + """ + self._set_config(alert_sel=only_crit) + + def set_alert_polarity(self, active_high=False) -> None: + """Set the alert output polarity. + + Set the alert output polarity to active high or active low. + Args: + ``active_high`` (bool, optional): Set the alert output polarity to active high. + Defaults to False. + Returns: + ``None`` + """ + self._set_config(alert_pol=active_high) + + def set_alert_mode(self, irq=False) -> None: + """Set the alert output mode. + + Set the alert output mode to interrupt or comparator. + Args: + ``irq`` (bool, optional): Set the alert output mode to interrupt. Defaults to False. + Returns: + ``None`` + """ + self._set_config(alert_mode=irq) + + def set_alert_upper_limit(self, upper_limit: float) -> None: + """Set the alert upper limit. + + Args: + upper_limit (float | int): The upper limit to set. + It will rounded to the nearest 0.25°C. + Raises: + ``TypeError``: If the limit is not a float or int. + ``ValueError``: If the limit is out of the range [-128, 127]. + Debug: + - Issue a warning if the threshold is outside of the operational range. + - Issue a warning if the alert limit was not set correctly. + Returns: + ``None`` + """ + self._set_alert_limit(upper_limit, self.REG_ATU) + + def set_alert_lower_limit(self, lower_limit: float) -> None: + """Set the alert lower limit. + + Args: + lower_limit (float | int): The lower limit to set. + Raises: + ``TypeError``: If the limit is not a float or int. + ``ValueError``: If the limit is out of the range [-128, 127]. + Debug: + - Issue a warning if the threshold is outside of the operational range. + - Issue a warning if the alert limit was not set correctly. + Returns: + ``None`` + """ + self._set_alert_limit(lower_limit, self.REG_ATL) + + def set_alert_crit_limit(self, crit_limit: float) -> None: + """Set the alert critical limit. + + Args: + crit_limit (float | int): The critical limit to set. + Raises: + ``TypeError``: If the limit is not a float or int. + ``ValueError``: If the limit is out of the range [-128, 127]. + Debug: + - Issue a warning if the threshold is outside of the operational range. + - Issue a warning if the alert limit was not set correctly. + Returns: + ``None`` + """ + self._set_alert_limit(crit_limit, self.REG_ATC) + + def get_temperature(self) -> float: + """Get the temperature from the sensor. + + Returns: + ``float``: The temperature in degrees Celsius. + """ + # Read temperature register from sensor + buf: bytes = self._i2c.readfrom_mem(self._addr, self.REG_TEM, 2) + # Extract the sign bit + sign: int = buf[0] & 0x10 + # Calculate the 4 upper bit of the integral by left shifting the first byte by 4 + upper: int = (buf[0] << 4) & 0xFF + # Calculate the 4 lower bit of the integral and the fractional part into a float + # dividing by 16 the second buf byte + lower: float = (buf[1] & 0xFF) / 16 + # Calculate the temperature as a float, adding the upper byte (leftmost 4 bit of integral) + # and the lower byte (rightmost 4 bit of integral + fractional). + # In case of negative value subtract 256 from the sum to convert from 2's complement 8+4bit + # fractional value to negative float + temp: float = (upper + lower) - 256 if sign else upper + lower + return temp + + def get_alert_triggers(self) -> tuple[bool, bool, bool]: + """Get the alert triggers. + + Trigger bits are not influenced by the alert output mode (compare or interrupt) or by the + alert polarity (active high or active low) or by the alert control (enable or disable). + Returns: + ``tuple[bool, bool, bool]``: A tuple containing the alert triggers. + The first element is True if the temperature is greater or equal to the critical + limit. + The second element is True if the temperature is greater than the upper limit. + The third element is True if the temperature is less than the lower limit. + """ + # Read temperature register from sensor + buf: bytes = self._i2c.readfrom_mem(self._addr, self.REG_TEM, 2) + # Extract the 16th bit (last), Ta vs. Tcrit. False = Ta < Tcrit | True = Ta >= Tcrit + ta_tcrit = bool(buf[0] & 0x80) + # Extract the 15th bit, Ta vs. Tupper. False = Ta <= Tupper | True = Ta > Tupper + ta_tupper = bool(buf[0] & 0x40) + # Extract the 14th bit, Ta vs Tlower. False = Ta >= Tlower | True = Ta < Tlower + ta_tlower = bool(buf[0] & 0x20) + + return ta_tcrit, ta_tupper, ta_tlower + + def set_resolution(self, resolution=RES_0_0625) -> None: + """Set the resolution of the sensor. + + Args: + resolution (int, optional): The resolution to set. + Valid values are RES_0_5, RES_0_25, RES_0_125, RES_0_0625. + Defaults to RES_0_0625. + Raises: + ValueError: If the resolution is not a valid value. + Debug: + - Issue a warning if the resolution was not set correctly. + Returns: + ``None`` + """ + # Check if resolution is a compatible value + if resolution not in [RES_0_5, RES_0_25, RES_0_125, RES_0_0625]: + raise ValueError( + f"Invalid resolution: {resolution}. Value should be between 0 and 3 inclusive.", + ) + + buf = bytearray(b"\x00") + + buf[0] |= resolution & 0x03 + self._i2c.writeto_mem(self._addr, self.REG_RES, buf) + if self._debug: + check = self._i2c.readfrom_mem(self._addr, self.REG_RES, 1) + if check != buf: + print(f"[WARN] Failed to set resolution. Set {resolution} got {check[0]}") + + @property + def hyst_mode(self) -> int: + """Get the hysteresis mode. + + Returns: + ``int``: The hysteresis mode. + """ + self._get_config() + return self._hyst_mode + + @hyst_mode.setter + def hyst_mode(self, hyst_mode: int) -> None: + """Set the hysteresis mode. + + Args: + ``hyst_mode`` (int): The hysteresis mode to set. + Valid values are HYST_00, HYST_15, HYST_30, HYST_60. + """ + self._set_config(hyst_mode=hyst_mode) + + @property + def shdn(self) -> bool: + """Get the shutdown mode. + + Returns: + ``bool``: The shutdown mode. + """ + self._get_config() + return self._shdn + + @property + def crit_lock(self) -> bool: + """Get the critical temperature register lock. + + Returns: + ``bool``: The critical temperature register lock. + """ + self._get_config() + return self._crit_lock + + @property + def alerts_lock(self) -> bool: + """Get the alerts temperature registers lock. + + Returns: + ``bool``: The alerts temperature registers lock. + """ + self._get_config() + return self._alerts_lock + + @property + def alert(self) -> bool: + """Get the alert control. + + Returns: + ``bool``: The alert control. + """ + self._get_config() + return self._alert + + @property + def alert_ctrl(self) -> bool: + """Get the alert control. + + Returns: + ``bool``: The alert control. + """ + self._get_config() + return self._alert_ctrl + + @property + def alert_sel(self) -> bool: + """Get the alert output select. + + Returns: + ``bool``: The alert output select. + """ + self._get_config() + return self._alert_sel + + @property + def alert_mode(self) -> bool: + """Get the alert output mode. + + Returns: + ``bool``: The alert output mode. + """ + self._get_config() + return self._alert_mode diff --git a/micropython/drivers/sensor/mcp9808/test_mcp9808.py b/micropython/drivers/sensor/mcp9808/test_mcp9808.py new file mode 100644 index 000000000..0376d4743 --- /dev/null +++ b/micropython/drivers/sensor/mcp9808/test_mcp9808.py @@ -0,0 +1,379 @@ +"""MIT License + +Copyright (c) 2024 Marco Miano + +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. + + + + +Microchip MCP9808 driver/sensor test suite for MicroPython + +THE MCP9808 IS A COMPLEX SENSOR WITH MANY FEATURES. IS IT ADVISABLE TO READ THE DATASHEET. + +DO NOT ACCESS REGISTERS WITH ADDRESSES HIGHER THAN 0x08 AS THEY CONTAIN CALIBRATION CODES. +DOING SO MAY IRREPARABLY DAMAGE THE SENSOR. + +This test suite is designed to check the correct operation of the MCP9808 sensor driver and the +sensor itself. It is advisable to run this test suite if anything is changed in the driver code, +or if the sensor is not behaving right. +This test suite is written and tested on a Raspberry Pi Pico W Board with MicroPython v1.24.1. + + + +Pin connections: + - POWER: pin15 + The sensor is powered from a GPIO pin to be able to power cycle the sensor during the + tests. + CHECK THAT YOUR GPIO PIN CAN SUPPLY ENOUGH CURRENT AND VOLTAGE TO POWER THE SENSOR. + (2.7V-5.5V AT 0.4mA) + - SDA: pin16 + - SCL: pin17 + - ALERT: pin18 + The sensor alert pin is connected to a GPIO pin to check if the sensor is triggering + alerts. The pin is pulled to VCC via the board internal pull-up resistor. + The pin is active low. Wire an external pull-up resistor to VCC if needed. + +Prerequisites: + - Install the unittest module in the MicroPython device. + - Install the MCP9808 driver in the MicroPython device. + - Wire the sensor to the board as described above. + - Run the test suite. +""" + +import unittest +import mcp9808 +from mcp9808 import MCP9808 +from machine import SoftI2C, Pin +from time import sleep_ms + +############################################## +# Change the pin numbers to match your setup # +############################################## +power_pin = Pin(15, Pin.OUT) +alert_pin = Pin(18, Pin.IN, pull=Pin.PULL_UP) +i2c_bus = SoftI2C(scl=Pin(17), sda=Pin(16), freq=400000) + + +class TestMCP9808(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.power: Pin = power_pin + cls.alert: Pin = alert_pin + cls.i2c: SoftI2C = i2c_bus + cls.sensor = MCP9808(cls.i2c) + + def setUp(self) -> None: + self.power.off() + sleep_ms(20) + self.power.on() + sleep_ms(2000) + + def test_powerup_defaults(self) -> None: + self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_00) + self.assertFalse(self.sensor.shdn) + self.assertFalse(self.sensor.crit_lock) + self.assertFalse(self.sensor.alerts_lock) + self.assertFalse(self.sensor.irq_clear_bit) + self.assertFalse(self.sensor.alert) + self.assertFalse(self.sensor.alert_ctrl) + self.assertFalse(self.sensor.alert_sel) + self.assertFalse(self.sensor.alert_pol) + self.assertFalse(self.sensor.alert_mode) + + def test_hysteresis_set(self) -> None: + self.sensor.hyst_mode = mcp9808.HYST_15 + self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_15) + self.sensor.hyst_mode = mcp9808.HYST_30 + self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_30) + self.sensor.hyst_mode = mcp9808.HYST_60 + self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_60) + self.sensor.hyst_mode = mcp9808.HYST_00 + self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_00) + + def test_shutdown(self) -> None: + self.sensor.shutdown() + self.assertTrue(self.sensor.shdn) + self.sensor.wake() + self.assertFalse(self.sensor.shdn) + + def test_crit_lock(self) -> None: + # Lock critical limit register + self.sensor.lock_crit_limit() + # Check if the critical limit register is locked + self.assertTrue(self.sensor.crit_lock) + # Try to enable alerts + self.sensor.enable_alert() + # Alerts should not be enabled + self.assertFalse(self.sensor.alert_ctrl) + # Reset sensor + self.setUp() + # Check if the critical limit register is unlocked + self.assertFalse(self.sensor.crit_lock) + + def test_alerts_lock(self) -> None: + # Lock alerts limit registers + self.sensor.lock_alerts_limit() + # Check if the alerts limit registers are locked + self.assertTrue(self.sensor.alerts_lock) + # Try to enable alerts + self.sensor.enable_alert() + # Alerts should not be enabled + self.assertTrue(self.sensor.alerts_lock) + # Reset sensor + self.setUp() + # Check if the alerts limit registers are unlocked + self.assertFalse(self.sensor.alerts_lock) + + def test_alert_control(self) -> None: + # Get current temperature + temp: float = self.sensor.get_temperature() + # Enable alerts + self.sensor.enable_alert() + # Check if alerts are enabled + self.assertTrue(self.sensor.alert_ctrl) + # Set lower limit to current temperature + 10°C + self.sensor.set_alert_lower_limit(temp + 10) + sleep_ms(10) + # Check if hardware alert is triggered + self.assertEqual(self.alert.value(), 0) + # Disable alerts + self.sensor.disable_alert() + # Check if alerts are disabled + self.assertFalse(self.sensor.alert_ctrl) + # Check if hardware alert is cleared + self.assertEqual(self.alert.value(), 1) + + def test_comp_lower_alerts(self) -> None: + # Get current temperature + temp: float = self.sensor.get_temperature() + # Check if alert is disabled and in comparator mode + self.assertFalse(self.sensor.alert_ctrl) + self.assertFalse(self.sensor.alert_mode) + # Set upper limit to 100°C + # crit limit to 100°C + # lower limit to current temperature + 10°C + self.sensor.set_alert_upper_limit(100) + self.sensor.set_alert_crit_limit(100) + self.sensor.set_alert_lower_limit(temp + 10) + # Enable alerts + self.sensor.enable_alert() + sleep_ms(10) + # Check if hardware alert is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is set + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, True)) + # Set lower limit to current temperature - 10°C (simulating temperature increase) + self.sensor.set_alert_lower_limit(temp - 10) + sleep_ms(10) + # Check if hardware alert is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False)) + + def test_comp_upper_alerts(self) -> None: + # Get current temperature + temp: float = self.sensor.get_temperature() + # Check if alert is disabled and in comparator mode + self.assertFalse(self.sensor.alert_ctrl) + self.assertFalse(self.sensor.alert_mode) + # Set lower limit to 0°C + # crit limit to 100°C + # upper limit to current temperature - 10°C + self.sensor.set_alert_lower_limit(0) + self.sensor.set_alert_crit_limit(100) + self.sensor.set_alert_upper_limit(temp - 10) + # Enable alerts + self.sensor.enable_alert() + sleep_ms(10) + # Check if hardware alert is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is set + self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False)) + # Set lower limit to current temperature + 10°C (simulating temperature drop) + self.sensor.set_alert_upper_limit(temp + 10) + sleep_ms(10) + # Check if hardware alert is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False)) + + def test_comp_crit_alerts(self) -> None: + # Get current temperature + temp: float = self.sensor.get_temperature() + # Check if alert is disabled and in comparator mode + self.assertFalse(self.sensor.alert_ctrl) + self.assertFalse(self.sensor.alert_mode) + # Set lower limit to 0°C + # upper limit to 100°C + # crit limit to current temperature - 10°C + self.sensor.set_alert_lower_limit(0) + self.sensor.set_alert_upper_limit(100) + self.sensor.set_alert_crit_limit(temp - 10) + # Enable alerts + self.sensor.enable_alert() + sleep_ms(10) + # Check if hardware alert is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is set + self.assertEqual(self.sensor.get_alert_triggers(), (True, False, False)) + # Set lower limit to current temperature + 10°C (simulating temperature drop) + self.sensor.set_alert_crit_limit(temp + 10) + sleep_ms(10) + # Check if hardware alert is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False)) + + def test_irq_lower_alerts(self) -> None: + # Get current temperature + temp: float = self.sensor.get_temperature() + # Check if alert is disabled and in comparator mode + self.assertFalse(self.sensor.alert_ctrl) + self.assertFalse(self.sensor.alert_mode) + # Set and check alert mode to IRQ + self.sensor.set_alert_mode(irq=True) + self.assertTrue(self.sensor.alert_mode) + # Set lower limit to current temperature + 10°C + # upper limit to 100°C + # crit limit to 100°C + self.sensor.set_alert_lower_limit(temp + 10) + self.sensor.set_alert_upper_limit(100) + self.sensor.set_alert_crit_limit(100) + # Enable alerts + self.sensor.enable_alert() + sleep_ms(10) + # Check if hardware IRQ is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is set + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, True)) + # Clear IRQ + self.sensor.irq_clear() + # Check if hardware IRQ is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is still set + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, True)) + # Set lower limit to current temperature - 10°C (simulating temperature increase) + self.sensor.set_alert_lower_limit(temp - 10) + sleep_ms(10) + # Check if hardware IRQ is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False)) + # Clear IRQ + self.sensor.irq_clear() + # Check if alert is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False)) + + def test_irq_upper_alerts(self) -> None: + # Get current temperature + temp: float = self.sensor.get_temperature() + # Check if alert is disabled and in comparator mode + self.assertFalse(self.sensor.alert_ctrl) + self.assertFalse(self.sensor.alert_mode) + # Set and check alert mode to IRQ + self.sensor.set_alert_mode(irq=True) + self.assertTrue(self.sensor.alert_mode) + # Set lower limit 0°C + # upper limit to current temperature - 10°C + # crit limit to 100°C + self.sensor.set_alert_lower_limit(0) + self.sensor.set_alert_upper_limit(temp - 10) + self.sensor.set_alert_crit_limit(100) + # Enable alerts + self.sensor.enable_alert() + sleep_ms(10) + # Check if hardware IRQ is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is set + self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False)) + # Clear IRQ + self.sensor.irq_clear() + # Check if hardware IRQ is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is still set + self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False)) + # Set upper limit to current temperature + 10°C (simulating temperature drop) + self.sensor.set_alert_upper_limit(temp + 10) + sleep_ms(10) + # Check if hardware IRQ is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False)) + # Clear IRQ + self.sensor.irq_clear() + # Check if alert is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False)) + + def test_irq_crit_alerts(self) -> None: + # Get current temperature + temp: float = self.sensor.get_temperature() + # Check if alert is disabled and in comparator mode + self.assertFalse(self.sensor.alert_ctrl) + self.assertFalse(self.sensor.alert_mode) + # Set and check alert mode to IRQ + self.sensor.set_alert_mode(irq=True) + self.assertTrue(self.sensor.alert_mode) + # Set lower limit 0°C + # upper limit to current temperature - 10°C + # crit limit to 100°C + self.sensor.set_alert_lower_limit(0) + self.sensor.set_alert_upper_limit(temp - 10) + self.sensor.set_alert_crit_limit(100) + # Enable alerts (first IRQ from upper limit) + self.sensor.enable_alert() + sleep_ms(10) + # Check if alert is triggered + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is set + self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False)) + # Clear IRQ + self.sensor.irq_clear() + # Check if alert is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is still set + self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False)) + # Set crit limit to current temperature - 5°C (to trigger second IRQ from crit limit) + self.sensor.set_alert_crit_limit(temp - 5) + sleep_ms(10) + # Check if alert is triggered (second IRQ from crit limit) + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is set + self.assertEqual(self.sensor.get_alert_triggers(), (True, True, False)) + # Clear IRQ + self.sensor.irq_clear() + # Check if alert is NOT cleard + self.assertEqual(self.alert.value(), 0) + # Check if correct alert trigger bit is still set + self.assertEqual(self.sensor.get_alert_triggers(), (True, True, False)) + # Set crit limit to current temperature + 5°C (simulating temperature drop) + self.sensor.set_alert_crit_limit(temp + 5) + sleep_ms(10) + # Check if alert is cleared + self.assertEqual(self.alert.value(), 1) + # Check if correct alert trigger bit is cleared + self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False)) + + +if __name__ == "__main__": + unittest.main()