From nobody Mon Sep 28 12:33:12 2026 Received: from mail-pf1-f172.google.com (mail-pf1-f172.google.com [209.85.210.172]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 52C283AFD17 for ; Fri, 21 Aug 2026 21:47:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.172 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787348862; cv=none; b=hAM+Q00FoENr8+XTImIyDIlSlcWFnS9rNcN6qPUjn5+z5wJPdf38BLeylVZSID1t7wGXVVxabRUl6aC8E698utiamfQAaT5lqijfhexdntddEw21Zp5cB7gy9yxGDjHtl62vMn83ullMKsojqhUSuP5VyyONyo+XDhe69H702Nc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787348862; c=relaxed/simple; bh=F4LO7wmBLLIwQF+4rvvhzn4HaS/bCVrX06hJixd63Xg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=MIqgCzd3BkhpfiE4ui+7XY9UsvaAqF9alu5ldXSBtOjC+prdPCOA8lQfrdqgmYexJK6gP62E1suPoIq8Dd4U4xYDCRNFQJ+hzycECayRSC97duYxhfoBt07stFT7WlvD0wK/KTskyAxokAAE8pYTyYh/IwBJebCCxoq+7RFtFtY= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=IZU23ykz; arc=none smtp.client-ip=209.85.210.172 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="IZU23ykz" Received: by mail-pf1-f172.google.com with SMTP id d2e1a72fcca58-848761b5897so623558b3a.3 for ; Fri, 21 Aug 2026 14:47:41 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787348861; x=1787953661; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=fkDDTrK1TveWR5ufs0Rnfw82A2Z1a3+coe/mtCRQsac=; b=IZU23ykzsGxzVPT+PBOlLNvMBm8ESNMYGLYt4Qp5pm0BWNPnaivAjQGXZd2CL9ZEFW 8z8uwfP7EjM1p/zVRgSClgQul3bTdFTxKvZETKXnMLWcY0jBKNEQmHp3ZuSnwiTsml6d ZXJ9QRoyGG4Pb+LPb3QnBC9QPZQBMG1nfKHLICOfaLP1z2lnj3kaQxYW9vwk9FxxCtzM SdtLK3vHMeeWUbgPdJ2UsMtzZl0iYkLMBxerxsJu37wU6py1+5FgtaL5NAGcwzwGphAA PJY68alBVfNIcnftIiaYfdNEvd5EF3hytbZFjOSP7yGZNvOwoLZ6GrtOYuKHsZwIastM zWEw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787348861; x=1787953661; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=fkDDTrK1TveWR5ufs0Rnfw82A2Z1a3+coe/mtCRQsac=; b=QPQ4wyIAskEXBw9i5ErHSgCzX6yHvbkyBALMEdu0jH4bdj1YbNyXVmVrgBRWG/t461 sGgLFo1TbetHHwU8wD/NOWQdsT03xlBiNjugHVxtUD4KKdiM0ClQgVLV+BpZ61+gMymD HkkPJrA1FxQcPmd7ExUVFyXisWwLJustJ1leLXNAyR5qIsdHVJZHEiv9q+LKSv0XDxzH CrdideegfnGOAzIdv+/UGksE3tVhKBs7G4P33qYJ2zLtc/VyNgnivVgL27lsgo+b3UAE kJllftM88QO7L/une8rYz0+r0Reaw2QmrBbTP3BdLwBfA0fJjHSglguEwH3glASySBO4 gtkA== X-Forwarded-Encrypted: i=1; AHgh+Rr1rOC7sQyqYeV6L0kR93vddqnFiSyB9TmQEGaA2zR2HlSGtL89qjK8VvYRIxoW9QDgPKuyvhhc4RvRDTg=@vger.kernel.org X-Gm-Message-State: AFuF++kaBG4782FVMXl5j7GTH/Tz89EYY5ab9SBZWgZdsXUZv+WOseng I++Miah5PxeM6u3TNcx6X7jQZUCUiriaB06+W7O9QMtgIpagLTbZvU5t X-Gm-Gg: AR+sD115HtkYmQhrwl3e9DinDi1ndQ4a5XQmsQ6iDTzPXfIabYzRd041paji6r7vvYl 7MSRFcZ3UIbqqBxypCx06uMTlumG0hTlc/sgbrAhI7BvpAYEHSqEv/POs1ffbZ0e5XwWHHMcKLH MMlT4ykSoEvqcWqpOJtn2XXAEF/B5Z8yxy1RuxuolRB3XRaJznEkon7TtoGgVEWQcGDfAdGZ/Ak 335BK3CaN/mWnrIop0h1nBlWheig+zCit+T5uYoS1FBRglGnyGa4XEf2BtxLTDF99iZQ6lsik9Q wryhMDhQeQtEywHPo1qgNczKeMYTwf46YjbBoWR0cNsgYZ18beO9+tj7+6XlLGmOpFKebknvzSM PCWz1OcYwVOQR1iLqRNTMPlLFZBqpQnnNeDtBCx6yljS2DEEok5jG2UWb+6WVDThqZuS+lOnxAx uqmJ+MktJbTUjwVHjFU47inB1fWXWNO0bf5BJ90YtABiLvPNo3Mqm56B7xFmYEcizqYa7fT4kQm 3uPMOcuItbityHHLHg= X-Received: by 2002:a05:6a20:684a:b0:3cd:1037:36d6 with SMTP id adf61e73a8af0-3cd2fa5643bmr19087616637.0.1787348860697; Fri, 21 Aug 2026 14:47:40 -0700 (PDT) Received: from localhost.localdomain ([103.16.69.193]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-327bf10d497sm45664198eec.14.2026.08.21.14.47.35 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Fri, 21 Aug 2026 14:47:39 -0700 (PDT) From: Aditya Dash To: "Derek J. Clark" , =?UTF-8?q?Ilpo=20J=C3=A4rvinen?= , Armin Wolf , Hans de Goede , Mark Pearson Cc: Guenter Roeck , Jonathan Corbet , Shuah Khan , linux-doc@vger.kernel.org, linux-hwmon@vger.kernel.org, linux-kernel@vger.kernel.org, platform-driver-x86@vger.kernel.org Subject: [RFC PATCH 1/3] platform/x86: lenovo-wmi-other: Add Legion Go Full Speed control Date: Sat, 22 Aug 2026 03:17:25 +0530 Message-ID: <20260821214728.87773-2-mradityadash@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260821214728.87773-1-mradityadash@gmail.com> References: <20260821214728.87773-1-mradityadash@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Selected Legion Go firmware exposes Full Speed as Other Mode feature 0x04020000. Capability Data does not describe this feature, so lenovo-wmi-other currently ignores it. Probe the feature during HWMON setup on those products. If the read succeeds and returns a Boolean value, expose it as pwm1_enable. Value 0 selects Full Speed, and value 2 returns fan control to firmware automatic mode. Reject all other values. Assisted-by: Pi:gpt-5.6-sol Signed-off-by: Aditya Dash --- .../wmi/devices/lenovo-wmi-other.rst | 7 ++ drivers/platform/x86/lenovo/wmi-other.c | 108 +++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation= /wmi/devices/lenovo-wmi-other.rst index 011054d64eac..75f2deaaef16 100644 --- a/Documentation/wmi/devices/lenovo-wmi-other.rst +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst @@ -49,6 +49,13 @@ The following HWMON attributes are implemented: Due to the internal RPM divisor, the current/target RPMs are rounded down = to its nearest multiple. The divisor itself is not necessary to be a power of= two. =20 +Legion Go fan controls +---------------------- + +On supported Legion Go models, Other Mode feature ``0x04020000`` controls +Full Speed mode in firmware. The driver exposes it as ``pwm1_enable``. Val= ue 0 +enables Full Speed, and value 2 returns fan control to firmware. + LENOVO_CAPABILITY_DATA_01 ------------------------- =20 diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86= /lenovo/wmi-other.c index fbb32bf404f2..c180933e1d18 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -83,6 +84,7 @@ enum lwmi_feature_id_psu { LWMI_FEATURE_ID_PSU_CHARGE_BEHAVIOUR =3D 0x02, }; =20 +#define LWMI_FEATURE_ID_FAN_FULLSPEED 0x02 #define LWMI_FEATURE_ID_FAN_RPM 0x03 =20 #define LWMI_TYPE_ID_CROSSLOAD 0x01 @@ -102,6 +104,10 @@ enum lwmi_feature_id_psu { #define LWMI_CHARGE_TYPE_STANDARD 0x00 #define LWMI_CHARGE_TYPE_LONGLIFE 0x01 =20 +#define LWMI_ATTR_ID_FAN_FULLSPEED \ + lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_FULLSPEED, \ + LWMI_GZ_THERMAL_MODE_NONE, LWMI_TYPE_ID_NONE) + #define LWMI_ATTR_ID_FAN_RPM(x) \ lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_RPM, \ LWMI_GZ_THERMAL_MODE_NONE, LWMI_FAN_ID(x)) @@ -115,6 +121,50 @@ enum lwmi_feature_id_psu { =20 static DEFINE_IDA(lwmi_om_ida); =20 +static const struct dmi_system_id lwmi_fan_dmi_table[] =3D { + { + .ident =3D "Lenovo Legion Go 8APU1", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8APU1"), + }, + }, + { + .ident =3D "Lenovo Legion Go S 8APU1", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8APU1"), + }, + }, + { + .ident =3D "Lenovo Legion Go S 8ARP1", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8ARP1"), + }, + }, + { + .ident =3D "Lenovo Legion Go 8ASP2", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8ASP2"), + }, + }, + { + .ident =3D "Lenovo Legion Go 8AHP2", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8AHP2"), + }, + }, + {} +}; + +static bool lwmi_fan_supported(void) +{ + return dmi_check_system(lwmi_fan_dmi_table); +} + enum attribute_property { DEFAULT_VAL, MAX_VAL, @@ -144,6 +194,7 @@ struct lwmi_om_priv { int ida_id; =20 struct lwmi_fan_info fan_info[LWMI_FAN_NR]; + bool fullspeed_supported; =20 struct { bool capdata00_collected : 1; @@ -237,6 +288,38 @@ static int lwmi_om_fan_get_set(struct lwmi_om_priv *pr= iv, int channel, u32 *val, return (retval =3D=3D 0 || retval =3D=3D 1) ? 0 : -EIO; } =20 +static int lwmi_om_fullspeed_get(struct lwmi_om_priv *priv, long *enable) +{ + struct wmi_method_args_32 args =3D { + .arg0 =3D LWMI_ATTR_ID_FAN_FULLSPEED, + }; + u32 value; + int ret; + + ret =3D lwmi_dev_evaluate_int(priv->wdev, 0, LWMI_FEATURE_VALUE_GET, + (u8 *)&args, sizeof(args), &value); + if (ret) + return ret; + + if (value > 1) + return -ERANGE; + + *enable =3D value ? 0 : 2; + return 0; +} + +static int lwmi_om_fullspeed_set(struct lwmi_om_priv *priv, bool fullspeed) +{ + struct wmi_method_args_32 args =3D { + .arg0 =3D LWMI_ATTR_ID_FAN_FULLSPEED, + .arg1 =3D fullspeed, + }; + + /* The WMI method has no return value. */ + return lwmi_dev_evaluate_int(priv->wdev, 0, LWMI_FEATURE_VALUE_SET, + (u8 *)&args, sizeof(args), NULL); +} + /** * lwmi_om_hwmon_is_visible() - Determine visibility of HWMON attributes * @drvdata: Driver private data @@ -255,6 +338,10 @@ static umode_t lwmi_om_hwmon_is_visible(const void *dr= vdata, enum hwmon_sensor_t struct lwmi_om_priv *priv =3D (struct lwmi_om_priv *)drvdata; bool visible =3D false; =20 + if (type =3D=3D hwmon_pwm && priv->fullspeed_supported && channel =3D=3D = 0 && + attr =3D=3D hwmon_pwm_enable) + return 0644; + if (type =3D=3D hwmon_fan) { if (!(priv->fan_info[channel].supported & LWMI_SUPP_VALID)) return 0; @@ -311,6 +398,9 @@ static int lwmi_om_hwmon_read(struct device *dev, enum = hwmon_sensor_types type, u32 retval =3D 0; int err; =20 + if (type =3D=3D hwmon_pwm && attr =3D=3D hwmon_pwm_enable && channel =3D= =3D 0) + return lwmi_om_fullspeed_get(priv, val); + if (type =3D=3D hwmon_fan) { switch (attr) { /* @@ -366,6 +456,17 @@ static int lwmi_om_hwmon_write(struct device *dev, enu= m hwmon_sensor_types type, u32 raw, min_rpm, max_rpm; int err; =20 + if (type =3D=3D hwmon_pwm && attr =3D=3D hwmon_pwm_enable && channel =3D= =3D 0) { + switch (val) { + case 0: + return lwmi_om_fullspeed_set(priv, true); + case 2: + return lwmi_om_fullspeed_set(priv, false); + default: + return -EINVAL; + } + } + if (type =3D=3D hwmon_fan) { switch (attr) { case hwmon_fan_target: @@ -420,6 +521,7 @@ static const struct hwmon_channel_info * const lwmi_om_= hwmon_info[] =3D { HWMON_F_MIN | HWMON_F_MAX, HWMON_F_INPUT | HWMON_F_TARGET | HWMON_F_DIV | HWMON_F_MIN | HWMON_F_MAX), + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_ENABLE), NULL }; =20 @@ -440,6 +542,7 @@ static const struct hwmon_chip_info lwmi_om_hwmon_chip_= info =3D { */ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) { + long enable; int i, valid; =20 if (WARN_ON(priv->hwmon_dev)) @@ -458,6 +561,9 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) if (relax_fan_constraint) dev_warn(&priv->wdev->dev, "fan RPM constraint relaxed. Use with caution= \n"); =20 + priv->fullspeed_supported =3D + lwmi_fan_supported() && !lwmi_om_fullspeed_get(priv, &enable); + valid =3D 0; for (i =3D 0; i < LWMI_FAN_NR; i++) { if (!(priv->fan_info[i].supported & LWMI_SUPP_VALID)) @@ -474,7 +580,7 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) } } =20 - if (valid =3D=3D 0) { + if (valid =3D=3D 0 && !priv->fullspeed_supported) { dev_warn(&priv->wdev->dev, "fan reporting/tuning is unsupported on this device\n"); return; --=20 2.55.0 From nobody Mon Sep 28 12:33:12 2026 Received: from mail-pg1-f178.google.com (mail-pg1-f178.google.com [209.85.215.178]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3BB093C2BBE for ; Fri, 21 Aug 2026 21:47:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.215.178 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787348867; cv=none; b=IUA8hzOSdhPMDB+Gg0JNGIi5kvhhZTj16A/W/2W4C/g2VrVRKf9hd/ohflTCt1WaualZNFrRgtvNdgLjR5N/UhzUHY9D2KRTJ7OWGGfQbgCUDePknDHELRKEoveNG54rerNYzKiQlVSmBVOZANO2AC0qwo5HVuNpWMAtiMlJp7M= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787348867; c=relaxed/simple; bh=BON6K8iN4nul7ABm2ly+Mmj813l0uRbmJRamoL7U9ts=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=dnYTK/IwaPGqzIzjwTMsnh3Wh4oysTpy2ofE0UelBRTMoR1ZY4frC9cMRFkItpL1NBH8eiQB4kQQv8rjtblRVBq7hkamO2gf2Za6S/+Iq/rE1Xz+ItUH9nJA9apV8/C0TP6eIhtkBx/i+1d3UkH5TXjQwKa+KHYYsLqwlvA3hfw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=Ll62cRv4; arc=none smtp.client-ip=209.85.215.178 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="Ll62cRv4" Received: by mail-pg1-f178.google.com with SMTP id 41be03b00d2f7-ca766c1c9ccso1066895a12.0 for ; Fri, 21 Aug 2026 14:47:46 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787348865; x=1787953665; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=HwBWuHIqZxxUmc+OyU9PwGZ3GQWQsMUaZR3o5VyRTcE=; b=Ll62cRv4bgHpuFqsNArAtzDqmY1xBhwYZp+Q6Nn//BM+lnOzp8vunsKFhgCUXk+mns ECn9TkdFrbsp1SUz/0MfHMPQbId6/MdpFjbYBea0+GEl1Qn5fn2DkDaZv/GardQAJ/X5 CE+6KwLGHU2W7iTGHMWIl1FzHQsDeAkkR3E+K0TGeuniDcWcTcaSQN6ZcQhkf0LXPpqp O3wJT5jI20l1jGMRt9JuyfJAnvjpKiuwPqLncuYXdemATdom58EhHQrtUHkbWCUxM6N9 6508JbfOIBPEDId/Ewv9t6hptf1WUGNtWXgegkcxZeL2m8Im0OCYuMvGby2TulMdz1qB UG5A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787348865; x=1787953665; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=HwBWuHIqZxxUmc+OyU9PwGZ3GQWQsMUaZR3o5VyRTcE=; b=BVOZ66B94EtkhOFmLM2mmHtF94X09bRmssI9+bXoxPGzgM8Lxl8K7x/i9QVINE9BjY ZesNO4x7NEXUigHKi3/2LzxmMU5rE+cMJMRUfuVnnta4MTecKwFxdL0/wkqOi9AnbnM9 1f1tnKn4t3I4Y3V1pIlk+Ej5tLms9jtVhclvJbveEcQVN8uDp9GUaxFQ2iQ11RTbBX4d Eiq7+h+g43TBOO2DOoFNDl0N11Kc4Lm809+aTiDjW+ekFzR8AXVSzzsdNCl3V1sg1A1R txz5z3YpishmExrjrNB+/GHiNtHCVRotNGJT71tO8UIpUbQdi74iB33FYc5zL1u2uzzN bBVw== X-Forwarded-Encrypted: i=1; AHgh+RqKbrtwEQE05kelGwiKWij/ElwggXQfr5NYo9FAvf9Za2NKUOjFcz3Uy81yjh0mdQb8AbUTNXDDIWLZv/A=@vger.kernel.org X-Gm-Message-State: AFuF++kkZc/Um7BO4bqAa3IwMi7moHN0w5ur6Qtge/6ERDLD/k1jtJvc TuL5kPgh1llJq8czpvlWhp9krKX/cCE2yKL42ZWAdB3pIztDFk66TxIe X-Gm-Gg: AR+sD13MRzwz4pH7qOFhKbIKfByuMlniHIuTutJwbynk/EZBv3pl3ZhyKJZf05v59yF koo2wGQK9qFfs5dMwCxCqbaLy7vbeF/pGBZyhlPMEgEfohpcniZR9IgP5a7i/ePv4EJs2OjBkEo M2OpytvInxuNCV46QkKbrmb3Y89GXovPJ99o4KmrzX5Z7uScJ9mll0ENB1peq+RR7SLgKL7bWIr PenEE2yvwAaPLSY+4imUmLhA9am54wq6OVLtoEvEpGscIlEZI+AJz1iuwUu4tRywbrS36XST5FF 2EQCvWLDVg8q6eyk2PG1pOhVdqWbGPAw3H+NSWWAwMgQn1TBYblGxGKR5DzlOx3x9ErGvlutOdg 4kzhoR5Bzo0Qp18Ld52ouA5jx2C7wbbVhHOcqxI29q+oZtl8IdCwiqtqliDXwNHUisbtcpW2MIp jNDUZDE62Y7q9VWMsFeXqsMV4aewMZrocFKEfuIMggbeGsrhTJlWHvcjOLv/uIwPvSERu43LF2/ CaVmuLl0TvO+7ZPz6k= X-Received: by 2002:a05:6a21:3117:b0:3bf:a543:e7f5 with SMTP id adf61e73a8af0-3cd4b9e7d2bmr3040455637.3.1787348865532; Fri, 21 Aug 2026 14:47:45 -0700 (PDT) Received: from localhost.localdomain ([103.16.69.193]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-327bf10d497sm45664198eec.14.2026.08.21.14.47.41 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Fri, 21 Aug 2026 14:47:44 -0700 (PDT) From: Aditya Dash To: "Derek J. Clark" , =?UTF-8?q?Ilpo=20J=C3=A4rvinen?= , Armin Wolf , Hans de Goede , Mark Pearson Cc: Guenter Roeck , Jonathan Corbet , Shuah Khan , linux-doc@vger.kernel.org, linux-hwmon@vger.kernel.org, linux-kernel@vger.kernel.org, platform-driver-x86@vger.kernel.org Subject: [RFC PATCH 2/3] platform/x86: lenovo-wmi-other: Add Legion Go fan RPM fallback Date: Sat, 22 Aug 2026 03:17:26 +0530 Message-ID: <20260821214728.87773-3-mradityadash@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260821214728.87773-1-mradityadash@gmail.com> References: <20260821214728.87773-1-mradityadash@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Some Legion Go firmware does not mark fan 1 RPM as readable in Capability Data, although Other Mode feature 0x04030001 returns the current RPM. As a result, lenovo-wmi-other hides fan1_input. When the normal VALID and GET flags are missing on a supported product, try one read from the Other Mode feature. Expose fan1_input as read-only if the read succeeds and does not return 0xffffffff. Treat a later 0xffffffff reply as an unavailable reading. Assisted-by: Pi:gpt-5.6-sol Signed-off-by: Aditya Dash --- Documentation/wmi/devices/lenovo-wmi-other.rst | 4 ++++ drivers/platform/x86/lenovo/wmi-other.c | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation= /wmi/devices/lenovo-wmi-other.rst index 75f2deaaef16..c93e9e6f3fd0 100644 --- a/Documentation/wmi/devices/lenovo-wmi-other.rst +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst @@ -56,6 +56,10 @@ On supported Legion Go models, Other Mode feature ``0x04= 020000`` controls Full Speed mode in firmware. The driver exposes it as ``pwm1_enable``. Val= ue 0 enables Full Speed, and value 2 returns fan control to firmware. =20 +Some Legion Go firmware does not advertise fan 1 RPM through Capability +Data. In that case, the driver reads Other Mode feature ``0x04030001`` for +``fan1_input``. Value ``0xffffffff`` means that RPM is unavailable. + LENOVO_CAPABILITY_DATA_01 ------------------------- =20 diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86= /lenovo/wmi-other.c index c180933e1d18..b4be7739b243 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -98,6 +98,7 @@ enum lwmi_feature_id_psu { #define LWMI_FAN_ID(x) ((x) + LWMI_FAN_ID_BASE) =20 #define LWMI_FAN_DIV 100 +#define LWMI_FAN_RPM_NORMAL_SUPPORT (LWMI_SUPP_VALID | LWMI_SUPP_GET) =20 #define LWMI_CHARGE_BEHAVIOR_DISCHARGE 0x00 #define LWMI_CHARGE_BEHAVIOR_AUTO 0x01 @@ -195,6 +196,7 @@ struct lwmi_om_priv { =20 struct lwmi_fan_info fan_info[LWMI_FAN_NR]; bool fullspeed_supported; + bool fan0_input_fallback; =20 struct { bool capdata00_collected : 1; @@ -343,6 +345,8 @@ static umode_t lwmi_om_hwmon_is_visible(const void *drv= data, enum hwmon_sensor_t return 0644; =20 if (type =3D=3D hwmon_fan) { + if (channel =3D=3D 0 && priv->fan0_input_fallback && attr =3D=3D hwmon_f= an_input) + return 0444; if (!(priv->fan_info[channel].supported & LWMI_SUPP_VALID)) return 0; =20 @@ -419,6 +423,8 @@ static int lwmi_om_hwmon_read(struct device *dev, enum = hwmon_sensor_types type, err =3D lwmi_om_fan_get_set(priv, channel, &retval, false); if (err) return err; + if (channel =3D=3D 0 && priv->fan0_input_fallback && retval =3D=3D U32_= MAX) + return -EIO; =20 *val =3D retval; return 0; @@ -542,6 +548,7 @@ static const struct hwmon_chip_info lwmi_om_hwmon_chip_= info =3D { */ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) { + u32 rpm; long enable; int i, valid; =20 @@ -563,6 +570,11 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *pri= v) =20 priv->fullspeed_supported =3D lwmi_fan_supported() && !lwmi_om_fullspeed_get(priv, &enable); + priv->fan0_input_fallback =3D + lwmi_fan_supported() && + (priv->fan_info[0].supported & LWMI_FAN_RPM_NORMAL_SUPPORT) !=3D + LWMI_FAN_RPM_NORMAL_SUPPORT && + !lwmi_om_fan_get_set(priv, 0, &rpm, false) && rpm !=3D U32_MAX; =20 valid =3D 0; for (i =3D 0; i < LWMI_FAN_NR; i++) { @@ -580,7 +592,7 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) } } =20 - if (valid =3D=3D 0 && !priv->fullspeed_supported) { + if (valid =3D=3D 0 && !priv->fullspeed_supported && !priv->fan0_input_fal= lback) { dev_warn(&priv->wdev->dev, "fan reporting/tuning is unsupported on this device\n"); return; --=20 2.55.0 From nobody Mon Sep 28 12:33:12 2026 Received: from mail-pj1-f50.google.com (mail-pj1-f50.google.com [209.85.216.50]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7D9023B52F8 for ; Fri, 21 Aug 2026 21:47:51 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.216.50 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787348873; cv=none; b=mqKyhZC78qwfrqOrTkEms9lSeNW1nvehpIpzn5jrSqvQcvL58Zi/2F8SKzYxPokIh69r1Qa01PnLDcprL0I9TIkXdM6GNCs56W+dP+Mfryf2ygplu1btQ08reBW8ATu0YShYkg4eD9JUnWb56ktSJQ+gf6IsrhjPp0E2AlOj2Gs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787348873; c=relaxed/simple; bh=Pr+KO7YvysTbeph7TGLEHTJkVKFWzI6sdg5T9h6pLyE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=IKYmV9e6NruuFWxd3lbxE6gwfm9OIsIXr739YvRlt3XGFXxCowWIMA+dyJuFanZ4B+18GSSRvOmijvY3wIz4L1Tl5OBmJTs7OZP1y77awLsZRw3r3CxtvIyA+hsKuAIM5rI0C8K+LlWxV/6OapxPGWpoPD4i4vLJtxZxVwfoR2Q= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=hZiEdlEH; arc=none smtp.client-ip=209.85.216.50 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="hZiEdlEH" Received: by mail-pj1-f50.google.com with SMTP id 98e67ed59e1d1-38e58034d05so1270521a91.2 for ; Fri, 21 Aug 2026 14:47:51 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787348871; x=1787953671; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=jnZI8+nc+Knc3+TFhQOTQt3dD43caZ7wHkTTDp8PK3I=; b=hZiEdlEHtZgWy0+CY1zyuzP1T3hicMxm/0Lz5eaTJ+rX3RdeYH20+3zviVquTxTyva DKRWdIZz9n21i4h0lblh1Nl9r91zpReZsBzT6wU7u4TUGPVWbw9gwbi8WV4i9RLYQebk bExfidGgKHDtdMJ7IgYQCpvASuBBVKt1Y5CMIHjpRFe7F8w7qV0AON0jNHAlteG2n/BY fF5wgwoe5p8dMdAYnC0yfZPpW5zbJM+ISmsm1iI+E7shvBVLvQ67VF0aoz+5rEoKZvBV gwzc7EpyQjk45SSPl4fO6iJOvpO7NMoBrozqZbp0KhWWSiuZ1OiMiKDUsCqTbJRPang1 SfZA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787348871; x=1787953671; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=jnZI8+nc+Knc3+TFhQOTQt3dD43caZ7wHkTTDp8PK3I=; b=mTuteb+LaFHHanaUvb1lO9U+4GQO5ZtJESInBxlXA72IIdNrqTXcO4m2jUxbxO6Kw/ MhlsqOnfuj22/V6Yo6VKzMuu/kJ3WBMsHD4o6B5cgst3WExogpV0akWxtkJ5/n1AUEvJ 0LYqPthaw/cx/w5QYZ5cM3QFBfiCU8cc14bvZ5sAzk2yk40hRcMViRjtgi+eDG6b9DgS SbQ1Z6y9nhuASj8C+pxplJCX1owKfnow3suvzjdKJwv1ix5/VQ8SxpR0KTLu13PAn2jv zIRdHj1EbVpg3kG2WfZ1VP3OR/gqLIUN62s5g+iN6mRdq6r97+O8alCqeqXHzA5ywegm OBiQ== X-Forwarded-Encrypted: i=1; AHgh+Rp1SOrUkIQ5aBqfnH899D59DyMANqLqWivJCMWbCgkZk21C5UlC04SWIImN5YhfphPErzQaWXFLXRfixE8=@vger.kernel.org X-Gm-Message-State: AFuF++nRCQimqMktmi7S8UKmMDhWwMFKIhkZkyU+5VSNTpfZT+fgQ89W D6BC9hx2ZCS97N7bvRx0CNULghLSidZgNiB2i7vLDZgwQe9k1jxyX2tN X-Gm-Gg: AR+sD1055zxM4E87xKdXDW19+jVrzaGKQIGlY2HLpiVhp+HP3x2C5sTOE+LmLUPHANo hbgYernR20HEjZDYdlYY10qudkk6iNGv0uHSXbggWgZ21kwMJu1+w06YgegNYIuCTq0CY5lE+b5 mU+lSHK9hzIZr+syAxSatWfIiDiu6SPPThjF6upeW54/UnR/LLpiedIqI/ITjXMbiw5FYkAhxdH F+uHclpyWZwyIba1g/YGgEqwh4LXI9s6RK1QG5c5qnT5z2hWgELyctqGeCOAP937e1x7F0m37UK 62GfxSvzcVbNgnsstU3tN+uv3UNLRLCED9X1vOQ+MEKdY8mfzQ2K4Uqo7fE+Favx/tL7eNpcIIj MAjl9MFwPciVRoBD4zdezhYDjhuH7EuFEgNcauhMx1KONRBMQ9+OYzRU8fm33ibRBqQSLDGn8iq A8HRR0RvGILbvhm/aEoHCJU/9PLFaMXH3Ad6mdsA0mjq4KU7HYyWYtbPTfHdpocXHtoYKGii5ld UpDYBEZKMVSeIQNo3g= X-Received: by 2002:a17:90a:e7c5:b0:36d:9e0b:3801 with SMTP id 98e67ed59e1d1-395c34f47cbmr15716824a91.8.1787348870524; Fri, 21 Aug 2026 14:47:50 -0700 (PDT) Received: from localhost.localdomain ([103.16.69.193]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-327bf10d497sm45664198eec.14.2026.08.21.14.47.45 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Fri, 21 Aug 2026 14:47:49 -0700 (PDT) From: Aditya Dash To: "Derek J. Clark" , =?UTF-8?q?Ilpo=20J=C3=A4rvinen?= , Armin Wolf , Hans de Goede , Mark Pearson Cc: Guenter Roeck , Jonathan Corbet , Shuah Khan , linux-doc@vger.kernel.org, linux-hwmon@vger.kernel.org, linux-kernel@vger.kernel.org, platform-driver-x86@vger.kernel.org Subject: [RFC PATCH 3/3] platform/x86: lenovo: Add Legion Go Fan Method curve driver Date: Sat, 22 Aug 2026 03:17:27 +0530 Message-ID: <20260821214728.87773-4-mradityadash@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260821214728.87773-1-mradityadash@gmail.com> References: <20260821214728.87773-1-mradityadash@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Legion Go firmware keeps its custom fan curve in a separate Fan Method WMI interface. Method 5 returns ten control values and ten temperatures, while Method 6 replaces the complete table. The GUID also appears on Lenovo systems with a different table layout. Limit the driver to Legion Go 8APU1, Legion Go 8ASP2, Legion Go 8AHP2, Legion Go S 8ARP1, and Legion Go S 8APU1 products. These products use the same ten-point layout and Method 6 request. The curve applies to the same fan as the existing Other Mode attributes. Add a separate WMI driver and attach the ten automatic-point pairs to the HWMON device that Other Mode already provides. Match devices from the same WMI provider. Other Mode continues to work without Fan Method. Probe does not invoke either method. Validate both counts, every control value, and the complete temperature table before using a Method 5 reply. A point write holds the mutex while it reads the current table, changes one control value, and submits the complete Method 6 request. The other values remain unchanged. The HWMON mapping is not clear. On the tested Legion Go 8APU1, the fan responds to values from 0 through 115. This is Lenovo's 0 through 115 percent range. Lenovo software shows 0 through 100 percent to the user, and 115 reaches the RPM observed in Full Speed mode. Firmware accepts values through 255, but values above 115 caused no observed RPM increase. This RFC exposes the firmware values unchanged. The fixed temperatures are read-only. Assisted-by: Pi:gpt-5.6-sol Signed-off-by: Aditya Dash --- .../wmi/devices/lenovo-wmi-fan-method.rst | 34 ++ .../wmi/devices/lenovo-wmi-other.rst | 3 + MAINTAINERS | 1 + drivers/platform/x86/lenovo/Kconfig | 11 + drivers/platform/x86/lenovo/Makefile | 1 + drivers/platform/x86/lenovo/wmi-fan-method.c | 380 ++++++++++++++++++ drivers/platform/x86/lenovo/wmi-other.c | 97 ++++- 7 files changed, 520 insertions(+), 7 deletions(-) create mode 100644 Documentation/wmi/devices/lenovo-wmi-fan-method.rst create mode 100644 drivers/platform/x86/lenovo/wmi-fan-method.c diff --git a/Documentation/wmi/devices/lenovo-wmi-fan-method.rst b/Document= ation/wmi/devices/lenovo-wmi-fan-method.rst new file mode 100644 index 000000000000..67c9a92ed992 --- /dev/null +++ b/Documentation/wmi/devices/lenovo-wmi-fan-method.rst @@ -0,0 +1,34 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +Lenovo Fan Method WMI Driver +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +WMI GUID ``92549549-4BDE-4F06-AC04-CE8BF898DBAA`` + +The Lenovo Fan Method interface provides a ten-point firmware fan table on +Legion Go 8APU1, Legion Go 8ASP2, Legion Go 8AHP2, Legion Go S 8ARP1, and +Legion Go S 8APU1 products. + +The driver adds ``pwm1_auto_point1_*`` through +``pwm1_auto_point10_*`` to the HWMON device that the Lenovo Other Mode dri= ver +owns. The temperature attributes are fixed and read-only. They contain poi= nts +from 10 through 100 degrees Celsius in 10-degree steps. + +Each ``pwm1_auto_point*_pwm`` attribute is read-write and passes a firmware +control value from 0 through 255 without scaling. On the tested Legion Go +8APU1, the hardware responds from 0 through 115, corresponding to Lenovo's +0 through 115 percent range. Lenovo software shows 0 through 100 percent to +the user, and 115 percent reaches the RPM observed in Full Speed mode. The +firmware accepts values through 255, but values above 115 caused no observ= ed +RPM increase. + +A control-value read validates both ten-entry tables before returning data= . A +point write reads the current table, changes one control value, and submit= s the +complete request. It preserves the other nine control values and all retur= ned +temperatures. + +The Fan Method and Other Mode drivers use separate modules and WMI devices. +The component framework associates devices that belong to the same WMI +provider. Fan Method curve attributes are absent when either interface is = not +available. diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation= /wmi/devices/lenovo-wmi-other.rst index c93e9e6f3fd0..34352dbd6fb9 100644 --- a/Documentation/wmi/devices/lenovo-wmi-other.rst +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst @@ -60,6 +60,9 @@ Some Legion Go firmware does not advertise fan 1 RPM thro= ugh Capability Data. In that case, the driver reads Other Mode feature ``0x04030001`` for ``fan1_input``. Value ``0xffffffff`` means that RPM is unavailable. =20 +When the separate Lenovo Fan Method driver binds to the same WMI provider,= it +adds ten automatic fan-curve points to this HWMON device. + LENOVO_CAPABILITY_DATA_01 ------------------------- =20 diff --git a/MAINTAINERS b/MAINTAINERS index 3820b286f09f..8612c2323540 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14689,6 +14689,7 @@ M: Mark Pearson M: Derek J. Clark L: platform-driver-x86@vger.kernel.org S: Maintained +F: Documentation/wmi/devices/lenovo-wmi-fan-method.rst F: Documentation/wmi/devices/lenovo-wmi-gamezone.rst F: Documentation/wmi/devices/lenovo-wmi-other.rst F: drivers/platform/x86/lenovo/* diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/len= ovo/Kconfig index 4443f40ef8aa..e69942ce3093 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -276,3 +276,14 @@ config LENOVO_WMI_TUNING =20 To compile this driver as a module, choose M here: the module will be called lenovo-wmi-other. + +config LENOVO_WMI_FAN_METHOD + tristate "Lenovo Fan Method WMI Driver" + depends on DMI + depends on LENOVO_WMI_TUNING + help + Say Y here to add the firmware fan table on supported Lenovo + Legion Go products to the Lenovo Other Mode HWMON device. + + To compile this driver as a module, choose M here: the module will + be called lenovo-wmi-fan-method. diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/le= novo/Makefile index 91a9370f11b3..171a19e01479 100644 --- a/drivers/platform/x86/lenovo/Makefile +++ b/drivers/platform/x86/lenovo/Makefile @@ -17,6 +17,7 @@ lenovo-target-$(CONFIG_LENOVO_WMI_EVENTS) +=3D wmi-events= .o lenovo-target-$(CONFIG_LENOVO_WMI_HELPERS) +=3D wmi-helpers.o lenovo-target-$(CONFIG_LENOVO_WMI_GAMEZONE) +=3D wmi-gamezone.o lenovo-target-$(CONFIG_LENOVO_WMI_TUNING) +=3D wmi-other.o +lenovo-target-$(CONFIG_LENOVO_WMI_FAN_METHOD) +=3D wmi-fan-method.o =20 # Add 'lenovo' prefix to each module listed in lenovo-target-* define LENOVO_OBJ_TARGET diff --git a/drivers/platform/x86/lenovo/wmi-fan-method.c b/drivers/platfor= m/x86/lenovo/wmi-fan-method.c new file mode 100644 index 000000000000..4f29c4134caf --- /dev/null +++ b/drivers/platform/x86/lenovo/wmi-fan-method.c @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Lenovo Fan Method WMI interface driver. + * + * This driver exposes the firmware fan table through HWMON automatic-point + * attributes on selected Lenovo Legion Go products. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LENOVO_FAN_METHOD_GUID "92549549-4BDE-4F06-AC04-CE8BF898DBAA" + +#define LENOVO_FAN_METHOD_GET_CURVE 5 +#define LENOVO_FAN_METHOD_SET_CURVE 6 +#define LENOVO_FAN_CURVE_POINTS 10 +#define LENOVO_FAN_CURVE_REPLY_SIZE 88 +#define LENOVO_FAN_CURVE_WRITE_SIZE 64 + +#define LENOVO_FAN_REPLY_SPEED_COUNT_OFFSET 0 +#define LENOVO_FAN_REPLY_SPEED_OFFSET 4 +#define LENOVO_FAN_REPLY_TEMP_COUNT_OFFSET 44 +#define LENOVO_FAN_REPLY_TEMP_OFFSET 48 + +#define LENOVO_FAN_WRITE_SPEED_COUNT_OFFSET 2 +#define LENOVO_FAN_WRITE_SPEED_OFFSET 6 +#define LENOVO_FAN_WRITE_TEMP_TYPE_OFFSET 26 +#define LENOVO_FAN_WRITE_TEMP_COUNT_OFFSET 27 +#define LENOVO_FAN_WRITE_TEMP_OFFSET 31 +#define LENOVO_FAN_WRITE_TRAILER_OFFSET 51 +#define LENOVO_FAN_WRITE_TRAILER_VALUE_OFFSET 53 + +#define LENOVO_FAN_WRITE_TEMP_TYPE 1 +#define LENOVO_FAN_WRITE_TRAILER 0x5a +#define LENOVO_FAN_WRITE_TRAILER_VALUE 100 + +static const struct dmi_system_id lwmi_fan_dmi_table[] =3D { + { + .ident =3D "Lenovo Legion Go 8APU1", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8APU1"), + }, + }, + { + .ident =3D "Lenovo Legion Go S 8APU1", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8APU1"), + }, + }, + { + .ident =3D "Lenovo Legion Go S 8ARP1", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8ARP1"), + }, + }, + { + .ident =3D "Lenovo Legion Go 8ASP2", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8ASP2"), + }, + }, + { + .ident =3D "Lenovo Legion Go 8AHP2", + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8AHP2"), + }, + }, + {} +}; + +static const u16 lwmi_fan_temperatures[LENOVO_FAN_CURVE_POINTS] =3D { + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 +}; + +struct lwmi_fan_method_curve { + u16 speed[LENOVO_FAN_CURVE_POINTS]; + u16 temperature[LENOVO_FAN_CURVE_POINTS]; +}; + +struct lwmi_fan_method_priv; + +struct lwmi_fan_method_attr { + struct device_attribute dev_attr; + struct lwmi_fan_method_priv *priv; + u8 index; +}; + +struct lwmi_fan_method_priv { + struct wmi_device *wdev; + struct mutex lock; /* Serializes all Fan Method calls. */ + struct lwmi_fan_method_attr point_attrs[LENOVO_FAN_CURVE_POINTS * 2]; + struct attribute *attrs[LENOVO_FAN_CURVE_POINTS * 2 + 1]; + struct attribute_group group; +}; + +static int lwmi_fan_method_get_curve(struct lwmi_fan_method_priv *priv, + struct lwmi_fan_method_curve *curve) +{ + u8 input[] =3D { 1, 1 }; + struct wmi_buffer in =3D { + .length =3D sizeof(input), + .data =3D input, + }; + struct wmi_buffer out =3D {}; + const u8 *buffer; + int ret, i; + + ret =3D wmidev_invoke_method(priv->wdev, 0, + LENOVO_FAN_METHOD_GET_CURVE, &in, &out, + LENOVO_FAN_CURVE_REPLY_SIZE); + if (ret) + return ret; + + buffer =3D out.data; + if (get_unaligned_le32(buffer + LENOVO_FAN_REPLY_SPEED_COUNT_OFFSET) !=3D + LENOVO_FAN_CURVE_POINTS || + get_unaligned_le32(buffer + LENOVO_FAN_REPLY_TEMP_COUNT_OFFSET) !=3D + LENOVO_FAN_CURVE_POINTS) { + ret =3D -ERANGE; + goto out_free; + } + + for (i =3D 0; i < LENOVO_FAN_CURVE_POINTS; i++) { + u32 speed =3D get_unaligned_le32(buffer + LENOVO_FAN_REPLY_SPEED_OFFSET + + i * sizeof(u32)); + u32 temperature =3D + get_unaligned_le32(buffer + LENOVO_FAN_REPLY_TEMP_OFFSET + + i * sizeof(u32)); + + if (speed > U8_MAX || temperature !=3D lwmi_fan_temperatures[i]) { + ret =3D -ERANGE; + goto out_free; + } + + curve->speed[i] =3D speed; + curve->temperature[i] =3D temperature; + } + +out_free: + kfree(out.data); + return ret; +} + +static int lwmi_fan_method_set_curve(struct lwmi_fan_method_priv *priv, + const struct lwmi_fan_method_curve *curve) +{ + u8 buffer[LENOVO_FAN_CURVE_WRITE_SIZE] =3D { 0xff, 0x01 }; + struct wmi_buffer in =3D { + .length =3D sizeof(buffer), + .data =3D buffer, + }; + int i; + + put_unaligned_le32(LENOVO_FAN_CURVE_POINTS, + buffer + LENOVO_FAN_WRITE_SPEED_COUNT_OFFSET); + for (i =3D 0; i < LENOVO_FAN_CURVE_POINTS; i++) + put_unaligned_le16(curve->speed[i], + buffer + LENOVO_FAN_WRITE_SPEED_OFFSET + + i * sizeof(u16)); + + buffer[LENOVO_FAN_WRITE_TEMP_TYPE_OFFSET] =3D LENOVO_FAN_WRITE_TEMP_TYPE; + put_unaligned_le32(LENOVO_FAN_CURVE_POINTS, + buffer + LENOVO_FAN_WRITE_TEMP_COUNT_OFFSET); + for (i =3D 0; i < LENOVO_FAN_CURVE_POINTS; i++) + put_unaligned_le16(curve->temperature[i], + buffer + LENOVO_FAN_WRITE_TEMP_OFFSET + + i * sizeof(u16)); + + buffer[LENOVO_FAN_WRITE_TRAILER_OFFSET] =3D LENOVO_FAN_WRITE_TRAILER; + put_unaligned_le16(LENOVO_FAN_WRITE_TRAILER_VALUE, + buffer + LENOVO_FAN_WRITE_TRAILER_VALUE_OFFSET); + + return wmidev_invoke_procedure(priv->wdev, 0, + LENOVO_FAN_METHOD_SET_CURVE, &in); +} + +static ssize_t lwmi_fan_method_pwm_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lwmi_fan_method_attr *point_attr =3D + container_of(attr, struct lwmi_fan_method_attr, dev_attr); + struct lwmi_fan_method_priv *priv =3D point_attr->priv; + struct lwmi_fan_method_curve curve; + int ret; + + guard(mutex)(&priv->lock); + + ret =3D lwmi_fan_method_get_curve(priv, &curve); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", curve.speed[point_attr->index]); +} + +static ssize_t lwmi_fan_method_pwm_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct lwmi_fan_method_attr *point_attr =3D + container_of(attr, struct lwmi_fan_method_attr, dev_attr); + struct lwmi_fan_method_priv *priv =3D point_attr->priv; + struct lwmi_fan_method_curve curve; + unsigned long pwm; + int ret; + + ret =3D kstrtoul(buf, 10, &pwm); + if (ret) + return ret; + if (pwm > U8_MAX) + return -EINVAL; + + guard(mutex)(&priv->lock); + + ret =3D lwmi_fan_method_get_curve(priv, &curve); + if (ret) + return ret; + + curve.speed[point_attr->index] =3D pwm; + ret =3D lwmi_fan_method_set_curve(priv, &curve); + + return ret ? ret : count; +} + +static ssize_t lwmi_fan_method_temp_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lwmi_fan_method_attr *point_attr =3D + container_of(attr, struct lwmi_fan_method_attr, dev_attr); + + return sysfs_emit(buf, "%u\n", + lwmi_fan_temperatures[point_attr->index] * 1000); +} + +#define LWMI_FAN_METHOD_POINT_NAMES(_point) \ + "pwm1_auto_point" #_point "_pwm", \ + "pwm1_auto_point" #_point "_temp" + +static const char * const lwmi_fan_method_attr_names[] =3D { + LWMI_FAN_METHOD_POINT_NAMES(1), + LWMI_FAN_METHOD_POINT_NAMES(2), + LWMI_FAN_METHOD_POINT_NAMES(3), + LWMI_FAN_METHOD_POINT_NAMES(4), + LWMI_FAN_METHOD_POINT_NAMES(5), + LWMI_FAN_METHOD_POINT_NAMES(6), + LWMI_FAN_METHOD_POINT_NAMES(7), + LWMI_FAN_METHOD_POINT_NAMES(8), + LWMI_FAN_METHOD_POINT_NAMES(9), + LWMI_FAN_METHOD_POINT_NAMES(10), +}; + +#undef LWMI_FAN_METHOD_POINT_NAMES + +static void lwmi_fan_method_attrs_init(struct lwmi_fan_method_priv *priv) +{ + int i; + + for (i =3D 0; i < LENOVO_FAN_CURVE_POINTS; i++) { + struct lwmi_fan_method_attr *pwm =3D &priv->point_attrs[i * 2]; + struct lwmi_fan_method_attr *temp =3D &priv->point_attrs[i * 2 + 1]; + + sysfs_attr_init(&pwm->dev_attr.attr); + pwm->dev_attr.attr.name =3D lwmi_fan_method_attr_names[i * 2]; + pwm->dev_attr.attr.mode =3D 0644; + pwm->dev_attr.show =3D lwmi_fan_method_pwm_show; + pwm->dev_attr.store =3D lwmi_fan_method_pwm_store; + pwm->priv =3D priv; + pwm->index =3D i; + priv->attrs[i * 2] =3D &pwm->dev_attr.attr; + + sysfs_attr_init(&temp->dev_attr.attr); + temp->dev_attr.attr.name =3D lwmi_fan_method_attr_names[i * 2 + 1]; + temp->dev_attr.attr.mode =3D 0444; + temp->dev_attr.show =3D lwmi_fan_method_temp_show; + temp->index =3D i; + priv->attrs[i * 2 + 1] =3D &temp->dev_attr.attr; + } + + priv->group.attrs =3D priv->attrs; +} + +static int lwmi_fan_method_master_bind(struct device *dev) +{ + struct lwmi_fan_method_priv *priv =3D dev_get_drvdata(dev); + + return component_bind_all(dev, &priv->group); +} + +static void lwmi_fan_method_master_unbind(struct device *dev) +{ + component_unbind_all(dev, NULL); +} + +static const struct component_master_ops lwmi_fan_method_master_ops =3D { + .bind =3D lwmi_fan_method_master_bind, + .unbind =3D lwmi_fan_method_master_unbind, +}; + +static int lwmi_fan_method_component_compare(struct device *dev, void *dat= a) +{ + struct device *master =3D data; + + return dev->driver && + !strcmp(dev->driver->name, "lenovo_wmi_other") && + dev->parent =3D=3D master->parent; +} + +static int lwmi_fan_method_probe(struct wmi_device *wdev, const void *cont= ext) +{ + struct component_match *master_match =3D NULL; + struct lwmi_fan_method_priv *priv; + + if (!dmi_check_system(lwmi_fan_dmi_table)) + return -ENODEV; + + priv =3D devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->wdev =3D wdev; + mutex_init(&priv->lock); + lwmi_fan_method_attrs_init(priv); + dev_set_drvdata(&wdev->dev, priv); + + component_match_add(&wdev->dev, &master_match, + lwmi_fan_method_component_compare, &wdev->dev); + if (IS_ERR(master_match)) + return PTR_ERR(master_match); + + return component_master_add_with_match(&wdev->dev, + &lwmi_fan_method_master_ops, + master_match); +} + +static void lwmi_fan_method_remove(struct wmi_device *wdev) +{ + component_master_del(&wdev->dev, &lwmi_fan_method_master_ops); +} + +static const struct wmi_device_id lwmi_fan_method_id_table[] =3D { + { LENOVO_FAN_METHOD_GUID, NULL }, + {} +}; + +static struct wmi_driver lwmi_fan_method_driver =3D { + .driver =3D { + .name =3D "lenovo_wmi_fan_method", + .probe_type =3D PROBE_PREFER_ASYNCHRONOUS, + }, + .id_table =3D lwmi_fan_method_id_table, + .probe =3D lwmi_fan_method_probe, + .remove =3D lwmi_fan_method_remove, + .no_singleton =3D true, +}; + +MODULE_DEVICE_TABLE(wmi, lwmi_fan_method_id_table); +module_wmi_driver(lwmi_fan_method_driver); + +MODULE_AUTHOR("Aditya Dash "); +MODULE_DESCRIPTION("Lenovo Fan Method WMI Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86= /lenovo/wmi-other.c index b4be7739b243..72d3510b17b2 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -192,11 +192,13 @@ struct lwmi_om_priv { struct device *fw_attr_dev; struct kset *fw_attr_kset; struct wmi_device *wdev; + const struct attribute_group *fan_method_group; int ida_id; =20 struct lwmi_fan_info fan_info[LWMI_FAN_NR]; bool fullspeed_supported; bool fan0_input_fallback; + bool fan_method_group_added; =20 struct { bool capdata00_collected : 1; @@ -542,6 +544,31 @@ static const struct hwmon_chip_info lwmi_om_hwmon_chip= _info =3D { .info =3D lwmi_om_hwmon_info, }; =20 +static int lwmi_om_fan_method_group_add(struct lwmi_om_priv *priv) +{ + int ret; + + if (!priv->hwmon_dev || !priv->fan_method_group || + priv->fan_method_group_added) + return 0; + + ret =3D sysfs_create_group(&priv->hwmon_dev->kobj, + priv->fan_method_group); + if (!ret) + priv->fan_method_group_added =3D true; + + return ret; +} + +static void lwmi_om_fan_method_group_remove(struct lwmi_om_priv *priv) +{ + if (!priv->hwmon_dev || !priv->fan_method_group_added) + return; + + sysfs_remove_group(&priv->hwmon_dev->kobj, priv->fan_method_group); + priv->fan_method_group_added =3D false; +} + /** * lwmi_om_hwmon_add() - Register HWMON device if all info is collected * @priv: Driver private data @@ -550,9 +577,9 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) { u32 rpm; long enable; - int i, valid; + int i, ret, valid; =20 - if (WARN_ON(priv->hwmon_dev)) + if (priv->hwmon_dev) return; =20 if (!priv->fan_flags.capdata00_collected || !priv->fan_flags.capdata_fan_= collected) { @@ -592,7 +619,8 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv) } } =20 - if (valid =3D=3D 0 && !priv->fullspeed_supported && !priv->fan0_input_fal= lback) { + if (valid =3D=3D 0 && !priv->fullspeed_supported && + !priv->fan0_input_fallback && !priv->fan_method_group) { dev_warn(&priv->wdev->dev, "fan reporting/tuning is unsupported on this device\n"); return; @@ -609,6 +637,11 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *pri= v) return; } =20 + ret =3D lwmi_om_fan_method_group_add(priv); + if (ret) + dev_warn(&priv->wdev->dev, + "failed to register Fan Method attributes: %d\n", ret); + dev_dbg(&priv->wdev->dev, "registered HWMON device\n"); } =20 @@ -623,6 +656,7 @@ static void lwmi_om_hwmon_remove(struct lwmi_om_priv *p= riv) if (!priv->hwmon_dev) return; =20 + lwmi_om_fan_method_group_remove(priv); hwmon_device_unregister(priv->hwmon_dev); priv->hwmon_dev =3D NULL; } @@ -1801,10 +1835,44 @@ static const struct component_master_ops lwmi_om_ma= ster_ops =3D { .unbind =3D lwmi_om_master_unbind, }; =20 +static int lwmi_om_fan_method_component_bind(struct device *component, + struct device *master, void *data) +{ + const struct attribute_group *group =3D data; + struct lwmi_om_priv *priv =3D dev_get_drvdata(component); + int ret; + + if (!group) + return -EINVAL; + + priv->fan_method_group =3D group; + lwmi_om_hwmon_add(priv); + ret =3D lwmi_om_fan_method_group_add(priv); + if (ret) + priv->fan_method_group =3D NULL; + + return ret; +} + +static void lwmi_om_fan_method_component_unbind(struct device *component, + struct device *master, void *data) +{ + struct lwmi_om_priv *priv =3D dev_get_drvdata(component); + + lwmi_om_fan_method_group_remove(priv); + priv->fan_method_group =3D NULL; +} + +static const struct component_ops lwmi_om_fan_method_component_ops =3D { + .bind =3D lwmi_om_fan_method_component_bind, + .unbind =3D lwmi_om_fan_method_component_unbind, +}; + static int lwmi_other_probe(struct wmi_device *wdev, const void *context) { struct component_match *master_match =3D NULL; struct lwmi_om_priv *priv; + int ret; =20 priv =3D devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) @@ -1816,16 +1884,31 @@ static int lwmi_other_probe(struct wmi_device *wdev= , const void *context) priv->wdev =3D wdev; dev_set_drvdata(&wdev->dev, priv); =20 + ret =3D component_add(&wdev->dev, &lwmi_om_fan_method_component_ops); + if (ret) + return ret; + lwmi_cd_match_add_all(&wdev->dev, &master_match); - if (IS_ERR(master_match)) - return PTR_ERR(master_match); + if (IS_ERR(master_match)) { + ret =3D PTR_ERR(master_match); + goto err_component; + } + + ret =3D component_master_add_with_match(&wdev->dev, &lwmi_om_master_ops, + master_match); + if (ret) + goto err_component; + + return 0; =20 - return component_master_add_with_match(&wdev->dev, &lwmi_om_master_ops, - master_match); +err_component: + component_del(&wdev->dev, &lwmi_om_fan_method_component_ops); + return ret; } =20 static void lwmi_other_remove(struct wmi_device *wdev) { + component_del(&wdev->dev, &lwmi_om_fan_method_component_ops); component_master_del(&wdev->dev, &lwmi_om_master_ops); } =20 --=20 2.55.0