]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
platform/x86: acer-wmi: Add fan control support
authorArmin Wolf <W_Armin@gmx.de>
Thu, 16 Oct 2025 18:00:06 +0000 (20:00 +0200)
committerIlpo Järvinen <ilpo.jarvinen@linux.intel.com>
Thu, 30 Oct 2025 16:28:39 +0000 (18:28 +0200)
Add support for controlling the fan speed using the
SetGamingFanSpeed() and GetGamingFanSpeed() WMI methods.

This feature is only enabled if the machine has ACER_CAP_PWM enabled
and depend on ACER_CAP_HWMON for detecting the number of available
fans.

Reviewed-by: Kurt Borja <kuurtb@gmail.com>
Signed-off-by: Armin Wolf <W_Armin@gmx.de>
Link: https://patch.msgid.link/20251016180008.465593-3-W_Armin@gmx.de
[ij: if nested inside else block -> else if]
Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
drivers/platform/x86/acer-wmi.c

index 6fdfb1d0001f7e7ff4c9b0223718744a090d17ca..44ab6bef788f913e88ce5f12411c682be5d72c53 100644 (file)
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
 #include <linux/kernel.h>
+#include <linux/minmax.h>
 #include <linux/module.h>
 #include <linux/init.h>
 #include <linux/types.h>
 #include <linux/dmi.h>
+#include <linux/fixp-arith.h>
 #include <linux/backlight.h>
 #include <linux/leds.h>
 #include <linux/platform_device.h>
@@ -69,6 +71,9 @@ MODULE_LICENSE("GPL");
 #define ACER_WMID_GET_GAMING_LED_METHODID 4
 #define ACER_WMID_GET_GAMING_SYS_INFO_METHODID 5
 #define ACER_WMID_SET_GAMING_FAN_BEHAVIOR_METHODID 14
+#define ACER_WMID_GET_GAMING_FAN_BEHAVIOR_METHODID 15
+#define ACER_WMID_SET_GAMING_FAN_SPEED_METHODID 16
+#define ACER_WMID_GET_GAMING_FAN_SPEED_METHODID 17
 #define ACER_WMID_SET_GAMING_MISC_SETTING_METHODID 22
 #define ACER_WMID_GET_GAMING_MISC_SETTING_METHODID 23
 
@@ -79,6 +84,12 @@ MODULE_LICENSE("GPL");
 #define ACER_GAMING_FAN_BEHAVIOR_ID_MASK GENMASK_ULL(15, 0)
 #define ACER_GAMING_FAN_BEHAVIOR_SET_CPU_MODE_MASK GENMASK(17, 16)
 #define ACER_GAMING_FAN_BEHAVIOR_SET_GPU_MODE_MASK GENMASK(23, 22)
+#define ACER_GAMING_FAN_BEHAVIOR_GET_CPU_MODE_MASK GENMASK(9, 8)
+#define ACER_GAMING_FAN_BEHAVIOR_GET_GPU_MODE_MASK GENMASK(15, 14)
+
+#define ACER_GAMING_FAN_SPEED_STATUS_MASK GENMASK_ULL(7, 0)
+#define ACER_GAMING_FAN_SPEED_ID_MASK GENMASK_ULL(7, 0)
+#define ACER_GAMING_FAN_SPEED_VALUE_MASK GENMASK_ULL(15, 8)
 
 #define ACER_GAMING_MISC_SETTING_STATUS_MASK GENMASK_ULL(7, 0)
 #define ACER_GAMING_MISC_SETTING_INDEX_MASK GENMASK_ULL(7, 0)
@@ -129,6 +140,11 @@ enum acer_wmi_predator_v4_sensor_id {
        ACER_WMID_SENSOR_GPU_TEMPERATURE        = 0x0A,
 };
 
+enum acer_wmi_gaming_fan_id {
+       ACER_WMID_CPU_FAN       = 0x01,
+       ACER_WMID_GPU_FAN       = 0x04,
+};
+
 enum acer_wmi_gaming_fan_mode {
        ACER_WMID_FAN_MODE_AUTO         = 0x01,
        ACER_WMID_FAN_MODE_TURBO        = 0x02,
@@ -292,6 +308,7 @@ struct hotkey_function_type_aa {
 #define ACER_CAP_TURBO_FAN             BIT(9)
 #define ACER_CAP_PLATFORM_PROFILE      BIT(10)
 #define ACER_CAP_HWMON                 BIT(11)
+#define ACER_CAP_PWM                   BIT(12)
 
 /*
  * Interface type flags
@@ -386,6 +403,7 @@ struct quirk_entry {
        u8 cpu_fans;
        u8 gpu_fans;
        u8 predator_v4;
+       u8 pwm;
 };
 
 static struct quirk_entry *quirks;
@@ -405,6 +423,9 @@ static void __init set_quirks(void)
        if (quirks->predator_v4)
                interface->capability |= ACER_CAP_PLATFORM_PROFILE |
                                         ACER_CAP_HWMON;
+
+       if (quirks->pwm)
+               interface->capability |= ACER_CAP_PWM;
 }
 
 static int __init dmi_matched(const struct dmi_system_id *dmi)
@@ -1653,6 +1674,39 @@ static int WMID_gaming_set_fan_behavior(u16 fan_bitmap, enum acer_wmi_gaming_fan
        return 0;
 }
 
+static int WMID_gaming_get_fan_behavior(u16 fan_bitmap, enum acer_wmi_gaming_fan_mode *mode)
+{
+       acpi_status status;
+       u32 input = 0;
+       u64 result;
+       int value;
+
+       input |= FIELD_PREP(ACER_GAMING_FAN_BEHAVIOR_ID_MASK, fan_bitmap);
+       status = WMI_gaming_execute_u32_u64(ACER_WMID_GET_GAMING_FAN_BEHAVIOR_METHODID, input,
+                                           &result);
+       if (ACPI_FAILURE(status))
+               return -EIO;
+
+       /* The return status must be zero for the operation to have succeeded */
+       if (FIELD_GET(ACER_GAMING_FAN_BEHAVIOR_STATUS_MASK, result))
+               return -EIO;
+
+       /* Theoretically multiple fans can be specified, but this is currently unused */
+       if (fan_bitmap & ACER_GAMING_FAN_BEHAVIOR_CPU)
+               value = FIELD_GET(ACER_GAMING_FAN_BEHAVIOR_GET_CPU_MODE_MASK, result);
+       else if (fan_bitmap & ACER_GAMING_FAN_BEHAVIOR_GPU)
+               value = FIELD_GET(ACER_GAMING_FAN_BEHAVIOR_GET_GPU_MODE_MASK, result);
+       else
+               return -EINVAL;
+
+       if (value < ACER_WMID_FAN_MODE_AUTO || value > ACER_WMID_FAN_MODE_CUSTOM)
+               return -ENXIO;
+
+       *mode = value;
+
+       return 0;
+}
+
 static void WMID_gaming_set_fan_mode(enum acer_wmi_gaming_fan_mode mode)
 {
        u16 fan_bitmap = 0;
@@ -1666,6 +1720,55 @@ static void WMID_gaming_set_fan_mode(enum acer_wmi_gaming_fan_mode mode)
        WMID_gaming_set_fan_behavior(fan_bitmap, mode);
 }
 
+static int WMID_gaming_set_gaming_fan_speed(u8 fan, u8 speed)
+{
+       acpi_status status;
+       u64 input = 0;
+       u64 result;
+
+       if (speed > 100)
+               return -EINVAL;
+
+       input |= FIELD_PREP(ACER_GAMING_FAN_SPEED_ID_MASK, fan);
+       input |= FIELD_PREP(ACER_GAMING_FAN_SPEED_VALUE_MASK, speed);
+
+       status = WMI_gaming_execute_u64(ACER_WMID_SET_GAMING_FAN_SPEED_METHODID, input, &result);
+       if (ACPI_FAILURE(status))
+               return -EIO;
+
+       switch (FIELD_GET(ACER_GAMING_FAN_SPEED_STATUS_MASK, result)) {
+       case 0x00:
+               return 0;
+       case 0x01:
+               return -ENODEV;
+       case 0x02:
+               return -EINVAL;
+       default:
+               return -ENXIO;
+       }
+}
+
+static int WMID_gaming_get_gaming_fan_speed(u8 fan, u8 *speed)
+{
+       acpi_status status;
+       u32 input = 0;
+       u64 result;
+
+       input |= FIELD_PREP(ACER_GAMING_FAN_SPEED_ID_MASK, fan);
+
+       status = WMI_gaming_execute_u32_u64(ACER_WMID_GET_GAMING_FAN_SPEED_METHODID, input,
+                                           &result);
+       if (ACPI_FAILURE(status))
+               return -EIO;
+
+       if (FIELD_GET(ACER_GAMING_FAN_SPEED_STATUS_MASK, result))
+               return -ENODEV;
+
+       *speed = FIELD_GET(ACER_GAMING_FAN_SPEED_VALUE_MASK, result);
+
+       return 0;
+}
+
 static int WMID_gaming_set_misc_setting(enum acer_wmi_gaming_misc_setting setting, u8 value)
 {
        acpi_status status;
@@ -2792,6 +2895,16 @@ static const enum acer_wmi_predator_v4_sensor_id acer_wmi_fan_channel_to_sensor_
        [1] = ACER_WMID_SENSOR_GPU_FAN_SPEED,
 };
 
+static const enum acer_wmi_gaming_fan_id acer_wmi_fan_channel_to_fan_id[] = {
+       [0] = ACER_WMID_CPU_FAN,
+       [1] = ACER_WMID_GPU_FAN,
+};
+
+static const u16 acer_wmi_fan_channel_to_fan_bitmap[] = {
+       [0] = ACER_GAMING_FAN_BEHAVIOR_CPU,
+       [1] = ACER_GAMING_FAN_BEHAVIOR_GPU,
+};
+
 static umode_t acer_wmi_hwmon_is_visible(const void *data,
                                         enum hwmon_sensor_types type, u32 attr,
                                         int channel)
@@ -2803,6 +2916,11 @@ static umode_t acer_wmi_hwmon_is_visible(const void *data,
        case hwmon_temp:
                sensor_id = acer_wmi_temp_channel_to_sensor_id[channel];
                break;
+       case hwmon_pwm:
+               if (!has_cap(ACER_CAP_PWM))
+                       return 0;
+
+               fallthrough;
        case hwmon_fan:
                sensor_id = acer_wmi_fan_channel_to_sensor_id[channel];
                break;
@@ -2810,8 +2928,12 @@ static umode_t acer_wmi_hwmon_is_visible(const void *data,
                return 0;
        }
 
-       if (*supported_sensors & BIT(sensor_id - 1))
+       if (*supported_sensors & BIT(sensor_id - 1)) {
+               if (type == hwmon_pwm)
+                       return 0644;
+
                return 0444;
+       }
 
        return 0;
 }
@@ -2820,6 +2942,9 @@ static int acer_wmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
                               u32 attr, int channel, long *val)
 {
        u64 command = ACER_WMID_CMD_GET_PREDATOR_V4_SENSOR_READING;
+       enum acer_wmi_gaming_fan_mode mode;
+       u16 fan_bitmap;
+       u8 fan, speed;
        u64 result;
        int ret;
 
@@ -2845,6 +2970,80 @@ static int acer_wmi_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
 
                *val = FIELD_GET(ACER_PREDATOR_V4_SENSOR_READING_BIT_MASK, result);
                return 0;
+       case hwmon_pwm:
+               switch (attr) {
+               case hwmon_pwm_input:
+                       fan = acer_wmi_fan_channel_to_fan_id[channel];
+                       ret = WMID_gaming_get_gaming_fan_speed(fan, &speed);
+                       if (ret < 0)
+                               return ret;
+
+                       *val = fixp_linear_interpolate(0, 0, 100, U8_MAX, speed);
+                       return 0;
+               case hwmon_pwm_enable:
+                       fan_bitmap = acer_wmi_fan_channel_to_fan_bitmap[channel];
+                       ret = WMID_gaming_get_fan_behavior(fan_bitmap, &mode);
+                       if (ret < 0)
+                               return ret;
+
+                       switch (mode) {
+                       case ACER_WMID_FAN_MODE_AUTO:
+                               *val = 2;
+                               return 0;
+                       case ACER_WMID_FAN_MODE_TURBO:
+                               *val = 0;
+                               return 0;
+                       case ACER_WMID_FAN_MODE_CUSTOM:
+                               *val = 1;
+                               return 0;
+                       default:
+                               return -ENXIO;
+                       }
+               default:
+                       return -EOPNOTSUPP;
+               }
+       default:
+               return -EOPNOTSUPP;
+       }
+}
+
+static int acer_wmi_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
+                               u32 attr, int channel, long val)
+{
+       enum acer_wmi_gaming_fan_mode mode;
+       u16 fan_bitmap;
+       u8 fan, speed;
+
+       switch (type) {
+       case hwmon_pwm:
+               switch (attr) {
+               case hwmon_pwm_input:
+                       fan = acer_wmi_fan_channel_to_fan_id[channel];
+                       speed = fixp_linear_interpolate(0, 0, U8_MAX, 100,
+                                                       clamp_val(val, 0, U8_MAX));
+
+                       return WMID_gaming_set_gaming_fan_speed(fan, speed);
+               case hwmon_pwm_enable:
+                       fan_bitmap = acer_wmi_fan_channel_to_fan_bitmap[channel];
+
+                       switch (val) {
+                       case 0:
+                               mode = ACER_WMID_FAN_MODE_TURBO;
+                               break;
+                       case 1:
+                               mode = ACER_WMID_FAN_MODE_CUSTOM;
+                               break;
+                       case 2:
+                               mode = ACER_WMID_FAN_MODE_AUTO;
+                               break;
+                       default:
+                               return -EINVAL;
+                       }
+
+                       return WMID_gaming_set_fan_behavior(fan_bitmap, mode);
+               default:
+                       return -EOPNOTSUPP;
+               }
        default:
                return -EOPNOTSUPP;
        }
@@ -2860,11 +3059,16 @@ static const struct hwmon_channel_info *const acer_wmi_hwmon_info[] = {
                           HWMON_F_INPUT,
                           HWMON_F_INPUT
                           ),
+       HWMON_CHANNEL_INFO(pwm,
+                          HWMON_PWM_INPUT | HWMON_PWM_ENABLE,
+                          HWMON_PWM_INPUT | HWMON_PWM_ENABLE
+                          ),
        NULL
 };
 
 static const struct hwmon_ops acer_wmi_hwmon_ops = {
        .read = acer_wmi_hwmon_read,
+       .write = acer_wmi_hwmon_write,
        .is_visible = acer_wmi_hwmon_is_visible,
 };