Skip to content

Commit

Permalink
ITE SuperIO fan control (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
xCuri0 committed Jun 10, 2023
1 parent 9b5d754 commit b150248
Show file tree
Hide file tree
Showing 11 changed files with 948 additions and 52 deletions.
30 changes: 30 additions & 0 deletions Docs/SuperIOs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## SMCSuperIO fan configuration
SMCSuperIO can read fan speeds on ITE/Nuvoton/Winbond and set fan speeds on ITE. The default configuration often works but there can be issues such as fan speed controlling a different fan, set speed not matching or non-existent fans showing.

You can find out how to configure this module here.

`#` in the property name should be equal to the fan number displayed in your fan control software (eg. first fan = fan0, second fan = fan1). Hide non-existent fans last as doing it first will make it harder to figure out correct fan index.

### fan#-hide
Hides the fan when set to value 01 or above. Example `01` **MUST** be type **Data**.

### fan#-control
The control index to use for the fan, useful for fixing a different/non-existent fan being controlled when setting speed. By default it's equal to the fan index which works for some configurations. Example `02`, **MUST** be type **Data**.

### fan#-pwm
RPM to PWM curve. It can be automatically generated with tool `fanpwmgen`.

```
❯ ./fanpwmgen -h
VirtualSMC fan#-pwm generation tool
Usage:
./fanpwmgen [options]
-f <num> : the fan number to use
-s <steps> : number of steps to use when generating curve, default 16 and max 256
-t <time> : time to wait before going to next step, default 2 seconds
-h : help
```

Before running `fanpwmgen` you **MUST** make sure there is no `fan#-pwm` curve already set for that fan. Also **make sure all fan control software is closed**

For example to generate a curve for fan #1 use `./fanpwmgen -f 1`. Once finished running it will output the fan curve after `fan#-pwm: ` which you can add to DeviceProperties. **MUST** be type **String**.
20 changes: 20 additions & 0 deletions Sensors/SMCSuperIO/Devices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,24 @@ class Device_0x8688 final : public GeneratedITEDevice_6 {

};

class Device_0x8689 final : public GeneratedITEDevice_6 {
public:
static SuperIODevice *createDevice(uint16_t deviceId) {
if (deviceId == 0x8689)
return new Device_0x8689();
return nullptr;
}

uint8_t getLdn() override {
return 0x04;
}

const char* getModelName() override {
return "ITE IT8689E";
}

};

class Device_0x8795 final : public GeneratedITEDevice_6 {
public:
static SuperIODevice *createDevice(uint16_t deviceId) {
Expand Down Expand Up @@ -3365,6 +3383,8 @@ SuperIODevice *createDeviceITE(uint16_t deviceId) {
if (device) return device;
device = Device_0x8688::createDevice(deviceId);
if (device) return device;
device = Device_0x8689::createDevice(deviceId);
if (device) return device;
device = Device_0x8795::createDevice(deviceId);
if (device) return device;
device = Device_0x8665::createDevice(deviceId);
Expand Down
243 changes: 238 additions & 5 deletions Sensors/SMCSuperIO/ITEDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
#include "Devices.hpp"

namespace ITE {
uint16_t _pwmCurve[ITE_MAX_TACHOMETER_COUNT][256];
uint8_t _fanControlIndex[ITE_MAX_TACHOMETER_COUNT];
uint8_t _initialFanPwmControl[ITE_MAX_TACHOMETER_COUNT];
uint8_t _initialFanOutputModeEnabled[ITE_MAX_TACHOMETER_COUNT];
uint8_t _initialFanPwmControlExt[ITE_MAX_TACHOMETER_COUNT];
bool _restoreDefaultFanPwmControlRequired[ITE_MAX_TACHOMETER_COUNT];
bool _hasExtReg;

uint16_t ITEDevice::tachometerRead8bit(uint8_t index) {
if (index > 2) {
Expand All @@ -36,6 +43,63 @@ namespace ITE {
return value > 0x3f && value < 0xffff ? (1.35e6f + value) / (value * 2) : 0;
}

void ITEDevice::tachometerWrite(uint8_t index, uint8_t value, bool enabled) {
if (enabled) {
tachometerSaveDefault(index);

if (index < 3 && !_initialFanOutputModeEnabled[index])
writeByte(FAN_MAIN_CTRL_REG, readByte(FAN_MAIN_CTRL_REG) | (1 << index));

if (_hasExtReg)
{
if (strcmp(getModelName(), "ITE IT8689E"))
writeByte(FAN_PWM_CTRL_REG[index], 0x7F);
else
writeByte(FAN_PWM_CTRL_REG[index], _initialFanPwmControl[index] & 0x7F);

writeByte(FAN_PWM_CTRL_EXT_REG[index], value);
}
else
writeByte(FAN_PWM_CTRL_REG[index], (value >> 1));

} else
tachometerRestoreDefault(index);
}

void ITEDevice::tachometerSaveDefault(uint8_t index) {
if (!_restoreDefaultFanPwmControlRequired[index]) {
_initialFanPwmControl[index] = readByte(FAN_PWM_CTRL_REG[index]);

if (index < 3)
_initialFanOutputModeEnabled[index] = readByte(FAN_MAIN_CTRL_REG) != 0; // Save default control reg value.

if (_hasExtReg)
_initialFanPwmControlExt[index] = readByte(FAN_PWM_CTRL_EXT_REG[index]);

_restoreDefaultFanPwmControlRequired[index] = true;
}
}

void ITEDevice::tachometerRestoreDefault(uint8_t index) {
if (_restoreDefaultFanPwmControlRequired[index]) {
writeByte(FAN_PWM_CTRL_REG[index], _initialFanPwmControl[index]);

if (index < 3) {
uint8_t value = readByte(FAN_MAIN_CTRL_REG);

bool isEnabled = (value & (1 << index)) != 0;
if (isEnabled != _initialFanOutputModeEnabled[index])
writeByte(FAN_MAIN_CTRL_REG, value ^ (1 << index));
}

if (_hasExtReg)
writeByte(FAN_PWM_CTRL_EXT_REG[index], _initialFanPwmControlExt[index]);

_restoreDefaultFanPwmControlRequired[index] = false;
}
}


uint16_t ITEDevice::tachometerReadEC(uint8_t index) {
uint16_t value = readByteEC(ITE_EC_FAN_TACHOMETER_REG[index]);
value |= readByteEC(ITE_EC_FAN_TACHOMETER_EXT_REG[index]) << 8;
Expand Down Expand Up @@ -104,14 +168,171 @@ namespace ITE {
::outb(addrPort, ITE_I2EC_D2DAT_REG);
::outb(dataPort, value);
}


float lerp(float a, float b, float f) {
return a * (1.0 - f) + (b * f);
}

uint16_t getCurveMin(uint8_t index) {
int i;
for (i = 0; i <= 255; ++i) {
if (_pwmCurve[index][i] != UINT16_MAX)
break;
}
return _pwmCurve[index][i];;
}

uint16_t getCurveMax(uint8_t index) {
int i;
for (i = 255; i >= 0; --i) {
if (_pwmCurve[index][i] != UINT16_MAX)
break;
}
return _pwmCurve[index][i];;
}

// probably could do this better
uint8_t getCurveValue(uint8_t index, uint16_t rpm) {
for (int i = 0; i <= 255; ++i) {
if (_pwmCurve[index][i] == UINT16_MAX)
continue;
if (_pwmCurve[index][i] >= rpm)
return i;
}
return getCurveValue(index, getCurveMax(index));
}

void curveFromStr(uint8_t index, char* str) {
char *ptr, *split, *rpmS, *pwmS;
uint8_t pwm;
uint16_t rpm;

while (true) {
split = strsep(&str, "|");

if (!split)
break;

rpmS = strsep(&split, ",");
pwmS = strsep(&split, ",");

// RPM value can't be NULL, no need to check it
if (!pwmS)
continue;

rpm = strtoul(rpmS, &ptr, 10);
pwm = strtoul(pwmS, &ptr, 10);

_pwmCurve[index][pwm] = rpm;
}
}

void computeCurve(uint8_t index) {
for (int i = 0; i <= 255; i++) {
if (_pwmCurve[index][i] == UINT16_MAX)
continue;

for (int j = i + 1; j <= 255; j++) {
if (_pwmCurve[index][j] == UINT16_MAX)
continue;

// No points in between
if (j == i + 1)
break;

// We have the 2 points now so lets interpolate in between them.
for (int k = i + 1; k <= j ; ++k) {
_pwmCurve[index][k] = lerp(_pwmCurve[index][i], _pwmCurve[index][j], (float)(k - i) / (float)(j - i));
}

i = j - 1;
break;
}
}
}

void ITEDevice::updateTargets() {
// Update target speeds
for (uint8_t index = 0; index < getTachometerCount(); index++) {
DBGLOG("ssio", "ITEDevice Fan %u RPM %d Manual %u", index, getTargetValue(index), getManualValue(index));

ITEDevice::tachometerWrite(_fanControlIndex[index], getCurveValue(index, getTargetValue(index)), getManualValue(index));
}
}

void ITEDevice::setupKeys(VirtualSMCAPI::Plugin &vsmcPlugin) {
VirtualSMCAPI::addKey(KeyFNum, vsmcPlugin.data,
VirtualSMCAPI::valueWithUint8(getTachometerCount(), nullptr, SMC_KEY_ATTRIBUTE_CONST | SMC_KEY_ATTRIBUTE_READ));
uint8_t fanCount = 0;

for (uint8_t index = 0; index < getTachometerCount(); ++index) {
VirtualSMCAPI::addKey(KeyF0Ac(index), vsmcPlugin.data,
VirtualSMCAPI::valueWithFp(0, SmcKeyTypeFpe2, new TachometerKey(getSmcSuperIO(), this, index)));
char name[16];
uint32_t tmp;
IORegistryEntry *lpc;
char *nameVal = NULL;

lpc = smcSuperIO->getParentEntry(gIOServicePlane);

// Skip hidden fans
snprintf(name, sizeof(name), "fan%u-hide", index);
if (WIOKit::getOSDataValue<uint8_t>(lpc, name, tmp))
continue;

// Set control index
snprintf(name, sizeof(name), "fan%u-control", index);
if (WIOKit::getOSDataValue<uint8_t>(lpc, name, tmp) && tmp <= getTachometerCount())
_fanControlIndex[index] = tmp;
else
_fanControlIndex[index] = index;

// Set default PWM values
for (int i = 0; i < 256; ++i) _pwmCurve[index][i] = UINT16_MAX;

// Set PWM curve
snprintf(name, sizeof(name), "fan%u-pwm", index);
auto nameP = lpc->getProperty(name);
auto nameData = OSDynamicCast(OSData, nameP);

if (nameData) {
nameVal = STRDUP(static_cast<const char *>(nameData->getBytesNoCopy()), nameData->getLength());
curveFromStr(index, nameVal);
} else {
_pwmCurve[index][0] = 0;
_pwmCurve[index][255] = 3315;
}
computeCurve(index);

setMinValue(index, getCurveMin(index));
setMaxValue(index, getCurveMax(index));

// Use fanCount for key names, they will still use the proper index.
//
// Current speed
VirtualSMCAPI::addKey(KeyF0Ac(fanCount), vsmcPlugin.data,
VirtualSMCAPI::valueWithFp(0, SmcKeyTypeFpe2, new TachometerKey(getSmcSuperIO(), this, index), SMC_KEY_ATTRIBUTE_WRITE | SMC_KEY_ATTRIBUTE_READ));

// We must add keys in alphabetical order
if (getLdn() != EC_ENDPOINT) {
// Enable manual control
VirtualSMCAPI::addKey(KeyF0Md(fanCount), vsmcPlugin.data,
VirtualSMCAPI::valueWithUint8(0, new ManualKey(getSmcSuperIO(), this, index), SMC_KEY_ATTRIBUTE_WRITE | SMC_KEY_ATTRIBUTE_READ));
}
// Min speed
VirtualSMCAPI::addKey(KeyF0Mn(fanCount), vsmcPlugin.data,
VirtualSMCAPI::valueWithFp(0, SmcKeyTypeFpe2, new MinKey(getSmcSuperIO(), this, index), SMC_KEY_ATTRIBUTE_WRITE | SMC_KEY_ATTRIBUTE_READ));
// Max speed
VirtualSMCAPI::addKey(KeyF0Mx(fanCount), vsmcPlugin.data,
VirtualSMCAPI::valueWithFp(0, SmcKeyTypeFpe2, new MaxKey(getSmcSuperIO(), this, index), SMC_KEY_ATTRIBUTE_WRITE | SMC_KEY_ATTRIBUTE_READ));

if (getLdn() != EC_ENDPOINT) {
// Target speed
VirtualSMCAPI::addKey(KeyF0Tg(fanCount), vsmcPlugin.data,
VirtualSMCAPI::valueWithFp(0, SmcKeyTypeFpe2, new TargetKey(getSmcSuperIO(), this, index), SMC_KEY_ATTRIBUTE_WRITE | SMC_KEY_ATTRIBUTE_READ));
}

fanCount++;
}

VirtualSMCAPI::addKey(KeyFNum, vsmcPlugin.data,
VirtualSMCAPI::valueWithUint8(fanCount, nullptr, SMC_KEY_ATTRIBUTE_CONST | SMC_KEY_ATTRIBUTE_READ));
}

/**
Expand Down Expand Up @@ -157,9 +378,21 @@ namespace ITE {
static_cast<ITEDevice *>(detectedDevice)->readByteEC(ITE_EC_GCTRL_BASE + ITE_EC_GCTRL_ECHIPID1),
static_cast<ITEDevice *>(detectedDevice)->readByteEC(ITE_EC_GCTRL_BASE + ITE_EC_GCTRL_ECHIPID2),
static_cast<ITEDevice *>(detectedDevice)->readByteEC(ITE_EC_GCTRL_BASE + ITE_EC_GCTRL_ECHIPVER));
} else {
if (strcmp(detectedDevice->getModelName(), "ITE IT8721F") || strcmp(detectedDevice->getModelName(), "ITE IT8728F") || strcmp(detectedDevice->getModelName(), "ITE IT8665E") || strcmp(detectedDevice->getModelName(), "ITE IT8686E") || strcmp(detectedDevice->getModelName(), "ITE IT8688E") || strcmp(detectedDevice->getModelName(), "ITE IT8689E") ||
strcmp(detectedDevice->getModelName(), "ITE IT8795E") || strcmp(detectedDevice->getModelName(), "ITE IT8628E") ||
strcmp(detectedDevice->getModelName(), "ITE IT8625E") || strcmp(detectedDevice->getModelName(), "ITE IT8620E") ||
strcmp(detectedDevice->getModelName(), "ITE IT8613E") || strcmp(detectedDevice->getModelName(), "ITE IT8792E") ||
strcmp(detectedDevice->getModelName(), "ITE IT8655E") || strcmp(detectedDevice->getModelName(), "ITE IT8631E"))
{
_hasExtReg = true;
}
if (strcmp(detectedDevice->getModelName(), "ITE IT8665E") || strcmp(detectedDevice->getModelName(), "ITE IT8625E"))
lilu_os_memcpy(&FAN_PWM_CTRL_REG, &FAN_PWM_CTRL_REG_ALT, ITE_MAX_TACHOMETER_COUNT);
}
}
leave(port);

return detectedDevice;
}

Expand Down
20 changes: 17 additions & 3 deletions Sensors/SMCSuperIO/ITEDevice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@

namespace ITE {

static constexpr uint8_t ITE_MAX_TACHOMETER_COUNT = 5;
static constexpr uint8_t ITE_MAX_TACHOMETER_COUNT = 6;
static constexpr uint8_t ITE_MAX_VOLTAGE_COUNT = 9;

static constexpr uint8_t ITE_ADDRESS_REGISTER_OFFSET = 0x05;
static constexpr uint8_t ITE_DATA_REGISTER_OFFSET = 0x06;
static constexpr uint8_t ITE_FAN_TACHOMETER_DIVISOR_REGISTER = 0x0B;
static constexpr uint8_t ITE_FAN_TACHOMETER_REG[ITE_MAX_TACHOMETER_COUNT] = { 0x0d, 0x0e, 0x0f, 0x80, 0x82 };
static constexpr uint8_t ITE_FAN_TACHOMETER_EXT_REG[ITE_MAX_TACHOMETER_COUNT] = { 0x18, 0x19, 0x1a, 0x81, 0x83 };
static constexpr uint8_t FAN_MAIN_CTRL_REG = 0x13;

static uint8_t FAN_PWM_CTRL_REG[ITE_MAX_TACHOMETER_COUNT] = { 0x15, 0x16, 0x17, 0x7f, 0xa7, 0xaf };
static constexpr uint8_t FAN_PWM_CTRL_REG_ALT[ITE_MAX_TACHOMETER_COUNT] = { 0x15, 0x16, 0x17, 0x1e, 0x1f, 0x92 };

static constexpr uint8_t FAN_PWM_CTRL_EXT_REG[ITE_MAX_TACHOMETER_COUNT] = { 0x63, 0x6b, 0x73, 0x7b, 0xa3, 0xab };
static constexpr uint8_t ITE_FAN_TACHOMETER_REG[ITE_MAX_TACHOMETER_COUNT] = { 0x0d, 0x0e, 0x0f, 0x80, 0x82, 0x4c };
static constexpr uint8_t ITE_FAN_TACHOMETER_EXT_REG[ITE_MAX_TACHOMETER_COUNT] = { 0x18, 0x19, 0x1a, 0x81, 0x83, 0x4d };
static constexpr uint8_t ITE_VOLTAGE_REG[ITE_MAX_VOLTAGE_COUNT] = { 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28 };

// ITE Debugger interface for EC memory snooping. Refer to "EC Memory Snoop (ECMS)" section in datasheet.
Expand Down Expand Up @@ -73,6 +79,14 @@ namespace ITE {
uint16_t tachometerRead8bit(uint8_t);
uint16_t tachometerReadEC(uint8_t);

void tachometerWrite(uint8_t index, uint8_t value, bool enabled);
void tachometerSaveDefault(uint8_t);
void tachometerRestoreDefault(uint8_t);
/**
* This is a stub to keep the code generator happy since updateTargets is overridden.
*/
void updateTargets() override;

/**
* Reads voltage data. Invoked from update() only.
*/
Expand Down

0 comments on commit b150248

Please sign in to comment.