Fan control scripts
This page provides several scripts for controlling the ThinkPad's system fan according the its thermal sensors (overriding the embedded controller), in order to reduce fan noise and decrease power consumption.
Contents
Variable speed control scripts
The following scripts sets the fan speed according to the system's thermal sensors. 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. The method of controlling fan speed is documented here.
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
With no further ado, here is the script:
#!/bin/bash # tp-fancontrol 0.2.4 (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 evidence, 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 T41/2 Z60t 43-26xx # min max # ---------- ------- ----- ----- --------------------------- 50 70 # EC 0x78 CPU CPU ? CPU 47 60 # EC 0x79 miniPCI ? HDAPS HDAPS, through EC 43 55 # EC 0x7A HDD ? ? ? 49 68 # EC 0x7B GPU GPU ? GPU 40 50 # EC 0x7C BAT BAT BAT BAT (front left of battery) 45 55 # EC 0x7D n/a n/a n/a n/a 37 47 # EC 0x7E BAT BAT BAT BAT (rear right of battery) 45 55 # EC 0x7F n/a n/a n/a n/a 45 55 # EC 0xC0 ? n/a ? ? 48 60 # EC 0xC1 ? n/a ? Southbridge under miniPCI? 50 65 # EC 0xC2 ? n/a ? Power circuitry (under CDC) 47 60 # HDAPS HDAPS HDAPS HDAPS HDAPS, direct access ) # On T43, the HDAPS sensor under center left edge of miniPCI (top left of touchpad) # On Z60t, it is on the bottom of the motherboard next to the port replicator port. 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=3 # 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).
Fan enable/disable scripts
The following scripts were written before it was known how to control the fan speed, so they only toggle between fan disabled and default (noisy) fan behavior. In some models, they also do not monitor all available thermal sensors.
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:
- It catches various signals and turns the fan on before it quits.
- 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
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.
- Yury Polyanskiy has a kernel patch for automatic fan control in kernelspace (only enable/disable based on maximum temperature).