Skip to content

Add clear lut #10

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

Closed
wants to merge 12 commits into from
243 changes: 20 additions & 223 deletions adafruit_emc2101.py → adafruit_emc2101/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -62,153 +56,14 @@

_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, "<B")
_fan_lut_s1 = UnaryStruct(0x51, "<B")

_fan_lut_t2 = UnaryStruct(0x52, "<B")
_fan_lut_s2 = UnaryStruct(0x53, "<B")

_fan_lut_t3 = UnaryStruct(0x54, "<B")
_fan_lut_s3 = UnaryStruct(0x55, "<B")

_fan_lut_t4 = UnaryStruct(0x56, "<B")
_fan_lut_s4 = UnaryStruct(0x57, "<B")

_fan_lut_t5 = UnaryStruct(0x58, "<B")
_fan_lut_s5 = UnaryStruct(0x59, "<B")

_fan_lut_t6 = UnaryStruct(0x5A, "<B")
_fan_lut_s6 = UnaryStruct(0x5B, "<B")

_fan_lut_t7 = UnaryStruct(0x5C, "<B")
_fan_lut_s7 = UnaryStruct(0x5D, "<B")

_fan_lut_t8 = UnaryStruct(0x5E, "<B")
_fan_lut_s8 = UnaryStruct(0x5F, "<B")

_lut_speed_setters = [
_fan_lut_s1,
_fan_lut_s2,
_fan_lut_s3,
_fan_lut_s4,
_fan_lut_s5,
_fan_lut_s6,
_fan_lut_s7,
_fan_lut_s8,
]
_lut_temp_setters = [
_fan_lut_t1,
_fan_lut_t2,
_fan_lut_t3,
_fan_lut_t4,
_fan_lut_t5,
_fan_lut_t6,
_fan_lut_t7,
_fan_lut_t8,
]

def __init__(self, fan_obj):
self.emc_fan = fan_obj
self.lut_values = {}
self.i2c_device = fan_obj.i2c_device

def __getitem__(self, index):
if not isinstance(index, int):
raise IndexError
if not index in self.lut_values:
raise IndexError
return self.lut_values[index]

def __setitem__(self, index, value):
if not isinstance(index, int):
raise IndexError
self.lut_values[index] = value
self._set_lut(self.lut_values)

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 = 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:
"""struct helper"""

@classmethod
def add_values(cls, value_tuples):
"creates CV entires"
"creates CV entries"
cls.string = {}
cls.lsb = {}

Expand Down Expand Up @@ -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, "<B")
_mfg_id = ROUnaryStruct(_REG_MFGID, "<B")
_int_temp = ROUnaryStruct(_INTERNAL_TEMP, "<b")

# Some of these registers are defined as two halves because
# the chip does not support multi-byte reads or writes, and there
# is currently no way to tell Struct to do a transaction for each byte.

# IMPORTANT! the sign bit for the external temp is in the msbyte so mark it as signed
# and lsb as unsigned
_ext_temp_msb = ROUnaryStruct(_EXTERNAL_TEMP_MSB, "<b")
_ext_temp_lsb = ROUnaryStruct(_EXTERNAL_TEMP_LSB, "<B")

# _tach_read = ROUnaryStruct(_TACH_LSB, "<H")
_tach_read_lsb = ROUnaryStruct(_TACH_LSB, "<B")
_tach_read_msb = ROUnaryStruct(_TACH_MSB, "<B")
_tach_mode_enable = RWBit(_REG_CONFIG, 2)
Expand All @@ -310,11 +173,6 @@ class EMC2101: # pylint: disable=too-many-instance-attributes
"""When set to True, the magnitude of the fan output signal is inverted, making 0 the maximum
value and 100 the minimum value"""

_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, "<B")

dac_output_enabled = RWBit(_REG_CONFIG, 4)
"""When set, the fan control signal is output as a DC voltage instead of a PWM signal"""

Expand All @@ -324,27 +182,18 @@ class EMC2101: # pylint: disable=too-many-instance-attributes
_spin_time = RWBits(3, _FAN_SPINUP, 0)
_spin_tach_limit = RWBit(_FAN_SPINUP, 5)

lut_temperature_hysteresis = UnaryStruct(_LUT_HYSTERESIS, "<B")
"""The amount of hysteresis in Degrees celcius of hysteresis applied to temperature readings
used for the LUT. As the temperature drops, the controller will switch to a lower LUT entry when
the measured value is belowthe lower entry's threshold, minus the hysteresis value"""

def __init__(self, i2c_bus):
self.i2c_device = i2cdevice.I2CDevice(i2c_bus, _I2C_ADDR)

if not self._part_id in [0x16, 0x28] or self._mfg_id != 0x5D:
raise AttributeError("Cannot find a EMC2101")
# self._lut = {}

self.initialize()
self._lut = FanSpeedLUT(self)

def initialize(self):
"""Reset the controller to an initial default configuration"""
self._tach_mode_enable = True
self.lut_enabled = False
self._enabled_forced_temp = False
self._fan_pwm_clock_override = True
self._spin_tach_limit = False

@property
Expand All @@ -364,54 +213,6 @@ def external_temperature(self):

return full_tmp

def set_pwm_clock(self, use_preset=False, use_slow=False):
"""
Select the PWM clock source, chosing between two preset clocks or by configuring the
clock using `pwm_frequency` and `pwm_frequency_divisor`.

:param bool use_preset:
True: Select between two preset clock sources
False: The PWM clock is set by `pwm_frequency` and `pwm_frequency_divisor`
:param bool use_slow:
True: Use the 1.4kHz clock
False: Use the 360kHz clock.
:type priority: integer or None
:return: None
:raises AttributeError: if use_preset is not a `bool`
:raises AttributeError: if use_slow is not a `bool`

"""

if not isinstance(use_preset, bool):
raise AttributeError("use_preset must be given a bool")
if not isinstance(use_slow, bool):
raise AttributeError("use_slow_pwm must be given a bool")

self._fan_pwm_clock_override = not use_preset
self._fan_pwm_clock_select = use_slow

@property
def pwm_frequency(self):
"""Selects the base clock frequency used for the fan PWM output"""
return self._pwm_freq

@pwm_frequency.setter
def pwm_frequency(self, value):
if value < 0 or value > 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)"""
Expand All @@ -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
Expand All @@ -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):
Expand Down
Loading