diff --git a/adafruit_emc2101.py b/adafruit_emc2101/__init__.py similarity index 53% rename from adafruit_emc2101.py rename to adafruit_emc2101/__init__.py index 3ed3002..0ccec77 100644 --- a/adafruit_emc2101.py +++ b/adafruit_emc2101/__init__.py @@ -21,9 +21,8 @@ * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases - -# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice -# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register +* 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 @@ -39,7 +38,6 @@ _EXTERNAL_TEMP_MSB = const(0x01) _EXTERNAL_TEMP_LSB = const(0x10) -_STATUS = const(0x02) _REG_CONFIG = const(0x03) _TEMP_FORCE = const(0x0C) _TACH_LSB = const(0x46) @@ -49,11 +47,7 @@ _FAN_CONFIG = const(0x4A) _FAN_SPINUP = const(0x4B) _REG_FAN_SETTING = const(0x4C) -_PWM_FREQ = const(0x4D) -_PWM_DIV = const(0x4E) -_LUT_HYSTERESIS = const(0x4F) -_TEMP_FILTER = const(0xBF) _REG_PARTID = const(0xFD) # 0x16 _REG_MFGID = const(0xFE) # 0xFF16 @@ -62,145 +56,6 @@ _I2C_ADDR = const(0x4C) _FAN_RPM_DIVISOR = const(5400000) -_TEMP_LSB = 0.125 - - -def _speed_to_lsb(percentage): - return round((percentage / 100.0) * MAX_LUT_SPEED) - - -def _lsb_to_speed(lsb_speed): - return round((lsb_speed / MAX_LUT_SPEED) * 100.0) - - -class FanSpeedLUT: - """A class used to provide a dict-like interface to the EMC2101's Temperature to Fan speed - Look Up Table""" - - # seems like a pain but ¯\_(ツ)_/¯ - _fan_lut_t1 = UnaryStruct(0x50, "" % id(self) - - def __str__(self): - """return the official string representation of the LUT""" - value_strs = [] - lut_keys = list(self.lut_values.keys()) - lut_keys.sort() - 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 _set_lut(self, lut_dict): - lut_keys = list(lut_dict.keys()) - lut_size = len(lut_dict) - # Make sure we're not going to try to set more entries than we have slots - if lut_size > 8: - raise AttributeError("LUT can only contain a maximum of 8 items") - - # we want to assign the lowest temperature to the lowest LUT slot, so we sort the keys/temps - for k in lut_keys: - # Verify that the value is a correct amount - lut_value = lut_dict[k] - if lut_value > 100.0 or lut_value < 0: - raise AttributeError("LUT values must be a fan speed from 0-100%") - - # add the current temp/speed to our internal representation - self.lut_values[k] = lut_value - current_mode = self.emc_fan.lut_enabled - - # Disable the lut to allow it to be updated - self.emc_fan.lut_enabled = False - - # get and sort the new lut keys so that we can assign them in order - lut_keys = list(self.lut_values.keys()) - lut_keys.sort() - for idx in range(lut_size): - current_temp = lut_keys[idx] - current_speed = _speed_to_lsb(self.lut_values[current_temp]) - getattr(self, "_fan_lut_t%d" % (idx + 1)).__set__(self, current_temp) - getattr(self, "_fan_lut_s%d" % (idx + 1)).__set__(self, current_speed) - - # self.emc_fan._lut_temp_setters[idx].__set__(self.emc_fan, current_temp) - # self.emc_fan._lut_speed_setters[idx].__set__(self.emc_fan, current_speed) - - # Set the remaining LUT entries to the default (Temp/Speed = max value) - for idx in range(8)[lut_size:]: - getattr(self, "_fan_lut_t%d" % (idx + 1)).__set__(self, MAX_LUT_TEMP) - getattr(self, "_fan_lut_s%d" % (idx + 1)).__set__(self, MAX_LUT_SPEED) - self.emc_fan.lut_enabled = current_mode class CV: @@ -208,7 +63,7 @@ class CV: @classmethod def add_values(cls, value_tuples): - "creates CV entires" + "creates CV entries" cls.string = {} cls.lsb = {} @@ -277,20 +132,28 @@ class SpinupTime(CV): class EMC2101: # pylint: disable=too-many-instance-attributes - """Driver for the EMC2101 Fan Controller. + """Basic driver for the EMC2101 Fan Controller. + :param ~busio.I2C i2c_bus: The I2C bus the EMC is connected to. + + 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. """ _part_id = ROUnaryStruct(_REG_PARTID, " 0x1F: - raise AttributeError("pwm_frequency must be from 0-31") - self._pwm_freq = value - - @property - def pwm_frequency_divisor(self): - """The Divisor applied to the PWM frequency to set the final frequency""" - 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") - self._pwm_freq_div = divisor - @property def fan_speed(self): """The current speed in Revolutions per Minute (RPM)""" @@ -431,7 +232,7 @@ def manual_fan_speed(self): @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 ") + raise AttributeError("manual_fan_speed must be from 0-100") # convert from a percentage to an lsb value percentage = fan_speed / 100.0 @@ -444,17 +245,13 @@ def manual_fan_speed(self, fan_speed): @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`""" - return not self._fan_lut_prog - - @lut_enabled.setter - def lut_enabled(self, enable_lut): - self._fan_lut_prog = not enable_lut + to a fan speed. - @property - def lut(self): - """The dict-like representation of the LUT""" - return self._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): diff --git a/adafruit_emc2101/emc2101_lut.py b/adafruit_emc2101/emc2101_lut.py new file mode 100644 index 0000000..be770ca --- /dev/null +++ b/adafruit_emc2101/emc2101_lut.py @@ -0,0 +1,244 @@ +# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_emc2101.emc2101_lut` +================================================================================ + +Brushless fan controller: extended functionality + + +* Author(s): Bryan Siepert, Ryan Pavlik + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit EMC2101 Breakout `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +* 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 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 adafruit_register.i2c_bits import RWBits +from . import EMC2101 + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EMC2101.git" + +_FAN_CONFIG = const(0x4A) +_PWM_FREQ = const(0x4D) +_PWM_DIV = const(0x4E) +_LUT_HYSTERESIS = const(0x4F) +_LUT_BASE = const(0x50) + +MAX_LUT_SPEED = 0x3F # 6-bit value +MAX_LUT_TEMP = 0x7F # 7-bit + + +def _speed_to_lsb(percentage): + return round((percentage / 100.0) * MAX_LUT_SPEED) + + +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`. + To clear all entries, use :meth:`FanSpeedLUT.clear` + """ + + # 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())): + current_speed = _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,)) + + def clear(self): + """Clear all LUT entries.""" + self.lut_values = {} + self._update_lut() + + +class EMC2101_LUT(EMC2101): # pylint: disable=too-many-instance-attributes + """Driver for the EMC2101 Fan Controller, with PWM frequency and LUT control. + + See :class:`adafruit_emc2101.EMC2101` for the base/common functionality. + + :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 = RWBits(5, _PWM_FREQ, 0) + _pwm_freq_div = UnaryStruct(_PWM_DIV, " 0x1F: + raise AttributeError("pwm_frequency must be from 0-31") + self._pwm_freq_div = value + + @property + def pwm_frequency_divisor(self): + """The Divisor applied to the PWM frequency to set the final frequency""" + 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") + self._pwm_freq_div = divisor + + @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`""" + return not self._fan_lut_prog + + @lut_enabled.setter + def lut_enabled(self, enable_lut): + self._fan_lut_prog = not enable_lut + + @property + def lut(self): + """The dict-like representation of the LUT, actually of type :class:`FanSpeedLUT`""" + return self._lut + + def clear_lut(self): + """Clear all LUT entries.""" + self._lut.clear() diff --git a/docs/api.rst b/docs/api.rst index daa9bd5..a798197 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,3 +6,7 @@ .. automodule:: adafruit_emc2101 :members: + + +.. automodule:: adafruit_emc2101.emc2101_lut + :members: diff --git a/docs/examples.rst b/docs/examples.rst index 89b6025..5adfe22 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -11,6 +11,8 @@ LUT Usage Example ----------------- Use the temperature to fan speed Look Up Table to automatically control the fan speed. +This example requires more memory than the first one because it needs to use the extended +:class:`adafruit_emc2101.emc2101_lut.EMC2101_LUT` driver to access LUT functionality. .. literalinclude:: ../examples/emc2101_lut_example.py :caption: examples/emc2101_lut_example.py @@ -21,6 +23,8 @@ PWM Tuning ----------------- Adjust the EMC2101s PWM settings to fit your application. +This example requires more memory than the first one because it needs to use the extended +:class:`adafruit_emc2101.emc2101_lut.EMC2101_LUT` driver to access LUT functionality. .. literalinclude:: ../examples/set_pwm_freq.py :caption: examples/set_pwm_freq.py diff --git a/examples/emc2101_lut_example.py b/examples/emc2101_lut_example.py index a68f703..2e4ce04 100644 --- a/examples/emc2101_lut_example.py +++ b/examples/emc2101_lut_example.py @@ -4,7 +4,7 @@ import time import board import busio -from adafruit_emc2101 import EMC2101 +from adafruit_emc2101.emc2101_lut import EMC2101_LUT as EMC2101 i2c = busio.I2C(board.SCL, board.SDA) @@ -27,6 +27,6 @@ time.sleep(3) print("50%% duty cycle is %f RPM:" % emc.fan_speed) -emc.forced_ext_temp = 43 # over 30, should be 50% +emc.forced_ext_temp = 43 # over 42, should be 75% time.sleep(3) print("75%% duty cycle is %f RPM:" % emc.fan_speed) diff --git a/examples/set_pwm_freq.py b/examples/set_pwm_freq.py index faf8dc8..78df320 100644 --- a/examples/set_pwm_freq.py +++ b/examples/set_pwm_freq.py @@ -4,7 +4,7 @@ import time import board import busio -from adafruit_emc2101 import EMC2101 +from adafruit_emc2101.emc2101_lut import EMC2101_LUT as EMC2101 i2c = busio.I2C(board.SCL, board.SDA)