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()
@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