Created
November 12, 2019 13:10
-
-
Save jonasmalacofilho/167fac429d39d40a27c034c9d6803647 to your computer and use it in GitHub Desktop.
[liquidctl] [PATCH] Implement fan control for Seasonic E PSUs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 3433ce9c3752c6937ecf97300b09fa2eb343740e Mon Sep 17 00:00:00 2001 | |
From: Jonas Malaco <[email protected]> | |
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