Skip to content

Instantly share code, notes, and snippets.

@Ropid

Ropid/fanauto Secret

Last active Aug 28, 2020
Embed
What would you like to do?
Fan control script
#!/bin/bash
# This script is intended to control the fan speeds on a Gigabyte Z77X-D3H
# motherboard, while also using a GPU's temperatures as input.
#
# --------------------------------
# Gigabyte GA-Z77X-D3H Motherboard
# --------------------------------
#
# This board's sensors are at /sys/devices/platform/it87.2608/.
#
# pwm1 controls voltage of the CPU fan header (has to be disabled if using a PWM
# fan).
#
# pwm2 controls the system fan headers (voltage on SYS_FAN1 and PWM on SYS_FAN[23]).
#
# pwm3 controls the PWM signal for the CPU fan header.
#
# temp1 is the "system" temperature.
#
# temp2 is the PCH's temperature.
#
# temp3 is the CPU temperature.
#
# Setting pwm2_enable to 0 disables control, will output a 100% voltage/PWM
# signal.
#
# Setting pwm2_enable to 2 will cause the board's sensor chip logic to use some
# sort of automatic to tie temperatures to fan speeds. The value in pwm2 seems
# to be used as a multiplier for that automatic.
#
# Setting pwm2_enable to 1 will cause the chip to use a fixed speed as set in
# pwm2. The value in pwm2 seems to have the value range of an unsigned 8-bit
# number. The 0 to 255 value translates into a 0 to 100% fan speed signal.
#
#
# ------------------
# Intel i5-3570k CPU
# ------------------
#
# The CPU's sensors are at /sys/devices/platform/coretemp.0/hwmon/hwmon*/.
#
# temp[2345] are the individual core temperatures.
#
# temp1 seems to be the highest core temperature.
#
#
# -------------------------------
# NVIDIA GPU, using NVIDIA's blob
# -------------------------------
#
# There's no lm_sensors device.
#
# The nvidia-smi tool seems to be the least resource intensive method to read
# temperatures from the card. The following outputs a single number (for GPU0)
# in degrees Celsius (so without a factor of 1000 like lm_sensors):
#
# $ nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader --id=0
#
#
# -------------
# AMD RX480 GPU
# -------------
#
# The card's sensors are at
# /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/hwmon/hwmon*/.
#
# [ "$(whoami)" = root ] || exec sudo "$0"
pidfile="/var/run/fanauto.pid"
# Temperature limits in degrees Celsius:
gpu_temp_to_sysfan_min=40
gpu_temp_to_sysfan_max=55
gpu_temp_to_gpufan_min=35
gpu_temp_to_gpufan_max=55
cpu_temp_to_sysfan_min=45
cpu_temp_to_sysfan_max=110
cpu_temp_to_cpufan_min=45
cpu_temp_to_cpufan_max=75
# Fan speed limits in percent
sysfan_min=25
sysfan_max=50
cpufan_min=25
cpufan_max=50
gpufan_min=25
gpufan_max=65
# Fan speed history buffer length and update interval in seconds
buf_length=40
update_interval=3
# Number of intervals to skip after each output, so only outputs every 3+3*2=9 seconds
output_skip=2
#device_path='/sys/class/hwmon/hwmon1'
printf -v device_path "%s" /sys/devices/platform/it87.2608/hwmon/hwmon*
printf -v device_cpu_temp "%s" /sys/devices/platform/coretemp.0/hwmon/hwmon*/temp1_input
printf -v device_gpu "%s" '/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/hwmon'/hwmon*
printf -v device_gpu_temp "%s" '/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/hwmon'/hwmon*/temp1_input
# Temperature input
function read_temperatures {
read -r cpu_temp < "$device_cpu_temp" || (( error_counter++ ))
#read cpu_temp < /sys/class/hwmon/hwmon1/temp3_input
#read system_temp < /sys/class/hwmon/hwmon1/temp1_input
#read pch_temp < /sys/class/hwmon/hwmon1/temp2_input
#gpu_temp=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader --id=0)000
read -r gpu_temp < "$device_gpu_temp" || (( error_counter++ ))
}
# Enable PWM control
function enable_pwm {
echo 1 > "$device_path"/pwm2_enable || (( error_counter++ ))
echo 1 > "$device_path"/pwm3_enable || (( error_counter++ ))
echo 1 > "$device_gpu"/pwm1_enable || (( error_counter++ ))
}
function output_pwm {
# $1: sysfan
# $2: cpufan
# $3: gpufan
echo "$1" > "$device_path"/pwm2 || (( error_counter++ ))
echo "$2" > "$device_path"/pwm3 || (( error_counter++ ))
echo "$3" > "$device_gpu"/pwm1 || (( error_counter++ ))
}
# Save PWM setup
function save_pwm {
read -r save_pwm2 < "$device_path"/pwm2
read -r save_pwm3 < "$device_path"/pwm3
read -r save_pwm2_enable < "$device_path"/pwm2_enable
read -r save_pwm3_enable < "$device_path"/pwm3_enable
read -r save_gpu_pwm1 < "$device_gpu"/pwm1
read -r save_gpu_pwm1_enable < "$device_gpu"/pwm1_enable
}
# Restore PWM setup
function restore_pwm {
enable_pwm
echo "$save_pwm2" > "$device_path"/pwm2
echo "$save_pwm3" > "$device_path"/pwm3
echo "$save_pwm2_enable" > "$device_path"/pwm2_enable
echo "$save_pwm3_enable" > "$device_path"/pwm3_enable
echo "$save_gpu_pwm1" > "$device_gpu"/pwm1
echo "$save_gpu_pwm1_enable" > "$device_gpu"/pwm1_enable
rm -f "$pidfile"
exit "$1"
}
#
# --------------------------------------------------------------------------------
#
(( sysfan_max_255 = (sysfan_max * 255 + 50) / 100 )) # +50 to make rounding work
(( cpufan_max_255 = (cpufan_max * 255 + 50) / 100 ))
(( gpufan_max_255 = (gpufan_max * 255 + 50) / 100 ))
(( gpu_temp_to_sysfan_min *= 1000 ))
(( gpu_temp_to_sysfan_max *= 1000 ))
(( gpu_temp_to_gpufan_min *= 1000 ))
(( gpu_temp_to_gpufan_max *= 1000 ))
(( cpu_temp_to_sysfan_min *= 1000 ))
(( cpu_temp_to_sysfan_max *= 1000 ))
(( cpu_temp_to_cpufan_min *= 1000 ))
(( cpu_temp_to_cpufan_max *= 1000 ))
(( sysfan_min = sysfan_min * 1000 * 255 / 100 ))
(( sysfan_max = sysfan_max * 1000 * 255 / 100 ))
(( cpufan_min = cpufan_min * 1000 * 255 / 100 ))
(( cpufan_max = cpufan_max * 1000 * 255 / 100 ))
(( gpufan_min = gpufan_min * 1000 * 255 / 100 ))
(( gpufan_max = gpufan_max * 1000 * 255 / 100 ))
(( gpu_temp_to_sysfan_width = gpu_temp_to_sysfan_max - gpu_temp_to_sysfan_min ))
(( gpu_temp_to_gpufan_width = gpu_temp_to_gpufan_max - gpu_temp_to_gpufan_min ))
(( cpu_temp_to_sysfan_width = cpu_temp_to_sysfan_max - cpu_temp_to_sysfan_min ))
(( cpu_temp_to_cpufan_width = cpu_temp_to_cpufan_max - cpu_temp_to_cpufan_min ))
(( sysfan_width = sysfan_max - sysfan_min ))
(( cpufan_width = cpufan_max - cpufan_min ))
(( gpufan_width = gpufan_max - gpufan_min ))
function calc_fan_speeds {
# translate both GPU and CPU temperature to system fan speed and use max
(( sysfan_gpu = (gpu_temp - gpu_temp_to_sysfan_min) * sysfan_width / gpu_temp_to_sysfan_width ))
(( sysfan_cpu = (cpu_temp - cpu_temp_to_sysfan_min) * sysfan_width / cpu_temp_to_sysfan_width ))
# translate CPU temperature to CPU fan speed
(( cpufan_cpu = (cpu_temp - cpu_temp_to_cpufan_min) * cpufan_width / cpu_temp_to_cpufan_width ))
# translate GPU temperature to GPU fan speed
(( gpufan_gpu = (gpu_temp - gpu_temp_to_gpufan_min) * gpufan_width / gpu_temp_to_gpufan_width ))
# set correct minimum
sysfan=0
(( sysfan_gpu > sysfan )) && sysfan=$sysfan_gpu
(( sysfan_cpu > sysfan )) && sysfan=$sysfan_cpu
(( sysfan += sysfan_min ))
cpufan=0
(( cpufan_cpu > cpufan )) && cpufan=$cpufan_cpu
(( cpufan += cpufan_min ))
gpufan=0
(( gpufan_gpu > gpufan )) && gpufan=$gpufan_gpu
(( gpufan += gpufan_min ))
}
# set up a history of fan speeds that is intended to be used as a ring buffer
cpufan_buf=()
gpufan_buf=()
sysfan_buf=()
buf_ringptr=0
read_temperatures
calc_fan_speeds
for (( i = 0; i < buf_length; i++ ))
do
(( sysfan_buf[i] = sysfan ))
(( cpufan_buf[i] = cpufan ))
(( gpufan_buf[i] = gpufan ))
done
(( sysfan_sum = sysfan * buf_length ))
(( cpufan_sum = cpufan * buf_length ))
(( gpufan_sum = gpufan * buf_length ))
progname=$(basename "$0")
if [[ -f "$pidfile" ]] ; then
echo "$progname: $pidfile exists" >&2
exit 1
fi
echo $$ > "$pidfile"
save_pwm
trap 'restore_pwm 0' SIGQUIT SIGTERM
trap 'restore_pwm 1' SIGHUP SIGINT EXIT
enable_pwm
error_counter=0
#function error_handler {
# (( error_counter++ ))
# echo "error: $(caller)"
#}
#trap 'error_handler' ERR
#set -E
# load sleep as builtin in Arch
[[ -x /usr/lib/bash/sleep ]] && enable -f /usr/lib/bash/sleep sleep
# main loop
output_limit_counter=0
while true
do
read_temperatures
calc_fan_speeds
(( sysfan_sum += sysfan - sysfan_buf[buf_ringptr] ))
(( cpufan_sum += cpufan - cpufan_buf[buf_ringptr] ))
(( gpufan_sum += gpufan - gpufan_buf[buf_ringptr] ))
(( sysfan_buf[buf_ringptr] = sysfan ))
(( cpufan_buf[buf_ringptr] = cpufan ))
(( gpufan_buf[buf_ringptr] = gpufan ))
(( ++buf_ringptr >= buf_length )) && buf_ringptr=0
(( sysfan_out = (sysfan_sum / buf_length + 500) / 1000 )) # +500 to make rounding work
(( cpufan_out = (cpufan_sum / buf_length + 500) / 1000 ))
(( gpufan_out = (gpufan_sum / buf_length + 500) / 1000 ))
(( sysfan_out > sysfan_max_255 )) && sysfan_out=$sysfan_max_255
(( cpufan_out > cpufan_max_255 )) && cpufan_out=$cpufan_max_255
(( gpufan_out > gpufan_max_255 )) && gpufan_out=$gpufan_max_255
# PWM output
if (( --output_limit_counter < 0 )) ; then
(( output_limit_counter = output_skip ))
output_pwm $sysfan_out $cpufan_out $gpufan_out
# echo cpu $((cpu_temp/1000))°C, gpu $((gpu_temp/1000))°C, \
# cpu-fan $(((cpufan_out*1000+500)/2550))% \($(((cpufan+500)/2550))%\), \
# sys-fan $(((sysfan_out*1000+500)/2550))% \($(((sysfan+500)/2550))%\), \
# gpu-fan $(((gpufan_out*1000+500)/2550))% \($(((gpufan+500)/2550))%\)
fi
if (( error_counter )) ; then
error_counter=0
enable_pwm # an error can happen after PC standby/resume, so trying to reenable pwm might work
if (( error_counter )) ; then
echo "something doesn't work!" >&2
exit 1
fi
fi
# sleep $update_interval &
# wait "$!"
sleep $update_interval
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment