From nobody Mon Oct 6 19:10:02 2025 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0E60923E355; Thu, 11 Sep 2025 06:56:38 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1757573799; cv=none; b=YLNWZSlSI0TmVS0uneUhEBg5HXd1C3XqR0PoiZlZ+z9wr5uFVjBrPJVxyz+aj44RewO9xYHqMY080T0WDx3fGovwys9qd0GOlIv/1lSLj9mv6Yzd/FkQJ0/e+VBLyKyrNB6iBhRx9VHl5yxBvUl8+aWNovMmteCEiXeA+CXoNRw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1757573799; c=relaxed/simple; bh=DZq5PF5WpuiiVFYoa4k236RbxuQNrHf7APXf2EU8+OM=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=LgCINJ+TLjyAz5EoUuhhgJczVgMA4zb9YuYqAQ1375FtB43AA+akqdRWWE+oy4cnn+EA17Z64MvL1rmSML7n+/36N0MrG58xSbgm8IfHEojMi8sQV89ogNpGa6zu2N4f8/j4Wq5KdlA67BomOvy4s8AGmSvgECfdzGKH7KvxpnQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=LtUCLutl; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="LtUCLutl" Received: by smtp.kernel.org (Postfix) with ESMTPS id ADFF5C4CEFA; Thu, 11 Sep 2025 06:56:38 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1757573798; bh=DZq5PF5WpuiiVFYoa4k236RbxuQNrHf7APXf2EU8+OM=; h=From:Date:Subject:References:In-Reply-To:To:Cc:Reply-To:From; b=LtUCLutlJjXVjwIVswUpd2bKQJbaF945PBOjJURsEF74APVMLwWqAQTkKTjR8Uw38 yyW2f8fuRyuKPGebRZtdHt9pKPUF9fvDMBgmhFkkyJfgL0rM6xCHIN/vG5HospIgVG MX77ErvPOcEq9n8ZT+h1e8boGS1nzDIwnBkwsPKK7FEiKE7nSslUuLPpp/SnqbbKKE ZG+i6m8UBoHSWXsbELFifkcmwTsBMvKbV9UrG/3RrHMZ+bKxHcuWljH+RzjfyqjPGE S6F01eh6oe2F6LS55qCnrVsWV3gK6NKZg+wRU0hUxRxHH91usQ92fwG6m6ZKGjdRcJ wG9uVfESf/FrA== Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id A24B9CA1016; Thu, 11 Sep 2025 06:56:38 +0000 (UTC) From: Sung-Chi Li via B4 Relay Date: Thu, 11 Sep 2025 06:56:35 +0000 Subject: [PATCH v6 2/3] hwmon: (cros_ec) add PWM control over fans Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20250911-cros_ec_fan-v6-2-a1446cc098af@google.com> References: <20250911-cros_ec_fan-v6-0-a1446cc098af@google.com> In-Reply-To: <20250911-cros_ec_fan-v6-0-a1446cc098af@google.com> To: Benson Leung , Guenter Roeck , =?utf-8?q?Thomas_Wei=C3=9Fschuh?= , Guenter Roeck , Jonathan Corbet Cc: chrome-platform@lists.linux.dev, linux-kernel@vger.kernel.org, linux-hwmon@vger.kernel.org, linux-doc@vger.kernel.org, Sung-Chi Li , Sung-Chi Li X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=ed25519-sha256; t=1757573797; l=12761; i=lschyi@google.com; s=20250911; h=from:subject:message-id; bh=L7UPcPw/zTFGqL5aBqs/m1MT71ZNWl6D8FXPICeuwwc=; b=gdBuQHqivgqYqo9YE0pAyJA9lC4u7g28r7HmlOeytEbayGsLXj70GVX2rMX6k0ZSwB2JRb6zW vqDpc0CklGqC3Ot5A4LHR/pAvwKbs9RC9jEUKliEEvUHjtNd65ZsiLo X-Developer-Key: i=lschyi@google.com; a=ed25519; pk=fBhhFxZrEyInLLODzeoq06UxQhKVqNjmZ2680pwMBCM= X-Endpoint-Received: by B4 Relay for lschyi@google.com/20250911 with auth_id=518 X-Original-From: Sung-Chi Li Reply-To: lschyi@google.com From: Sung-Chi Li Newer EC firmware supports controlling fans through host commands, so adding corresponding implementations for controlling these fans in the driver for other kernel services and userspace to control them. The driver will first probe the supported host command versions (get and set of fan PWM values, get and set of fan control mode) to see if the connected EC fulfills the requirements of controlling the fan, then exposes corresponding sysfs nodes for userspace to control the fan with corresponding read and write implementations. As EC will automatically change the fan mode to auto when the device is suspended, the power management hooks are added as well to keep the fan control mode and fan PWM value consistent during suspend and resume. As we need to access the hwmon device in the power management hook, update the driver by storing the hwmon device in the driver data as well. Signed-off-by: Sung-Chi Li Acked-by: Thomas Wei=C3=9Fschuh --- Documentation/hwmon/cros_ec_hwmon.rst | 5 +- drivers/hwmon/cros_ec_hwmon.c | 230 ++++++++++++++++++++++++++++++= ++++ 2 files changed, 234 insertions(+), 1 deletion(-) diff --git a/Documentation/hwmon/cros_ec_hwmon.rst b/Documentation/hwmon/cr= os_ec_hwmon.rst index 47ecae983bdbef4bfcafc5dd2fff3de039f77f8e..355557a08c9a54b4c177bafde37= 43e7dc02218be 100644 --- a/Documentation/hwmon/cros_ec_hwmon.rst +++ b/Documentation/hwmon/cros_ec_hwmon.rst @@ -23,4 +23,7 @@ ChromeOS embedded controller used in Chromebooks and othe= r devices. =20 The channel labels exposed via hwmon are retrieved from the EC itself. =20 -Fan and temperature readings are supported. +Fan and temperature readings are supported. PWM fan control is also suppor= ted if +the EC also supports setting fan PWM values and fan mode. Note that EC will +switch fan control mode back to auto when suspended. This driver will rest= ore +the fan state to what they were before suspended when resumed. diff --git a/drivers/hwmon/cros_ec_hwmon.c b/drivers/hwmon/cros_ec_hwmon.c index 9991c3fa020ac859cbbff29dfb669e53248df885..9eddc554ddefde42f70c09689b6= 4ad9e636a3020 100644 --- a/drivers/hwmon/cros_ec_hwmon.c +++ b/drivers/hwmon/cros_ec_hwmon.c @@ -7,6 +7,7 @@ =20 #include #include +#include #include #include #include @@ -17,10 +18,17 @@ =20 #define DRV_NAME "cros-ec-hwmon" =20 +#define CROS_EC_HWMON_PWM_GET_FAN_DUTY_CMD_VERSION 0 +#define CROS_EC_HWMON_PWM_SET_FAN_DUTY_CMD_VERSION 1 +#define CROS_EC_HWMON_THERMAL_AUTO_FAN_CTRL_CMD_VERSION 2 + struct cros_ec_hwmon_priv { struct cros_ec_device *cros_ec; const char *temp_sensor_names[EC_TEMP_SENSOR_ENTRIES + EC_TEMP_SENSOR_B_E= NTRIES]; u8 usable_fans; + bool fan_control_supported; + u8 manual_fans; /* bits to indicate whether the fan is set to manual */ + u8 manual_fan_pwm[EC_FAN_SPEED_ENTRIES]; }; =20 static int cros_ec_hwmon_read_fan_speed(struct cros_ec_device *cros_ec, u8= index, u16 *speed) @@ -36,6 +44,42 @@ static int cros_ec_hwmon_read_fan_speed(struct cros_ec_d= evice *cros_ec, u8 index return 0; } =20 +static int cros_ec_hwmon_read_pwm_value(struct cros_ec_device *cros_ec, u8= index, u8 *pwm_value) +{ + struct ec_params_pwm_get_fan_duty req =3D { + .fan_idx =3D index, + }; + struct ec_response_pwm_get_fan_duty resp; + int ret; + + ret =3D cros_ec_cmd(cros_ec, CROS_EC_HWMON_PWM_GET_FAN_DUTY_CMD_VERSION, + EC_CMD_PWM_GET_FAN_DUTY, &req, sizeof(req), &resp, sizeof(resp)); + if (ret < 0) + return ret; + + *pwm_value =3D (u8)DIV_ROUND_CLOSEST(le32_to_cpu(resp.percent) * 255, 100= ); + return 0; +} + +static int cros_ec_hwmon_read_pwm_enable(struct cros_ec_device *cros_ec, u= 8 index, + u8 *control_method) +{ + struct ec_params_auto_fan_ctrl_v2 req =3D { + .cmd =3D EC_AUTO_FAN_CONTROL_CMD_GET, + .fan_idx =3D index, + }; + struct ec_response_auto_fan_control resp; + int ret; + + ret =3D cros_ec_cmd(cros_ec, CROS_EC_HWMON_THERMAL_AUTO_FAN_CTRL_CMD_VERS= ION, + EC_CMD_THERMAL_AUTO_FAN_CTRL, &req, sizeof(req), &resp, sizeof(resp)); + if (ret < 0) + return ret; + + *control_method =3D resp.is_auto ? 2 : 1; + return 0; +} + static int cros_ec_hwmon_read_temp(struct cros_ec_device *cros_ec, u8 inde= x, u8 *temp) { unsigned int offset; @@ -75,6 +119,8 @@ static int cros_ec_hwmon_read(struct device *dev, enum h= wmon_sensor_types type, { struct cros_ec_hwmon_priv *priv =3D dev_get_drvdata(dev); int ret =3D -EOPNOTSUPP; + u8 control_method; + u8 pwm_value; u16 speed; u8 temp; =20 @@ -92,6 +138,17 @@ static int cros_ec_hwmon_read(struct device *dev, enum = hwmon_sensor_types type, if (ret =3D=3D 0) *val =3D cros_ec_hwmon_is_error_fan(speed); } + } else if (type =3D=3D hwmon_pwm) { + if (attr =3D=3D hwmon_pwm_enable) { + ret =3D cros_ec_hwmon_read_pwm_enable(priv->cros_ec, channel, + &control_method); + if (ret =3D=3D 0) + *val =3D control_method; + } else if (attr =3D=3D hwmon_pwm_input) { + ret =3D cros_ec_hwmon_read_pwm_value(priv->cros_ec, channel, &pwm_value= ); + if (ret =3D=3D 0) + *val =3D pwm_value; + } } else if (type =3D=3D hwmon_temp) { if (attr =3D=3D hwmon_temp_input) { ret =3D cros_ec_hwmon_read_temp(priv->cros_ec, channel, &temp); @@ -124,6 +181,74 @@ static int cros_ec_hwmon_read_string(struct device *de= v, enum hwmon_sensor_types return -EOPNOTSUPP; } =20 +static int cros_ec_hwmon_set_fan_pwm_val(struct cros_ec_device *cros_ec, u= 8 index, u8 val) +{ + struct ec_params_pwm_set_fan_duty_v1 req =3D { + .fan_idx =3D index, + .percent =3D DIV_ROUND_CLOSEST((uint32_t)val * 100, 255), + }; + int ret; + + ret =3D cros_ec_cmd(cros_ec, CROS_EC_HWMON_PWM_SET_FAN_DUTY_CMD_VERSION, + EC_CMD_PWM_SET_FAN_DUTY, &req, sizeof(req), NULL, 0); + if (ret < 0) + return ret; + return 0; +} + +static int cros_ec_hwmon_write_pwm_input(struct cros_ec_device *cros_ec, u= 8 index, u8 val) +{ + u8 control_method; + int ret; + + ret =3D cros_ec_hwmon_read_pwm_enable(cros_ec, index, &control_method); + if (ret) + return ret; + if (control_method !=3D 1) + return -EOPNOTSUPP; + + return cros_ec_hwmon_set_fan_pwm_val(cros_ec, index, val); +} + +static int cros_ec_hwmon_write_pwm_enable(struct cros_ec_device *cros_ec, = u8 index, u8 val) +{ + struct ec_params_auto_fan_ctrl_v2 req =3D { + .fan_idx =3D index, + .cmd =3D EC_AUTO_FAN_CONTROL_CMD_SET, + }; + int ret; + + /* No CrOS EC supports no fan speed control */ + if (val =3D=3D 0) + return -EOPNOTSUPP; + + req.set_auto =3D (val !=3D 1) ? true : false; + ret =3D cros_ec_cmd(cros_ec, CROS_EC_HWMON_THERMAL_AUTO_FAN_CTRL_CMD_VERS= ION, + EC_CMD_THERMAL_AUTO_FAN_CTRL, &req, sizeof(req), NULL, 0); + if (ret < 0) + return ret; + return 0; +} + +static int cros_ec_hwmon_write(struct device *dev, enum hwmon_sensor_types= type, u32 attr, + int channel, long val) +{ + struct cros_ec_hwmon_priv *priv =3D dev_get_drvdata(dev); + + if (type =3D=3D hwmon_pwm) { + switch (attr) { + case hwmon_pwm_input: + return cros_ec_hwmon_write_pwm_input(priv->cros_ec, channel, val); + case hwmon_pwm_enable: + return cros_ec_hwmon_write_pwm_enable(priv->cros_ec, channel, val); + default: + return -EOPNOTSUPP; + } + } + + return -EOPNOTSUPP; +} + static umode_t cros_ec_hwmon_is_visible(const void *data, enum hwmon_senso= r_types type, u32 attr, int channel) { @@ -132,6 +257,9 @@ static umode_t cros_ec_hwmon_is_visible(const void *dat= a, enum hwmon_sensor_type if (type =3D=3D hwmon_fan) { if (priv->usable_fans & BIT(channel)) return 0444; + } else if (type =3D=3D hwmon_pwm) { + if (priv->fan_control_supported && priv->usable_fans & BIT(channel)) + return 0644; } else if (type =3D=3D hwmon_temp) { if (priv->temp_sensor_names[channel]) return 0444; @@ -147,6 +275,11 @@ static const struct hwmon_channel_info * const cros_ec= _hwmon_info[] =3D { HWMON_F_INPUT | HWMON_F_FAULT, HWMON_F_INPUT | HWMON_F_FAULT, HWMON_F_INPUT | HWMON_F_FAULT), + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE, + HWMON_PWM_INPUT | HWMON_PWM_ENABLE), HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_FAULT | HWMON_T_LABEL, HWMON_T_INPUT | HWMON_T_FAULT | HWMON_T_LABEL, @@ -178,6 +311,7 @@ static const struct hwmon_channel_info * const cros_ec_= hwmon_info[] =3D { static const struct hwmon_ops cros_ec_hwmon_ops =3D { .read =3D cros_ec_hwmon_read, .read_string =3D cros_ec_hwmon_read_string, + .write =3D cros_ec_hwmon_write, .is_visible =3D cros_ec_hwmon_is_visible, }; =20 @@ -233,6 +367,25 @@ static void cros_ec_hwmon_probe_fans(struct cros_ec_hw= mon_priv *priv) } } =20 +static inline bool is_cros_ec_cmd_available(struct cros_ec_device *cros_ec, + u16 cmd, u8 version) +{ + int ret; + + ret =3D cros_ec_get_cmd_versions(cros_ec, cmd); + return ret >=3D 0 && (ret & EC_VER_MASK(version)); +} + +static bool cros_ec_hwmon_probe_fan_control_supported(struct cros_ec_devic= e *cros_ec) +{ + return is_cros_ec_cmd_available(cros_ec, EC_CMD_PWM_GET_FAN_DUTY, + CROS_EC_HWMON_PWM_GET_FAN_DUTY_CMD_VERSION) && + is_cros_ec_cmd_available(cros_ec, EC_CMD_PWM_SET_FAN_DUTY, + CROS_EC_HWMON_PWM_SET_FAN_DUTY_CMD_VERSION) && + is_cros_ec_cmd_available(cros_ec, EC_CMD_THERMAL_AUTO_FAN_CTRL, + CROS_EC_HWMON_THERMAL_AUTO_FAN_CTRL_CMD_VERSION); +} + static int cros_ec_hwmon_probe(struct platform_device *pdev) { struct device *dev =3D &pdev->dev; @@ -259,13 +412,88 @@ static int cros_ec_hwmon_probe(struct platform_device= *pdev) =20 cros_ec_hwmon_probe_temp_sensors(dev, priv, thermal_version); cros_ec_hwmon_probe_fans(priv); + priv->fan_control_supported =3D cros_ec_hwmon_probe_fan_control_supported= (priv->cros_ec); =20 hwmon_dev =3D devm_hwmon_device_register_with_info(dev, "cros_ec", priv, &cros_ec_hwmon_chip_info, NULL); + platform_set_drvdata(pdev, priv); =20 return PTR_ERR_OR_ZERO(hwmon_dev); } =20 +static int cros_ec_hwmon_suspend(struct platform_device *pdev, pm_message_= t state) +{ + struct cros_ec_hwmon_priv *priv =3D platform_get_drvdata(pdev); + u8 control_method; + size_t i; + int ret; + + if (!priv->fan_control_supported) + return 0; + + /* EC sets fan control to auto after suspended, store settings before sus= pending. */ + for (i =3D 0; i < EC_FAN_SPEED_ENTRIES; i++) { + if (!(priv->usable_fans & BIT(i))) + continue; + + ret =3D cros_ec_hwmon_read_pwm_enable(priv->cros_ec, i, &control_method); + if (ret) { + dev_warn(&pdev->dev, "failed to get mode setting for fan %zu: %d\n", i, + ret); + continue; + } + + if (control_method !=3D 1) { + priv->manual_fans &=3D ~BIT(i); + continue; + } else { + priv->manual_fans |=3D BIT(i); + } + + ret =3D cros_ec_hwmon_read_pwm_value(priv->cros_ec, i, &priv->manual_fan= _pwm[i]); + /* + * If storing the value failed, invalidate the stored mode value by sett= ing it + * to auto control. EC will automatically switch to auto mode for that f= an after + * suspended. + */ + if (ret) { + dev_warn(&pdev->dev, "failed to get PWM setting for fan %zu: %pe\n", i, + ERR_PTR(ret)); + priv->manual_fans &=3D ~BIT(i); + continue; + } + } + + return 0; +} + +static int cros_ec_hwmon_resume(struct platform_device *pdev) +{ + const struct cros_ec_hwmon_priv *priv =3D platform_get_drvdata(pdev); + size_t i; + int ret; + + if (!priv->fan_control_supported) + return 0; + + /* EC sets fan control to auto after suspend, restore to settings before = suspend. */ + for (i =3D 0; i < EC_FAN_SPEED_ENTRIES; i++) { + if (!(priv->manual_fans & BIT(i))) + continue; + + /* + * Setting fan PWM value to EC will change the mode to manual for that f= an in EC as + * well, so we do not need to issue a separate fan mode to manual call. + */ + ret =3D cros_ec_hwmon_set_fan_pwm_val(priv->cros_ec, i, priv->manual_fan= _pwm[i]); + if (ret) + dev_warn(&pdev->dev, "failed to restore settings for fan %zu: %pe\n", i, + ERR_PTR(ret)); + } + + return 0; +} + static const struct platform_device_id cros_ec_hwmon_id[] =3D { { DRV_NAME, 0 }, {} @@ -274,6 +502,8 @@ static const struct platform_device_id cros_ec_hwmon_id= [] =3D { static struct platform_driver cros_ec_hwmon_driver =3D { .driver.name =3D DRV_NAME, .probe =3D cros_ec_hwmon_probe, + .suspend =3D pm_ptr(cros_ec_hwmon_suspend), + .resume =3D pm_ptr(cros_ec_hwmon_resume), .id_table =3D cros_ec_hwmon_id, }; module_platform_driver(cros_ec_hwmon_driver); --=20 2.51.0.384.g4c02a37b29-goog