Jump to content
black.dragon74

How to implement custom fan control on ASUS laptops

Recommended Posts

Heya!

 

After a lot of playing around with ASUS acpi tables I have finally come up with a SSDT that will work nearly on all ASUS laptops (Haswell or above) for reading FAN RPM, CPU TEMP and also Custom Controlling FAN. I do not have hardware below Haswell so can't test.

 

If someone get's it working using this SSDT on machines prior to haswell. Let me and others know.

 

Background Info:

The system FAN is generally controlled by the embedded controller (EC) but there are methods in ACPI that can let you read and control your system FAN.

 

As you might know, ASUS machines are the best for hackintosh as they have the best written ACPI code. Like, if it was HP instead of ASUS you would have to acquire a mutex object. Write a value to the EC and then release the object. But in ASUS machines there is a method that takes arguments and does this job automatically. Still.

 

There are multiple methods in our DSDT that allow us to read and control system FAN.

 

For example to read FAN speed, there are two ways. If you observe, the FAN speed is at offset 0x93 of EC (Use RWEverything to find out)

 

So, when we search for Offset 0x93 in the DSDT we get a result like this:

Offset (0x93), 
TAH0,   16,  // TAH0 stands for FAN1
TAH1,   16,  // TAH1 stands for FAN2 (in case your laptop has 2 fans)
TSTP,   16,  // TSTP stores current fan value in some bytes

Note: Instead of using RWEverything if you have a look at method TACH you can see that it stores the values in TAH0 and TAH1 depending upon the Args supplied. So, searching for TAH0 or TAH1 we can see that they are located at offset 0x93

 

So, now we can read the value from these registers in some units and then we will have to use some formula to convert that unit to RPM.

 

In order to find that formula, if you have a look at Method TACH in dsdt, you will see:

Method (TACH, 1, Serialized)
{
    Name (_T_0, Zero)
    If (ECAV ())
    {
        While (One)
        {
            _T_0 = Arg0
            If ((_T_0 == Zero))
            {
                Local0 = TAH0
                Break
            }
            ElseIf ((_T_0 == One))
            {
                Local0 = TAH1
                Break
            }
            Else
            {
                Return (Ones)
            }

            Break
        }

        Local0 *= 0x02
        If ((Local0 != Zero))
        {
            Divide (0x0041CDB4, Local0, Local1, Local0)
            Return (Local0)
        }
        Else
        {
            Return (Ones)
        }
    }
    Else
    {
        Return (Ones)
    }
}

So, we know that we have to store the value from TAH0 or TAH1 (depends on Arg0) and then we have to multiply it by 2 and then we have to divide it by 0x0041CDB4 (4312500) to get the value in RPMs.

 

Once we know that, we could write a simple ACPI code to return the value in RPMs like:

// GRPM means get RPM
Method (GRPM, 0)
{
	// Store value in Local0
	Local0 = \_SB.PCI0.LPCB.EC0.TAH0

	// If local0 is not equal to 0
	If (Local0 != 0){
		// Multiply by 2
		Local0 = Local0 * 2
		// Divide by 4312500
		Divide (0x0041CDB4, Local0, Local1, Local0)
	}

	// Return the value
	Return (Local0)
}

As you can see this is very expensive method. But what we learned here is, If we execute method TACH with Arg0 as 0 (Zero) it will give us the speed in RPM of FAN 1. Similarly, if we use Arg0 as 1 (One) it will return the value in RPM for FAN 2

 

My laptop only has one FAN so, I can execute method TACH like,

\SB.PCI0.LPCB.EC0.TACH(0) // Using Arg0 as Zero for FAN 1

And it will give me FANs RPM, so, instead of reading from EC and converting bits to RPM this is more preferable. Also, EC bytes may change while patching DSDT so it is a good idea to use dynamic methods so that a single SSDT could work for all machines. One such example is, If you use ACPIBatteryManager then you will have to convert 16bits registers to 8bits. And then, to use that "reading RPM from EC method you will have to create a new method to combine 2 8 bits registers to 1 16 bit like,

// JEBR = Join 8 bit registers
Method (JEBR, 2)
{
    Return ((Arg0 | (Arg1 << 8))) // Arg0 and Arg1 will be 8Bit register 1 and 2 respectively (AH00, AH01)
}

Now moving to FAN control, If you have a look at method QMOD in DSDT:

Method (QMOD, 1, NotSerialized)
{
    If ((Arg0 == Zero))
    {
        Return (Zero) // If arg0 is Zero. Terminate by returning 0
    }

    If ((Arg0 == One))
    {
        ^^PCI0.LPCB.EC0.ST98 (QFAN) // Hmm, ST98 is somewhat related to FAN control
    }

    If ((Arg0 == 0x02))
    {
        ^^PCI0.LPCB.EC0.ST98 (0xFF) // Okay, ST98 again.. Something is interesting
    }

    Return (One)
}

Now, if you look at ST98 you can see:

Method (ST98, 1, Serialized)
{
    If (ECAV ()) // Checked if EC is available, will you write to it?
    {
        Acquire (MU4T, 0xFFFF) // Oh! So you are acquiring a mutex object
        CMD = 0xFF
        EDA1 = 0x98 
        EDA2 = Arg0 // Oh, so you did write Arg0 in EDA2 (Located in EC01 OperationRegion)
        ECAC ()
        Release (MU4T) // Released it here, you did write something to the EC for sure
        Return (Zero)
    }

    Return (Ones)
}

Now, we can understand that, method QMOD (Quiet Mode?) Takes 1 argument that could be (0, 1 or 2). We can eliminate 0 as when we pass Arg0 as 0 it simply returns. 

 

Interesting are args 1 and 2

 

When using 1 it passes Arg0 to ST98 as QFAN's value (Hmm.. What is this QFAN)

When using 2 it passes Arg0 to ST98 as 0xFF (255) (Oh wait! according to ACPI spec, 255 is max allowed FAN value.)

 

Gotcha! We can use ST98 and pass it an arg ranging between 0x0 (0) to 0xFF (255) where 0 is for FAN off and 255 is for Max allowed or auto.

 

But, why is there a QMOD method then? There might be some good reason for that. So, we will not invoke ST98 directly but will use the modus operandi of method QMOD

 

Like, we will first store that max allowed FAN value in QFAN

Then, we will invoke QMOD with Arg0 as 1 (One). Which will invoke ST98 for us hence, setting the max allowed speed for FAN.

 

So, the ACPI code for this would be:

Method (SETR, 0)
{
    QFAN = 200 // Suppose we want to use max allowed value as 200

    // Now we can call QMOD with Arg0 as 1
    QMOD (1)
}

Now we know what to do. We now just need to write a method that can calculate the CPU temperature and then set FAN RPM accordingly. This is when you will use my SSDT-FAN

 

How to implement:

Requirements:

  • FakeSMC kext along with sensors installed at /L/E or /S/L/E (DO NOT INJECT USING CLOVER)
  • ACPIPoller.kext
  • HWMonitor for monitoring CPU Temp and FANS
  • My SSDT-FAN.aml

Installation:

  • Place SSDT-FAN.aml to /EFI/CLOVER/ACPI/patched (If using sorted order make sure you add SSDT-FAN to it)
  • Install ACPIPoller to /L/E or /S/L/E (not both and definitely do not inject using CLOVER)

Configuration:

  • You can set "Name (UCFC, One)" to "Name (UCFC, Zero)" in my SSDT-FAN in case you want to use default FAN control method as provided by your OEM. My SSDT will only provide FAN RPM reading and CPU Temp reading.

Achievements:

  • Default scaling that ASUS provided was from 2200RPM to 2900RPM (Fan spinning fast without use)
  • I managed to bring scale it from 255RPM to 5026RPM (Fan turns off if temp
  • Temp rarely goes above 53˚C (Went up to 68 earlier)

Technicalities:

As we know know how to implement custom fan control using various methods in our DSDT we can't really use them as is in real life scenarios as FAN will literally be dancing. You might have observed that temperature keeps fluctuating a few degrees every second while you are working on something. Moreover, we need an automated method that can read and set FAN RPM.

 

So, my SSDT calculates average temperature and then also waits for 2 seconds (to handle fluctuation) before increasing the RPM and waits for 5 seconds (to let CPU cool) before lowering RPM. You can edit this timeout by editing "Name (FCTU, 2)" for FanControlTimeoutUp and "Name (FCTD, 5)" for FanControlTimeoutDown.

 

Method to calculate average accredited to RehabMan as he wrote the code first. There is only one known way to calculate average in maths. LOL.

 

If you want to know how it is implemented, read this code:

Note: This code is a part of my single optimizer SSDT project for ASUS laptops (Means, I am working on a single SSDT that you could place in your CLOVER/ACPI/patched and will have everything working without patching DSDT. You can customize SSDT using Device ANKD (A Nick's Device) like you can configure this SSDT to use custom FAN control or not). You can have a look at WIP code here

// SSDT for FAN readings and custom FAN control for ASUS laptops

// Copyright, black.dragon74 <www.osxlatitude.com>

// Please configure the options in Device ANKD before compiling this SSDT

DefinitionBlock("SSDT-FAN", "SSDT", 2, "Nick", "AsusFan", 0)
{
    // Declare externals
    External (\_SB.QFAN, FieldUnitObj)
    External (\_SB.ATKD.QMOD, MethodObj)
    External (\_SB.PCI0.LPCB.EC0.ECAV, MethodObj)
    External (\_SB.PCI0.LPCB.EC0.ECPU, FieldUnitObj)
    External (\_SB.PCI0.LPCB.EC0.ST83, MethodObj)
    External (\_SB.PCI0.LPCB.EC0.ST98, MethodObj)
    External (\_SB.PCI0.LPCB.EC0.TACH, MethodObj)
    
    
    // Create a Nick's device to take care of this SSDT's configurations
    Device (ANKD)
    {
        Name (_HID, "ANKD0000") // Required. DO NOT change
        Name (UCFC, 1) // Set this to 0 if you don't wanna use my custom FAN control
    }

    // Create devices required by FakeSMC_ACPISensors
    Device (SMCD)
    {
        Name (_HID, "FAN0000") // Required, DO NOT change
        
        // Add tachometer
        Name (TACH, Package()
        {
            "System FAN", "FAN0"
        })
        
        // Add CPU heatsink
        Name (TEMP, Package()
        {
            "CPU Heatsink", "TCPU"
        })
        
        // Method to read FAN RPM (tachometer)
        Method (FAN0, 0)
        {
            // Check is EC is ready
            If (\_SB.PCI0.LPCB.EC0.ECAV())
            {
                // Continue
                Local0 = \_SB.PCI0.LPCB.EC0.ST83(0) // Method ST83 acquires mutex and writes value to EC. O stands for FAN 1, Use 1 for FAN 2
                If (Local0 == 255)
                {
                    // If ST83 is 0xFF (Max fan speed) terminate by returning FAN RPM
                    Return (Local0)
                }
                
                // Else, Get RPM and store it in Local0
                Local0 = \_SB.PCI0.LPCB.EC0.TACH(0) // Method TACH in DSDT returns current FAN RPM in 100s, Arg0 as 0 is for FAN 1, for FAN 2, use Arg0 as 1
                    
            }
            Else
            {
                // Terminate, return Zero
                Local0 = 0
            }
            
            // Return 255, 0 or Fan RPM based on conditionals above
            Return (Local0)  
        }
        
        // Method to read CPU temp (CPU Heatsink)
        Method (TCPU, 0)
        {
            // Check if EC is ready
            If (\_SB.PCI0.LPCB.EC0.ECAV())
            {
                // Then
                Local0 = \_SB.PCI0.LPCB.EC0.ECPU // EC Field storing current CPU temp
                Local1 = 60 // From DSDT
                
                If (Local0 < 128)
                {
                    Local1 = Local0
                }
                
            }
            Else
            {
                // Terminate, return Zero
                Local1 = 0
            }
        
            // Return final CPU temp. ACPISensors take care of unit conversion.
            Return (Local1)
        }
        
        // Custom FAN table by black.dragon74 for ASUS laptops based on RehabMan's idea
        // Quietest fan operation yet coolest CPU.
        // Scaling from values as low as 255 RPM to values as high as 5026 RPM (That's great!)
        // Scaling that ASUS provided was from 2200 RPM to 2900 RPM (Duh!)
        
        // Temperatures. 0xFF means if temp is above 52C, let bios take control of things(auto).
        Name(FTA1, Package()
        {
            32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 0xFF,
        })
        
        // Fan speeds. 255(0xFF) is max/auto, 0(0x00) is for fan off
        Name(FTA2, Package()
        {
            0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 160, 185, 205, 225, 245, 250, 255
        })
        
        // Time out values
        Name (FCTU, 2) // RPM Up
        Name (FCTD, 5) // RPM Down

        // Table to keep track of past temperatures (to track average)
        Name (FHST, Buffer() { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }) // Size should match the count of above FTA1 and FTA2 package
        Name (FIDX, 0) 	// current index in buffer above
        Name (FNUM, 0) 	// number of entries in above buffer to count in avg
        Name (FSUM, 0) 	// current sum of entries in buffer
        
        // Keeps track of last fan speed set, and counter to set new one
        Name (FLST, 0xFF)	    // last index for fan control
        Name (FCNT, 0)		// count of times it has been "wrong", 0 means no counter

        // Method to control FAN wrt TEMP
        // Name in ACPIPoller.kext's Info.plist should be FCPU with HID FAN0000
        Method (FCPU, 0)
        {
            // If UCFC is set to 0, terminate
            If (\ANKD.UCFC == 0)
            {
                Return (0)
            }
            
            // If EC is not ready, terminate
            If (!\_SB.PCI0.LPCB.EC0.ECAV())
            {
                Return (0)
            }    
                
            Local5 = \_SB.PCI0.LPCB.EC0.ECPU // Current temperature of the CPU Heatsink
            If (Local5 < 128)
            {
                Local0 = Local5 // Store temperature in Local0
            }
            Else
            {
                Local0 = 60 // As per BIOS
            }    

            // calculate average temperature
            Local1 = Local0 + FSUM
            Local2 = FIDX
            Local1 -= DerefOf(FHST[Local2])
            FHST[Local2] = Local0
            FSUM = Local1  // Local1 is new sum
            
            // adjust current index into temperature history table
            Local2++
            if (Local2 >= SizeOf(FHST)) { Local2 = 0 }
            FIDX = Local2
            
            // adjust total items collected in temp table
            Local2 = FNUM
            if (Local2 != SizeOf(FHST))
            {
                Local2++
                FNUM = Local2
            }
            
            // Local1 is new sum, Local2 is number of entries in sum
            Local0 = Local1 / Local2 // Local0 is now average temp

            // table based search (use avg temperature to search)
            if (Local0 > 255) { Local0 = 255 }
            Local2 = Match(FTA1, MGE, Local0, MTR, 0, 0)

            // calculate difference between current and found index
            if (Local2 > FLST)
            {
                Local1 = Local2 - FLST
                Local4 = FCTU
            }
            else
            {
                Local1 = FLST - Local2
                Local4 = FCTD
            }

            // set new fan speed, if necessary
            if (!Local1)
            {
                // no difference, so leave current fan speed and reset count
                FCNT = 0
            }
            else
            {
                // there is a difference, start/continue process of changing fan
                Local3 = FCNT
                FCNT++
                // how long to wait depends on how big the difference
                // 20 secs if diff is 2, 5 secs if diff is 4, etc.
                Local1 = Local4 / Local1
                if (Local3 >= Local1)
                {
                    // timeout expired, so start setting new fan speed
                    FLST = Local2
                    
                    // Method 1 (Recommended)
                    
                    // Store custom fan value from table in Local5
                    Local5 = DerefOf(FTA2[Local2])
                    
                    // Set QFAN value to that of Local5
                    \_SB.QFAN = Local5
                    
                    // Execute QMOD with Arg0 as 1(One) to set FAN's max allowed speed to that of \_SB.QFAN
                    \_SB.ATKD.QMOD(1)
                    
                    // End Method 1
                    
                    // Method 2 (Works but not recommended) Uncomment the line below to use this (remember to comment lines in method 1)
                    
                    // \_SB.PCI0.LPCB.EC0.ST98 (DerefOf(FTA2[Local2]))
                    
                    // End Method 2
                    
                    // Reset FAN count (Required in either methods)
                    FCNT = 0
                }
            }
            
            Return (1) // Return something as this is a requirement of a ACPI Method
        }                    
    }
}    

Moment of joy:

 

old.png

 

Problem Reporting:

Attach proper problem reporting files. See How to generate proper problem reporting files

 

Note: If you see FAN RPM = 255 and hear a lot of noise from FAN it means your FAN is running at it's maximum speed.

 

Regards

ACPIPoller.zip

SSDT-FAN.zip

  • Like 2

Share this post


Link to post
Share on other sites

Update 11 Dec 2017:

 

Please note that in HWMonitor you will see two system fans namely: System Fan and System FAN (note the capitalization in the second name). It is not harmful but just in case you hate two system FANs in HWMonitor, you can fix this easily.

 

To fix this, once you download the attached SSDT-FAN.aml you can open it and in Device (SMCD) scope you will see a name field like:

 

Name (TACH, Package(){

"System FAN", "FAN0"

}

 

Change it to:

 

Name (TACH, Package(){

"System Fan", "FAN0"

}

 

Regards

Share this post


Link to post
Share on other sites

Very good coding, black.dragon74!

Your method works on my Asus K501LX. And will probably work on any Asus laptop as QMOD Method is universal.

 

I have only changed FTA1 & FTA2 to make my laptop even more quiet, and turn the FAN ON only when CPU temperature is above 50.

Share this post


Link to post
Share on other sites

Yes it is universal and will work for all ASUS laptops as mentioned in post #1

 

Re: FTA1 & FTA2, it's bad implementation. You should keep the minimum threshold to somewhere around 45˚C

Share this post


Link to post
Share on other sites

Excellent article. However my laptop is a newer model and none of these actually work. As far as i see, my laptop has 4 fan levels. When offset 103 and 105 are 4, the fans almost dont spin. And when they are 01, they are spinning in the fastest mode.
 
Laptop almost always runs in 04 and its almost silent. But it gets extremely loud during gaming cause the fans go to speed 01... However this is overkill. The noise in 02 mode is very acceptable and going to 01 drops temps about 5 degrees (75 to 65)..
 
I checked my DSDT file, i have absolutely no programming experience and couldnt understand much. How could i find out the register which indicates "Max Fan RPM Value" and keep the fans from 01 no matter how hot it is?

Share this post


Link to post
Share on other sites

If your vendor is ASUS, upload your native DSDT here, untouched. (Press F4 at CLOVER boot screen).

 

P.S: If by newer you mean upto coffeelake(8th gen), it is working in my case (Vivobook S15).

 

Regards

Share this post


Link to post
Share on other sites

There is a way to use Your method  if my laptop ( Asus X542UN) run windows/linux?

I don' have practical programming experience  but I understood 35-40% of what you did here.

Thank you for sharing this!

 

Share this post


Link to post
Share on other sites

Hi, 

I have Asus UX430UA and fans speed is to fast(loud) with temp ~38/40' - i think fans should start ~45'
I saw there is new BIOS v304 for that laptop which slow down fans but it breaks my hackintosh(do not have a time to generate all again so i stuck with BIOS v300)

Can you help me with generate SSDT-FAN for that model ?

 

Thank you.

ux430ua.zip

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×