Skip to content

Instantly share code, notes, and snippets.

@ikus060
Last active October 30, 2024 20:58
Show Gist options
  • Save ikus060/26a33ce1e82092b4d2dbdf18c3610fde to your computer and use it in GitHub Desktop.
Save ikus060/26a33ce1e82092b4d2dbdf18c3610fde to your computer and use it in GitHub Desktop.
Python script to edit the X8DTL-iF Registry to control fan speed.
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import sys
import argparse
from smbus import SMBus
from contextlib import contextmanager
#
# All register ceom from
# https://github.com/torvalds/linux/blob/5924bbecd0267d87c24110cbe2041b5075173a25/drivers/hwmon/w83795.c
#
#
ADDRESS=0x2f
NUVOTON_VENDOR_ID = 0xa3
CHIP_ID = 0x79
W83795_REG_BANKSEL = 0x00
# Fan Control Mode Selection Registers (FCMS)
W83795_REG_FCMS1 = 0x201
W83795_REG_FCMS2 = 0x208
W83795_REG_TFMR = lambda index: 0x202 + index
W83795_REG_TSS = lambda index: 0x209 + index
W83795_REG_SFIV_TEMP = lambda index: range(0x280 + index * 0x10, 0x280 + index * 0x10 + 7)
W83795_REG_SFIV_DCPWM = lambda index: range(0x288 + index * 0x10, 0x288 + index * 0x10 + 7)
W83795_REG_CTFS = lambda index: 0x268 + index
#Fan Output PWM Frequency Prescalar (FOPFP)
W83795_REG_FOPFP = lambda index: 0x218 + index
#Fan Output Nonstop Value (FONV)
W83795_REG_FONV = lambda index: 0x228 + index
#Hystersis of Temperature (HT)
W83795_REG_HT = lambda index: 0x270 + index
#Target Temperature of Temperature Inputs
W83795_TTTI = lambda index: 0x260 + index
#Read temperature
W83795_REG_VRLSB = 0x3C
W83795_REG_TEMP_READ = [0x21, 0x22, 0x23, 0x24, 0x1f, 0x20]
FANS = range(0,8)
TEMPS = range(0,6)
@contextmanager
def bank(bus, value):
prev_value = w83795_set_bank(bus, value)
yield
w83795_set_bank(bus, prev_value)
def w83795_set_bank(bus, bank):
assert bank in [0,1,2,3]
# Read current bank value
cur_bank = bus.read_byte_data(ADDRESS, W83795_REG_BANKSEL)
# If the bank is already set, nothing to do
if cur_bank == bank:
return cur_bank
# Change the bank
bus.write_byte_data(ADDRESS, W83795_REG_BANKSEL, bank)
# Return previous bank value
return cur_bank
def w83795_write(bus, reg, value):
"""
Write into the given registry.
"""
with bank(bus, reg >> 8):
return bus.write_byte_data(ADDRESS, reg & 0xff, value & 0xff)
def w83795_read(bus, reg):
"""
Read the given registry.
"""
if hasattr(reg, '__iter__'):
with bank(bus, reg[0] >> 8):
return map(lambda r: bus.read_byte_data(ADDRESS, r & 0xff), reg)
with bank(bus, reg >> 8):
return bus.read_byte_data(ADDRESS, reg & 0xff)
def to_degree(val, low=0, hi=127):
"""Convert hex value to degree."""
return val
def to_perc(value):
"""Convert hex value to percentage."""
return value * 100 / 255
def from_degree(value):
"""Convert degree to hex"""
value = max(min(int(value) + 1,127),0)
return value
def from_perc(value):
"""Convert perc to hex"""
value = max(min(int(value) + 1,100),0)
return (value * 255 / 100) & 0xff
def main():
# Read arguments
parser = argparse.ArgumentParser(description='Change SuperMicro X8 Fan Control.')
parser.add_argument('-m', '--mode', type=str, choices=['smart', 'cruise'], help='Set the fan mode: smart or cruise. smart: to use Smart Fan mode. cruise: to use Thermal Cruise mode.')
parser.add_argument('-p', '--pwm', type=str, help='Set Fan Duty (in percentage).')
parser.add_argument('-t', '--temp', type=str, help='Set Temperature (in Celsius) associated to pwm.')
parser.add_argument('-H', '--hystersis', type=int, help='Set Hystersis value in degree (0-15)')
args = parser.parse_args()
# Open SMBus
try:
bus = SMBus(0)
except:
print("Failed to open i2c bus (/dev/i2d-0). Make sure i2c-dev module is loaded.")
return
#Check if we have the right device.
try:
vendor = w83795_read(bus, 0xfd)
chipid = w83795_read(bus, 0xfe)
#debug("vendor %s, chipid %s" % (vendor, chipid))
if vendor != NUVOTON_VENDOR_ID or chipid != CHIP_ID:
print("unexpected vendor %s, chipid %s" % (vendor, chipid))
return
# Check if Smarts Fan Control is enabled
if args.mode == 'smart':
# Check arguments
if not args.pwm or not args.temp:
print('pwm and temp are required')
return
pwm = args.pwm.split(',')
temp = args.temp.split(',')
if len(pwm) != len(temp):
print("pwm and temp must have the same number of values")
return
# Change Smart Fan Control value
for i in range(0,7):
p = pwm[i] if i < len(pwm) else pwm[-1]
tt = temp[i] if i < len(temp) else temp[-1]
print("Set Smart Fan Control %s%% - %sC" % (p, tt))
for t in TEMPS:
w83795_write(bus, W83795_REG_SFIV_DCPWM(t)[i], from_perc(p))
w83795_write(bus, W83795_REG_SFIV_TEMP(t)[i], from_degree(tt))
# Change Minimum PWM
for f in FANS:
w83795_write(bus, W83795_REG_FONV(f), from_perc(pwm[0]))
# Change critical Temp
for t in TEMPS:
w83795_write(bus, W83795_REG_CTFS(t), from_degree(temp[-1]))
# Set Smart Fan Control Mode T6FC - T1FC
w83795_write(bus, W83795_REG_FCMS1, 0x0)
w83795_write(bus, W83795_REG_FCMS2, 0x3f)
elif args.mode == 'cruise':
# Check arguments
if not args.temp:
print('temp is required')
return
temp = int(args.temp)
print("Set Thermal Cruise %sC" % (temp,))
for t in TEMPS:
w83795_write(bus, W83795_TTTI(t), from_degree(temp))
# Change critical Temp
for t in TEMPS:
w83795_write(bus, W83795_REG_CTFS(t), from_degree(80))
# Set Thermal Cruise Mode.
w83795_write(bus, W83795_REG_FCMS1, 0x0)
w83795_write(bus, W83795_REG_FCMS2, 0x0)
# Set hystersis
if args.hystersis:
ht = max(min(args.hystersis,15),0)
print("Set hystersis %sC" % ht)
# Change Smart Fan Control value
for t in range(0,6):
w83795_write(bus, W83795_REG_HT(t), ht)
if args.mode or args.pwm or args.temp or args.hystersis:
return
# Check if Smarts Fan Control is enabled
fcms1 = w83795_read(bus, W83795_REG_FCMS1)
fcms2 = w83795_read(bus, W83795_REG_FCMS2) & 0xff
# Default to show all data.
for t in TEMPS:
print("Temp%s to Fan mapping Relationships (T%sFMR)" % (t+1, t+1))
tfmr = w83795_read(bus, W83795_REG_TFMR(t))
fans= [i+1 for i in FANS if tfmr & (0x1<<i)]
print(' '.join(['Fan%s' % i for i in fans]))
print("Smart Fan Control Table (SFIV)")
temp = w83795_read(bus, W83795_REG_SFIV_TEMP(t))
print(''.join([("%sC" % to_degree(v)).rjust(6) for v in temp]))
dcpwm = w83795_read(bus, W83795_REG_SFIV_DCPWM(t))
print(''.join([("%s%%" % to_perc(v)).rjust(6) for v in dcpwm]))
ttti = w83795_read(bus, W83795_TTTI(t))
print("Thermal Cruise (TTTI): %sC" % (ttti,))
ctfs = w83795_read(bus, W83795_REG_CTFS(t))
print("Critical Temperature (T%sCTFS): %sC" % (t, to_degree(ctfs)))
ht = w83795_read(bus, W83795_REG_HT(t)) & 0b1111
print("Hysteresis (HT%s): %sC" % (t, ht))
tmp = w83795_read(bus, W83795_REG_TEMP_READ[t])
print("Current Temperature (TEMP%s): %sC" % (t, to_degree(tmp),))
print('---')
for f in FANS:
fonv = w83795_read(bus, W83795_REG_FONV(f))
print("Fan%s Output Nonstop Value (F%sONV): %s%%" % (f+1, f+1, to_perc(fonv)))
#for f in range(0,6):
# w83795_write(bus, W83795_REG_FONV(f), 50)
#w83795_write(bus, W83795_REG_SFIV_DCPWM(0)[0], 50)
#w83795_write(bus, W83795_REG_SFIV_TEMP(0)[0], 85)
#w83795_write(bus, W83795_REG_SFIV_TEMP(1)[0], 85)
finally:
bus.close()
if __name__ == "__main__":
main()
@fatmandandan
Copy link

fatmandandan commented May 2, 2021

Hi! Thank you for the work on this script. I was trying to run it on my x8DTU-F, and I'm running into an error:

File "/usr/local/bin/smx8fancontrol", line 252, in
main()
File "/usr/local/bin/smx8fancontrol", line 138, in main
vendor = w83795_read(bus, 0xfd)
File "/usr/local/bin/smx8fancontrol", line 97, in w83795_read
return bus.read_byte_data(ADDRESS, reg & 0xff)
File "/usr/lib/python3.6/contextlib.py", line 88, in exit
next(self.gen)
File "/usr/local/bin/smx8fancontrol", line 64, in bank
w83795_set_bank(bus, prev_value)
File "/usr/local/bin/smx8fancontrol", line 68, in w83795_set_bank
assert bank in [0,1,2,3]
AssertionError

I was wondering if you could help me troubleshoot the issue. Thanks!

edit: I have verified that my board is using a w83795 chip

@BadCo-NZ
Copy link

BadCo-NZ commented May 2, 2021

Hi! Thank you for the work on this script. I was trying to run it on my x8DTU-F, and I'm running into an error:

File "/usr/local/bin/smx8fancontrol", line 252, in
main()
File "/usr/local/bin/smx8fancontrol", line 138, in main
vendor = w83795_read(bus, 0xfd)
File "/usr/local/bin/smx8fancontrol", line 97, in w83795_read
return bus.read_byte_data(ADDRESS, reg & 0xff)
File "/usr/lib/python3.6/contextlib.py", line 88, in exit
next(self.gen)
File "/usr/local/bin/smx8fancontrol", line 64, in bank
w83795_set_bank(bus, prev_value)
File "/usr/local/bin/smx8fancontrol", line 68, in w83795_set_bank
assert bank in [0,1,2,3]
AssertionError

I was wondering if you could help me troubleshoot the issue. Thanks!

edit: I have verified that my board is using a w83795 chip

See my comment above yours.

Also, often I find the first time the scrip runs it fails, but if I run it again it works.

@fatmandandan
Copy link

fatmandandan commented May 2, 2021

Hi! Thank you for the work on this script. I was trying to run it on my x8DTU-F, and I'm running into an error:

File "/usr/local/bin/smx8fancontrol", line 252, in
main()
File "/usr/local/bin/smx8fancontrol", line 138, in main
vendor = w83795_read(bus, 0xfd)
File "/usr/local/bin/smx8fancontrol", line 97, in w83795_read
return bus.read_byte_data(ADDRESS, reg & 0xff)
File "/usr/lib/python3.6/contextlib.py", line 88, in exit
next(self.gen)
File "/usr/local/bin/smx8fancontrol", line 64, in bank
w83795_set_bank(bus, prev_value)
File "/usr/local/bin/smx8fancontrol", line 68, in w83795_set_bank
assert bank in [0,1,2,3]
AssertionError

I was wondering if you could help me troubleshoot the issue. Thanks!
edit: I have verified that my board is using a w83795 chip

See my comment above yours.

Also, often I find the first time the scrip runs it fails, but if I run it again it works.

Thanks for the quick reply, and for the post! I believe I followed all the steps correctly, but I'm still running into the problem. I seem to be running into an error on line 68 instead of line 70 like the above posts. Running the script twice just outputs the same error string twice. Did you modify any other files before you were able to get it to run?

I was also reading some other posts on the STH forums, and when I run "i2cdump 0 0x2f" I get all x's. This leads me to believe that my ubuntu install is unable to communicate with the Nuvotron i2c bus. ( edit: Turns out I was dumping the wrong bus. I looked more carefully at the modprobe output and after running "i2cdump 1" instead of "i2cdump 0" I got a real output.) Any direction that you could point me in would be appreciated.

@BadCo-NZ
Copy link

BadCo-NZ commented May 2, 2021

Hi! Thank you for the work on this script. I was trying to run it on my x8DTU-F, and I'm running into an error:

File "/usr/local/bin/smx8fancontrol", line 252, in
main()
File "/usr/local/bin/smx8fancontrol", line 138, in main
vendor = w83795_read(bus, 0xfd)
File "/usr/local/bin/smx8fancontrol", line 97, in w83795_read
return bus.read_byte_data(ADDRESS, reg & 0xff)
File "/usr/lib/python3.6/contextlib.py", line 88, in exit
next(self.gen)
File "/usr/local/bin/smx8fancontrol", line 64, in bank
w83795_set_bank(bus, prev_value)
File "/usr/local/bin/smx8fancontrol", line 68, in w83795_set_bank
assert bank in [0,1,2,3]
AssertionError

I was wondering if you could help me troubleshoot the issue. Thanks!
edit: I have verified that my board is using a w83795 chip

See my comment above yours.
Also, often I find the first time the scrip runs it fails, but if I run it again it works.

Thanks for the quick reply, and for the post! I believe I followed all the steps correctly, but I'm still running into the problem. I seem to be running into an error on line 68 instead of line 70 like the above posts. Running the script twice just outputs the same error string twice. Did you modify any other files before you were able to get it to run?

I was also reading some other posts on the STH forums, and when I run "i2cdump 0 0x2f" I get all x's. This leads me to believe that my ubuntu install is unable to communicate with the Nuvotron i2c bus. Any direction that you could point me in would be appreciated.

Are running the script and i2cdump as root?

@fatmandandan
Copy link

Hi! Thank you for the work on this script. I was trying to run it on my x8DTU-F, and I'm running into an error:

File "/usr/local/bin/smx8fancontrol", line 252, in
main()
File "/usr/local/bin/smx8fancontrol", line 138, in main
vendor = w83795_read(bus, 0xfd)
File "/usr/local/bin/smx8fancontrol", line 97, in w83795_read
return bus.read_byte_data(ADDRESS, reg & 0xff)
File "/usr/lib/python3.6/contextlib.py", line 88, in exit
next(self.gen)
File "/usr/local/bin/smx8fancontrol", line 64, in bank
w83795_set_bank(bus, prev_value)
File "/usr/local/bin/smx8fancontrol", line 68, in w83795_set_bank
assert bank in [0,1,2,3]
AssertionError

I was wondering if you could help me troubleshoot the issue. Thanks!
edit: I have verified that my board is using a w83795 chip

See my comment above yours.
Also, often I find the first time the scrip runs it fails, but if I run it again it works.

Thanks for the quick reply, and for the post! I believe I followed all the steps correctly, but I'm still running into the problem. I seem to be running into an error on line 68 instead of line 70 like the above posts. Running the script twice just outputs the same error string twice. Did you modify any other files before you were able to get it to run?
I was also reading some other posts on the STH forums, and when I run "i2cdump 0 0x2f" I get all x's. This leads me to believe that my ubuntu install is unable to communicate with the Nuvotron i2c bus. Any direction that you could point me in would be appreciated.

Are running the script and i2cdump as root?

Yup, I ran both as root. As an experiment, I loaded w83795 in "/etc/modules" and ran "sensors". Surprisingly, I was able to see a normal output of voltages, fan speeds, and temps. I also tried commenting out "i2c_i80" in "/etc/modprobe.d/blacklist.conf" which didn't seem to do anything either.

@BadCo-NZ
Copy link

BadCo-NZ commented May 2, 2021

Hi! Thank you for the work on this script. I was trying to run it on my x8DTU-F, and I'm running into an error:

File "/usr/local/bin/smx8fancontrol", line 252, in
main()
File "/usr/local/bin/smx8fancontrol", line 138, in main
vendor = w83795_read(bus, 0xfd)
File "/usr/local/bin/smx8fancontrol", line 97, in w83795_read
return bus.read_byte_data(ADDRESS, reg & 0xff)
File "/usr/lib/python3.6/contextlib.py", line 88, in exit
next(self.gen)
File "/usr/local/bin/smx8fancontrol", line 64, in bank
w83795_set_bank(bus, prev_value)
File "/usr/local/bin/smx8fancontrol", line 68, in w83795_set_bank
assert bank in [0,1,2,3]
AssertionError

I was wondering if you could help me troubleshoot the issue. Thanks!
edit: I have verified that my board is using a w83795 chip

See my comment above yours.
Also, often I find the first time the scrip runs it fails, but if I run it again it works.

Thanks for the quick reply, and for the post! I believe I followed all the steps correctly, but I'm still running into the problem. I seem to be running into an error on line 68 instead of line 70 like the above posts. Running the script twice just outputs the same error string twice. Did you modify any other files before you were able to get it to run?
I was also reading some other posts on the STH forums, and when I run "i2cdump 0 0x2f" I get all x's. This leads me to believe that my ubuntu install is unable to communicate with the Nuvotron i2c bus. Any direction that you could point me in would be appreciated.

Are running the script and i2cdump as root?

Yup, I ran both as root. As an experiment, I loaded w83795 in "/etc/modules" and ran "sensors". Surprisingly, I was able to see a normal output of voltages, fan speeds, and temps. I also tried commenting out "i2c_i80" in "/etc/modprobe.d/blacklist.conf" which didn't seem to do anything either.

Ok I'm running out of ideas. Seeing as you have a different motherboard, try checking the forks of this scrip and see if those versions work.

@x8fan
Copy link

x8fan commented May 2, 2021

Can somebody who has had success with this script indicate which Linux distribution they used and which specific version of the distribution they used? We have tried ubuntu-16.04.7, ubuntu-18.04.5, ubuntu-20.04.2, Centos 7.9. In some, not all the prerequisite could be satisfied with just apt-get or yum, respectively. Others exhibited weird behavior even when running the script without any arguments, where it is just printing out information. Installing a Linux distribution is easy so the simplest solution is to just replicate the platform on which this works.
Thanks!

@ikus060
Copy link
Author

ikus060 commented May 3, 2021

@x8fan Could you explain how you are running the script and how it's failing ?

I write this script for Debian system. It should mostly work on all X8 board with IPMI. The script checks if the right chipset exists on the board. If not, the script won't work. Maybe it's your case ?

@x8fan
Copy link

x8fan commented May 3, 2021

@ikus060: Do you remember which specific version of Debian you were using? I think it will be much easier to just install that instead of trying to figure out incompatibilities. BTW, this is for an X8DTL-iF motherboard.

Again, what I want to do is replicate the platform. However, since you asked about the problems, here is some info.
ubuntu-16.04.7 refused ot create the i2c device. All the prerequisites installed and the boot log indicated that the i2c_dev module is builtin. However, no device files and thus even i2cdetect does not work.

ubuntu-20.04.2 came the closest to working. There is no python-smbus, but there is python3-smbus. However, running without arguments with python3, it generates output that looks like:
Temp1 to Fan mapping Relationships (T1FMR)
Fan1 Fan2 Fan3 Fan4 Fan5 Fan6 Fan7 Fan8
Smart Fan Control Table (SFIV)
255C 0C 255C 0C 255C 0C 227C
59.6078431372549%48.23529411764706%59.6078431372549%48.23529411764706%59.6078431372549%48.23529411764706%100.0%
Thermal Cruise (TTTI): 40C
Critical Temperature (T0CTFS): 95C
Hysteresis (HT0): 5C
Current Temperature (TEMP0): 77C

I made a few small changes to the code, such as, on line 110 changing "return value * 100 / 255" to "return int(value * 100 / 255)"
The output generated looked a little better. However, ultimately it didn't work so I figured that there is some other incompatibility that is not worth chasing.

ubuntu-18.04.5 fails with
Traceback (most recent call last):
File "./smx8fancontrol", line 258, in
main()
File "./smx8fancontrol", line 141, in main
vendor = w83795_read(bus, 0xfd)
File "./smx8fancontrol", line 100, in w83795_read
return bus.read_byte_data(ADDRESS, reg & 0xff)
File "/usr/lib/python2.7/contextlib.py", line 24, in exit
self.gen.next()
File "./smx8fancontrol", line 67, in bank
w83795_set_bank(bus, prev_value)
File "./smx8fancontrol", line 71, in w83795_set_bank
assert bank in [0,1,2,3]
AssertionError

@ikus060
Copy link
Author

ikus060 commented May 4, 2021

That probably was a Debian Stretch.

It too bad, I don't have a X8 system to test it on anymore. That explain, why I'm not official supporting the script anymore.

@x8fan If it's for business, I could probably take an hour or so with you to debug the script quickly. That should not be hard if we are doing this together.

@ikus060
Copy link
Author

ikus060 commented May 4, 2021

Ho and it's clearly a python2.7 script.

@fatmandandan
Copy link

Even though I haven't gotten it to work yet, thanks for the help @BadCo-NZ. @x8fan, I also tried running the script in 20.04 LTS on a x8DTU-f using python3 and ran into some of the same issues that you did. The fan control table spit out strange non-integer percentages, and other seemingly unformatted values. One thing I did notice is that in 20.04, the W83795 chip appears on bus 0 instead of bus 1 as in 18.04 when running i2cdetect. That seems to help it get past the assertion error in line 71. I don't have time right now to dig deeper into the documentation and @ikus060's code, but I will create a fork if I do end up getting it working.

@BadCo-NZ
Copy link

BadCo-NZ commented May 4, 2021

If it helps, I am using Proxmox 6.3 which is based on Debian Buster.

@fatmandandan
Copy link

fatmandandan commented May 4, 2021

If it helps, I am using Proxmox 6.3 which is based on Debian Buster.

As a moonshot, I installed Proxmox 6.4 (easiest iso to grab from the site), and the script works beautifully. Super interesting to see. I may play around with some newer versions of ubuntu to see if I can get it to work. If not, it isn't a big deal, I'm just planning to run zfs and gluster anyways which will be fine in proxmox.

@BadCo-NZ
Copy link

BadCo-NZ commented May 4, 2021

What are you using Ubuntu for? If it is a server I would recommended just running a hypervisor like Proxmox and containerising everything

@fatmandandan
Copy link

@BadCo-NZ I have a separate compute/containerization cluster setup, just planning to use this particular server as a storage node. Either way, now that you mention it, Proxmox definitely provides much more flexibility. Thanks for the quick responses and help! I may continue to work on figuring out the problems with Ubuntu 18.04/20.04 just because it bothers me, but at least I know that the hardware does work as expected.

@ikus060
Copy link
Author

ikus060 commented May 4, 2021

Keep us updated. I will update the gist afterwards

@x8fan
Copy link

x8fan commented May 5, 2021

The good news is that after installing Debian Stretch the script seemed to work.
The bad news is that the settings using smx8fancontrol are not maintained across a power cycle on our X8DTL-iF motherboard. We could have smx8fancontrol running at every bootup, but that would force us to use Debian Stretch.
There may well be something wrong with the hardware. Specifically, the smx8fancontrol script shows some crazy temperature sensor readings when the system is idle:
Temp1 Current Temperature (TEMP0): 64C
Temp2 Current Temperature (TEMP1): 128C
Temp3 Current Temperature (TEMP2): 128C
Temp4 Current Temperature (TEMP3): 128C
Temp5 Current Temperature (TEMP4): 42C (this may be okay)
Temp6 Current Temperature (TEMP5): 193C
For Temp1, without changes we have:
Smart Fan Control Table (SFIV)
85C 95C 95C 95C 95C 95C 95C
29% 100% 100% 100% 100% 100% 100%
To get reasonable noise level, we tried smx8fancontrol -m smart -p 25,35,100 -t 85,100,110 -H 5
resulting in
Smart Fan Control Table (SFIV)
86C 101C 111C 111C 111C 111C 111C
25% 35% 100% 100% 100% 100% 100%
This would be okay if it was maintained across power cycles.

@BadCo-NZ
Copy link

BadCo-NZ commented May 5, 2021

The system BIOS would take over every boot, you will have to run a cron job to start the script at boot.

@x8fan
Copy link

x8fan commented May 5, 2021

Re: @BadCo-NZ -- The system BIOS would take over every boot
Yes, I realize that. It's useless for us since we boot different systems with different kernels and the script is clearly very picky on what it works on.
The whole thing is weird -- if this problem is present in all Supermicro X8 IPMI motherboards, I don't see how they were able to market these products without endless customer complaints. Other Supermicro motherboards we have dealt with (X7, X10, X11) do not have these problems. If it wasn't for the existence of this script, my assumption would be that we are dealing with a bad board and should simply replace it or just junk the system (which is what we will probably do).

@BadCo-NZ
Copy link

BadCo-NZ commented May 5, 2021

You boot different OS's on the one system? It really sounds like you should be using proxmox anyway 😁

These x8 boards are very old and designed for servers with fans on full all the time. Even the IPMI is a novelty with how little functionality you actually gain haha.

x9 and x10 are a significant improvement.

@jeee
Copy link

jeee commented Jun 1, 2021

Set Smart Fan Control 25% - 42C
Traceback (most recent call last):
  File "/usr/local/bin/smx8fancontrol", line 252, in <module>
    main()
  File "/usr/local/bin/smx8fancontrol", line 163, in main
    w83795_write(bus, W83795_REG_SFIV_DCPWM(t)[i], from_perc(p))
  File "/usr/local/bin/smx8fancontrol", line 118, in from_perc
    return (value * 255 / 100) & 0xff
TypeError: unsupported operand type(s) for &: 'float' and 'int'```

fixed by:
```def from_degree(value):
  """Convert degree to hex"""
  value = max(min(int(value) + 1,127),0)
  return int(value * 255 / 127) & 0xff

def from_perc(value):
  """Convert perc to hex"""
  value = max(min(int(value) + 1,100),0)
  return int(value * 255 / 100) & 0xff

Since I had to comment out (#) w83795 from /etc/modules is there any way to display the fan speeds?

thanks a lot for sharing this script!

@randomlyalex
Copy link

randomlyalex commented Jul 16, 2021

I was having a look at this myself today in an effort to have a little better fan control. I've got an X8DTH motherboard for reference which may or may not be a contributing factor of below.

I have this working on a x8DTE-F and x8DTI-F with Proxmox as Host. A few key things to ensure:

  1. Verify you have the w83795 chip, you can do this by installing lm-sensors and running sensors-detect
  2. Install i2c-tools
  3. Install python-smbus
  4. Install python3-smbus
  5. Install libi2c-dev
  6. Comment out (#) w83795 from /etc/modules and reboot (thanks @bennet-esyoil)

I'm running unraid, which itself is based on slackware 14.2.

  1. Run sensor-detect, and verified.

  2. I've installed i2c-tools.

3-5) I don't find precompiled packages, so maybe i'll have to look into compiling, but I can't be the first to do this on slackware i'd have thought.

  1. Probably a slackware thing again, but I don't have an /etc/modules

If i run:
# python smx8fancontrol.py

Traceback (most recent call last):
  File "smx8fancontrol.py", line 515, in <module>
    main()
  File "smx8fancontrol.py", line 281, in main
    vendor = w83795_read(bus, 0xfd)
  File "smx8fancontrol.py", line 197, in w83795_read
    with bank(bus, reg >> 8):
  File "/usr/lib64/python2.7/contextlib.py", line 17, in __enter__
    return self.gen.next()
  File "smx8fancontrol.py", line 129, in bank
    prev_value = w83795_set_bank(bus, value)
  File "smx8fancontrol.py", line 145, in w83795_set_bank
    cur_bank = bus.read_byte_data(ADDRESS, W83795_REG_BANKSEL)
IOError: [Errno 16] Device or resource busy

I tried installing smbus and smbus2 with pip, but then i think i need to figure out a virtual env to wrap it all together, and my knowledge starts to get more shaky than it already is.

I'm going to have a look at getting packages installed first then look at virtual envs as a last resort.

@ikus060
Copy link
Author

ikus060 commented Jul 16, 2021 via email

@Confetteria
Copy link

Successfully ran this script on Proxmox 7 and a X8DTE-F, thank you @ikus060 and @jeee for the TypeError fix

@curtis8523
Copy link

curtis8523 commented Oct 30, 2024

I updated the script to use python3. I also added in unloading the w83795 when the script is run and then reenables it at the end. This give you the ability to use sensors to monitor the fan speed. This should make it more more useful for monitoring temps and fan speed when using it.

Screenshot 2024-10-30 145632

#!/usr/bin/env python3

import sys
import os
import argparse
from smbus import SMBus
from contextlib import contextmanager

ADDRESS = 0x2f
NUVOTON_VENDOR_ID = 0xa3
CHIP_ID = 0x79

W83795_REG_BANKSEL = 0x00
W83795_REG_FCMS1 = 0x201
W83795_REG_FCMS2 = 0x208
W83795_REG_TFMR = lambda index: 0x202 + index
W83795_REG_TSS = lambda index: 0x209 + index

W83795_REG_SFIV_TEMP = lambda index: range(0x280 + index * 0x10, 0x280 + index * 0x10 + 7)
W83795_REG_SFIV_DCPWM = lambda index: range(0x288 + index * 0x10, 0x288 + index * 0x10 + 7)

W83795_REG_CTFS = lambda index: 0x268 + index
W83795_REG_FOPFP = lambda index: 0x218 + index
W83795_REG_FONV = lambda index: 0x228 + index
W83795_REG_HT = lambda index: 0x270 + index
W83795_TTTI = lambda index: 0x260 + index
W83795_REG_VRLSB = 0x3C
W83795_REG_TEMP_READ = [0x21, 0x22, 0x23, 0x24, 0x1f, 0x20]

FANS = range(0, 8)
TEMPS = range(0, 6)

@contextmanager
def bank(bus, value):
    prev_value = w83795_set_bank(bus, value)
    yield
    w83795_set_bank(bus, prev_value)

def w83795_set_bank(bus, bank):
    assert bank in [0, 1, 2, 3]
    cur_bank = bus.read_byte_data(ADDRESS, W83795_REG_BANKSEL)
    if cur_bank == bank:
        return cur_bank
    bus.write_byte_data(ADDRESS, W83795_REG_BANKSEL, bank)
    return cur_bank

def w83795_write(bus, reg, value):
    with bank(bus, reg >> 8):
        bus.write_byte_data(ADDRESS, reg & 0xff, value & 0xff)

def w83795_read(bus, reg):
    if hasattr(reg, '__iter__'):
        with bank(bus, reg[0] >> 8):
            return list(map(lambda r: bus.read_byte_data(ADDRESS, r & 0xff), reg))
    with bank(bus, reg >> 8):
        return bus.read_byte_data(ADDRESS, reg & 0xff)

def to_degree(val, low=0, hi=127):
    return val

def to_perc(value):
    return value * 100 // 255

def from_degree(value):
    value = max(min(int(value) + 1, 127), 0)
    return value

def from_perc(value):
    value = max(min(int(value) + 1, 100), 0)
    return (value * 255 // 100) & 0xff

def unload_module():
    """Unload the w83795 kernel module."""
    os.system('sudo modprobe -r w83795')
    print("w83795 module unloaded.")

def load_module():
    """Reload the w83795 kernel module."""
    os.system('sudo modprobe w83795')
    print("w83795 module reloaded.")

def main():
    unload_module()  # Unload the kernel module before starting the main script

    parser = argparse.ArgumentParser(description='Change SuperMicro X8 Fan Control.')
    parser.add_argument('-m', '--mode', type=str, choices=['smart', 'cruise'], help='Set the fan mode: smart or cruise.')
    parser.add_argument('-p', '--pwm', type=str, help='Set Fan Duty (in percentage).')
    parser.add_argument('-t', '--temp', type=str, help='Set Temperature (in Celsius) associated with pwm.')
    parser.add_argument('-H', '--hystersis', type=int, help='Set Hystersis value in degree (0-15)')
    args = parser.parse_args()

    try:
        bus = SMBus(0)
    except Exception as e:
        print("Failed to open i2c bus (/dev/i2d-0). Make sure i2c-dev module is loaded.")
        load_module()  # Reload the module if the script fails
        return

    try:
        vendor = w83795_read(bus, 0xfd)
        chipid = w83795_read(bus, 0xfe)
        if vendor != NUVOTON_VENDOR_ID or chipid != CHIP_ID:
            print("unexpected vendor %s, chipid %s" % (vendor, chipid))
            return

        if args.mode == 'smart':
            if not args.pwm or not args.temp:
                print('pwm and temp are required')
                return
            pwm = args.pwm.split(',')
            temp = args.temp.split(',')
            if len(pwm) != len(temp):
                print("pwm and temp must have the same number of values")
                return

            for i in range(0, 7):
                p = pwm[i] if i < len(pwm) else pwm[-1]
                tt = temp[i] if i < len(temp) else temp[-1]
                print("Set Smart Fan Control %s%% - %sC" % (p, tt))
                for t in TEMPS:
                    w83795_write(bus, W83795_REG_SFIV_DCPWM(t)[i], from_perc(int(p)))
                    w83795_write(bus, W83795_REG_SFIV_TEMP(t)[i], from_degree(int(tt)))

            for f in FANS:
                w83795_write(bus, W83795_REG_FONV(f), from_perc(int(pwm[0])))

            for t in TEMPS:
                w83795_write(bus, W83795_REG_CTFS(t), from_degree(int(temp[-1])))

            w83795_write(bus, W83795_REG_FCMS1, 0x0)
            w83795_write(bus, W83795_REG_FCMS2, 0x3f)

        elif args.mode == 'cruise':
            if not args.temp:
                print('temp is required')
                return
            temp = int(args.temp)

            print("Set Thermal Cruise %sC" % (temp,))
            for t in TEMPS:
                w83795_write(bus, W83795_TTTI(t), from_degree(temp))

            for t in TEMPS:
                w83795_write(bus, W83795_REG_CTFS(t), from_degree(80))

            w83795_write(bus, W83795_REG_FCMS1, 0x0)
            w83795_write(bus, W83795_REG_FCMS2, 0x0)

        if args.hystersis:
            ht = max(min(args.hystersis, 15), 0)
            print("Set hystersis %sC" % ht)
            for t in range(0, 6):
                w83795_write(bus, W83795_REG_HT(t), ht)

        if args.mode or args.pwm or args.temp or args.hystersis:
            return

        fcms1 = w83795_read(bus, W83795_REG_FCMS1)
        fcms2 = w83795_read(bus, W83795_REG_FCMS2) & 0xff

        for t in TEMPS:
            print("Temp%s to Fan mapping Relationships (T%sFMR)" % (t + 1, t + 1))
            tfmr = w83795_read(bus, W83795_REG_TFMR(t))
            fans = [i + 1 for i in FANS if tfmr & (0x1 << i)]
            print(' '.join(['Fan%s' % i for i in fans]))

            print("Smart Fan Control Table (SFIV)")
            temp = w83795_read(bus, W83795_REG_SFIV_TEMP(t))
            print(''.join([("%sC" % to_degree(v)).rjust(6) for v in temp]))

            dcpwm = w83795_read(bus, W83795_REG_SFIV_DCPWM(t))
            print(''.join([("%s%%" % to_perc(v)).rjust(6) for v in dcpwm]))

            ttti = w83795_read(bus, W83795_TTTI(t))
            print("Thermal Cruise (TTTI): %sC" % (ttti,))

            ctfs = w83795_read(bus, W83795_REG_CTFS(t))
            print("Critical Temperature (T%sCTFS): %sC" % (t, to_degree(ctfs)))

            ht = w83795_read(bus, W83795_REG_HT(t)) & 0b1111
            print("Hysteresis (HT%s): %sC" % (t, ht))

            tmp = w83795_read(bus, W83795_REG_TEMP_READ[t])
            print("Current Temperature (TEMP%s): %sC" % (t, to_degree(tmp),))

            print('---')

        for f in FANS:
            fonv = w83795_read(bus, W83795_REG_FONV(f))
            print("Fan%s Output Nonstop Value (F%sONV): %s%%" % (f + 1, f + 1, to_perc(fonv)))

    finally:
        bus.close()
        load_module()  # Reload the kernel module after the script completes

if __name__ == "__main__":
    main()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment