Skip to content

Instantly share code, notes, and snippets.

@Ropid
Last active May 25, 2023 02:51
Show Gist options
  • Save Ropid/077816cec9e5a826ad417fc6ce5ac41a to your computer and use it in GitHub Desktop.
Save Ropid/077816cec9e5a826ad417fc6ce5ac41a to your computer and use it in GitHub Desktop.
Fan control script (Linux, lm_sensors, hwmon)
#!/bin/bash
# This script is intended to control the fan speeds on an ASRock X470 Taichi
# motherboard, while also using a GPU's temperatures as input.
#
# ------------------------------
# ASRock X470 Taichi Motherboard
# ------------------------------
#
# This board's sensors are at /sys/devices/platform/nct6775.656/
#
# pwm2 controls the CPU fan header (CPU_FAN1).
#
# pwm4 controls the system fan header #1 (CHA_FAN1).
#
# pwm5 controls the system fan header #2 (CHA_FAN2).
#
# pwm1 controls the system fan header #3 (CHA_FAN3).
#
# temp1 is the "system" temperature.
#
# temp2 is the CPU temperature.
#
#
# ---------------------
# AMD Ryzen 7 2700X CPU
# ---------------------
#
# The CPU's sensors are at /sys/devices/pci0000:00/0000:00:18.3/hwmon/hwmon*/.
#
# temp1 is the CPU temperature ("Tdie").
#
# temp2 is "Tctl".
#
#
# --------------------------------
# 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
# -------------
#
# When using the GA-Z77X-D3H board, then:
# The card's sensors are at
# /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/hwmon/hwmon*/.
#
# When using the X470 Taichi board, then:
# The card's sensors are at
# /sys/devices/pci0000:00/0000:00:03.1/0000:0d:00.0/hwmon/hwmon*/.
#
# [ "$(whoami)" = root ] || exec sudo "$0"
pidfile="/run/fanauto.pid"
configuration() {
buffer_length=30
update_interval=3
output_skip=2
define_input gpu_temp0 amdgpu edge 40000 70000
define_input gpu_temp1 amdgpu edge 35000 65000
define_input cpu_temp0 k10temp Tdie 50000 85000
define_input cpu_temp1 k10temp Tdie 45000 80000
define_output sys_fan0 nct6779 pwm3 35 77 # rear case fan
define_output sys_fan1 nct6779 pwm4 25 55 # front case fans
define_output sys_fan2 nct6779 pwm5 25 65 # PSU fan
define_output cpu_fan nct6779 pwm2 25 60
define_output gpu_fan0 amdgpu pwm1 35 65
define_output gpu_fan1 nct6779 pwm1 20 55 # GPU area case fan
define_control gpu_temp0 sys_fan0 sys_fan1 sys_fan2
define_control gpu_temp1 gpu_fan0 gpu_fan1
define_control cpu_temp0 sys_fan0 sys_fan1 sys_fan2
define_control cpu_temp1 cpu_fan
# DRYRUN=1 # test mode; just reading and no writing
# DEBUG=1 # print debug output
# ## AMDGPU power usage
# define_input gpu_power amdgpu power1_average 50000000 100000000
# define_control gpu_power gpu_fan0 gpu_fan1 sys_fan0 sys_fan1 sys_fan2
# ## AMDGPU power usage
# gpu_power_file=$(find_dev_path amdgpu)/power1_average
# get_gpu_power() {
# local power
# power=$(< "$gpu_power_file") || die
# echo $(( (power + 500000) / 1000000 ))
# }
# define_reader gpu_power get_gpu_power 50 100
# define_control gpu_power gpu_fan0 gpu_fan1 sys_fan0 sys_fan1 sys_fan2
# ## NVIDIA temperature
# get_nvidia_temp() {
# nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader --id=0
# }
# define_reader nvidia_temp get_nvidia_temp 35 65
# define_control nvidia_temp sys_fan0 sys_fan1 sys_fan2
}
#------------------------------------------------------------------------------
# helper function and trap for exiting (works inside command substitution)
trap 'exit 1' USR2
die() {
echo "<3>$*" 1>&2
kill -USR2 0
}
find_dev_path() {
local driver=$1
local input=$2
local path
for path in /sys/class/hwmon/hwmon*; do
if [[ $driver = $(< "$path"/name) ]]; then
if [[ -z $input ]]; then
echo "$path"
return 0
elif [[ -e "$path/$input" ]]; then
echo "$path/$input"
return 0
else
local file
for file in "$path"/*_label; do
if [[ $input = $(< "$file") ]]; then
echo "${file%_label}_input"
return 0
fi
done
# die "Could not find input '$input' for device '$driver'!"
return 1
fi
fi
done
# die "Could not find device '$driver'!"
return 1
}
# list of input $name; both key and value are set to $name
declare -A input_list
# input details; the structure is:
# input_rec[$name.file] = filename/function; is called to produce input data
# input_rec[$name.min] = used for normalizing the input data
# input_rec[$name.max] = used for normalizing the input data
# input_rec[$name.width] = min to max range: $(( $max - $min ))
# input_rec[$name.buffer] = string with space separated input values
# input_rec[$name.buffer_sum] = sum of values in buffer; used to calculate the average
declare -A input_rec
# holds (input-file => data) pairs to help with reading each input file just
# once instead of multiple times
declare -A input_file_rec
# keys in input_file_rec; both key and value are set to $file (or $function)
declare -A input_file_list
declare -A reader_function_list
define_input() {
local name=$1
local driver=$2
local file=$3
local min=$4
local max=$5
if ! file=$(find_dev_path "$driver" "$file") ; then
die "Could not find device using driver '$driver'!"
fi
define_input_aux "$name" "$file" "$min" "$max"
input_file_rec[$file]=0
input_file_list[$file]=$file
}
define_reader() {
local name=$1
local function=$2
local min=$3
local max=$4
define_input_aux "$name" "$function" "$min" "$max"
input_file_rec[$function]=0
reader_function_list[$function]=$function
}
define_input_aux() {
local name=$1
local file=$2
local min=$3
local max=$4
input_list[$name]=$name
input_rec+=(
[$name.file]="$file"
[$name.min]="$min"
[$name.max]="$max"
[$name.width]=$(( input_rec[$name.max] - input_rec[$name.min] ))
[$name.buffer]=$(
perl -e 'print join(" ", (shift) x shift), "\n"' \
"${input_rec[$name.min]}" $buffer_length
)
[$name.buffer_sum]=$(
perl -e 'print int(shift) * int(shift), "\n"' \
"${input_rec[$name.min]}" $buffer_length
)
)
}
declare -a output_list
declare -A output_rec
# see input_file_rec
declare -A output_file_rec
define_output() {
local name=$1
local driver=$2
local file=$3
local min=$4
local max=$5
output_list+=( "$name" )
if ! output_rec[$name.file]=$(find_dev_path "$driver")/$file ; then
die "Could not find device using driver '$driver'!"
fi
output_file_rec[${output_rec[$name.file]}]=0
# turn 0..100 into 0..255:
# min / 100 * 255 + 0.5 <-- +0.5 for rounding
# min * 255 / 100 + 50 / 100
# (min * 255 + 50) / 100
output_rec[$name.min]=$(( (min * 255 + 50) / 100 )) # +50 for rounding
output_rec[$name.max]=$(( (max * 255 + 50) / 100 ))
(( output_rec[$name.width] = output_rec[$name.max] - output_rec[$name.min] ))
output_rec[$name.pwm]=0 # used to combine inputs/controls, needs to be set to zero
}
declare -A pwm_save_rec
# Save PWM setup
save_pwm() {
(( DEBUG )) && echo "save_pwm():"
(( DEBUG )) && echo " output_file_rec = (${!output_file_rec[*]})"
local pwm
for pwm in "${!output_file_rec[@]}"; do
read -r pwm_save_rec["$pwm"] < "$pwm"
read -r pwm_save_rec["$pwm"_enable] < "$pwm"_enable
done
if (( DEBUG )); then
print_array pwm_save_rec | sed 's/^/ /'
fi
}
# Restore PWM setup
restore_pwm() {
(( DEBUG )) && echo "restore_pwm():"
enable_pwm | sed 's/^/ /'
local output
local -a pwm_list
for output in "${output_list[@]}"; do
pwm_list+=( "${output_rec[$output.file]}" )
done
(( DEBUG )) && print_array pwm_save_rec | sed 's/^/ /'
local pwm
for pwm in "${pwm_list[@]}"; do
if (( !DRYRUN )); then
echo "${pwm_save_rec["$pwm"]}" > "$pwm"
echo "${pwm_save_rec["$pwm"_enable]}" > "$pwm"_enable
fi
done
(( !DRYRUN )) && rm -f "$pidfile"
#exit
}
# Enable PWM control
enable_pwm() {
(( DEBUG )) && echo "enable_pwm():"
local output
local -a pwm_list
for output in "${output_list[@]}"; do
pwm_list+=( "${output_rec[$output.file]}" )
done
local pwm
for pwm in "${pwm_list[@]}"; do
(( DEBUG )) && echo " file ${pwm}_enable"
if (( !DRYRUN )); then
echo 1 > "$pwm"_enable || (( error_counter++ ))
fi
done
(( DEBUG )) && echo " error_counter $error_counter"
}
declare -a control_list
declare -A control_rec
define_control() {
local input=$1
shift # the rest of the arguments are output names
control_list+=( "$input" )
control_rec[$input]="$*"
}
read_inputs() {
(( DEBUG )) && echo "read_inputs():"
local file
for file in "${!input_file_list[@]}"; do
read -r input_file_rec[$file] < "$file" || (( error_counter++ ))
(( DEBUG )) && echo " file $file, value ${input_file_rec[$file]}"
done
local function
for function in "${!reader_function_list[@]}"; do
input_file_rec[$function]=$( "$function" ) || (( error_counter++ ))
(( DEBUG )) && echo " function $function, value ${input_file_rec[$function]}"
done
local name
for name in "${input_list[@]}"; do
local value
value=${input_file_rec[${input_rec[$name.file]}]}
local min=${input_rec[$name.min]}
(( value < min )) && value=$min
local head="${input_rec[$name.buffer]%% *}"
input_rec[$name.buffer]="${input_rec[$name.buffer]#* } $value"
(( input_rec[$name.buffer_sum] += value - head ))
(( DEBUG )) && echo " input $name, value $value, buffer average $(( input_rec[$name.buffer_sum] / buffer_length ))"
done
}
# sets temperature buffer average to last read value
reset_input_buffer() {
(( DEBUG )) && echo "reset_input_buffer():"
local input last
for input in "${input_list[@]}"; do
last="${input_rec[$input.buffer]##* }"
(( input_rec[$input.buffer_sum] = last * buffer_length ))
input_rec[$input.buffer]=$(
perl -E 'say join " ", (shift) x shift' "$last" "$buffer_length"
)
if (( DEBUG )); then
echo " input $input, last $last, buffer average $(( input_rec[$input.buffer_sum] / buffer_length ))"
echo " buffer = \"${input_rec[$input.buffer]}\""
fi
done
}
control_outputs() {
(( DEBUG )) && echo "control_outputs():"
local input output value pwm slice
for input in "${control_list[@]}"; do
(( value = input_rec[$input.buffer_sum] / buffer_length ))
# multiply by 1000 to turn it into a sort of fixed point number
(( slice = (value - input_rec[$input.min]) * 1000 / input_rec[$input.width] ))
if (( DEBUG )); then
echo " control $input, value $value, slice $(( (slice + 5) / 10 ))%"
fi
(( slice < 0 )) && slice=0
(( slice > 1000 )) && slice=1000
for output in ${control_rec[$input]}; do
(( pwm = (slice * output_rec[$output.width] + 500) / 1000 + output_rec[$output.min] ))
local file=${output_rec[$output.file]}
if (( output_file_rec[$file] < pwm )); then
output_file_rec[$file]=$pwm
fi
(( DEBUG )) && echo " output $output, pwm $pwm ($(( (pwm * 1000 + 1275) / 2550 ))%)"
done
done
local file
for file in "${!output_file_rec[@]}"; do
(( DEBUG )) && echo " file $file, pwm ${output_file_rec[$file]}"
if (( !DRYRUN )); then
echo "${output_file_rec[$file]}" > "$file" || (( error_counter++ ))
fi
output_file_rec[$file]=0
done
}
print_array() {
local name
for name; do
local -n ref="$name"
printf "%s:\n" "$name"
local i
for i in "${!ref[@]}"; do
printf " %s = %s\n" "$i" "${ref[$i]}"
done | sort
done
}
#------------------------------------------------------------------------------
# load sleep as builtin
if [[ $(type -t sleep) = file && -x /usr/lib/bash/sleep ]]; then
enable -f /usr/lib/bash/sleep sleep
fi
configuration
if (( DEBUG )); then
echo "-- Configuration: ------------------"
(( DEBUG )) && echo "sleep = $(type -t sleep)"
echo "buffer_length = $buffer_length"
print_array {input,output,control}_{list,rec}
echo "-- Start-up: -----------------------"
fi
progname=$(basename "$0")
if (( !DRYRUN )); then
if [[ -f "$pidfile" ]] ; then
die "$progname: $pidfile exists!"
fi
echo $$ > "$pidfile"
fi
save_pwm
trap 'restore_pwm' EXIT
error_counter=0
enable_pwm
read_inputs
reset_input_buffer
control_outputs
if (( error_counter )) ; then
die "Something doesn't work!"
fi
(( DEBUG )) && echo "sleep $update_interval"
sleep "$update_interval"
(( DEBUG )) && echo "------------------------------------"
output_counter=$output_skip
while true; do
read_inputs
(( error_counter )) && die "Something doesn't work!"
(( DEBUG )) && echo "output_counter = $output_counter"
if (( --output_counter < 0 )); then
output_counter=$output_skip
control_outputs
fi
if (( error_counter )) ; then
(( DEBUG )) && echo "write error: now trying enable_pwm a second time"
error_counter=0
enable_pwm # an error can happen after PC standby/resume, so trying to enable pwm again might work
if (( error_counter )) ; then
die "Something doesn't work!"
fi
fi
(( DEBUG )) && echo "sleep $update_interval"
sleep "$update_interval"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment