[PATCH 3/8] platform: arm64: qcom-hamoa-ec: Switch fan profile based on power supply state

Anvesh Jain P <[email protected]> Tue, 28 Jul 2026 23:14:31 +0530
Newsgroups org.kernel.vger.platform-driver-x86,org.kernel.vger.linux-arm-msm,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
Add the EC command definitions and handler functions for switching and
reading the EC fan profile.

Register a power supply notifier so the driver can react to AC/battery
transitions: on each PSY_EVENT_PROP_CHANGED notification, queue work
that checks power_supply_is_system_supplied() and, if the AC/battery
state has actually changed, switches the fan profile to the
performance profile on AC or the battery-saver profile on battery.
Track the last-applied state in on_ac_power to avoid redundant EC
writes. Run the initial sync once at probe time, and defer the actual
switch to a work item since the notifier callback runs in atomic
context.

Serialize the fan profile write under the existing io_lock mutex,
alongside the other EC command sequences.

Signed-off-by: Anvesh Jain P <[email protected]>
---
 drivers/platform/arm64/qcom-hamoa-ec.c | 158 +++++++++++++++++++++++++++++++++
 1 file changed, 158 insertions(+)

diff --git a/drivers/platform/arm64/qcom-hamoa-ec.c b/drivers/platform/arm64/qcom-hamoa-ec.c
index 4c745b78322c..f6ff77d4e8f6 100644
--- a/drivers/platform/arm64/qcom-hamoa-ec.c
+++ b/drivers/platform/arm64/qcom-hamoa-ec.c
@@ -19,6 +19,7 @@
 #include <linux/of_platform.h>
 #include <linux/platform_device.h>
 #include <linux/pm.h>
+#include <linux/power_supply.h>
 #include <linux/slab.h>
 #include <linux/thermal.h>
 
@@ -26,10 +27,14 @@
 #define EC_FW_VERSION_CMD	0x0e
 #define EC_SOC_TEMP_CMD	0x20
 #define EC_MODERN_STANDBY_CMD	0x23
+#define EC_FAN_PROFILE_CMD	0x24
 #define EC_FAN_DBG_CONTROL_CMD	0x30
 #define EC_SCI_EVT_CONTROL_CMD	0x35
 #define EC_THERMAL_CAP_CMD	0x42
 
+#define EC_FAN_PROFILE_BEST_PERF_PLUGGED_IN	0x6
+#define EC_FAN_PROFILE_BETTER_BATT_ON_BATT	0x3
+
 #define EC_SOC_TEMP_SOURCE_TJ			0x1
 #define EC_SOC_TEMP_SOURCE_TSKIN		0x2
 #define EC_SOC_TEMP_DATA_SIZE			2
@@ -92,11 +97,14 @@ struct qcom_ec {
 	struct i2c_client *client;
 	struct qcom_ec_cooling_dev *ec_cdev;
 	struct thermal_zone_device **soc_tj_zones;
+	struct notifier_block psy_nb;
 	struct delayed_work soc_tj_work;
+	struct work_struct psy_work;
 	struct mutex io_lock; /* serializes EC command sequences */
 	struct qcom_ec_thermal_cap thermal_cap;
 	struct qcom_ec_version version;
 	int num_soc_tj_zones;
+	int on_ac_power; /* -1 = unknown, 0 = battery, 1 = AC */
 };
 
 static int qcom_ec_read(struct qcom_ec *ec, u8 cmd, u8 resp_len, u8 *resp)
@@ -351,6 +359,140 @@ static int qcom_ec_send_soc_tj_temp(struct qcom_ec *ec)
 	return ret;
 }
 
+/*
+ * SoC to EC Fan Profile switch command:
+ *
+ * Command Payload:
+ * -----------------------------------------------------------------------
+ * | Offset	| Name		| Description				|
+ * -----------------------------------------------------------------------
+ * | 0x00	| Command = 0x24| SoC to EC Fan Profile switch command	|
+ * -----------------------------------------------------------------------
+ * | 0x01	| Fan Profile ID| Bit 0-3: Fan Profile			|
+ * |		|		| 0x0 : Invalid Profile			|
+ * |		|		| 0x1 : Battery saver profile		|
+ * |		|		| 0x2 : Better Battery with charger	|
+ * |		|		|       plugged in			|
+ * |		|		| 0x3 : Better Battery with charger	|
+ * |		|		|       removed				|
+ * |		|		| 0x4 : Better Performance with charger	|
+ * |		|		|       plugged in			|
+ * |		|		| 0x5 : Better Performance with charger	|
+ * |		|		|       plugged out			|
+ * |		|		| 0x6 : Best Performance with charger	|
+ * |		|		|       plugged in (default)		|
+ * |		|		| 0x7 : Best Performance with charger	|
+ * |		|		|       plugged out			|
+ * |		|		| 0x8 - 0xF : Reserved			|
+ * |		|		| Bit 4-7: Reserved			|
+ * -----------------------------------------------------------------------
+ *
+ */
+static int qcom_ec_set_fan_profile(struct qcom_ec *ec, u8 profile_id)
+{
+	int ret;
+
+	ret = i2c_smbus_write_byte_data(ec->client, EC_FAN_PROFILE_CMD, profile_id);
+	if (ret < 0)
+		dev_err(&ec->client->dev, "Failed to set fan profile 0x%x: %d\n",
+			profile_id, ret);
+	else
+		dev_dbg(&ec->client->dev, "Fan profile set to 0x%x\n", profile_id);
+
+	return ret;
+}
+
+/*
+ * SoC to EC Fan Profile read command:
+ *
+ * Read Response:
+ * -----------------------------------------------------------------------
+ * | Offset	| Name		| Description				|
+ * -----------------------------------------------------------------------
+ * | 0x00	| Fan Profile ID| Bit 0-3: Fan Profile			|
+ * |		|		| 0x0 : Invalid Profile			|
+ * |		|		| 0x1 : Battery saver profile		|
+ * |		|		| 0x2 : Better Battery with charger	|
+ * |		|		|       plugged in			|
+ * |		|		| 0x3 : Better Battery with charger	|
+ * |		|		|       removed				|
+ * |		|		| 0x4 : Better Performance with charger	|
+ * |		|		|       plugged in			|
+ * |		|		| 0x5 : Better Performance with charger	|
+ * |		|		|       plugged out			|
+ * |		|		| 0x6 : Best Performance with charger	|
+ * |		|		|       plugged in (default)		|
+ * |		|		| 0x7 : Best Performance with charger	|
+ * |		|		|       plugged out			|
+ * |		|		| 0x8 - 0xF : Reserved			|
+ * |		|		| Bit 4-7: Fan ID			|
+ * |		|		| 0x1 : Fan 1				|
+ * |		|		| 0x2 : Fan 2				|
+ * -----------------------------------------------------------------------
+ *
+ */
+static int qcom_ec_get_fan_profile(struct qcom_ec *ec, u8 *profile_id)
+{
+	int ret;
+
+	ret = i2c_smbus_read_byte_data(ec->client, EC_FAN_PROFILE_CMD);
+	if (ret < 0)
+		return ret;
+
+	*profile_id = (u8)ret & GENMASK(3, 0);
+
+	return 0;
+}
+
+static int qcom_ec_update_profile_from_power_supply(struct qcom_ec *ec)
+{
+	int on_ac_power;
+	u8 profile;
+	int ret = 0;
+
+	on_ac_power = power_supply_is_system_supplied() > 0 ? 1 : 0;
+
+	profile = on_ac_power ? EC_FAN_PROFILE_BEST_PERF_PLUGGED_IN :
+				EC_FAN_PROFILE_BETTER_BATT_ON_BATT;
+
+	mutex_lock(&ec->io_lock);
+
+	if (ec->on_ac_power != on_ac_power) {
+		ret = qcom_ec_set_fan_profile(ec, profile);
+		if (!ret)
+			ec->on_ac_power = on_ac_power;
+	}
+
+	mutex_unlock(&ec->io_lock);
+
+	return ret;
+}
+
+static void qcom_ec_unreg_psy_notifier(void *data)
+{
+	power_supply_unreg_notifier(data);
+}
+
+static void qcom_ec_psy_work_fn(struct work_struct *work)
+{
+	struct qcom_ec *ec = container_of(work, struct qcom_ec, psy_work);
+
+	qcom_ec_update_profile_from_power_supply(ec);
+}
+
+static int qcom_ec_psy_notifier(struct notifier_block *nb,
+				unsigned long event, void *ptr)
+{
+	struct qcom_ec *ec = container_of(nb, struct qcom_ec, psy_nb);
+
+	if (event != PSY_EVENT_PROP_CHANGED)
+		return NOTIFY_DONE;
+
+	queue_work(system_long_wq, &ec->psy_work);
+
+	return NOTIFY_OK;
+}
+
 static irqreturn_t qcom_ec_irq(int irq, void *data)
 {
 	struct qcom_ec *ec = data;
@@ -570,8 +712,13 @@ static int qcom_ec_probe(struct i2c_client *client)
 		return -ENOMEM;
 
 	ec->client = client;
+	ec->on_ac_power = -1;
 	mutex_init(&ec->io_lock);
 
+	ret = devm_work_autocancel(dev, &ec->psy_work, qcom_ec_psy_work_fn);
+	if (ret)
+		return ret;
+
 	ret = devm_request_threaded_irq(dev, client->irq, NULL, qcom_ec_irq,
 					IRQF_ONESHOT, "qcom_ec", ec);
 	if (ret < 0)
@@ -630,6 +777,17 @@ static int qcom_ec_probe(struct i2c_client *client)
 		}
 	}
 
+	ec->psy_nb.notifier_call = qcom_ec_psy_notifier;
+	ret = power_supply_reg_notifier(&ec->psy_nb);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to register power supply notifier\n");
+
+	ret = devm_add_action_or_reset(dev, qcom_ec_unreg_psy_notifier, &ec->psy_nb);
+	if (ret)
+		return ret;
+
+	qcom_ec_update_profile_from_power_supply(ec);
+
 	return 0;
 }
 

-- 
2.34.1