Skip to content

Commit 20e1c77

Browse files
authored
Merge pull request #10 from cgomesu/feature/controllers
Rework of the logistic controller and introduction of a PID controller
2 parents 4ad1b9a + 46db15b commit 20e1c77

File tree

1 file changed

+98
-30
lines changed

1 file changed

+98
-30
lines changed

pwm-fan.sh

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,40 @@
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
1616
DEFAULT_PWMCHIP='pwmchip1'
1717
DEFAULT_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
1920
DEFAULT_TIME_LOOP=10
2021
DEFAULT_MONIT_DEVICE='(soc|cpu)'
2122
DEFAULT_TEMPS_SIZE=6
2223
DEFAULT_THERMAL_ABS_THRESH_LOW=25
2324
DEFAULT_THERMAL_ABS_THRESH_HIGH=75
24-
DEFAULT_FAN_POWER_STATE=1
2525
DEFAULT_THERMAL_ABS_THRESH_OFF=0
2626
DEFAULT_THERMAL_ABS_THRESH_ON=1
2727
DEFAULT_DC_PERCENT_MIN=25
2828
DEFAULT_DC_PERCENT_MAX=100
2929
DEFAULT_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
3343
PWMCHIP_ROOT='/sys/class/pwm/'
3444
# path to the thermal dir in the sysfs interface
3545
THERMAL_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.
3848
CACHE_ROOT='/tmp/pwm-fan/'
3949
# required packages and commands to run the script
4050
REQUISITES=('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
239238
function_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+
249307
pwmchip () {
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

Comments
 (0)