1111# This is free. There is NO WARRANTY. Use at your own risk.
1212# ##############################################################################
1313
14- # DEFAULT_ variables are used in substitutions in case of missing the non-default variable
14+ # DEFAULT_ variables are used in substitutions in case of missing the non-default variable.
1515# DEFAULT_ variables can be overriden by corresponding CLI args
1616DEFAULT_PWMCHIP=' pwmchip1'
1717DEFAULT_CHANNEL=' pwm0'
18- DEFAULT_TIME_STARTUP=60
18+ # setting the startup time to low values might affect the reliability of thermal controllers
19+ DEFAULT_TIME_STARTUP=120
1920DEFAULT_TIME_LOOP=10
2021DEFAULT_MONIT_DEVICE=' (soc|cpu)'
2122DEFAULT_TEMPS_SIZE=6
2223DEFAULT_THERMAL_ABS_THRESH_LOW=25
2324DEFAULT_THERMAL_ABS_THRESH_HIGH=75
24- DEFAULT_FAN_POWER_STATE=1
2525DEFAULT_THERMAL_ABS_THRESH_OFF=0
2626DEFAULT_THERMAL_ABS_THRESH_ON=1
2727DEFAULT_DC_PERCENT_MIN=25
2828DEFAULT_DC_PERCENT_MAX=100
2929DEFAULT_PERIOD=25000000
30- # critical temperature for the logistic model. used to compute deviance from the mean over time.
31- CRITICAL_TEMP=75
30+ DEFAULT_THERMAL_CONTROLLER=' logistic'
31+ # tunnable controller parameters
32+ LOGISTIC_TEMP_CRITICAL=75
33+ LOGISTIC_a=1
34+ LOGISTIC_b=10
35+ # uncomment and edit PID_THERMAL_IDEAL to set a fixed IDEAL temperature for the PID controller;
36+ # otherwise, the initial temperature after startup (+2°C) is used as reference.
37+ # PID_THERMAL_IDEAL=45
38+ # https://en.wikipedia.org/wiki/PID_controller#Loop_tuning
39+ PID_Kp=$(( DEFAULT_PERIOD/ 100 ))
40+ PID_Ki=$(( DEFAULT_PERIOD/ 1000 ))
41+ PID_Kd=$(( DEFAULT_PERIOD/ 50 ))
3242# path to the pwm dir in the sysfs interface
3343PWMCHIP_ROOT=' /sys/class/pwm/'
3444# path to the thermal dir in the sysfs interface
3545THERMAL_ROOT=' /sys/class/thermal/'
3646# dir where temp files are stored. default caches to memory. be careful were you point this to
37- # because cache () will delete the directory.
47+ # because cleanup () will delete the directory.
3848CACHE_ROOT=' /tmp/pwm-fan/'
3949# required packages and commands to run the script
4050REQUISITES=(' bc' ' cat' ' echo' ' mkdir' ' touch' ' trap' ' sleep' )
@@ -182,37 +192,25 @@ fan_run_thermal () {
182192 TEMPS=()
183193 while true ; do
184194 TEMPS+=(" $( thermal_meter) " )
195+ # keep the array size lower or equal to TEMPS_SIZE
185196 if [[ " ${# TEMPS[@]} " -gt " ${TEMPS_SIZE:- $DEFAULT_TEMPS_SIZE } " ]]; then
186197 TEMPS=(" ${TEMPS[@]: 1} " )
187198 fi
199+ # determine if the fan should be OFF or ON
188200 if [[ " ${TEMPS[-1]} " -le " ${THERMAL_ABS_THRESH_OFF:- $DEFAULT_THERMAL_ABS_THRESH_OFF } " ]]; then
189201 echo " 0" 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
190- FAN_POWER_STATE=0
191202 elif [[ " ${TEMPS[-1]} " -ge " ${THERMAL_ABS_THRESH_ON:- $DEFAULT_THERMAL_ABS_THRESH_ON } " ]]; then
192- FAN_POWER_STATE=1
193- fi
194- if [[ " ${FAN_POWER_STATE:- $DEFAULT_FAN_POWER_STATE } " -eq 1 ]]; then
203+ # only use a controller when within lower and upper thermal thresholds
195204 if [[ " ${TEMPS[-1]} " -le " ${THERMAL_ABS_THRESH[0]} " ]]; then
196205 echo " ${DC_ABS_THRESH[0]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
197206 elif [[ " ${TEMPS[-1]} " -ge " ${THERMAL_ABS_THRESH[-1]} " ]]; then
198207 echo " ${DC_ABS_THRESH[-1]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
208+ # the thermal array must be greater than one to use any controller, so skip the first iteration
199209 elif [[ " ${# TEMPS[@]} " -gt 1 ]]; then
200- TEMPS_SUM=0
201- for TEMP in " ${TEMPS[@]} " ; do
202- (( TEMPS_SUM+= TEMP ))
203- done
204- # moving mid-point
205- MEAN_TEMP=" $(( TEMPS_SUM/ ${# TEMPS[@]} )) "
206- DEV_MEAN_CRITICAL=" $(( MEAN_TEMP- CRITICAL_TEMP)) "
207- X0=" ${DEV_MEAN_CRITICAL# -} "
208- # args: x, x0, L, a, b (k=a/b)
209- MODEL=$( function_logistic " ${TEMPS[-1]} " " $X0 " " ${DC_ABS_THRESH[-1]} " 1 10)
210- if [[ " $MODEL " -lt " ${DC_ABS_THRESH[0]} " ]]; then
211- echo " ${DC_ABS_THRESH[0]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
212- elif [[ " $MODEL " -gt " ${DC_ABS_THRESH[-1]} " ]]; then
213- echo " ${DC_ABS_THRESH[-1]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
214- else
215- echo " $MODEL " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
210+ if [[ " ${THERMAL_CONTROLLER:- $DEFAULT_THERMAL_CONTROLLER } " == " logistic" ]]; then
211+ controller_logistic
212+ elif [[ " ${THERMAL_CONTROLLER:- $DEFAULT_THERMAL_CONTROLLER } " == " pid" ]]; then
213+ controller_pid
216214 fi
217215 fi
218216 fi
@@ -236,6 +234,7 @@ fan_startup () {
236234 done
237235}
238236
237+ # takes 'x' 'x0' 'L' 'a' 'b' as arguments
239238function_logistic () {
240239 # https://en.wikipedia.org/wiki/Logistic_function
241240 # k=a/b
@@ -246,6 +245,65 @@ function_logistic () {
246245 echo " $result "
247246}
248247
248+ # logic for the logistic controller
249+ controller_logistic () {
250+ local temp temps_sum mean_temp dev_mean_critical x0 model
251+ temps_sum=0
252+ for temp in " ${TEMPS[@]} " ; do
253+ (( temps_sum+= temp))
254+ done
255+ # moving mid-point
256+ mean_temp=" $(( temps_sum/ ${# TEMPS[@]} )) "
257+ dev_mean_critical=" $(( mean_temp- LOGISTIC_TEMP_CRITICAL)) "
258+ x0=" ${dev_mean_critical# -} "
259+ # function_logistic args: 'x' 'x0' 'L' 'a' 'b'
260+ # the model is adjusted to ns and bound to the upper (raw) DC threshold value because of L="${DC_ABS_THRESH[-1]}"
261+ model=$( function_logistic " ${TEMPS[-1]} " " $x0 " " ${DC_ABS_THRESH[-1]} " " $LOGISTIC_a " " $LOGISTIC_b " )
262+ # bound to duty cycle thresholds first in case model-based value is outside the valid range
263+ if [[ " $model " -lt " ${DC_ABS_THRESH[0]} " ]]; then
264+ echo " ${DC_ABS_THRESH[0]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
265+ elif [[ " $model " -gt " ${DC_ABS_THRESH[-1]} " ]]; then
266+ echo " ${DC_ABS_THRESH[-1]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
267+ else
268+ echo " $model " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
269+ fi
270+ }
271+
272+ # takes 'p_error' 'i_error' 'd_error' 'Kp' 'Ki' 'Kd' as arguments
273+ function_pid () {
274+ # https://en.wikipedia.org/wiki/PID_controller
275+ local p_e i_e d_e Kp Ki Kd equation result
276+ p_e=" $1 " ; i_e=" $2 " ; d_e=" $3 " ; Kp=" $4 " ; Ki=" $5 " ; Kd=" $6 "
277+ equation=" output=($Kp *$p_e )+($Ki *$i_e )+($Kd *$d_e )"
278+ result=$( echo " scale=4;$equation ;scale=0;output/1" | bc -lq 2> /dev/null)
279+ echo " $result "
280+ }
281+
282+ # logic for the PID controller
283+ controller_pid () {
284+ # i_error cannot be local to be cumulative since it was first declared.
285+ local p_error d_error model duty_cycle
286+ p_error=" $(( ${TEMPS[-1]} - ${PID_THERMAL_IDEAL:- $((THERMAL_INITIAL+2))} )) "
287+ i_error=" $(( ${i_error:- 0} + p_error)) "
288+ d_error=" $(( ${TEMPS[-1]} - ${TEMPS[-2]} )) "
289+ # TODO: Kp, Ki, and Kd could be auto tunned here; currently, they are not declared and PID_ vars are used.
290+ # function_pid args: 'p_error' 'i_error' 'd_error' 'Kp' 'Ki' 'Kd'
291+ model=" $( function_pid " $p_error " " $i_error " " $d_error " " ${Kp:- $PID_Kp } " " ${Ki:- $PID_Ki } " " ${Kd:- $PID_Kd } " ) "
292+ duty_cycle=" $( cat " $CHANNEL_FOLDER " ' duty_cycle' ) "
293+ # bound to duty cycle thresholds first in case model-based value is outside the valid range
294+ if [[ $(( duty_cycle+ model)) -lt " ${DC_ABS_THRESH[0]} " ]]; then
295+ echo " ${DC_ABS_THRESH[0]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
296+ # reset i_error to prevent from acumulating further
297+ i_error=0
298+ elif [[ $(( duty_cycle+ model)) -gt " ${DC_ABS_THRESH[-1]} " ]]; then
299+ echo " ${DC_ABS_THRESH[-1]} " 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
300+ # reset i_error to prevent from acumulating further
301+ i_error=0
302+ else
303+ echo $(( duty_cycle+ model)) 2> /dev/null > " $CHANNEL_FOLDER " ' duty_cycle'
304+ fi
305+ }
306+
249307pwmchip () {
250308 PWMCHIP_FOLDER=" $PWMCHIP_ROOT ${PWMCHIP:- $DEFAULT_PWMCHIP } " ' /'
251309 if [[ ! -d " $PWMCHIP_FOLDER " ]]; then
@@ -305,8 +363,9 @@ thermal_monit () {
305363 for dir in " $THERMAL_ROOT " ' thermal_zone' * ; do
306364 if [[ $( cat " $dir " ' /type' ) =~ ${MONIT_DEVICE:- $DEFAULT_MONIT_DEVICE } && -f " $dir " ' /temp' ]]; then
307365 TEMP_FILE=" $dir " ' /temp'
366+ THERMAL_INITIAL=" $( thermal_meter) "
308367 message " Found the '${MONIT_DEVICE:- $DEFAULT_MONIT_DEVICE } ' temperature at '$TEMP_FILE '." ' INFO'
309- message " Current '${MONIT_DEVICE:- $DEFAULT_MONIT_DEVICE } ' temp is: $(( $(thermal_meter )) ) Celsius" ' INFO'
368+ message " Current '${MONIT_DEVICE:- $DEFAULT_MONIT_DEVICE } ' temp is: $THERMAL_INITIAL Celsius" ' INFO'
310369 message " Setting fan to monitor the '${MONIT_DEVICE:- $DEFAULT_MONIT_DEVICE } ' temperature." ' INFO'
311370 THERMAL_STATUS=1
312371 return
@@ -353,6 +412,7 @@ usage() {
353412 echo ' -h Show this HELP message.'
354413 echo ' -l int TIME (in seconds) to LOOP thermal reads. Lower means higher resolution but uses ever more resources. Default: 10'
355414 echo ' -m str Name of the DEVICE to MONITOR the temperature in the thermal sysfs interface. Default: (soc|cpu)'
415+ echo ' -o str Name of the THERMAL CONTROLLER. Options: logistic (default), pid.'
356416 echo ' -p int The fan PERIOD (in nanoseconds). Default (25kHz): 25000000.'
357417 echo ' -s int The MAX SIZE of the TEMPERATURE ARRAY. Interval between data points is set by -l. Default (store last 1min data): 6.'
358418 echo ' -t int Lowest TEMPERATURE threshold (in Celsius). Lower temps set the fan speed to min. Default: 25'
@@ -381,7 +441,7 @@ usage() {
381441
382442# ###########
383443# main logic
384- while getopts ' c:C:d:D:fF:hl:m:p:s:t:T:u:U:' OPT; do
444+ while getopts ' c:C:d:D:fF:hl:m:o: p:s:t:T:u:U:' OPT; do
385445 case ${OPT} in
386446 c)
387447 CHANNEL=" $OPTARG "
@@ -441,6 +501,14 @@ while getopts 'c:C:d:D:fF:hl:m:p:s:t:T:u:U:' OPT; do
441501 m)
442502 MONIT_DEVICE=" $OPTARG "
443503 ;;
504+ o)
505+ THERMAL_CONTROLLER=" $OPTARG "
506+ if [[ ! " $THERMAL_CONTROLLER " =~ ^(logistic| pid)$ ]]; then
507+ message " The value for the '-o' argument ($THERMAL_CONTROLLER ) is invalid." ' ERROR'
508+ message " The thermal controller must be either 'logistic' or 'pid'." ' ERROR'
509+ exit 1
510+ fi
511+ ;;
444512 p)
445513 PERIOD=" $OPTARG "
446514 if [[ ! " $PERIOD " =~ ^[0-9]+$ ]]; then
@@ -451,9 +519,9 @@ while getopts 'c:C:d:D:fF:hl:m:p:s:t:T:u:U:' OPT; do
451519 ;;
452520 s)
453521 TEMPS_SIZE=" $OPTARG "
454- if [[ ! " $TEMPS_SIZE " =~ ^[0-9]+$ ]]; then
522+ if [[ " $TEMPS_SIZE " -le 1 || ! " $TEMPS_SIZE " =~ ^[0-9]+$ ]]; then
455523 message " The value for the '-s' argument ($TEMPS_SIZE ) is invalid." ' ERROR'
456- message ' The max size of the temperature array must be an integer.' ' ERROR'
524+ message ' The max size of the temperature array must be an integer greater than 1 .' ' ERROR'
457525 exit 1
458526 fi
459527 ;;
0 commit comments