[PATCH 2/2] platform/x86: hp-wmi: Expose independent CPU/GPU pwm channels

From: foobisdweik

Date: Wed May 13 2026 - 15:41:04 EST


The Victus-S WMI fan-speed-set query (HPWMI_VICTUS_S_FAN_SPEED_SET_QUERY,
GM2E in firmware) takes a two-byte buffer [cpu, gpu] and writes the
values to EC SRP1/SRP2 setpoints independently. The driver however drove
both fans from a single hwmon pwm1 attribute, with the GPU value
derived as cpu + priv->gpu_delta from the fan table. This makes
asymmetric fan curves (common ask: GPU fan louder than CPU under
heavy gaming load) impossible from userspace fan-control tools that
expect one pwm attribute per fan.

Promote priv->pwm to a two-element array, add a second pwm hwmon
channel, and extend hp_wmi_hwmon_write() / hp_wmi_apply_fan_settings()
to drive the per-channel setpoints through GM2E.

The U8_MAX sentinel in hp_wmi_fan_speed_set() selects the new
per-fan path; existing callers using HP_FAN_SPEED_AUTOMATIC or a
direct rpm value keep their behavior. The second pwm channel is only
visible on Victus-S-capable boards, since other HP laptops still use
the legacy single-fan WMI commands.

Tested on OMEN by HP Laptop 16-b1xxx (8A13): writing pwm1=60, pwm2=200
in manual mode drives CPU fan to ~1600 RPM and GPU fan to ~5700 RPM
simultaneously, verified via fan1_input/fan2_input tachs and direct
read of EC offsets 0xB0..0xB3.

Signed-off-by: foobisdweik <dweikmferris@xxxxxxxxx>
---
drivers/platform/x86/hp/hp-wmi.c | 45 +++++++++++++++++++++++---------
1 file changed, 32 insertions(+), 13 deletions(-)

diff --git a/drivers/platform/x86/hp/hp-wmi.c b/drivers/platform/x86/hp/hp-wmi.c
index 389506a6d2e3..40eb3715583a 100644
--- a/drivers/platform/x86/hp/hp-wmi.c
+++ b/drivers/platform/x86/hp/hp-wmi.c
@@ -486,7 +486,7 @@ struct hp_wmi_hwmon_priv {
u8 max_rpm;
int gpu_delta;
u8 mode;
- u8 pwm;
+ u8 pwm[2];
struct delayed_work keep_alive_dwork;
};

@@ -822,12 +822,18 @@ static int hp_wmi_fan_speed_set(struct hp_wmi_hwmon_priv *priv, u8 speed)
fan_speed[GPU_FAN] = speed;

/*
- * GPU fan speed is always a little higher than CPU fan speed, we fetch
- * this delta value from the fan table during hwmon init.
- * Exception: Speed is set to HP_FAN_SPEED_AUTOMATIC, to revert to
- * automatic mode.
+ * Pass-through value U8_MAX: drive each fan from its own
+ * priv->pwm[] setpoint converted via pwm_to_rpm(). Used by the
+ * hwmon pwm1/pwm2 path that allows independent CPU/GPU fan control.
+ *
+ * Otherwise: GPU fan speed is always a little higher than CPU fan
+ * speed; we fetch this delta from the fan table during hwmon init.
+ * Exception: HP_FAN_SPEED_AUTOMATIC reverts to automatic mode.
*/
- if (speed != HP_FAN_SPEED_AUTOMATIC) {
+ if (speed == U8_MAX) {
+ fan_speed[CPU_FAN] = pwm_to_rpm(priv->pwm[CPU_FAN], priv);
+ fan_speed[GPU_FAN] = pwm_to_rpm(priv->pwm[GPU_FAN], priv);
+ } else if (speed != HP_FAN_SPEED_AUTOMATIC) {
gpu_speed = speed + priv->gpu_delta;
fan_speed[GPU_FAN] = clamp_val(gpu_speed, 0, U8_MAX);
}
@@ -2398,7 +2404,7 @@ static int hp_wmi_apply_fan_settings(struct hp_wmi_hwmon_priv *priv)
case PWM_MODE_MANUAL:
if (!is_victus_s_thermal_profile())
return -EOPNOTSUPP;
- ret = hp_wmi_fan_speed_set(priv, pwm_to_rpm(priv->pwm, priv));
+ ret = hp_wmi_fan_speed_set(priv, U8_MAX);
if (ret < 0)
return ret;
mod_delayed_work(system_wq, &priv->keep_alive_dwork,
@@ -2429,6 +2435,12 @@ static umode_t hp_wmi_hwmon_is_visible(const void *data,
{
switch (type) {
case hwmon_pwm:
+ /*
+ * Second pwm channel only exists on Victus-S-style boards
+ * which expose an independent GPU fan setpoint.
+ */
+ if (channel == 1 && !is_victus_s_thermal_profile())
+ return 0;
if (attr == hwmon_pwm_input && !is_victus_s_thermal_profile())
return 0;
return 0644;
@@ -2514,7 +2526,7 @@ static int hp_wmi_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
/* ensure PWM input is within valid fan speeds */
rpm = pwm_to_rpm(val, priv);
rpm = clamp_val(rpm, priv->min_rpm, priv->max_rpm);
- priv->pwm = rpm_to_pwm(rpm, priv);
+ priv->pwm[channel] = rpm_to_pwm(rpm, priv);
return hp_wmi_apply_fan_settings(priv);
}
switch (val) {
@@ -2525,13 +2537,18 @@ static int hp_wmi_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
if (!is_victus_s_thermal_profile())
return -EOPNOTSUPP;
/*
- * When switching to manual mode, set fan speed to
- * current RPM values to ensure a smooth transition.
+ * When switching to manual mode, seed each per-fan
+ * setpoint from its current measured RPM so the
+ * transition is smooth.
*/
- rpm = hp_wmi_get_fan_speed_victus_s(channel);
+ rpm = hp_wmi_get_fan_speed_victus_s(CPU_FAN);
+ if (rpm < 0)
+ return rpm;
+ priv->pwm[CPU_FAN] = rpm_to_pwm(rpm / 100, priv);
+ rpm = hp_wmi_get_fan_speed_victus_s(GPU_FAN);
if (rpm < 0)
return rpm;
- priv->pwm = rpm_to_pwm(rpm / 100, priv);
+ priv->pwm[GPU_FAN] = rpm_to_pwm(rpm / 100, priv);
priv->mode = PWM_MODE_MANUAL;
return hp_wmi_apply_fan_settings(priv);
case PWM_MODE_AUTO:
@@ -2547,7 +2564,9 @@ static int hp_wmi_hwmon_write(struct device *dev, enum hwmon_sensor_types type,

static const struct hwmon_channel_info * const info[] = {
HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT, HWMON_F_INPUT),
- HWMON_CHANNEL_INFO(pwm, HWMON_PWM_ENABLE | HWMON_PWM_INPUT),
+ HWMON_CHANNEL_INFO(pwm,
+ HWMON_PWM_ENABLE | HWMON_PWM_INPUT,
+ HWMON_PWM_ENABLE | HWMON_PWM_INPUT),
NULL
};

--
2.54.0