Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonasmalacofilho/167fac429d39d40a27c034c9d6803647 to your computer and use it in GitHub Desktop.
Save jonasmalacofilho/167fac429d39d40a27c034c9d6803647 to your computer and use it in GitHub Desktop.
[liquidctl] [PATCH] Implement fan control for Seasonic E PSUs
From 3433ce9c3752c6937ecf97300b09fa2eb343740e Mon Sep 17 00:00:00 2001
From: Jonas Malaco <jonas@protocubo.io>
Date: Sun, 13 Oct 2019 11:06:27 -0300
Subject: [PATCH] Implement fan control for Seasonic E PSUs
Uses the FAN_COMMAND_1 (0x3b) PMBus command, with the desired duty cycle
encoded in LINEAR11, and a PEC byte.
***
In the captures supplied by Jon[1][2] we can't see any missing commands
or bugs that would explain why fan control wasn't working in the initial
implementation. There is however an extra trailing byte passed with
FAN_COMMAND_1, and it clearly isn't leftover data from a previous use of
the buffer. Instead, it could be a Packed Error Code (PEC).
ac04603b17009b
ac04603b0000a7
^^
Unfortunately confirming this was not trivial and all the first attempts
of validating the observed PECs failed.
After that a lot of time was spent looking at captures, reading the
specs _again_ and verifying the CRC implementation, until...
The PMBus spec states that the PEC must be computed for the entire
message. It also states that the message always starts with an address
byte, which is then followed by the command byte.
So, the first thing we can do is compute the PEC only from the byte that
precedes the command byte forward. Additionally, Jon had already
speculated early on[3] that 0x60 could be the address.
ac04603b17009b
^^^^^^^^
While this appeared to make sense, it still didn't work.
However, I had previously disagreed with him on 0x60 being an address
because, according to the PMBus spec, the address byte should contain
the address on the 7 most significant bits and a read (1) or write (0)
least significant bit. Essentially, we should expect to see 0x60 only on
writes, such as these executions of FAN_COMMAND_1, but on all reads we
should see 0x61, which we don't.
Where we do see a pattern of LSB 1 for read and 0 for write is in the
first byte of all messages sent to the device. But we now know that the
other bits in that first byte can't be the PMBus slave address, because
of how the PEC has to be computed and later validated by the slave
itself.
So what if 0x60 is actually the slave *address*, not the address byte?
Well, shifting it left 1 bit seems to confirm that this is the case.
At least we get a successful PEC check when we interpret it like that!
So, in the hope that his will allow fan control to work, this patch adds
a PEC to the FAN_COMMAND_1 writes.
[1]
https://github.com/jonasmalacofilho/liquidctl-device-data/tree/master/NZXT%20E500/01%20-%20generic%20capture%20-%20jnettlet
[2]
https://github.com/jonasmalacofilho/liquidctl-device-data/tree/master/NZXT%20E500/02%20-%20generic%20capture%20-%20jnettlet
[3]
https://github.com/jonasmalacofilho/liquidctl/issues/31#issuecomment-500101710
***
Comparing the captures against the debug data supplied by Ivan shows
that, apparently, most of the response from a write word command is
simply whatever was left on the device output buffer.[4]
Thus, it seems that the only check we can do is for a first byte ==
0xaa.
[4] https://github.com/jonasmalacofilho/liquidctl/pull/55#issuecomment-550410701
---
liquidctl/driver/seasonic.py | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/liquidctl/driver/seasonic.py b/liquidctl/driver/seasonic.py
index 6ff9170..1547d78 100644
--- a/liquidctl/driver/seasonic.py
+++ b/liquidctl/driver/seasonic.py
@@ -10,7 +10,7 @@ Supported features
- […] general device monitoring
- [✓] electrical output monitoring
- - [ ] fan control
+ - [✓] fan control
- [ ] 12V multirail configuration
---
@@ -40,7 +40,7 @@ import time
from liquidctl.driver.usb import UsbHidDriver
from liquidctl.pmbus import CommandCode as CMD
-from liquidctl.pmbus import linear_to_float
+from liquidctl.pmbus import linear_to_float, float_to_linear11, compute_pec
LOGGER = logging.getLogger(__name__)
@@ -51,6 +51,7 @@ _ATTEMPTS = 3
_SEASONIC_READ_FIRMWARE_VERSION = CMD.MFR_SPECIFIC_FC
_RAILS = ['+12V #1', '+12V #2', '+12V #3', '+5V', '+3.3V']
+_MIN_FAN_DUTY = 0
class SeasonicEDriver(UsbHidDriver):
@@ -87,6 +88,20 @@ class SeasonicEDriver(UsbHidDriver):
self.device.release()
return status
+ def set_fixed_speed(self, channel, duty, **kwargs):
+ """Set channel to a fixed speed duty."""
+ duty = max(_MIN_FAN_DUTY, min(duty, 100))
+ LOGGER.info('setting fan PWM duty to %i%%', duty)
+ msg = [0xac, 0x04, 0x60, CMD.FAN_COMMAND_1] + list(float_to_linear11(duty))
+ msg.append(compute_pec([msg[2] << 1] + msg[3:]))
+ for _ in range(_ATTEMPTS):
+ self._write(msg)
+ res = self._read()
+ if res[0] == 0xaa:
+ self.device.release()
+ return
+ assert False, f'invalid response (attempts={_ATTEMPTS})'
+
def _write(self, data):
padding = [0x0]*(_WRITE_LENGTH - len(data))
LOGGER.debug('write %s (and %i padding bytes)',
--
2.24.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment