Tutorial: How I forced my laptop fan to spin under Linux

Introduction

I got a Fujitsu-Siemens Amilo Xi 1554. A good machine, but terribly hot, with an amazingly bad design of its heat-sink. I live in a tropical area so the air temperature is often more than 30°C. This and the fact that Fujitsu does not know how to design performing cooling solutions lead to my laptop producing a lot of heat, to the point that I have to use an external keyboard because it’s too warm.

Under Windows XP, I used the NHC (Notebook Hardware Control) application available for free at http://www.pbus-167.com. It worked pretty well. In the past, I modified an existing class to make it work with my Amilo 1554. It forced the fan to spin by writing fake temperature value to a defined ACPI field in my DSDT table (\_SB.PCI0.LPCB.EC0.XHPP), say 90°C for example, and did this several times per second using a timer so that it overrides the real temperature. This worked well.

Then I migrated to Windows 7 x64, but no luck, there is no released NHC version for 64 bits systems (as of today april 15, 2011) although there should have one soon. As the result, my laptop was damn hot even with the proper energy management policy defined in Windows 7.

Then I tried Debian Lenny and got addicted to it. But I was still out of luck : there is no way to control my fan using ACPI out-of-the-box. No /proc/acpi/thermal_zone/fan. Hot laptop.

Then I upgraded to Squeeze and liked it. Still can’t control my fan however.

How to force the fan to spin

How to force this fan to spin? Well NHC guided me to the right way. I have to write a fake temperature over the \_SB.PCI0.LPCB.EC0.XHPP field! How to do it under Linux? Just follow my instructions and see if it works for you.

First, you will need a way to use a custom DSDT. Depending on your distribution, there are one to several ways to do it. Here are the main ones:

  • The initrd way. In some Linux distributions, such as Ubuntu, you can just copy your DSDT.aml file in /boot/ and the DSDT file will override the one who is loaded normally. This will only work if your kernel has been compiled with the “ACPI_CUSTOM_DSDT_INITRD” option.
  • The kernel recompilation way. By including your DSDT.hex file in the include directory of your Linux source files and setting some parameters in your .config file, your custom DSDT would be loaded. This was the only way available for me as far as I know, as support for the ACPI_CUSTOM_DSDT_INITRD kernel config parameter was dropped in kernels released after the 2.6.25 version.

Second, you will need a way to query and execute methods on your DSDT. I used the acpi_call kernel module available at https://github.com/mkottman/acpi_call.

Third, you have to retrieve and disassemble your DSDT to analyze it. It’s basically a piece of AML code that is used for ACPI management. Each laptop has a different DSDT and it will change even with a different amount of RAM and different hardware devices.

Under Debian Squeeze, I installed iasl (“apt-get install iasl”) and followed the instructions described at http://www.lesswatts.org/projects/acpi/overridingDSDT.php. Below is the original AML code for my thermal zone:

Scope (_TZ)
{
    Name (THPP, Zero)
    ThermalZone (THRM)
    {
        Method (KELV, 1, NotSerialized)
        {
            If (LGreater (Arg0, 0x7F))
            {
                XOr (Arg0, 0xFF, Local0)
                Add (Local0, One, Local0)
                Multiply (Local0, 0x0A, Local0)
                Subtract (0x0AAC, Local0, Local1)
            }
            Else
            {
                Multiply (Arg0, 0x0A, Local0)
                Add (Local0, 0x0AAC, Local1)
            }
            Return (Local1)
        }

        Method (_TMP, 0, NotSerialized)
        {
            If (LEqual (THPP, 0x69))
            {
                Return (KELV (THPP))
            }

            If (LEqual (\_SB.PCI0.LPCB.EC0.FGEC, Zero))
            {
                Return (KELV (Zero))
            }
            Else
            {
                Multiply (\_SB.PCI0.LPCB.EC0.XHPP, 0x02, THPP)
                ShiftRight (THPP, One, THPP)
                Return (KELV (THPP))
            }
        }

        Method (_CRT, 0, NotSerialized)
        {
            Return (KELV (0x64))
        }
    }
}

As you can see, there is nothing to set the fan speeds. There are 3 methods in my thermal zone:

  • KELV(Arg0) converts a Celsius temperature to Kelvin degrees x 10. For example if Arg0 is 60°C, it will return 3332 °K
  • _TMP() returns the temperature in Kelvin degrees x 10
  • _CRT() returns the critical temperature in Kelvin degrees x10  (100°C or 3732°K here) which define the temperature at which your laptop will automatically shut down for security reasons.

The interesting method is the _TMP method. Especially the following code:

Multiply (\_SB.PCI0.LPCB.EC0.XHPP, 0x02, THPP)
ShiftRight (THPP, One, THPP)
Return (KELV (THPP))

I explain this code line by line below:

Multiply (\_SB.PCI0.LPCB.EC0.XHPP, 0x02, THPP)

This line multiplies the value contained in \_SB.PCI0.LPCB.EC0.XHPP by 2 and stores the result in the THPP named variable. For example 60 x 2 = 120.

ShiftRight (THPP, One, THPP)

This line shifts the bits of the value of THPP one step to the right. For example, 120 >> 1 = 60. So basically we get back our previous value (don’t ask me for the usefulness of those two lines, I don’t know too).

Return (KELV (THPP))

This will return the value in THPP converted in Kelvin degrees x 10. For example 60 x 10 + 2732 = 3332.

The interesting line is the one beginning with Multiply. We can guess that \_SB.PCI0.LPCB.EC0.XHPP contains the current temperature in Celsius degrees. That’s this value that we have to override!

Since the acpi_call  kernel module does not allow us to write to ACPI field directly (or does it?), I added my own method to my DSDT thermal zone, right after the KELV method:

/* Write an artificial temperature to XHPP */Method (DEFT, 1, NotSerialized)
{
    Store (Arg0, \_SB.PCI0.LPCB.EC0.XHPP)
    Return (\_SB.PCI0.LPCB.EC0.XHPP)
}

This method writes the value of its first argument  to the \_SB.PCI0.LPCB.EC0.XHPP field, and returns the new value of this field. I named it “DEFT” for DEFine Temperature.

I recompiled my modified DSDT (“iasl -tc new.dsl”) and corrected the few errors of the original one (I will not cover this in this tutorial). No errors.

Now I just have to get a kernel which allow me to load my custom DSDT. You can find my custom DSDT here but remember it is good for my computer only as I did other modifications (for example, my computer was recognized as an Amilo Xi 1526 after I changed the graphic card, so I corrected that in the DSDT).

I followed these instructions, also available in this PDF document, to compile my own kernel. After I had generated my .config file using make menuconfig, I had to edit the generated .config file and add/modify the following lines:

CONFIG_ACPI_CUSTOM_DSDT_FILE="DSDT.hex"
CONFIG_ACPI_CUSTOM_DSDT=y
CONFIG_STANDALONE=y

Then I copied my DSDT.hex file in the include directory of my Linux kernel sources and launched the compilation. This took about 1 hour on an Intel  Core 2 Duo T7200 (don’t forget to set the CONCURRENCY_LEVEL environment variable to the number of cores of your processor to compile faster).

Then installed the new kernel (explained in the previous PDF), rebooted successfully and ran a dmesg command. I was informed that the new kernel loaded my custom DSDT by the presence of the following lines:

[    0.000000] ACPI: RSDP 00000000000f77b0 00014 (v00 FSC   )
[    0.000000] ACPI: RSDT 000000007fe943e2 00048 (v01 PTLTD  PC       06040000  LTP 00000000)
[    0.000000] ACPI: FACP 000000007fe9ae20 00074 (v01 INTEL  CALISTGA 06040000 LOHR 0000005A)
[    0.000000] ACPI: Override [DSDT-F35_____], this is unsafe: tainting kernel
[    0.000000] Disabling lock debugging due to kernel taint
[    0.000000] ACPI: DSDT @ 0x000000007fe95ade Table override, replaced with:
[    0.000000] ACPI: DSDT ffffffff81493970 05362 (v01 UW____ F35_____ 06040000 INTL 20100528)
[    0.000000] ACPI: FACS 000000007fe9bfc0 00040
[    0.000000] ACPI: APIC 000000007fe9ae94 00068 (v01 INTEL  CALISTGA 06040000 LOHR 0000005A)
[    0.000000] ACPI: HPET 000000007fe9aefc 00038 (v01 INTEL  CALISTGA 06040000 LOHR 0000005A)
[    0.000000] ACPI: MCFG 000000007fe9af34 0003C (v01 INTEL  CALISTGA 06040000 LOHR 0000005A)
[    0.000000] ACPI: APIC 000000007fe9af70 00068 (v01 FSC    PC       06040000  LTP 00000000)
[    0.000000] ACPI: BOOT 000000007fe9afd8 00028 (v01 FSC    PC       06040000  LTP 00000001)
[    0.000000] ACPI: SSDT 000000007fe9548f 0064F (v01 SataRe  SataPri 00001000 INTL 20050624)
[    0.000000] ACPI: SSDT 000000007fe94dfd 00692 (v01 SataRe  SataSec 00001000 INTL 20050624)
[    0.000000] ACPI: SSDT 000000007fe9442a 004F6 (v01  PmRef    CpuPm 00003000 INTL 20050624)

So far, so good. Now is time to test the acpi_call kernel module. So extract its sources, and do the following:

# cd acpi_call_source_directory
# make
# make load

There should have an error about rmmod but that is totally normal, it is caused by the Makefile action trying to remove the module before installing the new compiled one. If you don’t have any error following the insmod command, then you are ready to make ACPI calls!

Basically, all you have to do is write your queries to the /proc/acpi/call file and the acpi_call module will execute it, and will return the result in the /proc/acpi/call file.

I tried a few commands in bash scripts in order to read /proc/acpi/call right after I wrote to it. For example:

#!/bin/bash
echo '\_TZ.THPP' > /proc/acpi/call
cat /proc/acpi/call

will return the current temperature (“0x3c” for example). I then tried to call my own method:

#!/bin/bash
echo '\_TZ.THRM.DEFT 90' > /proc/acpi/call
cat /proc/acpi/call

As expected, it returned 90 (0x5a). So I executed this several times, quickly. I heard my fan spinning up and the reported temperature was now 90°C!

Script to control the fan

We are now almost done. We now need a little script to control the behavior of the fan. I decided that I wanted the fan to spin whenever the temperature exceed 50°C, and let it do whatever it wants below. I also wanted the script to be an infinite loop that can be stopped by writing the value “STOP” to a file.  I placed this script in the same directory than the acpi_call module. You will find this script, fan_cooler, below:

#!/bin/bash

if ! lsmod | grep -q acpi_call; then
 cd $(dirname "$0")
 /sbin/insmod acpi_call.ko
fi
if ! lsmod | grep -q acpi_call; then
 echo "Error: acpi_call module not loaded"
 exit
fi

coolingtimer=0.05
coolingtemperature=50
coolingiterations=1000
coolingcheckduration=2
coolingidleduration=10
user=gabriel
coolingstatusfile=/home/$user/FAN_COOLING_STATUS

gksu -u $user -k "echo 'Started on $(date)' > $coolingstatusfile"

FAN_COOLING_STATUS=$(cat $coolingstatusfile)

until [ "$FAN_COOLING_STATUS" = "STOP" ];
do
    rawtemp=$(cat /proc/acpi/thermal_zone/THRM/temperature)
    set -- $rawtemp
    temperature=$2

    echo "Temperature is" $temperature

    if [ "$temperature" -ge "$coolingtemperature" ]; then
        echo "Forces the fan to spin for $coolingiterations iterations!"
        for i in $(seq 0 $coolingiterations)
        do
            echo "\_TZ.THRM.DEFT 90" > /proc/acpi/call
            if [ "$1" = "debug" ]; then
                cat /proc/acpi/call
                echo "--end--"
            fi
            FAN_COOLING_STATUS=$(cat $coolingstatusfile)
            if [ "$FAN_COOLING_STATUS" = "STOP" ]; then
                echo "Exit forced, script terminated."
                FAN_COOLING_STATUS="EXITED $(date)"
                gksu -u $user -k "echo $FAN_COOLING_STATUS > $coolingstatusfile"
                exit
            fi
            sleep $coolingtimer
        done
        sleep $coolingcheckduration
    else
        echo "Nothing to do, waiting for $coolingidleduration seconds"
        sleep $coolingidleduration
    fi
done
echo "Exit forced, script terminated."
FAN_COOLING_STATUS="EXITED $(date)"
gksu -u $user -k "echo $FAN_COOLING_STATUS > $coolingstatusfile"

The script is also available here if you want to download it.

Run this script as root (or modify it with gksu), and you’re done!

The fan is now magically forced to spin whenever the temperature reach 50°C, and stops after a while, continuing to poll the temperature.

If you want to make things more user-friendly, create a launcher with the command “gksu /path/to/fan_cooler” and launch it where you want! Note that using a launcher will not display any window or terminal.

You can also make the following script (stop_fan_cooler) and make a launcher of it to stop the other script whenever you want:

#!/bin/bash
echo "STOP" > /home/gabriel/FAN_COOLING_STATUS

Do not run this last script as root if you want the file FAN_COOLING_STATUS to be writable by your user.

You can modify this script as you want, I release it under the ISC license. For example, you could add several temperature threshold, leading to different fan speeds.

I believe that this tutorial is valid for the following laptop models because they share the same BIOS and DSDT table:

  • Amilo Xi 1554
  • Amilo Xi 1547
  • Amilo Xi 1546
  • Amilo Xi 1526
  • Amilo Pi 1556
  • Amilo Pi1 536
  • Amilo Xi 14xx

And probably others.

This tutorial is now over, I hope you appreciated it and that it gave you some ideas to control your fan!