diff --git a/README.rst b/README.rst index 1b11c95..f901e9c 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,40 @@ Usage Example time.sleep(2) # longer sleep to let it spin down from 100% print("Fan speed", emc.fan_speed) +Additional examples, including the use of the temperature look up table +(LUT) can be found in the examples/ folder: + +* emc2101_lut_example.py +* emc2101_set_pwm_freq.py +* emc2101_simpletest.py + +For access to some additional properties, but without configuring the LUT, +use the intermediate class EMC2101_EXT: + +.. code-block:: python3 + + import time + import board + from adafruit_emc2101 import EMC2101_EXT + + i2c = board.I2C() # uses board.SCL and board.SDA + + emc = EMC2101_EXT(i2c) + print("External limit temp is", emc.external_temp_high_limit) + print("Setting external limit temp to 50C") + emc.external_temp_high_limit = 50 + +When the temperature limits are exceeded the device sets the alert bit +in the status register and (if configured to do so) will raise the ALERT +output pin as an interrupt. + +EMC2101_EXT defines properties for internal and external temperature +limits, and has register definitions for all registers except the LUT +itself. The EMC2101_LUT class includes this as well. + +The EMC2101_Regs class is intended for internal use, and defines register +addresses. + Documentation ============= diff --git a/adafruit_emc2101/__init__.py b/adafruit_emc2101/__init__.py index 16466dc..109fb10 100644 --- a/adafruit_emc2101/__init__.py +++ b/adafruit_emc2101/__init__.py @@ -37,112 +37,19 @@ from adafruit_register.i2c_bits import RWBits import adafruit_bus_device.i2c_device as i2cdevice +from adafruit_emc2101 import emc2101_regs + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EMC2101.git" -_INTERNAL_TEMP = const(0x00) -_EXTERNAL_TEMP_MSB = const(0x01) -_EXTERNAL_TEMP_LSB = const(0x10) - -_REG_CONFIG = const(0x03) -_TEMP_FORCE = const(0x0C) -_TACH_LSB = const(0x46) -_TACH_MSB = const(0x47) -_TACH_LIMIT_LSB = const(0x48) -_TACH_LIMIT_MSB = const(0x49) -_FAN_CONFIG = const(0x4A) -_FAN_SPINUP = const(0x4B) -_REG_FAN_SETTING = const(0x4C) -_PWM_FREQ = const(0x4D) - -_REG_PARTID = const(0xFD) # 0x16 -_REG_MFGID = const(0xFE) # 0xFF16 - -MAX_LUT_SPEED = 0x3F # 6-bit value -MAX_LUT_TEMP = 0x7F # 7-bit - -_I2C_ADDR = const(0x4C) -_FAN_RPM_DIVISOR = const(5400000) - - -class CV: - """struct helper""" - - @classmethod - def add_values(cls, value_tuples): - """creates CV entries""" - cls.string = {} - cls.lsb = {} - - for value_tuple in value_tuples: - name, value, string, lsb = value_tuple - setattr(cls, name, value) - cls.string[value] = string - cls.lsb[value] = lsb - - @classmethod - def is_valid(cls, value): - "Returns true if the given value is a member of the CV" - return value in cls.string - - -class ConversionRate(CV): - """Options for ``conversion_rate``""" - - -ConversionRate.add_values( - ( - ("RATE_1_16", 0, str(1 / 16.0), None), - ("RATE_1_8", 1, str(1 / 8.0), None), - ("RATE_1_4", 2, str(1 / 4.0), None), - ("RATE_1_2", 3, str(1 / 2.0), None), - ("RATE_1", 4, str(1.0), None), - ("RATE_2", 5, str(2.0), None), - ("RATE_4", 6, str(4.0), None), - ("RATE_8", 7, str(8.0), None), - ("RATE_16", 8, str(16.0), None), - ("RATE_32", 9, str(32.0), None), - ) -) - - -class SpinupDrive(CV): - """Options for ``spinup_drive``""" - - -SpinupDrive.add_values( - ( - ("BYPASS", 0, "Disabled", None), - ("DRIVE_50", 1, "50% Duty Cycle", None), - ("DRIVE_75", 2, "25% Duty Cycle", None), - ("DRIVE_100", 3, "100% Duty Cycle", None), - ) -) - - -class SpinupTime(CV): - """Options for ``spinup_time``""" - - -SpinupTime.add_values( - ( - ("BYPASS", 0, "Disabled", None), - ("SPIN_0_05_SEC", 1, "0.05 seconds", None), - ("SPIN_0_1_SEC", 2, "0.1 seconds", None), - ("SPIN_0_2_SEC", 3, "0.2 seconds", None), - ("SPIN_0_4_SEC", 4, "0.4 seconds", None), - ("SPIN_0_8_SEC", 5, "0.8 seconds", None), - ("SPIN_1_6_SEC", 6, "1.6 seconds", None), - ("SPIN_3_2_SEC", 7, "3.2 seconds", None), - ) -) - class EMC2101: # pylint: disable=too-many-instance-attributes """Basic driver for the EMC2101 Fan Controller. :param ~busio.I2C i2c_bus: The I2C bus the EMC is connected to. + See :class:`adafruit_emc2101.EMC2101_EXT` for (almost) complete device register set. + See :class:`adafruit_emc2101.EMC2101_LUT` for the temperature look up table functionality. **Quickstart: Importing and using the device** @@ -169,58 +76,117 @@ class EMC2101: # pylint: disable=too-many-instance-attributes emc.manual_fan_speed = 25 - - If you need control over PWM frequency and the controller's built in temperature/speed look-up table (LUT), you will need :class:`emc2101_lut.EMC2101_LUT` which extends this class to add those features, at the cost of increased memory usage. + + Datasheet: https://ww1.microchip.com/downloads/en/DeviceDoc/2101.pdf """ - _part_id = ROUnaryStruct(_REG_PARTID, ">= 5 - full_tmp *= 0.125 + if full_tmp == emc2101_regs.TEMP_FAULT_OPENCIRCUIT: + raise RuntimeError("Open circuit") + if full_tmp == emc2101_regs.TEMP_FAULT_SHORT: + raise RuntimeError("Short circuit") + full_tmp *= 0.125 return full_tmp @property def fan_speed(self): - """The current speed in Revolutions per Minute (RPM)""" + """The current speed in Revolutions per Minute (RPM). + :return: float fan speed rounded to 2dp. + """ val = self._tach_read_lsb val |= self._tach_read_msb << 8 - return _FAN_RPM_DIVISOR / val + if val < 1: + raise OSError("Connection") + return round(emc2101_regs.FAN_RPM_DIVISOR / val, 2) def _calculate_full_speed(self, pwm_f=None, dac=None): """Determine the LSB value for a 100% fan setting""" @@ -264,7 +277,7 @@ def _calculate_full_speed(self, pwm_f=None, dac=None): if dac: # DAC mode is independent of PWM_F. - self._full_speed_lsb = float(MAX_LUT_SPEED) + self._full_speed_lsb = float(emc2101_regs.MAX_LUT_SPEED) return # PWM mode reaches 100% duty cycle at a 2*PWM_F setting. @@ -280,89 +293,188 @@ def _speed_to_lsb(self, percentage): @property def manual_fan_speed(self): - """The fan speed used while the LUT is being updated and is unavailable. The speed is - given as the fan's PWM duty cycle represented as a float percentage. - The value roughly approximates the percentage of the fan's maximum speed""" - raw_setting = self._fan_setting & MAX_LUT_SPEED - return (raw_setting / self._full_speed_lsb) * 100 + """The fan speed used while the LUT is being updated and is unavailable. The + speed is given as the fan's PWM duty cycle represented as a float percentage. + The value roughly approximates the percentage of the fan's maximum speed. + """ + raw_setting = self._fan_setting & emc2101_regs.MAX_LUT_SPEED + fan_speed = self._full_speed_lsb + if fan_speed < 1: + raise OSError("Connection") + return (raw_setting / fan_speed) * 100.0 @manual_fan_speed.setter def manual_fan_speed(self, fan_speed): - if fan_speed not in range(0, 101): - raise AttributeError("manual_fan_speed must be from 0-100") + """The fan speed used while the LUT is being updated and is unavailable. The + speed is given as the fan's PWM duty cycle represented as a float percentage. + The value roughly approximates the percentage of the fan's maximum speed. + + :raises ValueError: if the fan_speed is not in the valid range + """ + if not 0 <= fan_speed <= 100: + raise ValueError("manual_fan_speed must be from 0-100") fan_speed_lsb = self._speed_to_lsb(fan_speed) lut_disabled = self._fan_lut_prog + # Enable programming self._fan_lut_prog = True + # Set self._fan_setting = fan_speed_lsb + # Restore. self._fan_lut_prog = lut_disabled @property def dac_output_enabled(self): - """When set, the fan control signal is output as a DC voltage instead of a PWM signal""" + """When set, the fan control signal is output as a DC voltage instead + of a PWM signal.""" return self._dac_output_enabled @dac_output_enabled.setter def dac_output_enabled(self, value): + """When set, the fan control signal is output as a DC voltage instead of + a PWM signal. Be aware that the DAC output very likely requires different + hardware to the PWM output. See datasheet and examples for info. + """ self._dac_output_enabled = value self._calculate_full_speed(dac=value) @property def lut_enabled(self): - """Enable or disable the internal look up table used to map a given temperature - to a fan speed. + """Enable or disable the internal look up table used to map a given + temperature to a fan speed. - When the LUT is disabled (the default), fan speed can be changed with `manual_fan_speed`. - To actually set this to True and modify the LUT, you need to use the extended version of - this driver, :class:`emc2101_lut.EMC2101_LUT` - """ + When the LUT is disabled (the default), fan speed can be changed + with `manual_fan_speed`. To actually set this to True and modify + the LUT, you need to use the extended version of this driver, + :class:`emc2101_lut.EMC2101_LUT`.""" return not self._fan_lut_prog @property def tach_limit(self): - """The maximum /minimum speed expected for the fan""" + """The maximum speed expected for the fan. If the fan exceeds this + speed, the status register TACH bit will be set. + :return float: fan speed limit in RPM + :raises OSError: if the limit is 0 (not a permitted value) + """ low = self._tach_limit_lsb high = self._tach_limit_msb - - return _FAN_RPM_DIVISOR / ((high << 8) | low) + limit = high << 8 | low + if limit < 1: + raise OSError("Connection") + return round(emc2101_regs.FAN_RPM_DIVISOR / limit, 2) @tach_limit.setter def tach_limit(self, new_limit): - num = int(_FAN_RPM_DIVISOR / new_limit) + """Set the speed limiter on the fan PWM signal. The value of + 15000 is arbitrary, but very few fans run faster than this. If the + fan exceeds this speed, the status register TACH bit will be set. + + Note that the device will _not_ automatically adjust the PWM speed to + enforce this limit. + + :param new_limit: fan speed limit in RPM + :raises OSError: if the limit is 0 (not a permitted value) + :raises ValueError: if the new_limit is not in the valid range + """ + if not 1 <= new_limit <= 15000: + raise ValueError("tach_limit must be from 1-15000") + num = int(emc2101_regs.FAN_RPM_DIVISOR / new_limit) self._tach_limit_lsb = num & 0xFF self._tach_limit_msb = (num >> 8) & 0xFF @property def spinup_time(self): - """The amount of time the fan will spin at the current set drive strength. - Must be a `SpinupTime`""" + """The amount of time the fan will spin at the currently set drive + strength. + + :return int: corresponding to the SpinupTime enumeration. + """ return self._spin_time @spinup_time.setter def spinup_time(self, spin_time): + """Set the time that the SpinupDrive value will be used to get the + fan moving before the normal speed controls are activated. This is + needed because fan motors typically need a 'kick' to get them moving, + but after this they can slow down further. + + Usage: + .. code-block:: python + + from adafruit_emc2101_enums import SpinupTime + emc.spinup_drive = SpinupTime.SPIN_1_6_SEC + + :raises TypeError: if spin_drive is not an instance of SpinupTime + """ + # Not importing at top level so the SpinupTime is not loaded + # unless it is required, and thus 1KB bytecode can be avoided. + # pylint: disable=import-outside-toplevel + from emc2101_enums import SpinupTime + if not SpinupTime.is_valid(spin_time): - raise AttributeError("spinup_time must be a SpinupTime") + raise TypeError("spinup_time must be a SpinupTime") self._spin_time = spin_time @property def spinup_drive(self): - """The drive strength of the fan on spinup in % max RPM""" + """The drive strength of the fan on spinup in % max PWM duty cycle + (which approximates to max fan speed). + + :return int: corresponding to the SpinupDrive enumeration. + """ return self._spin_drive @spinup_drive.setter def spinup_drive(self, spin_drive): + """Set the drive (pwm duty percentage) that the SpinupTime value is applied + to move the fan before the normal speed controls are activated. This is needed + because fan motors typically need a 'kick' to get them moving, but after this + they can slow down further. + + Usage: + .. code-block:: python + + from adafruit_emc2101_enums import SpinupDrive + emc.spinup_drive = SpinupDrive.DRIVE_50 + + :raises TypeError: if spin_drive is not an instance of SpinupDrive + """ + # Not importing at top level so the SpinupDrive is not loaded + # unless it is required, and thus 1KB bytecode can be avoided. + # pylint: disable=import-outside-toplevel + from emc2101_enums import SpinupDrive + if not SpinupDrive.is_valid(spin_drive): - raise AttributeError("spinup_drive must be a SpinupDrive") + raise TypeError("spinup_drive must be a SpinupDrive") self._spin_drive = spin_drive @property def conversion_rate(self): - """The rate at which temperature measurements are taken. Must be a `ConversionRate`""" + """The rate at which temperature measurements are taken. + + :return int: corresponding to the ConversionRate enumeration.""" return self._conversion_rate @conversion_rate.setter def conversion_rate(self, rate): + """Set the rate at which the external temperature is checked by + by the device. Reducing this rate can reduce power consumption. + + Usage: + + .. code-block:: python + + from adafruit_emc2101_enums import ConversionRate + emc.conversion_rate = ConversionRate.RATE_1_2 + + :raises TypeError: if spin_drive is not an instance of ConversionRate + """ + # Not importing at top level so the ConversionRate is not loaded + # unless it is required, and thus 1KB bytecode can be avoided. + # pylint: disable=import-outside-toplevel + from emc2101_enums import ConversionRate + if not ConversionRate.is_valid(rate): - raise AttributeError("conversion_rate must be a `ConversionRate`") + raise ValueError("conversion_rate must be a `ConversionRate`") self._conversion_rate = rate diff --git a/adafruit_emc2101/emc2101_enums.py b/adafruit_emc2101/emc2101_enums.py new file mode 100644 index 0000000..68a4d19 --- /dev/null +++ b/adafruit_emc2101/emc2101_enums.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2022 Ruth Ivimey-Cook +# +# SPDX-License-Identifier: MIT +""" +`adafruit_emc2101.emc2101_enums` +================================================================================ + +Brushless fan controller + + +* Author(s): Bryan Siepert + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit EMC2101 Breakout + `_ (Product ID: 4808) + +**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 + +* Adafruit's Register library: + https://github.com/adafruit/Adafruit_CircuitPython_Register + +""" + + +class CV: + """struct helper""" + + @classmethod + def add_values(cls, value_tuples): + """Creates CV entries""" + cls.string = {} + cls.lsb = {} + for value_tuple in value_tuples: + name, value, string, lsb = value_tuple + setattr(cls, name, value) + cls.string[value] = string + cls.lsb[value] = lsb + + @classmethod + def is_valid(cls, value): + "Returns true if the given value is a member of the CV" + return value in cls.string + + +class ConversionRate(CV): + """Options for ``conversion_rate``""" + + +ConversionRate.add_values( + ( + ("RATE_1_16", 0, str(1 / 16.0), None), + ("RATE_1_8", 1, str(1 / 8.0), None), + ("RATE_1_4", 2, str(1 / 4.0), None), + ("RATE_1_2", 3, str(1 / 2.0), None), + ("RATE_1", 4, str(1.0), None), + ("RATE_2", 5, str(2.0), None), + ("RATE_4", 6, str(4.0), None), + ("RATE_8", 7, str(8.0), None), + ("RATE_16", 8, str(16.0), None), + ("RATE_32", 9, str(32.0), None), + ) +) + + +class SpinupDrive(CV): + """Options for ``spinup_drive``""" + + +SpinupDrive.add_values( + ( + ("BYPASS", 0, "Disabled", None), + ("DRIVE_50", 1, "50% Duty Cycle", None), + ("DRIVE_75", 2, "25% Duty Cycle", None), + ("DRIVE_100", 3, "100% Duty Cycle", None), + ) +) + + +class SpinupTime(CV): + """Options for ``spinup_time``""" + + +SpinupTime.add_values( + ( + ("BYPASS", 0, "Disabled", None), + ("SPIN_0_05_SEC", 1, "0.05 seconds", None), + ("SPIN_0_1_SEC", 2, "0.1 seconds", None), + ("SPIN_0_2_SEC", 3, "0.2 seconds", None), + ("SPIN_0_4_SEC", 4, "0.4 seconds", None), + ("SPIN_0_8_SEC", 5, "0.8 seconds", None), + ("SPIN_1_6_SEC", 6, "1.6 seconds", None), + ("SPIN_3_2_SEC", 7, "3.2 seconds", None), + ) +) diff --git a/adafruit_emc2101/emc2101_ext.py b/adafruit_emc2101/emc2101_ext.py new file mode 100644 index 0000000..3b9627c --- /dev/null +++ b/adafruit_emc2101/emc2101_ext.py @@ -0,0 +1,385 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022 Ruth Ivimey-Cook +# Derived from work by Bryan Siepert for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_emc2101.emc2101_ext` +================================================================================ + +Brushless fan controller: extended functionality + + +* Author(s): Bryan Siepert, Ryan Pavlik + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit EMC2101 Breakout `_ (Product ID: 4808) + +**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 + +* Adafruit's Register library: + https://github.com/adafruit/Adafruit_CircuitPython_Register + + +The class defined here may be used instead of :class:`adafruit_emc2101.EMC2101`, +if your device has enough RAM to support it. This class adds LUT control +and PWM frequency control to the base feature set. +""" + +from adafruit_register.i2c_struct import UnaryStruct +from adafruit_register.i2c_bit import RWBit +from adafruit_register.i2c_bits import RWBits + +from adafruit_emc2101 import emc2101_regs +from adafruit_emc2101 import EMC2101 + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EMC2101.git" + + +class EMC2101_EXT(EMC2101): # pylint: disable=too-many-instance-attributes + """Driver for EMC2101 Fan, adding definitions for all (but LUT) device registers. + + See :class:`adafruit_emc2101.EMC2101` for the base/common functionality. + See :class:`adafruit_emc2101.EMC2101_LUT` for the temperature look up table functionality. + + :param ~busio.I2C i2c_bus: The I2C bus the EMC is connected to. + """ + + _queue = RWBit(emc2101_regs.REG_CONFIG, 0) + """If set, select whether one (0) or three (0) consecutive over-temp + readings are required for the Alert & Status bits to signal an error.""" + _tcrit_override = RWBit(emc2101_regs.REG_CONFIG, 1) + """If set, permits the tcrit limit to be changed. The limit can only be + changed once per power-cycle.""" + + # In base class: + # _tach_mode_enable = RWBit(REG_CONFIG, 2) + # _dac_output_enabled = RWBit(REG_CONFIG, 4) + # not exposed: + # _disable_i2c_to = RWBit(REG_CONFIG, 3) + + _fan_standby = RWBit(emc2101_regs.REG_CONFIG, 5) + """Select whether the fan output is driven when the device is put into + standby mode.""" + _standby = RWBit(emc2101_regs.REG_CONFIG, 6) + """Selects the operational mode; if 0 (default) temperatures are monitored + and the fan output driven. If 1, temperatures are not monitored and the + fan may be disabled (depends on _fan_standby).""" + + _int_temp_limit = UnaryStruct(emc2101_regs.INT_TEMP_HI_LIM, ">= 5 + temp *= 0.125 + if not -64 <= temp <= 127: + # This should be impossible, if it happens the i2c data is corrupted. + raise OSError("Connection") + return temp + + @external_temp_low_limit.setter + def external_temp_low_limit(self, temp: float): + """Set the low limit temperature for the external sensor. The device + automatically compares live temp readings with this value and signal + the current reading is too low by setting the status register. + + Reading the status register clears the alert, unless the condition + persists. + + :param float temp: the new limit temperature + :raises ValueError: if the supplied temperature is out of range. + :raises RuntimeError: if auto_check_status and an alert status bit is set + """ + if not -64 <= temp <= 127: + raise ValueError("dev_temp_high_limit must be from -64..127") + + # Multiply by 8 to get 3 bits of fraction within the integer. + temp *= 8.0 + temp = int(temp) + # Mask 3 bits & shift to bits 5,6,7 in byte + temp_lsb = temp & 0x07 + temp_lsb = temp_lsb << 5 + # Now drop 3 fraction bits. + temp_msb = temp >> 3 + + # No ordering restrictions here. + self._ext_temp_lo_limit_lsb = temp_lsb + self._ext_temp_lo_limit_msb = temp_msb + self._check_status() + + @property + def external_temp_high_limit(self): + """The high limit temperature for the external sensor.""" + self._check_status() + + # No ordering restrictions here. + temp_lsb = self._ext_temp_hi_limit_lsb + temp_msb = self._ext_temp_hi_limit_msb + # Mask bottom bits of lsb, or with shifted msb + full_tmp = (temp_msb << 8) | (temp_lsb & 0xE0) + full_tmp >>= 5 + full_tmp *= 0.125 + if not -64 <= full_tmp <= 127: + # This should be impossible, if it happens the i2c data is corrupted. + raise OSError("Connection") + return full_tmp + + @external_temp_high_limit.setter + def external_temp_high_limit(self, temp: float): + """Set high limit temperature for the external sensor. The device + automatically compares live temp readings with this value and signal + the current reading is too high by setting the status register. + + Reading the status register clears the alert, unless the condition + persists. + + :param float temp: the new limit temperature + :raises ValueError: if the supplied temperature is out of range. + :raises RuntimeError: if auto_check_status and an alert status bit is set + """ + + if not -64 <= temp <= 127: + raise ValueError("dev_temp_high_limit must be from -64..127") + + # Multiply by 8 to get 3 bits of fraction. + temp *= 8.0 + temp = int(temp) + # Mask 3 bits & shift to bits 5,6,7 in byte + temp_lsb = temp & 0x07 + temp_lsb = temp_lsb << 5 + # Now drop 3 fraction bits. + temp_msb = temp >> 3 + # No ordering restrictions here. + self._ext_temp_hi_limit_lsb = temp_lsb + self._ext_temp_hi_limit_msb = temp_msb + self._check_status() diff --git a/adafruit_emc2101/emc2101_fanspeed.py b/adafruit_emc2101/emc2101_fanspeed.py new file mode 100644 index 0000000..fd6bce9 --- /dev/null +++ b/adafruit_emc2101/emc2101_fanspeed.py @@ -0,0 +1,212 @@ +# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2022 Ruth Ivimey-Cook +# +# SPDX-License-Identifier: MIT +""" +`adafruit_emc2101.emc2101_fanspeed` +================================================================================ + +Brushless fan controller: extended functionality + + +* Author(s): Bryan Siepert, Ryan Pavlik + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit EMC2101 Breakout `_ (Product ID: 4808) + +**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 + +* Adafruit's Register library: + https://github.com/adafruit/Adafruit_CircuitPython_Register +""" + +from adafruit_register.i2c_struct_array import StructArray + +from adafruit_emc2101 import emc2101_regs + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EMC2101.git" + + +class FanSpeedLUT: + """A class used to provide a dict-like interface to the EMC2101's + Temperature to Fan speed Look Up Table (LUT). + + Keys are integer temperatures, values are fan duty cycles between 0 and + 100. A max of 8 values may be stored. + + To remove a single stored point in the LUT, assign it as `None`. + + Usage: + At 50 C the speed should be 62% of max duty cycle. (Updates the chip + immediately). + + .. code-block:: python + + emc2101.lut[50] = 62 + + Set two values up and write to the chip on exit from 'with' block. + + .. code-block:: python + + with emc2101.lut as lut: + lut[20] = 0 + lut[40] = 10 + + Delete an unneeded temperature point: (Updates the chip immediately). + + .. code-block:: python + + emc2101.lut[20] = None + + Read a dict of the currently set values: + + .. code-block:: python + + values = emc2101.lut.lookup_table + # returns: + # { 40: 10, 50: 62 } + + Delete some LUT values, assign None: + + .. code-block:: python + + for temp in emc2101.lut.lookup_table: + emc2101.lut[temp] = None + + Delete all LUT values at once: + + .. code-block:: python + + emc2101.lut.clear() + """ + + # 8 (Temperature, Speed) pairs in increasing order + _fan_lut = StructArray(emc2101_regs.LUT_BASE, " 100.0 or value < 0: + # Range check + raise ValueError("LUT values must be a fan speed from 0-100%") + else: + self.lut_values[index] = value + + if not self._defer_update: + self._update_lut() + + def __repr__(self): + """return the official string representation of the LUT""" + # pylint: disable=consider-using-f-string + return "FanSpeedLUT {:x}".format(id(self)) + + def __str__(self): + """return the official string representation of the LUT""" + value_strs = [] + lut_keys = tuple(sorted(self.lut_values.keys())) + for temp in lut_keys: + fan_drive = self.lut_values[temp] + # pylint: disable=consider-using-f-string + value_strs.append("%d deg C => %.1f%% duty cycle" % (temp, fan_drive)) + + return "\n".join(value_strs) + + @property + def lookup_table(self): + """Return a dictionary of LUT values.""" + lut_keys = tuple(sorted(self.lut_values.keys())) + values = {} + for temp in lut_keys: + fan_drive = self.lut_values[temp] + values[temp] = fan_drive + return values + + def __len__(self): + return len(self.lut_values) + + # this function does a whole lot of work to organized the user-supplied lut dict into + # their correct spot within the lut table as pairs of set registers, sorted with the lowest + # temperature first + + def _update_lut(self): + # Make sure we're not going to try to set more entries than we have slots + if len(self.lut_values) > 8: + raise ValueError("LUT can only contain a maximum of 8 items") + + # Backup state + current_mode = self.emc_fan.lut_enabled + + # Disable the lut to allow it to be updated + self.emc_fan.lut_enabled = False + + # we want to assign the lowest temperature to the lowest LUT slot, so we sort the keys/temps + # get and sort the new lut keys so that we can assign them in order + for idx, current_temp in enumerate(sorted(self.lut_values.keys())): + # We don't want to make `_speed_to_lsb()` public, it is only needed here. + # pylint: disable=protected-access + current_speed = self.emc_fan._speed_to_lsb(self.lut_values[current_temp]) + self._set_lut_entry(idx, current_temp, current_speed) + + # Set the remaining LUT entries to the default (Temp/Speed = max value) + for idx in range(len(self.lut_values), 8): + self._set_lut_entry( + idx, emc2101_regs.MAX_LUT_TEMP, emc2101_regs.MAX_LUT_SPEED + ) + self.emc_fan.lut_enabled = current_mode + + def _set_lut_entry(self, idx, temp, speed): + """Internal function: add a value to the local LUT as a byte array, + suitable for block transfer to the EMC I2C interface. + """ + self._fan_lut[idx * 2] = bytearray((temp,)) + self._fan_lut[idx * 2 + 1] = bytearray((speed,)) + + def clear(self): + """Clear all LUT entries.""" + self.lut_values = {} + self._update_lut() diff --git a/adafruit_emc2101/emc2101_lut.py b/adafruit_emc2101/emc2101_lut.py index e5a71b8..5d03261 100644 --- a/adafruit_emc2101/emc2101_lut.py +++ b/adafruit_emc2101/emc2101_lut.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2022 Ruth Ivimey-Cook # # SPDX-License-Identifier: MIT """ @@ -34,125 +35,33 @@ and PWM frequency control to the base feature set. """ -from micropython import const -from adafruit_register.i2c_struct_array import StructArray from adafruit_register.i2c_struct import UnaryStruct -from adafruit_register.i2c_bit import RWBit -from . import EMC2101, MAX_LUT_SPEED, MAX_LUT_TEMP + +from adafruit_emc2101 import emc2101_regs +from adafruit_emc2101.emc2101_fanspeed import FanSpeedLUT +from adafruit_emc2101.emc2101_ext import EMC2101_EXT __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EMC2101.git" -_FAN_CONFIG = const(0x4A) -_PWM_DIV = const(0x4E) -_LUT_HYSTERESIS = const(0x4F) -_LUT_BASE = const(0x50) - - -class FanSpeedLUT: - """A class used to provide a dict-like interface to the EMC2101's Temperature to Fan speed - Look Up Table. - - Keys are integer temperatures, values are fan duty cycles between 0 and 100. - A max of 8 values may be stored. - To remove a single stored point in the LUT, assign it as `None`. - """ - - # 8 (Temperature, Speed) pairs in increasing order - _fan_lut = StructArray(_LUT_BASE, " 100.0 or value < 0: - # Range check - raise AttributeError("LUT values must be a fan speed from 0-100%") - else: - self.lut_values[index] = value - self._update_lut() - - def __repr__(self): - """return the official string representation of the LUT""" - return "FanSpeedLUT <%x>" % id(self) - - def __str__(self): - """return the official string representation of the LUT""" - value_strs = [] - lut_keys = tuple(sorted(self.lut_values.keys())) - for temp in lut_keys: - fan_drive = self.lut_values[temp] - value_strs.append("%d deg C => %.1f%% duty cycle" % (temp, fan_drive)) - - return "\n".join(value_strs) - - def __len__(self): - return len(self.lut_values) - - # this function does a whole lot of work to organized the user-supplied lut dict into - # their correct spot within the lut table as pairs of set registers, sorted with the lowest - # temperature first - - def _update_lut(self): - # Make sure we're not going to try to set more entries than we have slots - if len(self.lut_values) > 8: - raise AttributeError("LUT can only contain a maximum of 8 items") - - # Backup state - current_mode = self.emc_fan.lut_enabled - - # Disable the lut to allow it to be updated - self.emc_fan.lut_enabled = False - - # we want to assign the lowest temperature to the lowest LUT slot, so we sort the keys/temps - # get and sort the new lut keys so that we can assign them in order - for idx, current_temp in enumerate(sorted(self.lut_values.keys())): - # We don't want to make `_speed_to_lsb()` public, it is only needed here. - # pylint: disable=protected-access - current_speed = self.emc_fan._speed_to_lsb(self.lut_values[current_temp]) - self._set_lut_entry(idx, current_temp, current_speed) - - # Set the remaining LUT entries to the default (Temp/Speed = max value) - for idx in range(len(self.lut_values), 8): - self._set_lut_entry(idx, MAX_LUT_TEMP, MAX_LUT_SPEED) - self.emc_fan.lut_enabled = current_mode - - def _set_lut_entry(self, idx, temp, speed): - self._fan_lut[idx * 2] = bytearray((temp,)) - self._fan_lut[idx * 2 + 1] = bytearray((speed,)) - - -class EMC2101_LUT(EMC2101): # pylint: disable=too-many-instance-attributes - """Driver for the EMC2101 Fan Controller, with PWM frequency and LUT control. +class EMC2101_LUT(EMC2101_EXT): # pylint: disable=too-many-instance-attributes + """Driver for the EMC2101 Fan Controller, with PWM frequency and temperature + look-up-table (LUT) control. See :class:`adafruit_emc2101.EMC2101` for the base/common functionality. + See :class:`adafruit_emc2101.EMC2101_EXT` for (almost) complete device register + set but no temperature look-up-table LUT support. :param ~busio.I2C i2c_bus: The I2C bus the EMC is connected to. """ - _fan_pwm_clock_select = RWBit(_FAN_CONFIG, 3) - _fan_pwm_clock_override = RWBit(_FAN_CONFIG, 2) - _pwm_freq_div = UnaryStruct(_PWM_DIV, " 0x1F: - raise AttributeError("pwm_frequency must be from 0-31") + """Set the PWM (fan) output frequency, which is a value from the + datasheet. + + :param int: value the frequency value tag. + :raises ValueError: if the assigned frequency is not valid. + :raises RuntimeError: if auto_check_status and an alert status bit is set + """ + if not 0 <= value < 32: + raise ValueError("pwm_frequency must be from 0-31") self._pwm_freq = value self._calculate_full_speed(pwm_f=value) + self._check_status() @property def pwm_frequency_divisor(self): - """The Divisor applied to the PWM frequency to set the final frequency""" + """The Divisor applied to the PWM frequency to set the final frequency. + + :raises RuntimeError: if auto_check_status and an alert status bit is set + """ + self._check_status() return self._pwm_freq_div @pwm_frequency_divisor.setter def pwm_frequency_divisor(self, divisor): - if divisor < 0 or divisor > 255: - raise AttributeError("pwm_frequency_divisor must be from 0-255") + """Set the PWM (fan) output frequency divisor, which is a value from + the datasheet. + + :param int: value the frequency divisor tag. + :raises ValueError: if the assigned divisor is not valid. + :raises RuntimeError: if auto_check_status and an alert status bit is set + """ + if not 0 <= divisor <= 255: + raise ValueError("pwm_frequency_divisor must be from 0-255") self._pwm_freq_div = divisor + self._check_status() @property def lut_enabled(self): - """Enable or disable the internal look up table used to map a given temperature - to a fan speed. When the LUT is disabled fan speed can be changed with `manual_fan_speed`""" + """Enable or disable the internal look up table used to map a given + temperature to a fan speed. When the LUT is disabled fan speed can be + changed with `manual_fan_speed`. + :return enable_lut + """ + self._check_status() return not self._fan_lut_prog @lut_enabled.setter def lut_enabled(self, enable_lut): + """Enable or disable the internal look up table used to map a given + temperature to a fan speed. When the LUT is disabled fan speed can be + changed with `manual_fan_speed`. + + :param bool: enable_lut + :raises RuntimeError: if auto_check_status and an alert status bit is set + """ self._fan_lut_prog = not enable_lut + self._check_status() @property def lut(self): - """The dict-like representation of the LUT, actually of type :class:`FanSpeedLUT`""" + """The dict-like representation of the LUT, an instance of + :class:`FanSpeedLUT`. Use this to update or read the current LUT. + + You can use python 'with' on this class to perform a multiple + update of the LUT. Usage: + + .. code-block:: python + + with emc2101.lut as lut: + lut[20] = 0 + lut[40] = 10 + + The device only supports 8 entries in the LUT. If you try to add + more than this the update will fail with a ValueError. If the add + is part of a 'with' block, this happens when the block ends. + + To delete an entry from the current table, assign None to the + current temperature slot(s). + + .. code-block:: python + + with emc2101.lut as lut: + lut[20] = 0 + lut[40] = 10 + + emc2101.lut[20] = None + print(emc2101.lut.lookup_table) + + will print one item, for temp 40, speed 10%. + """ return self._lut diff --git a/adafruit_emc2101/emc2101_regs.py b/adafruit_emc2101/emc2101_regs.py new file mode 100644 index 0000000..e0db291 --- /dev/null +++ b/adafruit_emc2101/emc2101_regs.py @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022 Ruth Ivimey-Cook for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_emc2101.emc2101_regs` +================================================================================ + +Brushless fan controller EMC2101 Register addresses. + +Register offset definitions for the SMC EMC2101 fan controller. + +* Author(s): Bryan Siepert, Ruth Ivimey-Cook + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit EMC2101 Breakout + `_ (Product ID: 4808) + +**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 + +* Adafruit's Register library: + https://github.com/adafruit/Adafruit_CircuitPython_Register + +""" + +from micropython import const + +MFG_ID_SMSC = const(0x5D) +PART_ID_EMC2101 = const(0x16) +PART_ID_EMC2101R = const(0x28) + +I2C_ADDR = const(0x4C) + +MAX_LUT_SPEED = const(0x3F) # 6-bit value +MAX_LUT_TEMP = const(0x7F) # 7-bit + +# Bits in device status register for masks etc. +STATUS_BUSY = const(0x80) +STATUS_INTHIGH = const(0x40) +STATUS_EEPROM = const(0x20) +STATUS_EXTHIGH = const(0x10) +STATUS_EXTLOW = const(0x08) +STATUS_FAULT = const(0x04) +STATUS_TCRIT = const(0x02) +STATUS_TACH = const(0x01) + +STATUS_ALERT = ( + STATUS_TACH + | STATUS_TCRIT + | STATUS_FAULT + | STATUS_EXTLOW + | STATUS_EXTHIGH + | STATUS_INTHIGH +) + +# Bits in device configuration register for masks etc. +CONFIG_MASK = const(0x80) +CONFIG_STANDBY = const(0x40) +CONFIG_FAN_STANDBY = const(0x20) +CONFIG_DAC = const(0x10) +CONFIG_DIS_TO = const(0x08) +CONFIG_ALT_TACH = const(0x04) +CONFIG_TCRIT_OVR = const(0x02) +CONFIG_QUEUE = const(0x01) + +# Values of external temp register for fault conditions. +TEMP_FAULT_OPENCIRCUIT = const(0x3F8) +TEMP_FAULT_SHORT = const(0x3FF) + +# See datasheet section 6.14: +FAN_RPM_DIVISOR = const(5400000) + +# +# EMC2101 Register Addresses +# +INTERNAL_TEMP = const(0x00) # Readonly +EXTERNAL_TEMP_MSB = const(0x01) # Readonly, Read MSB first +EXTERNAL_TEMP_LSB = const(0x10) # Readonly +REG_STATUS = const(0x02) # Readonly +REG_CONFIG = const(0x03) # Also at 0x09 +CONVERT_RATE = const(0x04) # Also at 0x0A +INT_TEMP_HI_LIM = const(0x05) # Also at 0x0B +TEMP_FORCE = const(0x0C) +ONESHOT = const(0x0F) # Effectively Writeonly +SCRATCH_1 = const(0x11) +SCRATCH_2 = const(0x12) +EXT_TEMP_LO_LIM_LSB = const(0x14) +EXT_TEMP_LO_LIM_MSB = const(0x08) # Also at 0x0E +EXT_TEMP_HI_LIM_LSB = const(0x13) +EXT_TEMP_HI_LIM_MSB = const(0x07) # Also at 0x0D +ALERT_MASK = const(0x16) +EXT_IDEALITY = const(0x17) +EXT_BETACOMP = const(0x18) +TCRIT_TEMP = const(0x19) +TCRIT_HYST = const(0x21) +TACH_LSB = const(0x46) # Readonly, Read MSB first +TACH_MSB = const(0x47) # Readonly +TACH_LIMIT_LSB = const(0x48) +TACH_LIMIT_MSB = const(0x49) +FAN_CONFIG = const(0x4A) +FAN_SPINUP = const(0x4B) +REG_FAN_SETTING = const(0x4C) +PWM_FREQ = const(0x4D) +PWM_FREQ_DIV = const(0x4E) +FAN_TEMP_HYST = const(0x4F) +AVG_FILTER = const(0xBF) + +REG_PARTID = const(0xFD) # Readonly, 0x16 (or 0x28 for -R part) +REG_MFGID = const(0xFE) # Readonly, SMSC is 0x5D +REG_REV = const(0xFF) # Readonly, e.g. 0x01 + +LUT_HYSTERESIS = const(0x4F) +LUT_BASE = const(0x50) diff --git a/docs/api.rst b/docs/api.rst index 436c29f..3414142 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -8,6 +8,17 @@ :members: :exclude-members: CV +.. automodule:: adafruit_emc2101.emc2101_regs + :members: .. automodule:: adafruit_emc2101.emc2101_lut :members: + +.. automodule:: adafruit_emc2101.emc2101_fanspeed + :members: + +.. automodule:: adafruit_emc2101.emc2101_ext + :members: + +.. automodule:: adafruit_emc2101.emc2101_enums + :members: