From 8258a0ee36c29bf8223fcb08282290ca198e75e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Pe=C4=8Dovnik?= Date: Sun, 31 Oct 2021 12:12:06 +0100 Subject: [PATCH] Implement fan controller for Nanopi M4 (#3231) * Implement fan controller for Nanopi M2V2 * Fan works on all Nanopi M4 --- .../families/include/rockchip64_common.inc | 14 + .../bsp/nanopim4/nanopim4-pwm-fan.service | 16 + packages/bsp/nanopim4/nanopim4-pwm-fan.sh | 488 ++++++++++++++++++ 3 files changed, 518 insertions(+) create mode 100644 packages/bsp/nanopim4/nanopim4-pwm-fan.service create mode 100644 packages/bsp/nanopim4/nanopim4-pwm-fan.sh diff --git a/config/sources/families/include/rockchip64_common.inc b/config/sources/families/include/rockchip64_common.inc index 0e1e74ef4..07e5828eb 100644 --- a/config/sources/families/include/rockchip64_common.inc +++ b/config/sources/families/include/rockchip64_common.inc @@ -247,6 +247,12 @@ family_tweaks() fi + if [[ $BOARD == nanopim4* ]]; then + + # enable fan support + chroot $SDCARD /bin/bash -c "systemctl --no-reload enable nanopim4-pwm-fan.service >/dev/null 2>&1" + + fi if [[ $BOARD == pinebook-pro ]]; then @@ -297,6 +303,14 @@ family_tweaks_bsp() fi fi + if [[ $BOARD == nanopim4* ]]; then + + # Fan support + cp $SRC/packages/bsp/nanopim4/nanopim4-pwm-fan.service $destination/lib/systemd/system/ + install -m 755 $SRC/packages/bsp/nanopim4/nanopim4-pwm-fan.sh $destination/usr/bin/nanopim4-pwm-fan.sh + + fi + if [[ $BOARD == helios64 ]]; then mkdir -p $destination/etc/udev/rules.d/ diff --git a/packages/bsp/nanopim4/nanopim4-pwm-fan.service b/packages/bsp/nanopim4/nanopim4-pwm-fan.service new file mode 100644 index 000000000..449eabca6 --- /dev/null +++ b/packages/bsp/nanopim4/nanopim4-pwm-fan.service @@ -0,0 +1,16 @@ +[Unit] +Description=Fan control for the NanoPi M4 + +[Service] +Type=simple + +# Edit the directory below if you placed the Exec scripts elsewhere +ExecStart=/bin/bash /usr/bin/nanopim4-pwm-fan.sh +ExecStop=/bin/kill -2 $MAINPID + +# Restart options +Restart=on-failure +RestartSec=15 + +[Install] +WantedBy=multi-user.target diff --git a/packages/bsp/nanopim4/nanopim4-pwm-fan.sh b/packages/bsp/nanopim4/nanopim4-pwm-fan.sh new file mode 100644 index 000000000..6040a3109 --- /dev/null +++ b/packages/bsp/nanopim4/nanopim4-pwm-fan.sh @@ -0,0 +1,488 @@ +#!/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