Code/tp-fancontrol
#!/bin/bash # tp-fancontrol 0.2.9 (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. # # For optimal fan behavior during suspend and resume, invoke # "tp-fancontrol -u" during the suspend process. # # 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 (see http://thinkwiki.org/wiki/Thermal_sensors). # 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 ? ? Between CPU and PCMCIA slot 43 55 # EC 0x7A HDD ? ? PCMCIA slot 49 68 # EC 0x7B GPU GPU ? GPU 40 50 # EC 0x7C BAT BAT BAT Sys BAT (front left of battery) 45 55 # EC 0x7D n/a n/a n/a UltraBay BAT 37 47 # EC 0x7E BAT BAT BAT Sys BAT (rear right of battery) 45 55 # EC 0x7F n/a n/a n/a UltraBay BAT 45 60 # EC 0xC0 ? n/a ? Between northbridge and DRAM 48 62 # EC 0xC1 ? n/a ? Southbridge (under miniPCI) 50 65 # EC 0xC2 ? n/a ? Power circuitry (under CDC) 47 60 # HDAPS HDAPS HDAPS HDAPS HDAPS readout (same as EC 0x79) ) LEVELS=( 0 2 4 7) # Fan speed levels ANTIPULSE=( 0 1 1 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=180 # 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 # sample+refresh interval SETTLE_TIME=6 # wait this many seconds long before applying anti-pulsing RESETTLE_TIME=600 # briefly disable anti-pulsing at every N seconds SUSPEND_TIME=5 # seconds to sleep when receiving SIGUSR1 PID_FILE=/var/run/tp-fancontrol.pid QUIET=false DRY_RUN=false DAEMONIZE=false AM_DAEMON=false KILL_DAEMON=false SUSPEND_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 already-running daemon -u tell already-running daemon that the system is being suspended -p pid file location for daemon mode, default: $PID_FILE " exit 1; } while getopts 's:qtdlp:kuh' OPT; do case "$OPT" in s) # shift thresholds MIN_THRESH_SHIFT="$OPTARG" ;; t) # test mode DRY_RUN=true ;; q) # quiet mode QUIET=true ;; 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 ;; u) # suspend daemon SUSPEND_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." QUIET=false 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 Y1 Y2 Y3 Y4 Y5 Y6 Y7 Y8 Z < $IBM_ACPI/thermal Y="$Y1 $Y2 $Y3 $Y4 $Y5 $Y6 $Y7 $Y8" [ "$X" == "temperatures:" ] || { echo "$0: Bad temperatures: $X $Y $Z" >&2; exit 1; } echo -n "$Y "; if [[ "$Z" == *\ *\ * ]]; then # ibm_acpi provided the 3 extra sensors from at EC offsets 0xC0 to 0xC2? echo -n "$Z " else [ -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 fi # HDAPS temperature (optional): if [ -r $HDAPS_TEMP ]; then Y="`cat $HDAPS_TEMP`" (( "$Y" > 100 )) || echo -n "$Y " # the HDAPS readouts are nonsensical right after resume fi 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 } log() { $QUIET || echo "> $*" ! $SYSLOG || $LOGGER -t "`basename $0`[$$]" "$*" } cleanup() { # clean up after work $AM_DAEMON && rm -f "$PID_FILE" 2> /dev/null log "Shutting down, switching to automatic fan control" $DRY_RUN || echo enable > $IBM_ACPI/fan } floor_div() { echo $(( (($1)+1000*($2))/($2) - 1000 )) } init_state() { IDX=0 NEW_IDX=0 START_TIME=0 MAX_IDX=$(( ${#LEVELS[@]} - 1 )) SETTLE_LEFT=0 RESETTLE_LEFT=0 FIRST=true RESTART=false } 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 trap "log 'Got SIGUSR1'; setlevel 0; RESTART=true; sleep $SUSPEND_TIME" USR1 init_state log "Starting dynamic fan control" # Control loop: while true; do TEMPS=`thermometer` $QUIET || SPEED=`speedometer` $QUIET || 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 # Interrupted by a signal? if $RESTART; then init_state log "Resetting state" continue fi # Transition $FIRST && OLDLEVEL='?' || OLDLEVEL=${LEVELS[$IDX]} NEWLEVEL=${LEVELS[$NEW_IDX]} $QUIET || echo "L=$OLDLEVEL->$NEWLEVEL EC=$ECLEVEL RPM=`printf %4s $SPEED` T=($TEMP_STR) Z=$Z_STR" if [ "$OLDLEVEL" != "$NEWLEVEL" ]; then START_TIME=$NOW log "Changing fan level: $OLDLEVEL->$NEWLEVEL (temps: $TEMP_STR)" 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 || $SUSPEND_DAEMON; then if [ -f "$PID_FILE" ]; then set -e DPID="`cat \"$PID_FILE\"`" if $KILL_DAEMON; then kill "$DPID" rm "$PID_FILE" $QUIET || echo "Killed process $DPID" else # SUSPEND_DAEMON kill -USR1 "$DPID" $QUIET || echo "Sent SIGUSR1 to $DPID" fi else $QUIET || 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 QUIET=true control_fan 0<&- 1>&- 2>&- & echo $! > "$PID_FILE" exit 0 fi else [ -e "$PID_FILE" ] && echo "WARNING: daemon already running" control_fan fi