build/packages/bsp/nanopim4/nanopim4-pwm-fan.sh

489 lines
16 KiB
Bash

#!/bin/bash
###############################################################################
# Bash script to control the NanoPi M4 SATA hat 12v fan via the sysfs interface
###############################################################################
# Author: cgomesu
# Repo: https://github.com/cgomesu/nanopim4-satahat-fan
# Official pwm sysfs doc: https://www.kernel.org/doc/Documentation/pwm.txt
#
# This is free. There is NO WARRANTY. Use at your own risk.
###############################################################################
cache () {
if [[ -z "$1" ]]; then
echo '[pwm-fan] Cache file was not specified. Assuming generic.'
local FILENAME='generic'
else
local FILENAME="$1"
fi
# cache to memory
CACHE_ROOT='/tmp/pwm-fan/'
if [[ ! -d "$CACHE_ROOT" ]]; then
mkdir "$CACHE_ROOT"
fi
CACHE=$CACHE_ROOT$FILENAME'.cache'
if [[ ! -f "$CACHE" ]]; then
touch "$CACHE"
else
> "$CACHE"
fi
}
check_requisites () {
local REQUISITES=('bc' 'cat' 'echo' 'mkdir' 'touch' 'trap' 'sleep')
echo '[pwm-fan] Checking requisites: '${REQUISITES[@]}
for cmd in ${REQUISITES[@]}; do
if [[ -z $(command -v $cmd) ]]; then
echo '[pwm-fan] The following program is not installed or cannot be found in this users $PATH: '$cmd
echo '[pwm-fan] Fix it and try again.'
end "Missing important packages. Cannot continue." 1
fi
done
echo '[pwm-fan] All commands are accesible.'
}
cleanup () {
echo '---- cleaning up ----'
# disable the channel
unexport_pwmchip_channel
# clean cache files
if [[ -d "$CACHE_ROOT" ]]; then
rm -rf "$CACHE_ROOT"
fi
echo '--------------------'
}
config () {
pwmchip
export_pwmchip_channel
fan_startup
fan_initialization
thermal_monit
}
# takes message and status as argument
end () {
cleanup
echo '####################################################'
echo '# END OF THE PWM-FAN SCRIPT'
echo '# MESSAGE: '$1
echo '####################################################'
exit $2
}
export_pwmchip_channel () {
if [[ ! -d "$CHANNEL_FOLDER" ]]; then
local EXPORT=$PWMCHIP_FOLDER'export'
cache 'export'
local EXPORT_SET=$(echo 0 2> "$CACHE" > "$EXPORT")
if [[ ! -z $(cat "$CACHE") ]]; then
# on error, parse output
if [[ $(cat "$CACHE") =~ (P|p)ermission\ denied ]]; then
echo '[pwm-fan] This user does not have permission to use channel '$CHANNEL'.'
if [[ ! -z $(command -v stat) ]]; then
echo '[pwm-fan] Export is owned by user: '$(stat -c '%U' "$EXPORT")'.'
echo '[pwm-fan] Export is owned by group: '$(stat -c '%G' "$EXPORT")'.'
fi
local ERR_MSG='User permission error while setting channel.'
elif [[ $(cat "$CACHE") =~ (D|d)evice\ or\ resource\ busy ]]; then
echo '[pwm-fan] It seems the pin is already in use. Cannot write to export.'
local ERR_MSG=$PWMCHIP' was busy while setting channel.'
else
echo '[pwm-fan] There was an unknown error while setting the channel '$CHANNEL'.'
if [[ $(cat "$CACHE") =~ \ ([^\:]+)$ ]]; then
echo '[pwm-fan] Error: '${BASH_REMATCH[1]}'.'
fi
local ERR_MSG='Unknown error while setting channel.'
fi
end "$ERR_MSG" 1
fi
sleep 1
elif [[ -d "$CHANNEL_FOLDER" ]]; then
echo '[pwm-fan] '$CHANNEL' channel is already accessible.'
fi
}
fan_initialization () {
if [[ -z "$TIME_STARTUP" ]]; then
TIME_STARTUP=10
fi
cache 'test_fan'
local READ_MAX_DUTY_CYCLE=$(cat $CHANNEL_FOLDER'period')
echo $READ_MAX_DUTY_CYCLE 2> $CACHE > $CHANNEL_FOLDER'duty_cycle'
# on error, try setting duty_cycle to a lower value
if [[ ! -z $(cat $CACHE) ]]; then
local READ_MAX_DUTY_CYCLE=$(($(cat $CHANNEL_FOLDER'period')-100))
> $CACHE
echo $READ_MAX_DUTY_CYCLE 2> $CACHE > $CHANNEL_FOLDER'duty_cycle'
if [[ ! -z $(cat $CACHE) ]]; then
end 'Unable to set max duty_cycle.' 1
fi
fi
MAX_DUTY_CYCLE=$READ_MAX_DUTY_CYCLE
echo '[pwm-fan] Running fan at full speed for the next '$TIME_STARTUP' seconds...'
echo 1 > $CHANNEL_FOLDER'enable'
sleep $TIME_STARTUP
echo $((MAX_DUTY_CYCLE/2)) > $CHANNEL_FOLDER'duty_cycle'
echo '[pwm-fan] Initialization done. Duty cycle at 50% now: '$((MAX_DUTY_CYCLE/2))' ns.'
sleep 1
}
fan_run () {
if [[ $THERMAL_STATUS -eq 0 ]]; then
fan_run_max
else
fan_run_thermal
fi
}
fan_run_max () {
echo '[pwm-fan] Running fan at full speed until stopped (Ctrl+C or kill '$$')...'
while true; do
echo $MAX_DUTY_CYCLE > $CHANNEL_FOLDER'duty_cycle'
# run every so often to make sure it is maxed
sleep 120
done
}
fan_run_thermal () {
echo '[pwm-fan] Running fan in temp monitor mode until stopped (Ctrl+C or kill '$$')...'
if [[ -z $THERMAL_ABS_THRESH_LOW ]]; then
THERMAL_ABS_THRESH_LOW=25
fi
if [[ -z $THERMAL_ABS_THRESH_HIGH ]]; then
THERMAL_ABS_THRESH_HIGH=75
fi
THERMAL_ABS_THRESH=($THERMAL_ABS_THRESH_LOW $THERMAL_ABS_THRESH_HIGH)
if [[ -z $DC_PERCENT_MIN ]]; then
DC_PERCENT_MIN=25
fi
if [[ -z $DC_PERCENT_MAX ]]; then
DC_PERCENT_MAX=100
fi
DC_ABS_THRESH=($(((DC_PERCENT_MIN*MAX_DUTY_CYCLE)/100)) $(((DC_PERCENT_MAX*MAX_DUTY_CYCLE)/100)))
if [[ -z $TEMPS_SIZE ]]; then
TEMPS_SIZE=6
fi
if [[ -z $TIME_LOOP ]]; then
TIME_LOOP=10
fi
TEMPS=()
while [[ true ]]; do
TEMPS+=($(thermal_meter))
if [[ ${#TEMPS[@]} -gt $TEMPS_SIZE ]]; then
TEMPS=(${TEMPS[@]:1})
fi
if [[ ${TEMPS[-1]} -le ${THERMAL_ABS_THRESH[0]} ]]; then
echo ${DC_ABS_THRESH[0]} 2> /dev/null > $CHANNEL_FOLDER'duty_cycle'
elif [[ ${TEMPS[-1]} -ge ${THERMAL_ABS_THRESH[-1]} ]]; then
echo ${DC_ABS_THRESH[-1]} 2> /dev/null > $CHANNEL_FOLDER'duty_cycle'
elif [[ ${#TEMPS[@]} -gt 1 ]]; then
TEMPS_SUM=0
for TEMP in ${TEMPS[@]}; do
let TEMPS_SUM+=$TEMP
done
# moving mid-point
MEAN_TEMP=$((TEMPS_SUM/${#TEMPS[@]}))
DEV_MEAN_CRITICAL=$((MEAN_TEMP-100))
X0=${DEV_MEAN_CRITICAL#-}
# args: x, x0, L, a, b (k=a/b)
MODEL=$(function_logistic ${TEMPS[-1]} $X0 ${DC_ABS_THRESH[-1]} 1 10)
if [[ $MODEL -lt ${DC_ABS_THRESH[0]} ]]; then
echo ${DC_ABS_THRESH[0]} 2> /dev/null > $CHANNEL_FOLDER'duty_cycle'
elif [[ $MODEL -gt ${DC_ABS_THRESH[-1]} ]]; then
echo ${DC_ABS_THRESH[-1]} 2> /dev/null > $CHANNEL_FOLDER'duty_cycle'
else
echo $MODEL 2> /dev/null > $CHANNEL_FOLDER'duty_cycle'
fi
fi
sleep $TIME_LOOP
done
}
fan_startup () {
if [[ -z $PERIOD ]]; then
PERIOD=25000000
fi
while [[ -d "$CHANNEL_FOLDER" ]]; do
if [[ $(cat $CHANNEL_FOLDER'enable') -eq 0 ]]; then
set_default
break
elif [[ $(cat $CHANNEL_FOLDER'enable') -eq 1 ]]; then
echo '[pwm-fan] The fan is already enabled. Will disable it.'
echo 0 > $CHANNEL_FOLDER'enable'
sleep 1
set_default
break
else
echo '[pwm-fan] Unable to read the fan enable status.'
end 'Bad fan status' 1
fi
done
}
function_logistic () {
# https://en.wikipedia.org/wiki/Logistic_function
local x=$1
local x0=$2
local L=$3
# k=a/b
local a=$4
local b=$5
local equation="output=$L/(1+e(-($a/$b)*($x-$x0)));scale=0;output/1"
local result=$(echo $equation | bc -lq)
echo $result
}
interrupt () {
echo '!! ATTENTION !!'
end 'Received a signal to stop the script.' 0
}
pwmchip () {
if [[ -z $PWMCHIP ]]; then
PWMCHIP='pwmchip1'
fi
PWMCHIP_FOLDER='/sys/class/pwm/'$PWMCHIP'/'
if [[ ! -d "$PWMCHIP_FOLDER" ]]; then
echo '[pwm-fan] The sysfs interface for the '$PWMCHIP' is not accessible.'
end 'Cannot access '$PWMCHIP' sysfs interface.' 1
fi
echo '[pwm-fan] Working with the sysfs interface for the '$PWMCHIP'.'
echo '[pwm-fan] For reference, your '$PWMCHIP' supports '$(cat $PWMCHIP_FOLDER'npwm')' channel(s).'
if [[ -z $CHANNEL ]]; then
CHANNEL='pwm0'
fi
CHANNEL_FOLDER="$PWMCHIP_FOLDER""$CHANNEL"'/'
}
set_default () {
cache 'set_default_duty_cycle'
echo 0 2> $CACHE > $CHANNEL_FOLDER'duty_cycle'
if [[ ! -z $(cat $CACHE) ]]; then
# set higher than 0 values to avoid negative ones
echo 100 > $CHANNEL_FOLDER'period'
echo 10 > $CHANNEL_FOLDER'duty_cycle'
fi
cache 'set_default_period'
echo $PERIOD 2> $CACHE > $CHANNEL_FOLDER'period'
if [[ ! -z $(cat $CACHE) ]]; then
echo '[pwm-fan] The period provided ('$PERIOD') is not acceptable.'
echo '[pwm-fan] Trying to lower it by 100ns decrements. This may take a while...'
local decrement=100
local rate=$decrement
until [[ $PERIOD_NEW -le 200 ]]; do
local PERIOD_NEW=$((PERIOD-rate))
> $CACHE
echo $PERIOD_NEW 2> $CACHE > $CHANNEL_FOLDER'period'
if [[ -z $(cat $CACHE) ]]; then
break
fi
local rate=$((rate+decrement))
done
PERIOD=$PERIOD_NEW
if [[ $PERIOD -le 100 ]]; then
end 'Unable to set an appropriate value for the period.' 1
fi
fi
echo 'normal' > $CHANNEL_FOLDER'polarity'
echo '[pwm-fan] Default polarity: '$(cat $CHANNEL_FOLDER'polarity')
echo '[pwm-fan] Default period: '$(cat $CHANNEL_FOLDER'period')' ns'
echo '[pwm-fan] Default duty cycle: '$(cat $CHANNEL_FOLDER'duty_cycle')' ns'
}
start () {
echo '####################################################'
echo '# STARTING PWM-FAN SCRIPT'
echo '# Date and time: '$(date)
echo '####################################################'
check_requisites
}
thermal_meter () {
if [[ -f $TEMP_FILE ]]; then
local TEMP=$(cat $TEMP_FILE 2> /dev/null)
# TEMP is in millidegrees, so convert to degrees
echo $((TEMP/1000))
fi
}
thermal_monit () {
if [[ -z $MONIT_DEVICE ]]; then
# soc for legacy Kernel or cpu for latest Kernel
MONIT_DEVICE='(soc|cpu)'
fi
local THERMAL_FOLDER='/sys/class/thermal/'
if [[ -d $THERMAL_FOLDER && -z $SKIP_THERMAL ]]; then
for dir in $THERMAL_FOLDER'thermal_zone'*; do
if [[ $(cat $dir'/type') =~ $MONIT_DEVICE && -f $dir'/temp' ]]; then
TEMP_FILE=$dir'/temp'
echo '[pwm-fan] Found the '$MONIT_DEVICE' temperature at '$TEMP_FILE
echo '[pwm-fan] Current '$MONIT_DEVICE' temp is: '$(($(thermal_meter)))' Celsius'
echo '[pwm-fan] Setting fan to monitor the '$MONIT_DEVICE' temperature.'
THERMAL_STATUS=1
return
fi
done
echo '[pwm-fan] Did not find the temperature for the device type: '$MONIT_DEVICE
else
echo '[pwm-fan] -f mode enabled or the the thermal zone cannot be found at '$THERMAL_FOLDER
fi
echo '[pwm-fan] Setting fan to operate independent of the '$MONIT_DEVICE' temperature.'
THERMAL_STATUS=0
}
unexport_pwmchip_channel () {
if [[ -d "$CHANNEL_FOLDER" ]]; then
echo '[pwm-fan] Freeing up the channel '$CHANNEL' controlled by the '$PWMCHIP'.'
echo 0 > $CHANNEL_FOLDER'enable'
sleep 1
echo 0 > $PWMCHIP_FOLDER'unexport'
sleep 1
if [[ ! -d "$CHANNEL_FOLDER" ]]; then
echo '[pwm-fan] Channel '$CHANNEL' was disabled.'
else
echo '[pwm-fan] Channel '$CHANNEL' is still enabled. Please check '$CHANNEL_FOLDER'.'
fi
else
echo '[pwm-fan] There is no channel to disable.'
fi
}
usage() {
echo ''
echo 'Usage:'
echo ''
echo "$0" '[OPTIONS]'
echo ''
echo ' Options:'
echo ' -c str Name of the PWM CHANNEL (e.g., pwm0, pwm1). Default: pwm0'
echo ' -C str Name of the PWM CONTROLLER (e.g., pwmchip0, pwmchip1). Default: pwmchip1'
echo ' -d int Lowest DUTY CYCLE threshold (in percentage of the period). Default: 25'
echo ' -D int Highest DUTY CYCLE threshold (in percentage of the period). Default: 100'
echo ' -f Fan runs at FULL SPEED all the time. If omitted (default), speed depends on temperature.'
echo ' -F int TIME (in seconds) to run the fan at full speed during STARTUP. Default: 60'
echo ' -h Show this HELP message.'
echo ' -l int TIME (in seconds) to LOOP thermal reads. Lower means higher resolution but uses ever more resources. Default: 10'
echo ' -m str Name of the DEVICE to MONITOR the temperature in the thermal sysfs interface. Default: (soc|cpu)'
echo ' -p int The fan PERIOD (in nanoseconds). Default (25kHz): 25000000.'
echo ' -s int The MAX SIZE of the TEMPERATURE ARRAY. Interval between data points is set by -l. Default (store last 1min data): 6.'
echo ' -t int Lowest TEMPERATURE threshold (in Celsius). Lower temps set the fan speed to min. Default: 25'
echo ' -T int Highest TEMPERATURE threshold (in Celsius). Higher temps set the fan speed to max. Default: 75'
echo ''
echo ' If no options are provided, the script will run with default values.'
echo ' Defaults have been tested and optimized for the following hardware:'
echo ' - NanoPi-M4 v2'
echo ' - M4 SATA hat'
echo ' - Fan 12V (.08A and .2A)'
echo ' And software:'
echo ' - Kernel: Linux 4.4.231-rk3399'
echo ' - OS: Armbian Buster (20.08.9) stable'
echo ' - GNU bash v5.0.3'
echo ' - bc v1.07.1'
echo ''
echo 'Author: cgomesu'
echo 'Repo: https://github.com/cgomesu/nanopim4-satahat-fan'
echo ''
echo 'This is free. There is NO WARRANTY. Use at your own risk.'
echo ''
}
while getopts 'c:C:d:D:fF:hl:m:p:s:t:T:' OPT; do
case ${OPT} in
c)
CHANNEL="$OPTARG"
if [[ ! $CHANNEL =~ ^pwm[0-9]+$ ]]; then
echo 'The name of the pwm channel must contain pwm and at least a number (pwm0).'
exit 1
fi
;;
C)
PWMCHIP="$OPTARG"
if [[ ! $PWMCHIP =~ ^pwmchip[0-9]+$ ]]; then
echo 'The name of the pwm controller must contain pwmchip and at least a number (pwmchip1).'
exit 1
fi
;;
d)
DC_PERCENT_MIN="$OPTARG"
if [[ ! $DC_PERCENT_MIN =~ ^([0-6][0-9]?|70)$ ]]; then
echo 'The lowest duty cycle threshold must be an integer between 0 and 70.'
exit 1
fi
;;
D)
DC_PERCENT_MAX="$OPTARG"
if [[ ! $DC_PERCENT_MAX =~ ^([8-9][0-9]?|100)$ ]]; then
echo 'The highest duty cycle threshold must be an integer between 80 and 100.'
exit 1
fi
;;
f)
SKIP_THERMAL=1
;;
F)
TIME_STARTUP="$OPTARG"
if [[ ! $TIME_STARTUP =~ ^[0-9]+$ ]]; then
echo 'The time to run the fan at full speed during startup must be an integer.'
exit 1
fi
;;
h)
usage
exit 0
;;
l)
TIME_LOOP="$OPTARG"
if [[ ! $TIME_LOOP =~ ^[0-9]+$ ]]; then
echo 'The time to loop thermal reads must be an integer.'
exit 1
fi
;;
m)
MONIT_DEVICE="$OPTARG"
;;
p)
PERIOD="$OPTARG"
if [[ ! $PERIOD =~ ^[0-9]+$ ]]; then
echo 'The period must be an integer.'
exit 1
fi
;;
s)
TEMPS_SIZE="$OPTARG"
if [[ ! $TEMPS_SIZE =~ ^[0-9]+$ ]]; then
echo 'The max size of the temperature array must be an integer.'
exit 1
fi
;;
t)
THERMAL_ABS_THRESH_LOW="$OPTARG"
if [[ ! $THERMAL_ABS_THRESH_LOW =~ ^[0-4][0-9]?$ ]]; then
echo 'The lowest temperature threshold must be an integer between 0 and 49.'
exit 1
fi
;;
T)
THERMAL_ABS_THRESH_HIGH="$OPTARG"
if [[ ! $THERMAL_ABS_THRESH_HIGH =~ ^([5-9][0-9]?|1[0-1][0-9]?|120)$ ]]; then
echo 'The highest temperature threshold must be an integer between 50 and 120.'
exit 1
fi
;;
\?)
echo '!! ATTENTION !!'
echo '................................'
echo 'Detected an invalid option.'
echo 'Try: '"$0"' -h'
echo '................................'
exit 1
;;
esac
done
start
trap 'interrupt' SIGINT SIGHUP SIGTERM SIGKILL
config
fan_run