Last active
May 25, 2023 02:51
-
-
Save Ropid/077816cec9e5a826ad417fc6ce5ac41a to your computer and use it in GitHub Desktop.
Fan control script (Linux, lm_sensors, hwmon)
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
#!/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