diff --git a/LibreHardwareMonitorLib/Hardware/Motherboard/Lpc/Nct677X.cs b/LibreHardwareMonitorLib/Hardware/Motherboard/Lpc/Nct677X.cs
index 38f24b8..5ae8847 100644
--- a/LibreHardwareMonitorLib/Hardware/Motherboard/Lpc/Nct677X.cs
+++ b/LibreHardwareMonitorLib/Hardware/Motherboard/Lpc/Nct677X.cs
@@ -19,23 +19,62 @@ namespace LibreHardwareMonitor.Hardware.Motherboard.Lpc;
internal class Nct677X : ISuperIO
{
+ // ReSharper disable InconsistentNaming
+ private const ushort ADDRESS_REGISTER_OFFSET = 0x05;
+ private const byte BANK_SELECT_REGISTER = 0x4E;
+ private const uint DATA_REGISTER_OFFSET = 0x06;
+
+ // NCT668X EC space
+ private const uint EC_SPACE_PAGE_REGISTER_OFFSET = 0x04;
+ private const uint EC_SPACE_INDEX_REGISTER_OFFSET = 0x05;
+ private const uint EC_SPACE_DATA_REGISTER_OFFSET = 0x06;
+ private const byte EC_SPACE_PAGE_SELECT = 0xFF;
+
+ private const ushort NUVOTON_VENDOR_ID = 0x5CA3;
+
+ // NCT6687DR EC engine status register and flags (based on Linux nct6687d driver)
+ private const ushort NCT6687DR_REG_FAN_ENGINE_STS = 0xCF8;
+ private const byte NCT6687DR_FAN_CFG_LOCK = 1 << 6;
+ private const byte NCT6687DR_FAN_CFG_PHASE = 1 << 3;
+ private const byte NCT6687DR_FAN_CFG_INVALID = 1 << 4;
+ private const byte NCT6687DR_FAN_CFG_CHECK_DONE = 1 << 5;
+ private const byte NCT6687DR_FAN_CFG_REQ = 0x80;
+ private const byte NCT6687DR_FAN_CFG_DONE = 0x40;
+ // ReSharper restore InconsistentNaming
+
+ // Chip identity
+ private readonly LpcPort _lpcPort;
+ private readonly ushort _port;
+ private readonly byte _revision;
+ private readonly bool _isNuvotonVendor;
+
+ // Fan registers (chip-specific, initialized in constructor)
+ // ReSharper disable InconsistentNaming
+ private readonly ushort[] FAN_CONTROL_MODE_REG;
+ private readonly int[] FAN_CONTROL_MODE_BIT; // NCT6687DR only: maps array index → bit position in mode register
+ private readonly ushort[] FAN_PWM_COMMAND_REG;
+ private readonly ushort[] FAN_PWM_OUT_REG;
+ private readonly ushort[] FAN_PWM_REQUEST_REG;
+ private readonly ushort VENDOR_ID_HIGH_REGISTER;
+ private readonly ushort VENDOR_ID_LOW_REGISTER;
+ // ReSharper restore InconsistentNaming
+
+ // Sensor registers
private readonly ushort[] _fanCountRegister;
private readonly ushort[] _fanRpmRegister;
- private readonly byte[] _initialFanControlMode = new byte[7];
- private readonly byte[] _initialFanPwmCommand = new byte[7];
- private readonly bool _isNuvotonVendor;
- private readonly LpcPort _lpcPort;
private readonly int _maxFanCount;
private readonly int _minFanCount;
private readonly int _minFanRpm;
- private readonly ushort _port;
- private readonly bool[] _restoreDefaultFanControlRequired = new bool[7];
- private readonly byte _revision;
private readonly TemperatureSourceData[] _temperaturesSource;
private readonly ushort _vBatMonitorControlRegister;
private readonly ushort[] _voltageRegisters;
private readonly ushort _voltageVBatRegister;
+ // Fan control state (save/restore)
+ private readonly byte[] _initialFanControlMode = new byte[7];
+ private readonly byte[] _initialFanPwmCommand = new byte[7];
+ private readonly bool[] _restoreDefaultFanControlRequired = new bool[7];
+
public Nct677X(LpcPort lpcPort, Chip chip, byte revision, ushort port)
{
Chip = chip;
@@ -67,9 +106,34 @@ internal class Nct677X : ISuperIO
// Each index in the below arrays represents a fan header
// ARRAY_KEY = new ushort[] { CPU FAN, PUMP, CHIPSET, EZ-CONNECT FAN, null, null, null, null, null, SYSFAN7, SYSFAN1, SYSFAN2, SYSFAN3, SYSFAN4, SYSFAN5, SYSFAN6 };
FAN_PWM_OUT_REG = [0x160, 0x161, 0x162, 0x163, 0x164, 0x165, 0x166, 0x167, 0xFFF, 0xC93, 0xE05, 0xE04, 0xE03, 0xE02, 0xE01, 0xE00]; // Duty Cycle Sensors
- FAN_PWM_COMMAND_REG = [0xA28, 0xA29, 0xA2A, 0xA2B, 0xFFF, 0xFFF, 0xFFF, 0xFFF, 0xFFF, 0xBE0, 0xC70, 0xC58, 0xC40, 0xC28, 0xC10, 0xBF8]; // Control Registers for CPU/Pump/EZ-Connect Fan, Initial Fan Curve Registers for System Fans
- FAN_CONTROL_MODE_REG = [0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00];
+ // Direct PWM command registers ("pair 0" from extended unk_101E0 table in BIOS).
+ // CPU/Pump/Chipset/EZ-Connect: 0A:(28+ch). SYSFAN7(ch9): 08:E9. SYSFAN1-6(ch10-15): 02:(65..60).
+ // Setting the manual-mode bit in FAN_CONTROL_MODE_REG makes the EC use these for direct PWM,
+ // bypassing the SmartFAN curve engine and its inherent ~2%/sec smoothing.
+ FAN_PWM_COMMAND_REG = [0xA28, 0xA29, 0xA2A, 0xA2B, 0xFFF, 0xFFF, 0xFFF, 0xFFF, 0xFFF, 0x8E9, 0x265, 0x264, 0x263, 0x262, 0x261, 0x260];
+ // Manual-mode enable registers. Ch 0-7 use 0A:00 (one bit each); ch 8-15 use 08:0F.
+ FAN_CONTROL_MODE_REG = [0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0xA00, 0x80F, 0x80F, 0x80F, 0x80F, 0x80F, 0x80F, 0x80F];
FAN_PWM_REQUEST_REG = [0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01, 0xA01];
+
+ // Mapping from array index to bit position in the FAN_CONTROL_MODE_REG (0xA00) register.
+ // The EC firmware uses bits 0-7 for 8 fan channels; LHM's sparse array indices must map to the correct bit.
+ // Bit 0: CPU Fan, Bit 1: Pump, Bit 2: Chipset/SYSFAN1, Bit 3: EZ-Connect/SYSFAN2, etc.
+ // Based on the Linux nct6687d driver's msi_alt1 config: index 0=CPU, 1=Pump, 2-7=System fans.
+ // -1 means no valid mapping (unused index slots).
+ FAN_CONTROL_MODE_BIT = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]; // Defaults
+ FAN_CONTROL_MODE_BIT[0] = 0; // CPU Fan → bit 0 of 0A:00
+ FAN_CONTROL_MODE_BIT[1] = 1; // Pump → bit 1 of 0A:00
+ FAN_CONTROL_MODE_BIT[2] = 2; // Chipset → bit 2 of 0A:00
+ FAN_CONTROL_MODE_BIT[3] = 3; // EZ-Connect → bit 3 of 0A:00
+ // System fans: manual-mode bits in 08:0F (derived from BIOS unk_104C0 entry[1]).
+ // EC channel mapping: LHM idx 9=ch9, 10=ch15, 11=ch14, 12=ch13, 13=ch12, 14=ch11, 15=ch10.
+ FAN_CONTROL_MODE_BIT[9] = 1; // SYSFAN7 (ch 9) → bit 1 of 08:0F
+ FAN_CONTROL_MODE_BIT[10] = 7; // SYSFAN1 (ch 15) → bit 7 of 08:0F
+ FAN_CONTROL_MODE_BIT[11] = 6; // SYSFAN2 (ch 14) → bit 6 of 08:0F
+ FAN_CONTROL_MODE_BIT[12] = 5; // SYSFAN3 (ch 13) → bit 5 of 08:0F
+ FAN_CONTROL_MODE_BIT[13] = 4; // SYSFAN4 (ch 12) → bit 4 of 08:0F
+ FAN_CONTROL_MODE_BIT[14] = 3; // SYSFAN5 (ch 11) → bit 3 of 08:0F
+ FAN_CONTROL_MODE_BIT[15] = 2; // SYSFAN6 (ch 10) → bit 2 of 08:0F
}
else
{
@@ -608,8 +672,35 @@ internal class Nct677X : ISuperIO
// set output value
WriteByte(FAN_PWM_COMMAND_REG[index], value.Value);
}
+ else if (Chip is Chip.NCT6687DR)
+ {
+ // NCT6687DR (MSI AM5/LGA1851): Direct PWM mode for ALL fans.
+ // Set the manual-mode bit to bypass the SmartFAN curve engine entirely.
+ // CPU/Pump/Chipset/EZ-Connect: bit in 0xA00. System fans: bit in 0x80F.
+ // Derived from BIOS unk_104C0 table — entry[1] = set manual-mode bit.
+ int bitPos = FAN_CONTROL_MODE_BIT[index];
+ if (bitPos >= 0)
+ {
+ byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
+ byte bitMask = (byte)(0x01 << bitPos);
+ mode = (byte)(mode | bitMask);
+ WriteByte(FAN_CONTROL_MODE_REG[index], mode);
+ }
+
+ // Retry up to 3 times if EC rejects the configuration (INVALID bit)
+ for (int attempt = 0; attempt < 3; attempt++)
+ {
+ if (!StartFanCfgUpdate(index))
+ break;
+
+ Set6687DRControl(index, value.Value);
+ if (CompleteFanConfigUpdate(index))
+ break;
+ }
+ }
else
{
+ // NCT6683D / NCT6686D / NCT6687D (non-DR)
// Manual mode, bit(1 : set, 0 : unset)
// bit 0 : CPU Fan
// bit 1 : PUMP Fan
@@ -623,27 +714,12 @@ internal class Nct677X : ISuperIO
WriteByte(FAN_PWM_REQUEST_REG[index], 0x80);
Thread.Sleep(50);
- if (Chip is Chip.NCT6687DR) // For MSI AM5/LGA1851 NCT6687D functionality
- {
- if (index < 8) // Control fans traditionally if part of the old control scheme. Applies to CPU/Pump/EZ-Conn
- {
- byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
- byte bitMask = (byte)(0x01 << index);
- mode = (byte)(mode | bitMask);
- WriteByte(FAN_CONTROL_MODE_REG[index], mode);
- }
+ byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
+ byte bitMask = (byte)(0x01 << index);
+ mode = (byte)(mode | bitMask);
+ WriteByte(FAN_CONTROL_MODE_REG[index], mode);
- Set6687DRControl(index, value.Value);
- }
- else // All other Nuvoton SIO controllers and motherboards that use NCT6683/6686/6687
- {
- byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
- byte bitMask = (byte)(0x01 << index);
- mode = (byte)(mode | bitMask);
- WriteByte(FAN_CONTROL_MODE_REG[index], mode);
-
- WriteByte(FAN_PWM_COMMAND_REG[index], value.Value);
- }
+ WriteByte(FAN_PWM_COMMAND_REG[index], value.Value);
WriteByte(FAN_PWM_REQUEST_REG[index], 0x40);
Thread.Sleep(50);
@@ -1113,11 +1189,11 @@ internal class Nct677X : ISuperIO
//timeout: after 500ms, abort and force access
byte access;
- DateTime timeout = DateTime.UtcNow.AddMilliseconds(500);
+ Stopwatch timeout = Stopwatch.StartNew();
while (true)
{
access = _lpcPort.ReadIoPort((ushort)(_port + EC_SPACE_PAGE_REGISTER_OFFSET));
- if (access == EC_SPACE_PAGE_SELECT || DateTime.UtcNow > timeout)
+ if (access == EC_SPACE_PAGE_SELECT || timeout.Elapsed >= TimeSpan.FromMilliseconds(500))
break;
Thread.Sleep(1);
@@ -1159,11 +1235,11 @@ internal class Nct677X : ISuperIO
//timeout: after 500ms, abort and force access
byte access;
- DateTime timeout = DateTime.UtcNow.AddMilliseconds(500);
+ Stopwatch timeout = Stopwatch.StartNew();
while (true)
{
access = _lpcPort.ReadIoPort((ushort)(_port + EC_SPACE_PAGE_REGISTER_OFFSET));
- if (access == EC_SPACE_PAGE_SELECT || DateTime.UtcNow > timeout)
+ if (access == EC_SPACE_PAGE_SELECT || timeout.Elapsed >= TimeSpan.FromMilliseconds(500))
break;
Thread.Sleep(1);
@@ -1190,35 +1266,90 @@ internal class Nct677X : ISuperIO
((ReadByte(VENDOR_ID_HIGH_REGISTER) << 8) | ReadByte(VENDOR_ID_LOW_REGISTER)) == NUVOTON_VENDOR_ID;
}
- private void Set6687DRControl(int index, byte? value)
+ ///
+ /// Request the EC to enter fan configuration phase and wait until registers are unlocked.
+ /// Based on the Linux nct6687d driver's start_fan_cfg_update() function.
+ ///
+ /// true if the EC entered config phase successfully; false on timeout.
+ private bool StartFanCfgUpdate(int index)
{
- if (index > 8) // Brute Force System Fan Control
- {
- int initFanCurveReg = FAN_PWM_COMMAND_REG[index]; // Initial Register Address for the Fan Curve
- int targetFanCurveAddr = initFanCurveReg; // Address of the Current Fan Curve Register we're writing to
- ushort targetFanCurveReg; // Integer value of the current fan curve register address, not the value within
- byte currentSpeed = ReadByte(FAN_PWM_OUT_REG[index]); // Current Speed of the target fan
+ byte engineSts = ReadByte(NCT6687DR_REG_FAN_ENGINE_STS);
- // If current fan duty cycle matches requested duty cycle, skip re-writing the fan curve
- if (currentSpeed == value.Value)
- {
- return;
- }
- else
- {
- // Write 7-point fan curve
- for (int count = 0; count < 14; count += 2)
- {
- targetFanCurveAddr = initFanCurveReg+count;
- targetFanCurveReg = Convert.ToUInt16(targetFanCurveAddr);
- WriteByte(targetFanCurveReg, value.Value);
- }
- }
- }
- else // Control CPU, Pump, Chipset, or EZ-Connect Fan normally
+ // Already accessible
+ if ((engineSts & NCT6687DR_FAN_CFG_LOCK) == 0 && (engineSts & NCT6687DR_FAN_CFG_PHASE) != 0)
+ return true;
+
+ // Wait until any existing config phase is done and request flag is clear
+ Stopwatch sw = Stopwatch.StartNew();
+ while (sw.Elapsed < TimeSpan.FromSeconds(1))
{
- WriteByte(FAN_PWM_COMMAND_REG[index], value.Value);
+ engineSts = ReadByte(NCT6687DR_REG_FAN_ENGINE_STS);
+ if ((engineSts & NCT6687DR_FAN_CFG_PHASE) == 0)
+ {
+ byte req = ReadByte(FAN_PWM_REQUEST_REG[index]);
+ if ((req & NCT6687DR_FAN_CFG_REQ) == 0)
+ break;
+ }
+
+ Thread.Sleep(1);
}
+
+ // Send config request
+ WriteByte(FAN_PWM_REQUEST_REG[index], NCT6687DR_FAN_CFG_REQ);
+ Thread.Sleep(10); // CC_Engine: fixed 10ms delay after request
+
+ // Wait until EC enters config phase and unlocks registers
+ sw.Restart();
+ while (sw.Elapsed < TimeSpan.FromSeconds(1))
+ {
+ engineSts = ReadByte(NCT6687DR_REG_FAN_ENGINE_STS);
+ if ((engineSts & NCT6687DR_FAN_CFG_LOCK) == 0 && (engineSts & NCT6687DR_FAN_CFG_PHASE) != 0)
+ return true;
+
+ Thread.Sleep(1);
+ }
+
+ return false; // Timeout: EC did not enter config phase
+ }
+
+ ///
+ /// Signal the EC that fan configuration is complete and wait for acknowledgment.
+ /// Returns false if the EC set the INVALID bit (configuration rejected).
+ ///
+ private bool CompleteFanConfigUpdate(int index)
+ {
+ byte engineSts = ReadByte(NCT6687DR_REG_FAN_ENGINE_STS);
+
+ // Already not accessible
+ if ((engineSts & NCT6687DR_FAN_CFG_LOCK) != 0 || (engineSts & NCT6687DR_FAN_CFG_PHASE) == 0)
+ return false;
+
+ // Signal done — CC_Engine uses 0xC0 (REQ|DONE) to commit atomically
+ WriteByte(FAN_PWM_REQUEST_REG[index], NCT6687DR_FAN_CFG_REQ | NCT6687DR_FAN_CFG_DONE);
+ Thread.Sleep(10); // CC_Engine: fixed 10ms delay after commit
+
+ // Wait until EC checks the new configuration
+ Stopwatch sw = Stopwatch.StartNew();
+ while (sw.Elapsed < TimeSpan.FromSeconds(1))
+ {
+ engineSts = ReadByte(NCT6687DR_REG_FAN_ENGINE_STS);
+ if ((engineSts & NCT6687DR_FAN_CFG_CHECK_DONE) != 0)
+ break;
+
+ Thread.Sleep(1);
+ }
+
+ // Check if EC rejected the configuration (INVALID bit)
+ engineSts = ReadByte(NCT6687DR_REG_FAN_ENGINE_STS);
+ return (engineSts & NCT6687DR_FAN_CFG_INVALID) == 0;
+ }
+
+ ///
+ /// Write fan control value for NCT6687DR. All fans use direct PWM command registers.
+ ///
+ private void Set6687DRControl(int index, byte value)
+ {
+ WriteByte(FAN_PWM_COMMAND_REG[index], value);
}
private void SaveDefaultFanControl(int index)
@@ -1229,6 +1360,18 @@ internal class Nct677X : ISuperIO
{
_initialFanControlMode[index] = ReadByte(FAN_CONTROL_MODE_REG[index]);
}
+ else if (Chip is Chip.NCT6687DR)
+ {
+ // Use the correct bit position mapping for NCT6687DR
+ int bitPos = FAN_CONTROL_MODE_BIT[index];
+ if (bitPos >= 0)
+ {
+ byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
+ byte bitMask = (byte)(0x01 << bitPos);
+ _initialFanControlMode[index] = (byte)(mode & bitMask);
+ }
+
+ }
else
{
byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
@@ -1250,8 +1393,32 @@ internal class Nct677X : ISuperIO
WriteByte(FAN_CONTROL_MODE_REG[index], _initialFanControlMode[index]);
WriteByte(FAN_PWM_COMMAND_REG[index], _initialFanPwmCommand[index]);
}
+ else if (Chip is Chip.NCT6687DR)
+ {
+ // NCT6687DR: Restore original manual-mode bit for all fans.
+ // Clear the bit we set in SetControl to return to SmartFAN curve mode.
+ int bitPos = FAN_CONTROL_MODE_BIT[index];
+ if (bitPos >= 0)
+ {
+ byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
+ byte bitMask = (byte)(0x01 << bitPos);
+ mode = (byte)(mode & ~bitMask);
+ WriteByte(FAN_CONTROL_MODE_REG[index], mode);
+ }
+
+ for (int attempt = 0; attempt < 3; attempt++)
+ {
+ if (!StartFanCfgUpdate(index))
+ break;
+
+ Set6687DRControl(index, _initialFanPwmCommand[index]);
+ if (CompleteFanConfigUpdate(index))
+ break;
+ }
+ }
else
{
+ // NCT6683D / NCT6686D / NCT6687D (non-DR)
byte mode = ReadByte(FAN_CONTROL_MODE_REG[index]);
mode = (byte)(mode & ~_initialFanControlMode[index]);
WriteByte(FAN_CONTROL_MODE_REG[index], mode);
@@ -1259,16 +1426,7 @@ internal class Nct677X : ISuperIO
WriteByte(FAN_PWM_REQUEST_REG[index], 0x80);
Thread.Sleep(50);
- if (Chip is Chip.NCT6687DR)
- {
- // for MSI AM5/LGA1851 boards using NCT6687D
- Set6687DRControl(index, _initialFanPwmCommand[index]);
- }
- else
- {
- // All other motherboards that use NCT6683/6686/6687
- WriteByte(FAN_PWM_COMMAND_REG[index], _initialFanPwmCommand[index]);
- }
+ WriteByte(FAN_PWM_COMMAND_REG[index], _initialFanPwmCommand[index]);
WriteByte(FAN_PWM_REQUEST_REG[index], 0x40);
Thread.Sleep(50);
@@ -1390,27 +1548,4 @@ internal class Nct677X : ISuperIO
SYSTIN3 = 6,
PECI_0 = 12
}
-
- // ReSharper disable InconsistentNaming
- private const ushort ADDRESS_REGISTER_OFFSET = 0x05;
- private const byte BANK_SELECT_REGISTER = 0x4E;
- private const uint DATA_REGISTER_OFFSET = 0x06;
-
- // NCT668X
- private const uint EC_SPACE_PAGE_REGISTER_OFFSET = 0x04;
- private const uint EC_SPACE_INDEX_REGISTER_OFFSET = 0x05;
- private const uint EC_SPACE_DATA_REGISTER_OFFSET = 0x06;
- private const byte EC_SPACE_PAGE_SELECT = 0xFF;
-
- private const ushort NUVOTON_VENDOR_ID = 0x5CA3;
-
- private readonly ushort[] FAN_CONTROL_MODE_REG;
- private readonly ushort[] FAN_PWM_COMMAND_REG;
- private readonly ushort[] FAN_PWM_OUT_REG;
- private readonly ushort[] FAN_PWM_REQUEST_REG;
-
- private readonly ushort VENDOR_ID_HIGH_REGISTER;
- private readonly ushort VENDOR_ID_LOW_REGISTER;
-
- // ReSharper restore InconsistentNaming
}