Skip to content
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

ITE SuperIO fan control #72

Merged
merged 15 commits into from
Jun 10, 2023
Merged
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 @@ -3347,6 +3365,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