On Sun May 11, 2025 at 5:44 PM -03, Antheas Kapenekakis wrote: > From: Armin Wolf <W_Armin@xxxxxx> > > Adds fan curve support for the MSI platform. These devices contain > support for two fans, where they are named CPU and GPU but in the > case of the Claw series just map to left and right fan. > > Co-developed-by: Antheas Kapenekakis <lkml@xxxxxxxxxxx> > Signed-off-by: Antheas Kapenekakis <lkml@xxxxxxxxxxx> > Signed-off-by: Armin Wolf <W_Armin@xxxxxx> > --- > .../wmi/devices/msi-wmi-platform.rst | 26 ++ > drivers/platform/x86/msi-wmi-platform.c | 328 +++++++++++++++++- > 2 files changed, 337 insertions(+), 17 deletions(-) > > diff --git a/Documentation/wmi/devices/msi-wmi-platform.rst b/Documentation/wmi/devices/msi-wmi-platform.rst > index 73197b31926a5..704bfdac5203e 100644 > --- a/Documentation/wmi/devices/msi-wmi-platform.rst > +++ b/Documentation/wmi/devices/msi-wmi-platform.rst > @@ -169,6 +169,32 @@ The fan RPM readings can be calculated with the following formula: > > If the fan speed reading is zero, then the fan RPM is zero too. > > +The subfeature ``0x01`` is used to retrieve the fan speed table for the CPU fan. The output > +data contains the fan speed table and two bytes with unknown data. The fan speed table > +consists of six 8-bit entries, each containing a fan speed value in percent. > + > +The subfeature ``0x02`` is used tho retrieve the same data for the GPU fan. > + > +WMI method Set_Fan() > +-------------------- > + > +The fan speed tables can be accessed using subfeature ``0x01`` (CPU fan) and subfeature ``0x02`` > +(GPU fan). The input data has the same format as the output data of the ``Get_Fan`` WMI method. > + > +WMI method Get_AP() > +------------------- > + > +The current fan mode can be accessed using subfeature ``0x01``. The output data contains a flag > +byte and two bytes of unknown data. If the 7th bit inside the flag byte is cleared then all fans > +are operating in automatic mode, otherwise the fans operate based on the fan speed tables > +accessible thru the ``Get_Fan``/``Set_Fan`` WMI methods. > + > +WMI method Set_AP() > +------------------- > + > +The current fan mode can be changed using subfeature ``0x01``. The input data has the same format > +as the output data of the ``Get_AP`` WMI method. > + > WMI method Get_WMI() > -------------------- > > diff --git a/drivers/platform/x86/msi-wmi-platform.c b/drivers/platform/x86/msi-wmi-platform.c > index 408d42ab19e20..9ac3c6f1b3f1d 100644 > --- a/drivers/platform/x86/msi-wmi-platform.c > +++ b/drivers/platform/x86/msi-wmi-platform.c > @@ -16,13 +16,18 @@ > #include <linux/device/driver.h> > #include <linux/dmi.h> > #include <linux/errno.h> > +#include <linux/fixp-arith.h> > #include <linux/hwmon.h> > +#include <linux/hwmon-sysfs.h> > #include <linux/kernel.h> > +#include <linux/kstrtox.h> > +#include <linux/minmax.h> > #include <linux/module.h> > #include <linux/mutex.h> > #include <linux/printk.h> > #include <linux/rwsem.h> > #include <linux/string.h> > +#include <linux/sysfs.h> > #include <linux/types.h> > #include <linux/wmi.h> > > @@ -34,9 +39,11 @@ > > #define MSI_WMI_PLATFORM_INTERFACE_VERSION 2 > > +/* Get_WMI() WMI method */ > #define MSI_PLATFORM_WMI_MAJOR_OFFSET 1 > #define MSI_PLATFORM_WMI_MINOR_OFFSET 2 > > +/* Get_EC() and Set_EC() WMI methods */ > #define MSI_PLATFORM_EC_FLAGS_OFFSET 1 > #define MSI_PLATFORM_EC_MINOR_MASK GENMASK(3, 0) > #define MSI_PLATFORM_EC_MAJOR_MASK GENMASK(5, 4) > @@ -44,6 +51,18 @@ > #define MSI_PLATFORM_EC_IS_TIGERLAKE BIT(7) > #define MSI_PLATFORM_EC_VERSION_OFFSET 2 > > +/* Get_Fan() and Set_Fan() WMI methods */ > +#define MSI_PLATFORM_FAN_SUBFEATURE_FAN_SPEED 0x0 > +#define MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE 0x1 > +#define MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE 0x2 > +#define MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE 0x1 > +#define MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE 0x2 Would be nice to have all these subfeatures described in docs. > + > +/* Get_AP() and Set_AP() WMI methods */ > +#define MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE 0x1 > +#define MSI_PLATFORM_AP_FAN_FLAGS_OFFSET 1 > +#define MSI_PLATFORM_AP_ENABLE_FAN_TABLES BIT(7) > + > static bool force; > module_param_unsafe(force, bool, 0); > MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); > @@ -221,9 +240,201 @@ static int msi_wmi_platform_query(struct msi_wmi_platform_data *data, > } > } > > +static ssize_t msi_wmi_platform_fan_table_show(struct device *dev, struct device_attribute *attr, > + char *buf) > +{ > + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); > + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); > + u8 buffer[32] = { sattr->nr }; > + u8 fan_percent; > + int ret; > + > + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buffer, sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + fan_percent = buffer[sattr->index + 1]; > + if (fan_percent > 100) > + return -EIO; > + > + return sysfs_emit(buf, "%d\n", fixp_linear_interpolate(0, 0, 100, 255, fan_percent)); > +} > + > +static ssize_t msi_wmi_platform_fan_table_store(struct device *dev, struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); > + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); > + u8 buffer[32] = { sattr->nr }; > + long speed; > + int ret; > + > + ret = kstrtol(buf, 10, &speed); > + if (ret < 0) > + return ret; > + > + speed = clamp_val(speed, 0, 255); > + > + guard(mutex)(&data->wmi_lock); > + > + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_GET_FAN, > + buffer, sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + buffer[0] = sattr->nr; > + buffer[sattr->index + 1] = fixp_linear_interpolate(0, 0, 255, 100, speed); > + > + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_FAN, > + buffer, sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + return count; > +} > + > +static ssize_t msi_wmi_platform_temp_table_show(struct device *dev, struct device_attribute *attr, > + char *buf) > +{ > + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); > + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); > + u8 buffer[32] = { sattr->nr }; > + u8 temp_c; > + int ret; > + > + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_TEMPERATURE, > + buffer, sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + temp_c = buffer[sattr->index + 1]; HWMON ABI specifies temps are given in millidegrees for tempX_* attributes. It does not specify anything for `auto_point` attributes, but I think this should be millidegrees, for consistency. > + > + return sysfs_emit(buf, "%d\n", temp_c); > +} > + > +static ssize_t msi_wmi_platform_temp_table_store(struct device *dev, struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr); > + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); > + u8 buffer[32] = { sattr->nr }; > + long temp_c; > + int ret; > + > + ret = kstrtol(buf, 10, &temp_c); > + if (ret < 0) > + return ret; > + > + temp_c = clamp_val(temp_c, 0, 255); > + > + guard(mutex)(&data->wmi_lock); > + > + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_GET_TEMPERATURE, > + buffer, sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + buffer[0] = sattr->nr; > + buffer[sattr->index + 1] = temp_c; > + > + ret = msi_wmi_platform_query_unlocked(data, MSI_PLATFORM_SET_TEMPERATURE, > + buffer, sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + return count; > +} > + > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x0); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x3); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x4); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x5); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x6); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_TEMP_TABLE, 0x7); > + > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x1); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x2); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x3); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x4); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x5); > +static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_CPU_FAN_TABLE, 0x6); > + > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x0); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x3); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x4); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x5); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x6); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_temp, msi_wmi_platform_temp_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_TEMP_TABLE, 0x7); > + > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x1); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x2); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x3); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x4); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x5); > +static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_pwm, msi_wmi_platform_fan_table, > + MSI_PLATFORM_FAN_SUBFEATURE_GPU_FAN_TABLE, 0x6); > + > +static struct attribute *msi_wmi_platform_hwmon_attrs[] = { > + &sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point2_temp.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point3_temp.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point4_temp.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point5_temp.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point6_temp.dev_attr.attr, > + > + &sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point2_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point3_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point4_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point5_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm1_auto_point6_pwm.dev_attr.attr, > + > + &sensor_dev_attr_pwm2_auto_point1_temp.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point2_temp.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point3_temp.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point4_temp.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point5_temp.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point6_temp.dev_attr.attr, > + > + &sensor_dev_attr_pwm2_auto_point1_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point2_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point3_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point4_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point5_pwm.dev_attr.attr, > + &sensor_dev_attr_pwm2_auto_point6_pwm.dev_attr.attr, > + NULL > +}; > +ATTRIBUTE_GROUPS(msi_wmi_platform_hwmon); > + > static umode_t msi_wmi_platform_is_visible(const void *drvdata, enum hwmon_sensor_types type, > u32 attr, int channel) > { > + if (type == hwmon_pwm && attr == hwmon_pwm_enable) > + return 0644; > + > return 0444; > } > > @@ -233,24 +444,102 @@ static int msi_wmi_platform_read(struct device *dev, enum hwmon_sensor_types typ > struct msi_wmi_platform_data *data = dev_get_drvdata(dev); > u8 buffer[32] = { 0 }; > u16 value; > + u8 flags; > int ret; > > - ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buf, sizeof(buf)); > - if (ret < 0) > - return ret; > + switch (type) { > + case hwmon_fan: > + switch (attr) { > + case hwmon_fan_input: > + buffer[0] = MSI_PLATFORM_FAN_SUBFEATURE_FAN_SPEED; > + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_FAN, buffer, > + sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + value = get_unaligned_be16(&buffer[channel * 2 + 1]); > + if (!value) > + *val = 0; > + else > + *val = 480000 / value; > + > + return 0; > + default: > + return -EOPNOTSUPP; > + } > + case hwmon_pwm: > + switch (attr) { > + case hwmon_pwm_enable: > + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; > + ret = msi_wmi_platform_query(data, MSI_PLATFORM_GET_AP, buffer, > + sizeof(buffer)); > + if (ret < 0) > + return ret; > + > + flags = buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET]; > + if (flags & MSI_PLATFORM_AP_ENABLE_FAN_TABLES) > + *val = 1; > + else > + *val = 2; > + > + return 0; > + default: > + return -EOPNOTSUPP; > + } > + default: > + return -EOPNOTSUPP; > + } > +} > > - value = get_unaligned_be16(&buffer[channel * 2 + 1]); > - if (!value) > - *val = 0; > - else > - *val = 480000 / value; > +static int msi_wmi_platform_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, > + int channel, long val) > +{ > + struct msi_wmi_platform_data *data = dev_get_drvdata(dev); > + u8 buffer[32] = { }; > + int ret; > > - return 0; > + switch (type) { > + case hwmon_pwm: > + switch (attr) { > + case hwmon_pwm_enable: > + guard(mutex)(&data->wmi_lock); > + > + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; > + ret = msi_wmi_platform_query_unlocked( > + data, MSI_PLATFORM_GET_AP, buffer, > + sizeof(buffer)); Broken format on these calls. > + if (ret < 0) > + return ret; > + > + buffer[0] = MSI_PLATFORM_AP_SUBFEATURE_FAN_MODE; > + switch (val) { > + case 1: > + buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET] |> + MSI_PLATFORM_AP_ENABLE_FAN_TABLES; > + break; > + case 2: > + buffer[MSI_PLATFORM_AP_FAN_FLAGS_OFFSET] &> + ~MSI_PLATFORM_AP_ENABLE_FAN_TABLES; > + break; > + default: > + return -EINVAL; > + } > + > + return msi_wmi_platform_query_unlocked( > + data, MSI_PLATFORM_SET_AP, buffer, > + sizeof(buffer)); > + default: > + return -EOPNOTSUPP; > + } > + default: > + return -EOPNOTSUPP; > + } > } > > static const struct hwmon_ops msi_wmi_platform_ops = { > .is_visible = msi_wmi_platform_is_visible, > .read = msi_wmi_platform_read, > + .write = msi_wmi_platform_write, > }; > > static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { > @@ -260,6 +549,10 @@ static const struct hwmon_channel_info * const msi_wmi_platform_info[] = { > HWMON_F_INPUT, > HWMON_F_INPUT > ), > + HWMON_CHANNEL_INFO(pwm, > + HWMON_PWM_ENABLE, > + HWMON_PWM_ENABLE > + ), > NULL > }; > > @@ -268,8 +561,8 @@ static const struct hwmon_chip_info msi_wmi_platform_chip_info = { > .info = msi_wmi_platform_info, > }; > > -static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, size_t length, > - loff_t *offset) > +static ssize_t msi_wmi_platform_debugfs_write(struct file *fp, const char __user *input, Maybe mention this renames in commit messsage? -- ~ Kurt > + size_t length, loff_t *offset) > { > struct seq_file *seq = fp->private_data; > struct msi_wmi_platform_debugfs_data *data = seq->private; > @@ -303,7 +596,7 @@ static ssize_t msi_wmi_platform_write(struct file *fp, const char __user *input, > return length; > } > > -static int msi_wmi_platform_show(struct seq_file *seq, void *p) > +static int msi_wmi_platform_debugfs_show(struct seq_file *seq, void *p) > { > struct msi_wmi_platform_debugfs_data *data = seq->private; > int ret; > @@ -315,19 +608,19 @@ static int msi_wmi_platform_show(struct seq_file *seq, void *p) > return ret; > } > > -static int msi_wmi_platform_open(struct inode *inode, struct file *fp) > +static int msi_wmi_platform_debugfs_open(struct inode *inode, struct file *fp) > { > struct msi_wmi_platform_debugfs_data *data = inode->i_private; > > /* The seq_file uses the last byte of the buffer for detecting buffer overflows */ > - return single_open_size(fp, msi_wmi_platform_show, data, data->length + 1); > + return single_open_size(fp, msi_wmi_platform_debugfs_show, data, data->length + 1); > } > > static const struct file_operations msi_wmi_platform_debugfs_fops = { > .owner = THIS_MODULE, > - .open = msi_wmi_platform_open, > + .open = msi_wmi_platform_debugfs_open, > .read = seq_read, > - .write = msi_wmi_platform_write, > + .write = msi_wmi_platform_debugfs_write, > .llseek = seq_lseek, > .release = single_release, > }; > @@ -389,7 +682,8 @@ static int msi_wmi_platform_hwmon_init(struct msi_wmi_platform_data *data) > struct device *hdev; > > hdev = devm_hwmon_device_register_with_info(&data->wdev->dev, "msi_wmi_platform", data, > - &msi_wmi_platform_chip_info, NULL); > + &msi_wmi_platform_chip_info, > + msi_wmi_platform_hwmon_groups); > > return PTR_ERR_OR_ZERO(hdev); > }
Attachment:
signature.asc
Description: PGP signature