Difference between revisions of "Fan control scripts"

From ThinkWiki
Jump to: navigation, search
(See also)
(Migrated sensor-specific script from talk page)
Line 128: Line 128:
 
While the above scripts only toggle the fan on and off, the following scripts also sets the fan speed according to sytem temperatures. In addition, they include a hack for preventing the annoying fan pulsing that occurs on some systems. Note that the fan levels, thresholds and anti-pulsing hacks are system-specific, so you may need to adjust them.
 
While the above scripts only toggle the fan on and off, the following scripts also sets the fan speed according to sytem temperatures. In addition, they include a hack for preventing the annoying fan pulsing that occurs on some systems. Note that the fan levels, thresholds and anti-pulsing hacks are system-specific, so you may need to adjust them.
  
===<tt>bash</tt> script with fine control over fan speed (for unpatched kernels)===
+
===<tt>bash</tt> script with fine control over fan speed===
  
 
The following requires only [[ibm-acpi]] 0.11 or higher (e.g., as found in kernel 2.6.14 and higher) with the <tt>experimental=1</tt> module parameter. It supports (optional) daemon mode and logging to syslog.
 
The following requires only [[ibm-acpi]] 0.11 or higher (e.g., as found in kernel 2.6.14 and higher) with the <tt>experimental=1</tt> module parameter. It supports (optional) daemon mode and logging to syslog.
 +
 +
This scripts uses a different temperature range for each sensor, since they have different specs and thermal systems. For each sensor, a fan level is chosen based on the minimum and maximum temperatures configured for that sensor; then the actual fan level is set to the slowest that will satisfy all sensors. There are also some hysteresis features - see the script for the details.
 +
 +
Current options:
 +
 +
Usage: ./tp-fancontrol [OPTION]...
 +
 +
Available options:
 +
    -s N  shift up temperature thresholds by N degrees
 +
          (positive for quieter, negative for cooler)
 +
    -t    test mode
 +
    -q    quiet mode
 +
    -d    daemon mode, go into background (implies -q)
 +
    -l    log to syslog
 +
    -k    kill daemon (ignores all but -p)
 +
    -p    pid file location for daemon mode, default: /var/run/tp-fancontrol.pid
 +
 +
{{WARN|This script relies on undocumented hardware features and overrides nominal hardware behavior. It may thus cause arbitrary damage to your laptop or data. Watch your temperatures!}}
 +
{{WARN|The list of temperature ranges used below is much more liberal than the rules used by the embedded controller firmware, and is derived mostly from anecdotal evidene, hunches and wishful thinking. It is also model-specific.}}
 +
 +
With no further ado, here is the script:
  
 
<pre>
 
<pre>
 
#!/bin/bash
 
#!/bin/bash
 +
 +
# tp-fancontrol 0.2.2 (http://thinkwiki.org/wiki/ACPI_fan_control_script)
 +
# Provided under the GNU General Public License version 2 or later or
 +
# the GNU Free Documentation License version 1.2 or later, at your option.
 +
# See http://www.gnu.org/copyleft/gpl.html for the Warranty Disclaimer.
  
 
# This script dynamically controls fan speed on some ThinkPad models
 
# This script dynamically controls fan speed on some ThinkPad models
Line 140: Line 166:
 
# controller. It also implements a workaround for the fan noise pulse
 
# controller. It also implements a workaround for the fan noise pulse
 
# experienced every few seconds on some ThinkPads.
 
# experienced every few seconds on some ThinkPads.
 +
#
 +
# Run 'tp-fancontrol --help' for options.
 
#
 
#
 
# WARNING: This script relies on undocumented hardware features and
 
# WARNING: This script relies on undocumented hardware features and
Line 145: Line 173:
 
# damage to your laptop or data. Watch your temperatures!
 
# damage to your laptop or data. Watch your temperatures!
 
#
 
#
# This file is placed in the public domain and may be freely distributed.
+
# WARNING: The list of temperature ranges used below is much more liberal
 +
# than the rules used by the embedded controller firmware, and is
 +
# derived mostly from anecdotal evidene, hunches and wishful thinking.
 +
# It is also model-specific.
 +
 
 +
# Temperature ranges, per sensor:
 +
# (min temperature: when to step up from 0-th fan level,
 +
#  max temperature: when to step up to maximum fan level)
 +
THRESHOLDS=( #  Sensor    ThinkPad model
 +
            #            R51    T42  T43/p
 +
# min  max  #  ---------- ------- ---  ----
 +
  50  70    #  EC 0x78    CPU    CPU  CPU
 +
  47  60    #  EC 0x79    miniPCI ?    HDAPS through EC (under center left of miniPCI)
 +
  43  55    #  EC 0x7A    HDD    ?    ?
 +
  49  68    #  EC 0x7B    GPU    GPU  GPU
 +
  37  52    #  EC 0x7C    BAT    BAT  BAT (front left)
 +
  45  55    #  EC 0x7D    n/a    n/a  n/a
 +
  34  45    #  EC 0x7E    BAT    BAT  BAT (rear right)
 +
  45  55    #  EC 0x7F    n/a    n/a  n/a
 +
  45  55    #  EC 0xC0    ?      n/a  ?
 +
  48  60    #  EC 0xC1    ?      n/a  Southbridge under miniPCI?
 +
  47  60    #  EC 0xC2    ?      n/a  ?
 +
  47  60    #  HDAPS      HDAPS  HDAPS HDAPS direct access (under center left of miniPCI)
 +
)
  
 
LEVELS=(    0      2      4      7)  # Fan speed levels
 
LEVELS=(    0      2      4      7)  # Fan speed levels
UP_TEMPS=(      52    60    68  )  # Speed increase trip points
+
ANTIPULSE=( 0     1      0      0)  # Prevent fan pulsing noise at this level
DOWN_TEMPS=( 48    56    64    ) # Speed decrease trip points
+
                                    # (reduces frequency of fan RPM updates)
  
ANTIPULSE=( 0      1      0     0)  # Prevent fan pulsing noise at this level
+
OFF_THRESH_DELTA=2 # when gets this much cooler than 'min' above, may turn off fan
                                    #   (this also prevents fan speed updates)
+
MIN_THRESH_SHIFT=0 # increase min thresholds by this much
 +
MIN_WAIT=120 # minimum time (seconds) to spend in a given level before stepping down
  
 
IBM_ACPI=/proc/acpi/ibm
 
IBM_ACPI=/proc/acpi/ibm
 
HDAPS_TEMP=/sys/bus/platform/drivers/hdaps/hdaps/temp1
 
HDAPS_TEMP=/sys/bus/platform/drivers/hdaps/hdaps/temp1
 +
LOGGER=/usr/bin/logger
 +
INTERVAL=3
 +
SETTLE_TIME=6
 +
RESETTLE_TIME=300
 +
 
PID_FILE=/var/run/tp-fancontrol.pid
 
PID_FILE=/var/run/tp-fancontrol.pid
INTERVAL=3
 
 
VERBOSE=true
 
VERBOSE=true
 
DRY_RUN=false
 
DRY_RUN=false
Line 164: Line 220:
 
KILL_DAEMON=false
 
KILL_DAEMON=false
 
SYSLOG=false
 
SYSLOG=false
LOGGER=/usr/bin/logger
 
  
 
usage() {
 
usage() {
     echo "Usage: $0 [OPTION]..."
+
     echo "
    echo
+
Usage: $0 [OPTION]...
    echo "Available options:"
+
 
    echo "   -t    test mode"
+
Available options:
    echo "  -q    quiet mode"
+
  -s N   shift up temperature thresholds by N degrees
    echo "  -d    daemon mode, go into background (implies -q)"
+
          (positive for quieter, negative for cooler)
    echo "  -l    log to syslog"
+
  -t    test mode
     echo "  -p    pid file location for daemon mode, default: $PID_FILE"
+
  -q    quiet mode
    echo "  -k    kill daemon (ignores all but -p)"
+
  -d    daemon mode, go into background (implies -q)
     exit 1
+
  -l    log to syslog
 +
  -k     kill daemon (ignores all but -p)
 +
  -p    pid file location for daemon mode, default: $PID_FILE
 +
"
 +
     exit 1;
 
}
 
}
  
while getopts 'qtdlp:kh' OPT; do
+
while getopts 's:qtdlp:kh' OPT; do
 
     case "$OPT" in
 
     case "$OPT" in
 +
        s) # shift thresholds
 +
            MIN_THRESH_SHIFT="$OPTARG"
 +
            ;;
 
         t) # test mode
 
         t) # test mode
 
             DRY_RUN=true
 
             DRY_RUN=true
Line 207: Line 269:
 
     esac
 
     esac
 
done
 
done
[[ $OPTIND -gt $# ]] || usage  # no non-option args
+
[ $OPTIND -gt $# ] || usage  # no non-option args
  
 
# no logger found, no syslog capabilities
 
# no logger found, no syslog capabilities
$SYSLOG && [[ ! -x $LOGGER ]] && SYSLOG=false
+
$SYSLOG && [ ! -x $LOGGER ] && SYSLOG=false
  
 
if $DRY_RUN; then
 
if $DRY_RUN; then
Line 219: Line 281:
  
 
thermometer() { # output list of temperatures
 
thermometer() { # output list of temperatures
 +
    # Base temperatures from ibm-acpi:
 +
    [ -r $IBM_ACPI/thermal ] || { echo "$0: Cannot read $IBM_ACPI/thermal" 2>&1 ; exit 1; }
 
     read X Y < $IBM_ACPI/thermal
 
     read X Y < $IBM_ACPI/thermal
     if ! [[ "$X" == "temperatures:" ]]; then
+
     [ "$X" == "temperatures:" ] || { echo "$0: Bad temperatures: $X $Y" >&2exit 1; }
        echo "$0: Bad temperatures: $X $Y" >&2
 
        exit 1
 
    fi
 
 
     echo -n "$Y ";
 
     echo -n "$Y ";
 +
    # Extended temperatures at EC offsets 0xC0 to 0xC2:
 +
    [ -r $IBM_ACPI/ecdump ] || { echo "$0: Cannot read $IBM_ACPI/ecdump" 2>&1; exit 1; }
 +
    perl -e 'm/^EC 0xc0: .(..) .(..) .(..) / and print hex($1)." ".hex($2)." ".hex($3)." " and exit 0 while <>; exit 1' < $IBM_ACPI/ecdump
 +
    # HDAPS temperature (optional):
 
     [ -r $HDAPS_TEMP ] && echo -n "`cat $HDAPS_TEMP` "
 
     [ -r $HDAPS_TEMP ] && echo -n "`cat $HDAPS_TEMP` "
 
     return 0
 
     return 0
Line 235: Line 300:
 
setlevel() { # set fan speed level
 
setlevel() { # set fan speed level
 
     $DRY_RUN || echo 0x2F $1 > $IBM_ACPI/ecdump
 
     $DRY_RUN || echo 0x2F $1 > $IBM_ACPI/ecdump
 +
}
 +
 +
getlevel() { # get fan speed level
 +
    perl -e 'm/^EC 0x20: .* (..)$/ and print $1 and exit 0 while <>; exit 1' < $IBM_ACPI/ecdump
 
}
 
}
  
 
cleanup() { # clean up after work
 
cleanup() { # clean up after work
     $AM_DAEMON && rm -f $PID_FILE 2> /dev/null
+
     $AM_DAEMON && rm -f "$PID_FILE" 2> /dev/null
 
     $SYSLOG && $LOGGER -t "`basename $0`[$$]" \
 
     $SYSLOG && $LOGGER -t "`basename $0`[$$]" \
 
               "Shutting down, switching to automatic fan control"
 
               "Shutting down, switching to automatic fan control"
 
     $DRY_RUN || echo enable > $IBM_ACPI/fan
 
     $DRY_RUN || echo enable > $IBM_ACPI/fan
 +
}
 +
 +
floor_div() {
 +
    echo $(( (($1)+1000*($2))/($2) - 1000 ))
 
}
 
}
  
Line 251: Line 324:
  
 
     IDX=0
 
     IDX=0
 +
    START_TIME=0
 
     MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
 
     MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
     SETTLE=0
+
     SETTLE_LEFT=0
 +
    RESETTLE_LEFT=0
 
     FIRST=true
 
     FIRST=true
 
     $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Starting dynamic fan control"
 
     $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Starting dynamic fan control"
Line 260: Line 335:
 
         TEMPS=`thermometer`
 
         TEMPS=`thermometer`
 
         $VERBOSE && SPEED=`speedometer`
 
         $VERBOSE && SPEED=`speedometer`
   
+
        $VERBOSE && ECLEVEL=`getlevel`
         # Calculate new level
+
        NOW=`date +%s`
         NEWIDX=$IDX
+
 
         DOWN=$(( IDX > 0 ))
+
         # Calculate new level index by placing temperatures into Z-regions:
 +
         # Z >= 2*I means "must be at index I or higher"
 +
        # Z  = 2*I+1 is hysteresis: "don't step down if currently at I+1"
 +
        # hence the Z-regions are, for d=(MAX-MIN)/(2*MAX_IDX-1) :
 +
        #  Z=0:{-infty..MIN-d) Z=1:{MIN-d..MIN) Z=2:{MIN..MIN+d} Z=3:{MIN+d..MIN+2d} ... Z=2*MAX_IDX:{MAX-d, MAX}
 +
 
 +
         MAX_Z=$(( IDX>0 ? ( NOW>START_TIME+MIN_WAIT ? 2*(IDX-1) : 2*IDX ) : 0 ))
 +
        SENSOR=0
 +
        Z_STR="$MAX_Z+"
 +
        TEMP_STR="";
 
         for TEMP in $TEMPS; do
 
         for TEMP in $TEMPS; do
             # Increase speed as much as needed
+
             [ $((2*SENSOR+2)) -le ${#THRESHOLDS[@]} ] ||
            while [[ $NEWIDX -lt $MAX_IDX ]] &&
+
                { echo "Too many sensors, not enough values in THRESHOLDS" 2>&1; exit 1; }
                  [[ $TEMP -ge ${UP_TEMPS[$NEWIDX]} ]]; do
+
            if [ $TEMP == -128 ]; then
                 (( NEWIDX ++ ))
+
                Z='_'; TEMP='_' # inactive sensor
                 DOWN=0
+
            else
            done
+
                MIN=$((THRESHOLDS[SENSOR*2] + MIN_THRESH_SHIFT));
            # Allow decrease (by one index)?
+
                 MAX=$((THRESHOLDS[SENSOR*2+1]))
            if [[ $DOWN == 1 ]] &&
+
                 if (( TEMP < MIN - OFF_THRESH_DELTA )); then
              [[ $TEMP -gt ${DOWN_TEMPS[$(( IDX - 1 ))]} ]]; then
+
                    Z=0
                 DOWN=0
+
                else
 +
                    Z=$(( `floor_div $(( (TEMP-MIN)*(2*MAX_IDX-2) )) $((MAX-MIN))` + 2 ))
 +
                 fi
 +
                [ $MAX_Z -gt $Z ] || MAX_Z=$Z
 
             fi
 
             fi
 +
            Z_STR="${Z_STR}${Z}"
 +
            TEMP_STR="${TEMP_STR}${TEMP} "
 +
            (( ++SENSOR ))
 
         done
 
         done
         if [[ $DOWN == 1 ]]; then
+
         [ $SENSOR -gt 0 ] || { echo "No temperatures read" >&2; exit 1; }
             NEWIDX=$(( IDX - 1 ))
+
 
 +
        (( (MAX_Z == 2*IDX-1) && ++MAX_Z )) # hysteresis
 +
        NEW_IDX=$(( MAX_Z/2 ))
 +
        [ $NEW_IDX -le $MAX_IDX ] || NEW_IDX=$MAX_IDX
 +
 
 +
        # Transition
 +
        $FIRST && OLDLEVEL='?' || OLDLEVEL=${LEVELS[$IDX]}
 +
        NEWLEVEL=${LEVELS[$NEW_IDX]}
 +
        $VERBOSE && echo "L=$OLDLEVEL->$NEWLEVEL EC=$ECLEVEL RPM=`printf %4s $SPEED` T=($TEMP_STR) Z=$Z_STR"
 +
        if [ $OLDLEVEL != $NEWLEVEL ]; then
 +
             START_TIME=$NOW
 +
            $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Changing fan level: $OLDLEVEL->$NEWLEVEL"
 
         fi
 
         fi
   
 
        # Transition
 
        $FIRST && OLDLEVEL=unknown || OLDLEVEL=${LEVELS[$IDX]}
 
        NEWLEVEL=${LEVELS[$NEWIDX]}
 
        $VERBOSE && echo "Temps: $TEMPS  Fan: $SPEED  Level: $OLDLEVEL->$NEWLEVEL"
 
        $SYSLOG && [[ $OLDLEVEL != $NEWLEVEL ]] &&
 
                $LOGGER -t "`basename $0`[$$]" "Changing fan level: $OLDLEVEL->$NEWLEVEL"
 
  
 
         setlevel $NEWLEVEL
 
         setlevel $NEWLEVEL
   
+
 
 
         sleep $INTERVAL
 
         sleep $INTERVAL
   
+
 
         # If needed, apply anti-pulsing hack after a settle-down period:
+
         # If needed, apply anti-pulsing hack after a settle-down period (and occasionally re-settle):
         if [[ ${ANTIPULSE[${NEWIDX}]} == 1 ]]; then
+
         if [ ${ANTIPULSE[${NEW_IDX}]} == 1 ]; then  
             if [[ $NEWLEVEL == $OLDLEVEL ]]; then
+
             if [ $NEWLEVEL != $OLDLEVEL -o $RESETTLE_LEFT -le 0 ]; then # start settling?
                 if [[ $SETTLE -ge 0 ]]; then
+
                SETTLE_LEFT=$SETTLE_TIME
                    (( SETTLE -= INTERVAL ))
+
                 RESETTLE_LEFT=$RESETTLE_TIME
                else
+
            fi
                    setlevel 0x40 # disengaged
+
            if [ $SETTLE_LEFT -ge 0 ]; then
                    sleep 0.5
+
                SETTLE_LEFT=$((SETTLE_LEFT-INTERVAL))
                fi
 
 
             else
 
             else
                 SETTLE=6
+
                 setlevel 0x40 # disengage briefly to fool embedded controller
 +
                sleep 0.5
 +
                RESETTLE_LEFT=$((RESETTLE_LEFT-INTERVAL))
 
             fi
 
             fi
 
         fi
 
         fi
   
+
 
         IDX=$NEWIDX
+
         IDX=$NEW_IDX
FIRST=false
+
        FIRST=false
 
     done
 
     done
 
}
 
}
  
 
if $KILL_DAEMON ; then  
 
if $KILL_DAEMON ; then  
     if [ -f $PID_FILE ]; then
+
     if [ -f "$PID_FILE" ]; then
 
set -e
 
set -e
DPID="`cat $PID_FILE`"  
+
DPID="`cat \"$PID_FILE\"`"  
 
         kill "$DPID"
 
         kill "$DPID"
 
rm "$PID_FILE"
 
rm "$PID_FILE"
Line 332: Line 427:
 
     fi
 
     fi
 
else
 
else
 +
    [ -e "$PID_FILE" ] && echo "WARNING: daemon already running"
 
     control_fan
 
     control_fan
 
fi
 
fi
 
</pre>
 
</pre>
  
The authors of the script ([[User:Thinker|Thinker]] and [[User:Spiney|Spiney]]) disclaim all warranty for this script, and release it to the public domain (meaning you may use it and further distribute it under any terms you wish, including incorporating it into other software).
+
The authors of the script ([[User:Thinker|Thinker]] and [[User:Spiney|Spiney]]) disclaim all warranty for this script, and makes it available the terms of the [http://www.gnu.org/copyleft/gpl.html GPL] version 2 or later, or at your option, the [http://www.gnu.org/copyleft/fdl.html GFDL].
  
 
===<tt>bash</tt> script with fine control over fan speed (requires kernel patch)===
 
===<tt>bash</tt> script with fine control over fan speed (requires kernel patch)===

Revision as of 16:53, 29 November 2005

Fan enable/disable scripts

sh script example

#!/bin/sh

MAXTEMP=50

while [ 1 ];
do
       fan=no

       for temp in `sed s/temperatures:// < /proc/acpi/ibm/thermal`
       do
               test $temp -gt $MAXTEMP && fan=yes
       done

       command='disable'
       test "$fan" = "yes" && command='enable'
       echo $command > /proc/acpi/ibm/fan

       sleep 20
done

sh script with more features

#!/bin/sh

# fan control-script
#
# based upon ibm-acpi 0.11 (experimental=1 !)
#
# eliminates anoying "fan always on" in battery mode
# works with hysteresis (DELTA) so that always-turn-on/turn-off is avoided
# fan acivates at MAXTEMP and cools down CPU, GPU etc. to MAXTEMP-DELTA than the fan is turned off
# furthermore detects if AC is on and gives back fan control to default behaviour than
#
# one can change MAXTEMP and DELTA to individual values
# but take care of your THINKPAD don`t melt it!
#
# have fun!
# mk 05.05.05

MAXTEMP=51
DELTA=4

SWITCHTEMP=$MAXTEMP

#make sure the script doesn't leave the fan off on error
trap "echo enable > /proc/acpi/ibm/fan" EXIT

while [ 1 ];
do
  for ac in `sed s/state:// < /proc/acpi/ac_adapter/AC/state`
    do
     if [ "$ac" = "off-line" ]; then
         fan=no
         for temp in `sed s/temperatures:// < /proc/acpi/ibm/thermal`
           do
             test $temp -gt $SWITCHTEMP && fan=yes
           done

         if [ "$fan" = "yes" ]; then
           command='enable'
           SWITCHTEMP=`expr $MAXTEMP - $DELTA`
         else
           SWITCHTEMP=$MAXTEMP
           command='disable'
         fi

       else # ac-adapter on -> set fan control to standard behaviour
         command='enable'
       fi

       echo $command > /proc/acpi/ibm/fan
       sleep 15
     done 
  done

sh script with extra safety functionality

ibm_acpi usually works well. But to rely on it completely, this script provides some extra safety functionality:

  1. It catches various signals and turns the fan on before it quits.
  2. It turns off the fan under very strict conditions, leaving it on when unexpected errors occur.
#!/bin/sh

# july 2005 Erik Groeneveld, erik@cq2.nl
# It makes sure the fan is on in case of errors
# and only turns it off when all temps are ok.

IBM_ACPI=/proc/acpi/ibm
THERMOMETER=$IBM_ACPI/thermal
FAN=$IBM_ACPI/fan
MAXTRIPPOINT=65
MINTRIPPOINT=60
TRIPPOINT=$MINTRIPPOINT

echo fancontrol: Thermometer: $THERMOMETER, Fan: $FAN
echo fancontrol: Current `cat $THERMOMETER`
echo fancontrol: Controlling temperatures between $MINTRIPPOINT and $MAXTRIPPOINT degrees.

# Make sure the fan is turned on when the script crashes or is killed
trap "echo enable > $FAN; exit 0" HUP KILL INT ABRT STOP QUIT SEGV TERM

while [ 1 ];
do
       command=enable
       temperatures=`sed s/temperatures:// < $THERMOMETER`
       result=
       for temp in $temperatures
       do
               test $temp -le $TRIPPOINT && result=$result.Ok
       done
       if [ "$result" = ".Ok.Ok.Ok.Ok.Ok.Ok.Ok.Ok" ]; then
               command=disable
               TRIPPOINT=$MAXTRIPPOINT
       else
               command=enable
               TRIPPOINT=$MINTRIPPOINT
       fi
       echo $command > $FAN
       # Temperature ramps up quickly, so pick this not too large:
       sleep 5
done

Variable speed control scripts

While the above scripts only toggle the fan on and off, the following scripts also sets the fan speed according to sytem temperatures. In addition, they include a hack for preventing the annoying fan pulsing that occurs on some systems. Note that the fan levels, thresholds and anti-pulsing hacks are system-specific, so you may need to adjust them.

bash script with fine control over fan speed

The following requires only ibm-acpi 0.11 or higher (e.g., as found in kernel 2.6.14 and higher) with the experimental=1 module parameter. It supports (optional) daemon mode and logging to syslog.

This scripts uses a different temperature range for each sensor, since they have different specs and thermal systems. For each sensor, a fan level is chosen based on the minimum and maximum temperatures configured for that sensor; then the actual fan level is set to the slowest that will satisfy all sensors. There are also some hysteresis features - see the script for the details.

Current options:

Usage: ./tp-fancontrol [OPTION]...

Available options:
   -s N   shift up temperature thresholds by N degrees
          (positive for quieter, negative for cooler)
   -t     test mode
   -q     quiet mode
   -d     daemon mode, go into background (implies -q)
   -l     log to syslog
   -k     kill daemon (ignores all but -p)
   -p     pid file location for daemon mode, default: /var/run/tp-fancontrol.pid
ATTENTION!
This script relies on undocumented hardware features and overrides nominal hardware behavior. It may thus cause arbitrary damage to your laptop or data. Watch your temperatures!
ATTENTION!
The list of temperature ranges used below is much more liberal than the rules used by the embedded controller firmware, and is derived mostly from anecdotal evidene, hunches and wishful thinking. It is also model-specific.

With no further ado, here is the script:

#!/bin/bash

# tp-fancontrol 0.2.2 (http://thinkwiki.org/wiki/ACPI_fan_control_script)
# Provided under the GNU General Public License version 2 or later or
# the GNU Free Documentation License version 1.2 or later, at your option.
# See http://www.gnu.org/copyleft/gpl.html for the Warranty Disclaimer.

# This script dynamically controls fan speed on some ThinkPad models
# according to user-defined temperature thresholds.  It implements its
# own decision algorithm, overriding the ThinkPad embedded
# controller. It also implements a workaround for the fan noise pulse
# experienced every few seconds on some ThinkPads.
#
# Run 'tp-fancontrol --help' for options.
#
# WARNING: This script relies on undocumented hardware features and
# overrides nominal hardware behavior. It may thus cause arbitrary
# damage to your laptop or data. Watch your temperatures!
#
# WARNING: The list of temperature ranges used below is much more liberal
# than the rules used by the embedded controller firmware, and is
# derived mostly from anecdotal evidene, hunches and wishful thinking.
# It is also model-specific.

# Temperature ranges, per sensor:
# (min temperature: when to step up from 0-th fan level,
#  max temperature: when to step up to maximum fan level)
THRESHOLDS=( #  Sensor     ThinkPad model
             #             R51     T42   T43/p
# min  max   #  ---------- ------- ---   ----
  50   70    #  EC 0x78    CPU     CPU   CPU
  47   60    #  EC 0x79    miniPCI ?     HDAPS through EC (under center left of miniPCI)
  43   55    #  EC 0x7A    HDD     ?     ?
  49   68    #  EC 0x7B    GPU     GPU   GPU
  37   52    #  EC 0x7C    BAT     BAT   BAT (front left)
  45   55    #  EC 0x7D    n/a     n/a   n/a
  34   45    #  EC 0x7E    BAT     BAT   BAT (rear right)
  45   55    #  EC 0x7F    n/a     n/a   n/a
  45   55    #  EC 0xC0    ?       n/a   ?
  48   60    #  EC 0xC1    ?       n/a   Southbridge under miniPCI?
  47   60    #  EC 0xC2    ?       n/a   ?
  47   60    #  HDAPS      HDAPS   HDAPS HDAPS direct access (under center left of miniPCI)
)

LEVELS=(    0      2      4      7)  # Fan speed levels
ANTIPULSE=( 0      1      0      0)  # Prevent fan pulsing noise at this level
                                     # (reduces frequency of fan RPM updates)

OFF_THRESH_DELTA=2 # when gets this much cooler than 'min' above, may turn off fan
MIN_THRESH_SHIFT=0 # increase min thresholds by this much
MIN_WAIT=120 # minimum time (seconds) to spend in a given level before stepping down

IBM_ACPI=/proc/acpi/ibm
HDAPS_TEMP=/sys/bus/platform/drivers/hdaps/hdaps/temp1
LOGGER=/usr/bin/logger 
INTERVAL=3
SETTLE_TIME=6
RESETTLE_TIME=300

PID_FILE=/var/run/tp-fancontrol.pid
VERBOSE=true
DRY_RUN=false
DAEMONIZE=false
AM_DAEMON=false
KILL_DAEMON=false
SYSLOG=false

usage() {
    echo "
Usage: $0 [OPTION]...

Available options:
   -s N   shift up temperature thresholds by N degrees
          (positive for quieter, negative for cooler)
   -t     test mode
   -q     quiet mode
   -d     daemon mode, go into background (implies -q)
   -l     log to syslog
   -k     kill daemon (ignores all but -p)
   -p     pid file location for daemon mode, default: $PID_FILE
"
    exit 1;
}

while getopts 's:qtdlp:kh' OPT; do
    case "$OPT" in
        s) # shift thresholds
            MIN_THRESH_SHIFT="$OPTARG"
            ;;
        t) # test mode
            DRY_RUN=true
            ;;
        q) # quiet mode
            VERBOSE=false
            ;;
        d) # go into background and daemonize
            DAEMONIZE=true
            ;;
        l) # log to syslog
            SYSLOG=true
            ;;
        p) # different pidfile
            PID_FILE="$OPTARG"
            ;;
        k) # kill daemon
            KILL_DAEMON=true
            ;;
        h) # short help
            usage
            ;;
        \?) # error
            usage
            ;;
    esac
done
[ $OPTIND -gt $# ] || usage  # no non-option args

# no logger found, no syslog capabilities
$SYSLOG && [ ! -x $LOGGER ] && SYSLOG=false

if $DRY_RUN; then
    echo "$0: Dry run, will not change fan state."
    VERBOSE=true
    DAEMONIZE=false
fi

thermometer() { # output list of temperatures
    # Base temperatures from ibm-acpi:
    [ -r $IBM_ACPI/thermal ] || { echo "$0: Cannot read $IBM_ACPI/thermal" 2>&1 ; exit 1; }
    read X Y < $IBM_ACPI/thermal
    [ "$X" == "temperatures:" ] || { echo "$0: Bad temperatures: $X $Y" >&2;  exit 1; }
    echo -n "$Y ";
    # Extended temperatures at EC offsets 0xC0 to 0xC2:
    [ -r $IBM_ACPI/ecdump ] || { echo "$0: Cannot read $IBM_ACPI/ecdump" 2>&1; exit 1; }
    perl -e 'm/^EC 0xc0: .(..) .(..) .(..) / and print hex($1)." ".hex($2)." ".hex($3)." " and exit 0 while <>; exit 1' < $IBM_ACPI/ecdump
    # HDAPS temperature (optional):
    [ -r $HDAPS_TEMP ] && echo -n "`cat $HDAPS_TEMP` "
    return 0
}

speedometer() { # output fan speed RPM
    sed -n 's/^speed:[ \t]*//p' $IBM_ACPI/fan
}

setlevel() { # set fan speed level
    $DRY_RUN || echo 0x2F $1 > $IBM_ACPI/ecdump
}

getlevel() { # get fan speed level
    perl -e 'm/^EC 0x20: .* (..)$/ and print $1 and exit 0 while <>; exit 1' < $IBM_ACPI/ecdump
}

cleanup() { # clean up after work
    $AM_DAEMON && rm -f "$PID_FILE" 2> /dev/null
    $SYSLOG && $LOGGER -t "`basename $0`[$$]" \
               "Shutting down, switching to automatic fan control"
    $DRY_RUN || echo enable > $IBM_ACPI/fan
}

floor_div() {
    echo $(( (($1)+1000*($2))/($2) - 1000 ))
}

control_fan() {
    # Enable the fan in default mode if anything goes wrong:
    set -e -E -u
    trap "cleanup; exit 2" HUP INT ABRT QUIT SEGV TERM
    trap "cleanup" EXIT

    IDX=0
    START_TIME=0
    MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
    SETTLE_LEFT=0
    RESETTLE_LEFT=0
    FIRST=true
    $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Starting dynamic fan control"

    # Control loop:
    while true; do
        TEMPS=`thermometer`
        $VERBOSE && SPEED=`speedometer`
        $VERBOSE && ECLEVEL=`getlevel`
        NOW=`date +%s`

        # Calculate new level index by placing temperatures into Z-regions:
        # Z >= 2*I means "must be at index I or higher"
        # Z  = 2*I+1 is hysteresis: "don't step down if currently at I+1"
        # hence the Z-regions are, for d=(MAX-MIN)/(2*MAX_IDX-1) :
        #   Z=0:{-infty..MIN-d) Z=1:{MIN-d..MIN) Z=2:{MIN..MIN+d} Z=3:{MIN+d..MIN+2d} ... Z=2*MAX_IDX:{MAX-d, MAX}

        MAX_Z=$(( IDX>0 ? ( NOW>START_TIME+MIN_WAIT ? 2*(IDX-1) : 2*IDX ) : 0 ))
        SENSOR=0
        Z_STR="$MAX_Z+"
        TEMP_STR="";
        for TEMP in $TEMPS; do
            [ $((2*SENSOR+2)) -le ${#THRESHOLDS[@]} ] ||
                { echo "Too many sensors, not enough values in THRESHOLDS" 2>&1; exit 1; }
            if [ $TEMP == -128 ]; then
                Z='_'; TEMP='_' # inactive sensor
            else
                MIN=$((THRESHOLDS[SENSOR*2] + MIN_THRESH_SHIFT));
                MAX=$((THRESHOLDS[SENSOR*2+1]))
                if (( TEMP < MIN - OFF_THRESH_DELTA )); then
                    Z=0
                else
                    Z=$(( `floor_div $(( (TEMP-MIN)*(2*MAX_IDX-2) )) $((MAX-MIN))` + 2 ))
                fi
                [ $MAX_Z -gt $Z ] || MAX_Z=$Z
            fi
            Z_STR="${Z_STR}${Z}"
            TEMP_STR="${TEMP_STR}${TEMP} "
            (( ++SENSOR ))
        done
        [ $SENSOR -gt 0 ] || { echo "No temperatures read" >&2; exit 1; }

        (( (MAX_Z == 2*IDX-1) && ++MAX_Z )) # hysteresis
        NEW_IDX=$(( MAX_Z/2 ))
        [ $NEW_IDX -le $MAX_IDX ] || NEW_IDX=$MAX_IDX

        # Transition
        $FIRST && OLDLEVEL='?' || OLDLEVEL=${LEVELS[$IDX]}
        NEWLEVEL=${LEVELS[$NEW_IDX]}
        $VERBOSE && echo "L=$OLDLEVEL->$NEWLEVEL EC=$ECLEVEL RPM=`printf %4s $SPEED` T=($TEMP_STR) Z=$Z_STR"
        if [ $OLDLEVEL != $NEWLEVEL ]; then
            START_TIME=$NOW
            $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Changing fan level: $OLDLEVEL->$NEWLEVEL"
        fi

        setlevel $NEWLEVEL

        sleep $INTERVAL

        # If needed, apply anti-pulsing hack after a settle-down period (and occasionally re-settle):
        if [ ${ANTIPULSE[${NEW_IDX}]} == 1 ]; then 
            if [ $NEWLEVEL != $OLDLEVEL -o $RESETTLE_LEFT -le 0 ]; then # start settling?
                SETTLE_LEFT=$SETTLE_TIME
                RESETTLE_LEFT=$RESETTLE_TIME
            fi
            if [ $SETTLE_LEFT -ge 0 ]; then
                SETTLE_LEFT=$((SETTLE_LEFT-INTERVAL))
            else
                setlevel 0x40 # disengage briefly to fool embedded controller
                sleep 0.5
                RESETTLE_LEFT=$((RESETTLE_LEFT-INTERVAL))
            fi
        fi

        IDX=$NEW_IDX
        FIRST=false
    done
}

if $KILL_DAEMON ; then 
    if [ -f "$PID_FILE" ]; then
	set -e
	DPID="`cat \"$PID_FILE\"`" 
        kill "$DPID"
	rm "$PID_FILE"
	$VERBOSE && echo "Killed process $DPID"
    else
        $VERBOSE && echo "Daemon not running."
        exit 1
    fi
elif $DAEMONIZE ; then
    if [ -e "$PID_FILE" ]; then
        echo "$0: File $PID_FILE already exists, refusing to run."
        exit 1
    else
	AM_DAEMON=true VERBOSE=false control_fan 0<&- 1>&- 2>&- &
        echo $! > "$PID_FILE"
        exit 0
    fi
else
    [ -e "$PID_FILE" ] && echo "WARNING: daemon already running"
    control_fan
fi

The authors of the script (Thinker and Spiney) disclaim all warranty for this script, and makes it available the terms of the GPL version 2 or later, or at your option, the GFDL.

bash script with fine control over fan speed (requires kernel patch)

The following is a simpler patch (without extra features like daemon mode and logging). It requires the patch for controlling fan speed.

#!/bin/bash

# This script dynamically controls fan speed on some ThinkPad models
# according to user-defined temperature thresholds.  It implements its
# own decision algorithm, overriding the ThinkPad embedded
# controller. It also implements a workaround for the fan noise pulse
# experienced every few seconds on some ThinkPads.
#
# The script requires the ibm_acpi patch at 
# http://thinkwiki.org/wiki/Patch_for_controlling_fan_speed
#
# WARNING: This script relies on undocumented hardware features and
# overrides nominal hardware behavior. It may thus cause arbitrary
# damage to your laptop or data. Watch your temperatures!
#
# This file is placed in the public domain and may be freely distributed.

LEVELS=(    0      2      4      7)  # Fan speed levels
UP_TEMPS=(      52     60     68  )  # Speed increase trip points
DOWN_TEMPS=(  48     56     64    )  # Speed decrease trip points

ANTIPULSE=( 0      1      0      0)  # Prevent fan pulsing noise at this level
                                     #   (this also prevents fan speed updates)

IBM_ACPI=/proc/acpi/ibm
FAN=$IBM_ACPI/fan
INTERVAL=3
VERBOSE=true
DRY_RUN=false

[[ "$1" == "-t" ]] && { DRY_RUN=true; echo "$0: Dry run, will not change fan state."; }

# Enable the fan in default mode if anything goes wrong:
set -e -E -u
$DRY_RUN || trap "echo enable > $FAN; exit 0" EXIT HUP INT ABRT QUIT SEGV TERM


thermometer() { # output list of temperatures
    read X Y < $IBM_ACPI/thermal
    [[ "$X" == "temperatures:" ]] || { 
	echo "$0: Bad temperatures: $X $Y" >&2 
	exit 1
    }
    echo "$Y"; 
}

speedometer() { # output fan speed
    cat $FAN | sed '/^speed/!d; s/speed:[ \t]*//'
}

IDX=0
MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
SETTLE=0

while true; do
    TEMPS=`thermometer`
    $VERBOSE && SPEED=`speedometer`

    # Calculate new level
    NEWIDX=$IDX
    DOWN=$(( IDX > 0 ))
    for TEMP in $TEMPS; do
        # Increase speed as much as needed
        while [[ $NEWIDX -lt $MAX_IDX ]] && 
              [[ $TEMP -ge ${UP_TEMPS[$NEWIDX]} ]]; do
            (( NEWIDX ++ ))
            DOWN=0
        done
        # Allow decrease (by one index)?
        if [[ $DOWN == 1 ]] && 
           [[ $TEMP -gt ${DOWN_TEMPS[$(( IDX - 1 ))]} ]]; then
            DOWN=0
        fi
    done
    if [[ $DOWN == 1 ]]; then
        NEWIDX=$(( IDX - 1 ))
    fi

    # Transition
    OLDLEVEL=${LEVELS[$IDX]}
    NEWLEVEL=${LEVELS[$NEWIDX]}
    $VERBOSE && echo "tpfan: Temps: $TEMPS   Fan: $SPEED   Level: $OLDLEVEL->$NEWLEVEL"
    $DRY_RUN || echo level $NEWLEVEL > $FAN

    sleep $INTERVAL

    # If needed, apply anti-pulsing hack after a settle-down period:
    if [[ ${ANTIPULSE[${NEWIDX}]} == 1 ]]; then
	if [[ $NEWLEVEL == $OLDLEVEL ]]; then
	    if [[ $SETTLE -ge 0 ]]; then
		(( SETTLE -= INTERVAL ))
	    else
		$DRY_RUN || echo level disengaged >> $FAN
		sleep 0.5
	    fi
	else
	    SETTLE=6
	fi
    fi

    IDX=$NEWIDX
done

The author of the script disclaims all warranty for this script, and releases it to the public domain (meaning you may use it and further distribute it under any terms you wish, including incorporating it into other software).

Init scripts

Init script example

#! /bin/sh

N=/etc/init.d/fan

set -e

case "$1" in
 start)
       # make sure privileges don't persist across reboots
       if [ -d /var/run/fan ] && [ "x`ls /var/run/fan`" != x ]
       then
               touch -t 198501010000 /var/run/fan/*
       fi
       fan.sh &    # Script from above
       ;;
 stop|reload|restart|force-reload)
       killall fan.sh
       echo enable > /proc/acpi/ibm/fan
       ;;
 *)
       echo "Usage: $N {start|stop|restart|force-reload}" >&2
       exit 1
       ;;
esac

exit 0


Init script example for gentoo

Assume one of the above control scripts is /usr/sbin/ibm-fancontrold, for gentoo use the following init script in /etc/init.d/ibm-fancontrol. Copy the script to /etc/init.d/ibm-fancontrol, then do

# rc-update add ibm-fancontrol default

This will add the init script to the default runlevel.


#!/sbin/runscript
# 2005 Gilbert Tiefengruber
# Distributed under the terms of the GNU General Public License v2 
# IBM Fancontrol init script for IBM Thinkpad laptops (tested with R50)
# This init script was written for gentoo 2005.1, kernel 2.6.12
# You need the ibm_acpi kernel module version 0.11 or greater
# load the module with experimental=1 to enable the fan controls

depend() {
        need localmount
}
checkconfig() {
        if [ ! -e /proc/acpi/ibm/fan ]; then
                eerror "The ibm_acpi module must be loaded with (experimental=1)"
                return 1
        fi
} 
start() {
        checkconfig || return 1
        ebegin "Starting ibm-fancontrold"
        start-stop-daemon --quiet -p /var/run/ibm-fancontrold.pid -m -b --start -a /usr/sbin/ibm-fancontrold
        eend ${?}
} 
stop() {
        ebegin "Stopping ibm-fancontrold"
        start-stop-daemon --stop --quiet -p /var/run/ibm-fancontrold.pid
        eend ${?}
}

Other

fanctrld

fanctrld is a daemon (written in C) that controls the Thinkpad's fan. The basic approach is to monitor both temperature and fan speed. The fan is enabled when a certain temperature is exceeded, and disabled when the BIOS slows down the fan below a certain speed.

See also

  • Shimodax's ThinkPad fan control tool for a Windows offers functionality similar to these scripts; see the forum discussion at thinkpads.com.