Skip to content

Conversation

@qquique
Copy link

@qquique qquique commented Aug 7, 2025

Hi me again :), tldr of this PR :

  • Fix for fancurves for LOQ Model (Tested on LZCN model), reads and sets values when in Custom Mode for CPU Fan (min temp, max temp, rpm), GPU Fan (min temp, max temp, rpm), IC (min temp, max temp), and works!.
  • WMI methods to read/write current Fan Indices (read details)

Now the details :

When using the Lenovo Vantage Application when setting the custom mode and modifying its levels (points) :

  • Only changes position indices in an array that has the same size of the Fancurve.
  • Each element value of that array points to the element that contains the speed in the current Fancurve selected.
  • The Fancurve is selected depending on the thermal mode (quiet/balanced/performance/custom), the dGPU (by vendor id) and a PRID (value in DSDT) that I think is the mode of the laptop (hybrid, iGPU, dGPU, and another that wakes up the dGPU when used according to the Vantage app)

It can be queried with the following powershell script :

$classInstance = Get-CimInstance -Namespace root/wmi -ClassName LENOVO_FAN_METHOD
$classInstance | Invoke-CimMethod -MethodName Fan_Get_Table -Arguments @{FanID=0;SensorID=0}
FanTable        : {2, 2, 3, 4, 5, 6, 7, 8 ,9 ,9}
FanTableSize    : 10
SensorTable     : {2, 2, 3, 4, 5, 6 ,7, 8, 9 ,9}
SensorTableSize : 10
ReturnValue     : True

In this output the value 2 is repeated because I changed the first point of the fancurve to the same level as the second point (1400, 1400). This method is implemented now in function wmi_read_fancurve_idx. FanId and SensorId parameters doesnt make any difference.

There are many Fan Curves defined and can be queried with the following powershell :

$classInstance = Get-CimInstance -Namespace root/wmi -ClassName LENOVO_FAN_TABLE_DATA
$classInstance
Active                      : True
CurrentFanMaxSpeed          : 3900
CurrentFanMinSpeed          : 0
DesignMaxFanSpeedNumber     : 10
EndOnlyUpwardAdjustNumber   : 10
Fan_Id                      : 1
FanSpeedStep                : 100
FanTable_Data               : {1400, 1700, 1900, 2200, 2500, 2700, 2900, 3100, 3500, 3900}
FanTable_Len                : 10
InstanceName                : ACPI\PNP0C14\GMZN_0
MaxSensorTemperature        : 100
MinSensorTemperature        : 0
Mode                        : 3
Reserved                    : 0
Sensor_ID                   : 4
SensorTable_Data            : {60, 64, 68, 72, 76, 80, 84, 93, 99, 100}
SensorTable_Len             : 10
SensorTemperatureStep       : 1
StartOnlyUpwardAdjustNumber : 0
PSComputerName              :
...

This output is just one example, In Windows it shows 15 I assume it gets only the Fan Table specific for the laptop model, In the ACPI DSDT there are 4 Tables of 15 each one. Also the temps here are not used there are other tables with temps selected also by the thermal mode, dgpu and PRID logic.

We dont care too much about the Fan Curves predefined, maybe as a reference, however Vantage app doesnt use the 10th one that has the highest temp 120 for the IC sensor (the index doesnt start at 0 as C arrays it starts at 1), you can see in the output of Get_Fan_Table uses index 9 for the 10th position. This can be a warning when changing those values.

The WMI call that the Vantage App uses LENOVO_FAN_METHOD.Fan_Set_Table related to the ACPI/DSDT SFAN Method only works with the indices mapping, doesnt set directly RPM's it obtains them from its internal tables. This method is implemented in wmi_write_fancurve_idx.

The function wmi_read/write_fancurve_idx can be used as new feature on the variables exposed by the driver in hwmon or acpi/firmware (maybe) and also on the python client, if someone wants to develop it.

Besides the modes Quiet (1), Balanced (2), Performance (3) and Custom (255) there is a Extreme Mode (224).

So following the DSDT code as a guide of how to obtain the rpm's, I replaced the old ec_read/write_fancurve_loq to reset the index mapping to an ordered list (1-10) that represent the speed RPM's that are being passed as arguments (via cmd line, legion.py, legion_gui.py), sets the correct values on the offsets on the 3 groups (cpu, gpu, ic), and activates a flag to tell the device/ec to use the new values.

Note:

  • When changing the Power Profile to quiet/normal/performance with Fn+Q or echo "balanced" > /sys/firmware/acpi/platform_profile, the values are reset to the bios/dsdt defaults and the 3 sets of custom values are not changed, so when querying cat /sys/kernel/debug/legion/fancurve it will appear the custom values but not the current ones, I think that when changing powermodes also we have to call wmi_write_fancurve_idx so we reset the indices, and the acpi/dsdt set the default speeds and temps of the current powermode.

Test Output of Fancurve (Custom Mode, modes set in Vantage App):

Fan curve points size: 10
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       70      0       48      0       42
3        14      14      35      35      0       0       60      73      45      55      38      63
3        17      17      43      43      0       0       70      77      57      60      59      70
3        19      19      48      48      0       0       75      80      59      65      60      72
3        22      22      56      56      0       0       78      84      62      77      60      72
3        25      25      63      63      0       0       80      84      70      82      60      72
3        29      29      73      73      0       0       82      92      80      87      60      78
3        31      31      79      79      0       0       87      95      80      87      76      90
3        35      35      89      89      0       0       92      99      80      87      89      105
3        35      35      89      89      0       0       94      100     85      100     90      120

Changing the CPU and GPU fans

# cpu
export HWMONPATH=/sys/class/hwmon/hwmon5
echo 65 >$HWMONPATH/pwm1_auto_point2_temp_hyst
echo 72 >$HWMONPATH/pwm1_auto_point2_temp
echo 35 >$HWMONPATH/pwm1_auto_point2_pwm

# gpu
echo 46 >$HWMONPATH/pwm2_auto_point2_temp_hyst
echo 56 >$HWMONPATH/pwm2_auto_point2_temp
echo 35 >$HWMONPATH/pwm2_auto_point2_pwm

output of fancurve

u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       45      0       42
3        13      13      33      33      0       0       65      72      46      56      38      63
3        17      17      43      43      0       0       70      77      57      60      59      70
3        19      19      48      48      0       0       75      80      59      65      60      72
3        22      22      56      56      0       0       78      84      62      77      60      72
3        25      25      63      63      0       0       80      84      70      82      60      72
3        29      29      73      73      0       0       82      92      80      87      60      78
3        31      31      79      79      0       0       87      95      80      87      76      90
3        35      35      89      89      0       0       92      99      80      87      89      105
3        35      35      89      89      0       0       94      100     85      100     90      120

  • because of the issue with pwm rounding/truncation: 35 is converted to 33 🤷‍♂️

- Correct offset for read/writes CPU,GPU,IC groups
- WMI functions to read/write Fan Table Indices
- Read correctly the common ec memory area of CPU min temp, max temp,
  rpm, there is no GPU or IC data in that area, has no effect
  writing on this area to the current fan curve.
- Adds function wmi_write_fancurve_defaults to call WMI method SFAN
  to trigger the selection of fan curve from bios data for the
  custom powermode.
- Expose function wmi_write_fancurve_defaults as an entry in hwmon
  path as auto_points_defaults, use : 'echo 0 > auto_points_defaults'.
@qquique
Copy link
Author

qquique commented Oct 10, 2025

Hi, with the data from the comments in #352 , I added a commit to simplify the use for the models LZCN and NZCN :

  • NZCN and LZCN have a common ec memory area with just info for the cpu min temp, max temp, and rpm duplicated.
  • Modifying via ec does nothing on that area, so it stays just for read.
  • There is no WMI function between the two models to set the values of cpu, gpu ic individually for each point so far that I read both models dsl's, LZCN can modify via ec in an specific ec memory area as my previous commit but I'm disregarding that commit to simplify the use.
  • There is a common WMI function for LZCN and NZCN to trigger the selection of a fancurve from the bios/acpi data so I exposed it in the hwmon path as auto_points_defaults. When in balanced-performance mode (0xff, 255) a echo 0 > auto_points_defaults will trigger the bios selection of fancurve rpms and temps according to its internal tables for the powermode.

Example:

  • press Fn+Q now in Normal Mode (White)
  • echo 0 > auto_points_defaults does nothing
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        0       0       0       0       0       0       0       84      0       0       0       0
3        35      35      89      89      0       0       60      84      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       82      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • change to balanced-performance
  • echo 0 > auto_points_defaults , can be any value, changes to one of the fancurve of the fan tables that it has in the bios/acpi
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       58      70      0       0       0       0
3        19      19      48      48      0       0       70      73      0       0       0       0
3        22      22      56      56      0       0       70      77      0       0       0       0
3        25      25      63      63      0       0       70      79      0       0       0       0
3        27      27      68      68      0       0       70      82      0       0       0       0
3        29      29      73      73      0       0       70      85      0       0       0       0
3        31      31      79      79      0       0       70      92      0       0       0       0
3        35      35      89      89      0       0       70      97      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
$ sensors legion_hwmon-isa-0000
legion_hwmon-isa-0000
Adapter: ISA adapter
Fan 1:           1400 RPM  (max = 10000 RPM)
Fan 2:           1400 RPM  (max = 10000 RPM)
CPU Temperature:  +42.0°C
GPU Temperature:  +35.0°C
IC Temperature:    +0.0°C
pwm1:                 N/A  (mode = pwm)
Edit : There is a WMI Method that can return the selected fan curve from the internal fan tables, but I cant make it work, maybe someone knows how, keep getting ACPI Error 4100:

fwts

\_SB_.GZFD._WDG (8 of 34)
  GUID: 87FB2A6D-D802-48E7-9208-4576C5F5C8D8
  WMI Block:
    Flags          : 0x01 (Expensive)
    Object ID      : A7
    Instance       : 0x0f
PASSED: Test 1, 87FB2A6D-D802-48E7-9208-4576C5F5C8D8 has associated query method
\_SB_.GZFD.WQA7
PASSED: Test 1, 87FB2A6D-D802-48E7-9208-4576C5F5C8D8 has more than zero
instances

dsl

            Method (WQA7, 1, NotSerialized)
            {
                Return (SFTW (Arg0))
            }

            Method (SFTW, 1, NotSerialized)
            {
                If (((PRID == Zero) || (PRID == One)))
                {
                    If ((DGID == 0x02))
                    {
                        CopyObject (FNT2, Local1)
                    }
                    Else
                    {
                        CopyObject (FNT3, Local1)
                    }
                }
                ElseIf ((PRID == 0x03))
                {
                    CopyObject (FNT1, Local1)
                }
                Else
                {
                    CopyObject (FNT0, Local1)
                }

                Local0 = DerefOf (Local1 [ToInteger (Arg0)])
                FNIM = DerefOf (Local0 [Zero])
                FNID = DerefOf (Local0 [One])
                FNLE = DerefOf (Local0 [0x02])
                FNS0 = DerefOf (Local0 [0x03])
                FNS1 = DerefOf (Local0 [0x04])
                FNS2 = DerefOf (Local0 [0x05])
                FNS3 = DerefOf (Local0 [0x06])
                FNS4 = DerefOf (Local0 [0x07])
                FNS5 = DerefOf (Local0 [0x08])
                FNS6 = DerefOf (Local0 [0x09])
                FNS7 = DerefOf (Local0 [0x0A])
                FNS8 = DerefOf (Local0 [0x0B])
                FNS9 = DerefOf (Local0 [0x0C])
                SEID = DerefOf (Local0 [0x0D])
                STLE = DerefOf (Local0 [0x0E])
                SST0 = DerefOf (Local0 [0x0F])
                SST1 = DerefOf (Local0 [0x10])
                SST2 = DerefOf (Local0 [0x11])
                SST3 = DerefOf (Local0 [0x12])
                SST4 = DerefOf (Local0 [0x13])
                SST5 = DerefOf (Local0 [0x14])
                SST6 = DerefOf (Local0 [0x15])
                SST7 = DerefOf (Local0 [0x16])
                SST8 = DerefOf (Local0 [0x17])
                SST9 = DerefOf (Local0 [0x18])
                SOU1 = DerefOf (Local0 [0x19])
                SOU2 = DerefOf (Local0 [0x1A])
                CFMS = DerefOf (Local0 [0x1B])
                SOU3 = DerefOf (Local0 [0x1C])
                SOU4 = DerefOf (Local0 [0x1D])
                CFIS = DerefOf (Local0 [0x1E])
                FSSP = DerefOf (Local0 [0x1F])
                MST1 = DerefOf (Local0 [0x20])
                MST2 = DerefOf (Local0 [0x21])
                MSTP = DerefOf (Local0 [0x22])
                Return (FACT) /* \_SB_.GZFD.FACT */
            }
struct WMIFanTableReadLoq { // FACT table
	u16 FNIM;
	u16 FNID;
	u32 FNLE;
	u16 FNS0;
	u16 FNS1;
	u16 FNS2;
	u16 FNS3;
	u16 FNS4;
	u16 FNS5;
	u16 FNS6;
	u16 FNS7;
	u16 FNS8;
	u16 FNS9;
	u32 SEID;
	u32 STLE;
	u16 SST0;
	u16 SST1;
	u16 SST2;
	u16 SST3;
	u16 SST4;
	u16 SST5;
	u16 SST6;
	u16 SST7;
	u16 SST8;
	u16 SST9;
	u8  SOU1;
	u8  SOU2;
	u16 CFMS;
	u8  SOU3;
	u8  SOU4;
	u16 CFIS;
	u16 FSSP;
	u16 MST1;
	u16 MST2;
	u16 MSTP;
} __packed;

static ssize_t wmi_read_fancurve_idx(const struct model_config *model,
					struct fancurve *fancurve)
{
	u8 buffer[10];
	int err;

	u8 input[2] = { 0x09, 0x0 };
	struct acpi_buffer in_buf = {
		.length = sizeof(input),
		.pointer = input,
	};

	err = wmi_exec_ints("87FB2A6D-D802-48E7-9208-4576C5F5C8D8", 0,
					0xa7, &in_buf, buffer, sizeof(buffer));

	if (!err) {
		struct WMIFanTableReadLoq *fantable =
			(struct WMIFanTableReadLoq *)&buffer[0];

		fancurve->current_point_i = 0;
		fancurve->size = fantable->FNLE;
		fancurve->fan_speed_unit = FAN_SPEED_UNIT_RPM_HUNDRED;
		fancurve->points[0].speed1 = fantable->FNS0;
		fancurve->points[1].speed1 = fantable->FNS1;
		fancurve->points[2].speed1 = fantable->FNS2;
		fancurve->points[3].speed1 = fantable->FNS3;
		fancurve->points[4].speed1 = fantable->FNS4;
		fancurve->points[5].speed1 = fantable->FNS5;
		fancurve->points[6].speed1 = fantable->FNS6;
		fancurve->points[7].speed1 = fantable->FNS7;
		fancurve->points[8].speed1 = fantable->FNS8;
		fancurve->points[9].speed1 = fantable->FNS9;

		print_hex_dump(KERN_DEBUG, "legion_laptop fan table idx wmi buffer",
		       DUMP_PREFIX_ADDRESS, 16, 1, buffer, sizeof(buffer),
		       true);
	}
	return err;
}``
</details>

@qquique
Copy link
Author

qquique commented Oct 11, 2025

In Vantage App when the Custom Mode is selected, there is a drop down control to the right that says "Resets" and gives the options of powermodes : "Quiet, Balanced, Performance". It means that when in Custom Mode/balanced-performance 255 it can use any of the default fancurves from "Quiet, Balanced, Performance" modes. This last commit does that.

  • echo "balanced-performance" > platform_profile
  • echo 1 > auto_points_defaults : Quiet
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        0       0       0       0       0       0       0       70      0       0       0       0
3        14      14      35      35      0       0       68      86      0       0       0       0
3        17      17      43      43      0       0       84      92      0       0       0       0
3        19      19      48      48      0       0       84      92      0       0       0       0
3        22      22      56      56      0       0       84      92      0       0       0       0
3        25      25      63      63      0       0       84      92      0       0       0       0
3        29      29      73      73      0       0       84      92      0       0       0       0
3        31      31      79      79      0       0       84      92      0       0       0       0
3        35      35      89      89      0       0       88      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 2 > auto_points_defaults : Balanced
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        0       0       0       0       0       0       0       70      0       0       0       0
3        14      14      35      35      0       0       60      73      0       0       0       0
3        17      17      43      43      0       0       70      77      0       0       0       0
3        19      19      48      48      0       0       75      80      0       0       0       0
3        22      22      56      56      0       0       78      84      0       0       0       0
3        25      25      63      63      0       0       80      84      0       0       0       0
3        29      29      73      73      0       0       82      92      0       0       0       0
3        31      31      79      79      0       0       87      95      0       0       0       0
3        35      35      89      89      0       0       92      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 3 > auto_points_defaults : Performance
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       50      64      0       0       0       0
3        19      19      48      48      0       0       56      68      0       0       0       0
3        22      22      56      56      0       0       60      72      0       0       0       0
3        25      25      63      63      0       0       65      76      0       0       0       0
3        27      27      68      68      0       0       70      80      0       0       0       0
3        29      29      73      73      0       0       75      84      0       0       0       0
3        31      31      79      79      0       0       80      93      0       0       0       0
3        35      35      89      89      0       0       87      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 255 > auto_points_defaults : Custom
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       58      70      0       0       0       0
3        19      19      48      48      0       0       70      73      0       0       0       0
3        22      22      56      56      0       0       70      77      0       0       0       0
3        25      25      63      63      0       0       70      79      0       0       0       0
3        27      27      68      68      0       0       70      82      0       0       0       0
3        29      29      73      73      0       0       70      85      0       0       0       0
3        31      31      79      79      0       0       70      92      0       0       0       0
3        35      35      89      89      0       0       70      97      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 224 > auto_points_defaults : Extreme
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       50      64      0       0       0       0
3        19      19      48      48      0       0       56      68      0       0       0       0
3        22      22      56      56      0       0       60      72      0       0       0       0
3        25      25      63      63      0       0       65      76      0       0       0       0
3        27      27      68      68      0       0       70      80      0       0       0       0
3        29      29      73      73      0       0       75      84      0       0       0       0
3        31      31      79      79      0       0       80      93      0       0       0       0
3        35      35      89      89      0       0       87      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant