[PATCH 4/8] platform: arm64: qcom-hamoa-ec: Add fan RPM query and LUT calibration

Anvesh Jain P <[email protected]> Tue, 28 Jul 2026 23:14:32 +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 querying fan
RPM and programming per-fan LUTs (lookup tables mapping SoC
temperature to target RPM), and introduce a raw i2c_transfer()-based
transport for the two commands, since their variable-length,
sub-command-addressed payloads don't fit the smbus block-data model
used by the existing commands.

Fan LUT geometry and temperature breakpoints vary by board, so encode
them in per-compatible qcom_ec_lut_config data selected via
device_get_match_data(). Add configs for the Hamoa CRD, Hamoa/Purwa
IOT EVK, and Glymur CRD ECs; IOT EVK boards share the Hamoa LUT
geometry but are bring-up platforms without a calibrated fan curve,
so skip_lut_set suppresses the LUT-set command on those boards.

Since the LUT values depend on each fan's maximum RPM, which varies
per board assembly, calibrate it at runtime: drive the fan to full
PWM, wait for it to spin up, and read back the achieved RPM via the
new query command. Run calibration in a work item at probe time and
after EC reset recovery, and make it abortable via a completion so
suspend can cut it short. Track calibration completion state so the
LUT is programmed once calibration finishes and again on every
subsequent power-supply-driven profile switch.

Guard all of the above behind the presence of lut_cfg, since boards
without match data (or without a populated fan_cnt) have no LUT to
calibrate or program. Add a new state_lock mutex to guard the
per-fan cooling device state (current PWM state and calibrated
max_rpm) now that it can be read and written from more than one
context.

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

diff --git a/drivers/platform/arm64/qcom-hamoa-ec.c b/drivers/platform/arm64/qcom-hamoa-ec.c
index f6ff77d4e8f6..d0c45d9970ee 100644
--- a/drivers/platform/arm64/qcom-hamoa-ec.c
+++ b/drivers/platform/arm64/qcom-hamoa-ec.c
@@ -6,6 +6,7 @@
 
 #include <linux/bitfield.h>
 #include <linux/bits.h>
+#include <linux/completion.h>
 #include <linux/delay.h>
 #include <linux/devm-helpers.h>
 #include <linux/device.h>
@@ -20,14 +21,17 @@
 #include <linux/platform_device.h>
 #include <linux/pm.h>
 #include <linux/power_supply.h>
+#include <linux/property.h>
 #include <linux/slab.h>
 #include <linux/thermal.h>
 
 #define EC_SCI_EVT_READ_CMD	0x05
 #define EC_FW_VERSION_CMD	0x0e
 #define EC_SOC_TEMP_CMD	0x20
+#define EC_FAN_RPM_QUERY_CMD	0x22
 #define EC_MODERN_STANDBY_CMD	0x23
 #define EC_FAN_PROFILE_CMD	0x24
+#define EC_SET_FAN_LUT_CMD	0x28
 #define EC_FAN_DBG_CONTROL_CMD	0x30
 #define EC_SCI_EVT_CONTROL_CMD	0x35
 #define EC_THERMAL_CAP_CMD	0x42
@@ -41,8 +45,11 @@
 
 #define EC_FW_VERSION_RESP_LEN	4
 #define EC_THERMAL_CAP_RESP_LEN	3
+#define EC_FAN_RPM_RESP_LEN	3
 #define EC_FAN_DEBUG_CMD_LEN	6
 #define EC_FAN_SPEED_DATA_SIZE	4
+#define EC_LUT_MAX_DATA_SIZE	64
+#define EC_MAX_LUT_ENTRIES	16
 
 #define EC_MODERN_STANDBY_ENTER	0x01
 #define EC_MODERN_STANDBY_EXIT	0x00
@@ -71,6 +78,92 @@ enum qcom_ec_sci_events {
 	EC_RECOVERED_FROM_RESET_EVT = 0x3d,
 };
 
+struct qcom_ec_fan_lut_entry {
+	u8 target_rpm;	/* units of 100 RPM */
+	u8 temp_high;
+	u8 temp_low;
+};
+
+struct qcom_ec_fan_lut_temp_entry {
+	u8 temp_high;
+	u8 temp_low;
+};
+
+struct qcom_ec_lut_config {
+	u8 lut_data_size;
+	u8 lut_entry_stride;
+	u8 max_lut_entries;
+	const struct qcom_ec_fan_lut_temp_entry *temps;
+	u8 num_temps;
+	bool send_soc_tj;
+	bool skip_lut_set;
+};
+
+static const struct qcom_ec_fan_lut_temp_entry hamoa_lut_temps[] = {
+	{ 32,  25  },
+	{ 35,  32  },
+	{ 42,  35  },
+	{ 45,  42  },
+	{ 52,  45  },
+	{ 55,  52  },
+	{ 60,  55  },
+	{ 65,  60  },
+	{ 70,  65  },
+	{ 75,  70  },
+	{ 80,  75  },
+	{ 85,  80  },
+	{ 90,  85  },
+	{ 95,  90  },
+	{ 100, 95  },
+	{ 105, 100 },
+};
+
+static const struct qcom_ec_fan_lut_temp_entry glymur_lut_temps[] = {
+	{ 40, 35 },
+	{ 50, 45 },
+	{ 55, 50 },
+	{ 60, 55 },
+	{ 65, 60 },
+	{ 70, 65 },
+	{ 75, 70 },
+	{ 85, 80 },
+};
+
+static const struct qcom_ec_lut_config hamoa_crd_lut_config = {
+	.lut_data_size            = 64,
+	.lut_entry_stride         = 4,
+	.max_lut_entries          = 16,
+	.temps                    = hamoa_lut_temps,
+	.num_temps                = ARRAY_SIZE(hamoa_lut_temps),
+	.send_soc_tj              = true,
+};
+
+/*
+ * IOT EVK boards share the Hamoa LUT geometry but are bring-up platforms
+ * without a calibrated fan curve, so the LUT-set command is not issued.
+ */
+static const struct qcom_ec_lut_config hamoa_iot_lut_config = {
+	.lut_data_size            = 64,
+	.lut_entry_stride         = 4,
+	.max_lut_entries          = 16,
+	.temps                    = hamoa_lut_temps,
+	.num_temps                = ARRAY_SIZE(hamoa_lut_temps),
+	.send_soc_tj              = true,
+	.skip_lut_set             = true,
+};
+
+static const struct qcom_ec_lut_config glymur_lut_config = {
+	.lut_data_size            = 24,
+	.lut_entry_stride         = 3,
+	.max_lut_entries          = 8,
+	.temps                    = glymur_lut_temps,
+	.num_temps                = ARRAY_SIZE(glymur_lut_temps),
+	.send_soc_tj              = false,
+};
+
+static_assert(ARRAY_SIZE(hamoa_lut_temps) <= EC_MAX_LUT_ENTRIES);
+static_assert(ARRAY_SIZE(glymur_lut_temps) <= EC_MAX_LUT_ENTRIES);
+
 struct qcom_ec_version {
 	u8 main_version;
 	u8 sub_version;
@@ -91,36 +184,125 @@ struct qcom_ec_cooling_dev {
 	struct device *parent_dev;
 	u8 fan_id;
 	u8 state;
+	u16 max_rpm;
+	struct qcom_ec_fan_lut_entry lut_entries[EC_MAX_LUT_ENTRIES];
 };
 
 struct qcom_ec {
 	struct i2c_client *client;
+	const struct qcom_ec_lut_config *lut_cfg;
 	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 fan_calib_work;
 	struct work_struct psy_work;
-	struct mutex io_lock; /* serializes EC command sequences */
+	struct completion calib_abort;
+	struct mutex io_lock; /* serializes EC command sequences and calibrating */
+	struct mutex state_lock; /* guards per-fan cooling device state */
 	struct qcom_ec_thermal_cap thermal_cap;
 	struct qcom_ec_version version;
 	int num_soc_tj_zones;
+	u8 fan_profile;
 	int on_ac_power; /* -1 = unknown, 0 = battery, 1 = AC */
+	bool calibrating;
+	bool calib_done;
 };
 
-static int qcom_ec_read(struct qcom_ec *ec, u8 cmd, u8 resp_len, u8 *resp)
+static int qcom_ec_write(struct qcom_ec *ec, u8 cmd, const u8 *data, size_t len)
 {
+	struct i2c_msg msg;
 	int ret;
 
-	ret = i2c_smbus_read_i2c_block_data(ec->client, cmd, resp_len, resp);
-	if (ret < 0)
-		return ret;
-	else if (ret == 0 || ret == 0xff)
-		return -EOPNOTSUPP;
+	if (len > U16_MAX - 1)
+		return -EINVAL;
 
-	if (resp[0] >= resp_len)
+	if (!data && len)
 		return -EINVAL;
 
-	return 0;
+	msg = (struct i2c_msg){
+		.addr  = ec->client->addr,
+		.flags = I2C_M_DMA_SAFE,
+		.len   = 1 + len,
+	};
+
+	msg.buf = kmalloc(1 + len, GFP_KERNEL);
+	if (!msg.buf)
+		return -ENOMEM;
+
+	msg.buf[0] = cmd;
+	memcpy(&msg.buf[1], data, len);
+
+	ret = i2c_transfer(ec->client->adapter, &msg, 1);
+
+	kfree(msg.buf);
+
+	return ret == 1 ? 0 : (ret < 0 ? ret : -EIO);
+}
+
+static int qcom_ec_read(struct qcom_ec *ec, u8 cmd, const u8 *subcmd,
+			u8 subcmd_len, u8 resp_len, u8 *resp)
+{
+	struct i2c_client *client = ec->client;
+	struct i2c_msg msgs[2];
+	u8 *write_buf, *read_buf;
+	int ret;
+
+	if (subcmd_len > 3)
+		return -EINVAL;
+
+	if (!subcmd && subcmd_len)
+		return -EINVAL;
+
+	if (!resp || !resp_len)
+		return -EINVAL;
+
+	write_buf = kmalloc(1 + subcmd_len, GFP_KERNEL);
+	if (!write_buf)
+		return -ENOMEM;
+
+	read_buf = kmalloc(resp_len, GFP_KERNEL);
+	if (!read_buf) {
+		kfree(write_buf);
+		return -ENOMEM;
+	}
+
+	write_buf[0] = cmd;
+	if (subcmd && subcmd_len)
+		memcpy(&write_buf[1], subcmd, subcmd_len);
+
+	msgs[0].addr  = client->addr;
+	msgs[0].flags = I2C_M_DMA_SAFE;
+	msgs[0].len   = 1 + subcmd_len;
+	msgs[0].buf   = write_buf;
+
+	msgs[1].addr  = client->addr;
+	msgs[1].flags = I2C_M_RD | I2C_M_DMA_SAFE;
+	msgs[1].len   = resp_len;
+	msgs[1].buf   = read_buf;
+
+	ret = i2c_transfer(client->adapter, msgs, 2);
+	if (ret != 2) {
+		ret = ret < 0 ? ret : -EIO;
+		goto out;
+	}
+
+	if (read_buf[0] == 0 || read_buf[0] == 0xff) {
+		ret = -EOPNOTSUPP;
+		goto out;
+	}
+
+	if (read_buf[0] != resp_len - 1) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	memcpy(resp, read_buf, resp_len);
+	ret = 0;
+out:
+	kfree(write_buf);
+	kfree(read_buf);
+	return ret;
 }
 
 /*
@@ -150,7 +332,7 @@ static int qcom_ec_read_fw_version(struct device *dev)
 	int ret;
 
 	mutex_lock(&ec->io_lock);
-	ret = qcom_ec_read(ec, EC_FW_VERSION_CMD, EC_FW_VERSION_RESP_LEN, resp);
+	ret = qcom_ec_read(ec, EC_FW_VERSION_CMD, NULL, 0, EC_FW_VERSION_RESP_LEN, resp);
 	mutex_unlock(&ec->io_lock);
 	if (ret < 0)
 		return ret;
@@ -194,7 +376,7 @@ static int qcom_ec_thermal_capabilities(struct device *dev)
 	int ret;
 
 	mutex_lock(&ec->io_lock);
-	ret = qcom_ec_read(ec, EC_THERMAL_CAP_CMD, EC_THERMAL_CAP_RESP_LEN, resp);
+	ret = qcom_ec_read(ec, EC_THERMAL_CAP_CMD, NULL, 0, EC_THERMAL_CAP_RESP_LEN, resp);
 	mutex_unlock(&ec->io_lock);
 	if (ret < 0)
 		return ret;
@@ -444,6 +626,133 @@ static int qcom_ec_get_fan_profile(struct qcom_ec *ec, u8 *profile_id)
 	return 0;
 }
 
+/*
+ * Setting a Fan LUT from SoC to EC:
+ *
+ * Request (SoC -> EC):
+ * -------------------------------------------------------------------------------
+ * | Offset		| Name		| Description				|
+ * -------------------------------------------------------------------------------
+ * | 0x00		| Command = 0x28| Command to set Fan LUT from EC	|
+ * -------------------------------------------------------------------------------
+ * | 0x01		| Fan and	| Bit 0-3: Fan ID (1 - 2)		|
+ * |			| Profile ID	| Bit 4-7: Fan Profile ID (1 - 7)	|
+ * -------------------------------------------------------------------------------
+ * | 0x02		| Temp Source	| 0x1 : SoC Tj				|
+ * |			|		| 0x2 : Tskin				|
+ * |			|		| 0x3 - 0xFF : Reserved			|
+ * -------------------------------------------------------------------------------
+ * | 0x03		| Byte count=64	| EC LUT payload byte count		|
+ * -------------------------------------------------------------------------------
+ * | 0x04 - (0x04 +	| EC LUT data	| N* entries x 3 bytes:			|
+ * | (N* x 3))		|		| Byte 1: Target RPM (units of 100 RPM)	|
+ * |			|		| Byte 2: Temp High			|
+ * |			|		| Byte 3: Temp Low			|
+ * -------------------------------------------------------------------------------
+ *
+ * N* - Indicates the number of entries in the LUT.
+ *
+ */
+static int qcom_ec_set_fan_lut(struct qcom_ec *ec, u8 fan_id, u8 profile_id,
+			       u8 temp_source, const struct qcom_ec_fan_lut_entry *entries)
+{
+	const struct qcom_ec_lut_config *lut_cfg = ec->lut_cfg;
+	u8 write_buf[3 + EC_LUT_MAX_DATA_SIZE] = {};
+	int i, ret;
+
+	if (!lut_cfg)
+		return -EOPNOTSUPP;
+
+	if (lut_cfg->max_lut_entries == 0 ||
+	    lut_cfg->lut_entry_stride < 3 ||
+	    lut_cfg->lut_data_size > EC_LUT_MAX_DATA_SIZE ||
+	    (lut_cfg->max_lut_entries - 1) * lut_cfg->lut_entry_stride + 3 > lut_cfg->lut_data_size)
+		return -EINVAL;
+
+	write_buf[0] = (fan_id & 0x0f) | ((profile_id & 0x0f) << 4);
+	write_buf[1] = temp_source;
+	write_buf[2] = lut_cfg->lut_data_size;
+
+	for (i = 0; i < lut_cfg->max_lut_entries; i++) {
+		u8 *dst = &write_buf[3 + i * lut_cfg->lut_entry_stride];
+
+		dst[0] = entries[i].target_rpm;
+		dst[1] = entries[i].temp_high;
+		dst[2] = entries[i].temp_low;
+	}
+
+	ret = qcom_ec_write(ec, EC_SET_FAN_LUT_CMD, write_buf, 3 + lut_cfg->lut_data_size);
+	if (ret < 0)
+		dev_err(&ec->client->dev, "Failed to set fan LUT: %d\n", ret);
+
+	return ret;
+}
+
+/*
+ * EC Fan RPM Query:
+ *
+ * Request (SoC -> EC):
+ * ---------------------------------------------------------------
+ * | Offset	| Name		| Description			|
+ * ---------------------------------------------------------------
+ * | 0x00	| Command = 0x22| EC Fan RPM Query		|
+ * ---------------------------------------------------------------
+ * | 0x01	| Fan ID	| 0x1 : Fan 1			|
+ * |		|		| 0x2 : Fan 2			|
+ * ---------------------------------------------------------------
+ *
+ * Response (EC -> SoC):
+ * ---------------------------------------------------------------
+ * | Offset	| Name		| Description			|
+ * ---------------------------------------------------------------
+ * | 0x00	| Byte count = 2| Byte count for fan RPM value	|
+ * ---------------------------------------------------------------
+ * | 0x01 (LSB)| Fan Speed	| Fan speed in the resolution	|
+ * | 0x02	|		| of 1 RPM			|
+ * ---------------------------------------------------------------
+ */
+static int qcom_ec_get_fan_rpm(struct qcom_ec *ec, u8 fan_id, u16 *rpm)
+{
+	u8 resp[EC_FAN_RPM_RESP_LEN];
+	int ret;
+
+	ret = qcom_ec_read(ec, EC_FAN_RPM_QUERY_CMD, &fan_id, 1, sizeof(resp), resp);
+	if (ret < 0)
+		return ret;
+
+	*rpm = resp[1] | (resp[2] << 8);
+
+	return 0;
+}
+
+static int qcom_ec_set_lut_for_profile(struct qcom_ec *ec, u8 profile_id)
+{
+	const struct qcom_ec_lut_config *lut_cfg = ec->lut_cfg;
+	int i, ret;
+
+	if (!lut_cfg)
+		return -EOPNOTSUPP;
+
+	if (lut_cfg->skip_lut_set)
+		return 0;
+
+	for (i = 0; i < ec->thermal_cap.fan_cnt; i++) {
+		struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i];
+
+		ret = qcom_ec_set_fan_lut(ec, ec_cdev->fan_id, profile_id,
+					  EC_SOC_TEMP_SOURCE_TJ,
+					  ec_cdev->lut_entries);
+		if (ret < 0) {
+			dev_err(&ec->client->dev,
+				"Failed to set LUT for fan%u profile 0x%x: %d\n",
+				ec_cdev->fan_id, profile_id, ret);
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
 static int qcom_ec_update_profile_from_power_supply(struct qcom_ec *ec)
 {
 	int on_ac_power;
@@ -457,8 +766,10 @@ static int qcom_ec_update_profile_from_power_supply(struct qcom_ec *ec)
 
 	mutex_lock(&ec->io_lock);
 
-	if (ec->on_ac_power != on_ac_power) {
+	if (ec->on_ac_power != on_ac_power && !ec->calibrating) {
 		ret = qcom_ec_set_fan_profile(ec, profile);
+		if (!ret && ec->lut_cfg && ec->calib_done)
+			ret = qcom_ec_set_lut_for_profile(ec, profile);
 		if (!ret)
 			ec->on_ac_power = on_ac_power;
 	}
@@ -473,6 +784,24 @@ static void qcom_ec_unreg_psy_notifier(void *data)
 	power_supply_unreg_notifier(data);
 }
 
+static void qcom_ec_restore_fan_state(struct qcom_ec *ec)
+{
+	bool calib_done;
+
+	if (!ec->lut_cfg || !ec->thermal_cap.fan_cnt)
+		return;
+
+	mutex_lock(&ec->io_lock);
+	ec->on_ac_power = -1;
+	calib_done = ec->calib_done;
+	mutex_unlock(&ec->io_lock);
+
+	qcom_ec_update_profile_from_power_supply(ec);
+
+	if (!calib_done)
+		queue_work(system_long_wq, &ec->fan_calib_work);
+}
+
 static void qcom_ec_psy_work_fn(struct work_struct *work)
 {
 	struct qcom_ec *ec = container_of(work, struct qcom_ec, psy_work);
@@ -537,6 +866,7 @@ static irqreturn_t qcom_ec_irq(int irq, void *data)
 		break;
 	case EC_RECOVERED_FROM_RESET_EVT:
 		dev_dbg_ratelimited(dev, "EC recovered from reset\n");
+		qcom_ec_restore_fan_state(ec);
 		break;
 	default:
 		dev_notice_ratelimited(dev, "Unknown EC event: %d\n", val);
@@ -569,20 +899,30 @@ static void qcom_ec_sci_evt_disable(void *data)
 		dev_err(dev, "Failed to disable SCI events: %d\n", ret);
 }
 
-static int qcom_ec_fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state)
-{
-	*state = EC_FAN_MAX_PWM;
-
-	return 0;
-}
-
 static int qcom_ec_fan_get_cur_state(struct thermal_cooling_device *cdev, unsigned long *state)
 {
 	struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata;
+	struct i2c_client *client = to_i2c_client(ec_cdev->parent_dev);
+	struct qcom_ec *ec = i2c_get_clientdata(client);
+	struct device *dev = ec_cdev->parent_dev;
+	u16 rpm;
+	int ret;
+
+	mutex_lock(&ec->io_lock);
+	ret = qcom_ec_get_fan_rpm(ec, ec_cdev->fan_id, &rpm);
+	mutex_unlock(&ec->io_lock);
+	if (ret < 0)
+		dev_err(dev, "Failed to read fan%u RPM: %d\n", ec_cdev->fan_id, ret);
 
+	mutex_lock(&ec->state_lock);
+
+	if (ret >= 0 && ec_cdev->max_rpm)
+		ec_cdev->state = min_t(u32, (rpm * EC_FAN_MAX_PWM) / ec_cdev->max_rpm,
+				       EC_FAN_MAX_PWM);
 	*state = ec_cdev->state;
+	mutex_unlock(&ec->state_lock);
 
-	return 0;
+	return ret < 0 ? ret : 0;
 }
 
 /*
@@ -628,24 +968,116 @@ static int qcom_ec_fan_debug_mode_off(struct qcom_ec_cooling_dev *ec_cdev)
 	return ret;
 }
 
-static int qcom_ec_fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state)
+static int qcom_ec_fan_write_pwm(struct qcom_ec_cooling_dev *ec_cdev, u8 pwm)
 {
-	struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata;
 	struct device *dev = ec_cdev->parent_dev;
 	struct i2c_client *client = to_i2c_client(dev);
+	struct qcom_ec *ec = i2c_get_clientdata(client);
 	u8 request[6] = { ec_cdev->fan_id, EC_FAN_SPEED_DATA_SIZE,
 			  EC_FAN_DEBUG_MODE_ON | EC_FAN_ON | EC_FAN_DEBUG_TYPE_PWM,
-			  0, 0, state };
+			  0, 0, pwm };
 	int ret;
 
-	ret = i2c_smbus_write_i2c_block_data(client, EC_FAN_DBG_CONTROL_CMD,
-					     sizeof(request), request);
-	if (ret) {
+	ret = qcom_ec_write(ec, EC_FAN_DBG_CONTROL_CMD, request, sizeof(request));
+	if (ret)
 		dev_err(dev, "Failed to set fan pwm: %d\n", ret);
+
+	return ret;
+}
+
+static int qcom_ec_fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state)
+{
+	struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata;
+	struct i2c_client *client = to_i2c_client(ec_cdev->parent_dev);
+	struct qcom_ec *ec = i2c_get_clientdata(client);
+	int ret;
+
+	if (state > EC_FAN_MAX_PWM)
+		return -EINVAL;
+
+	mutex_lock(&ec->io_lock);
+
+	if (ec->calibrating) {
+		mutex_unlock(&ec->io_lock);
+		return -EBUSY;
+	}
+
+	ret = qcom_ec_fan_write_pwm(ec_cdev, state);
+	if (!ret) {
+		mutex_lock(&ec->state_lock);
+		ec_cdev->state = state;
+		mutex_unlock(&ec->state_lock);
+	}
+
+	mutex_unlock(&ec->io_lock);
+
+	return ret;
+}
+
+static int qcom_ec_fan_calibrate(struct qcom_ec *ec, struct qcom_ec_cooling_dev *ec_cdev)
+{
+	struct device *dev = ec_cdev->parent_dev;
+	u16 rpm;
+	int ret;
+
+	mutex_lock(&ec->io_lock);
+	ret = qcom_ec_fan_write_pwm(ec_cdev, EC_FAN_MAX_PWM);
+	mutex_unlock(&ec->io_lock);
+	if (ret) {
+		dev_err(dev, "Failed to set fan%u to max state: %d\n", ec_cdev->fan_id, ret);
 		return ret;
 	}
 
-	ec_cdev->state = state;
+	mutex_lock(&ec->state_lock);
+	ec_cdev->state = EC_FAN_MAX_PWM;
+	mutex_unlock(&ec->state_lock);
+
+	ret = wait_for_completion_interruptible_timeout(&ec->calib_abort, msecs_to_jiffies(10000));
+
+	mutex_lock(&ec->io_lock);
+
+	if (ret != 0) {
+		qcom_ec_fan_debug_mode_off(ec_cdev);
+		mutex_unlock(&ec->io_lock);
+		return ret < 0 ? ret : -ECANCELED;
+	}
+
+	ret = qcom_ec_get_fan_rpm(ec, ec_cdev->fan_id, &rpm);
+	if (ret) {
+		dev_err(dev, "Failed to read fan%u RPM at max state: %d\n", ec_cdev->fan_id, ret);
+		qcom_ec_fan_debug_mode_off(ec_cdev);
+		mutex_unlock(&ec->io_lock);
+		return ret;
+	}
+
+	if (!rpm) {
+		dev_err(dev, "Fan%u reported 0 RPM at max state, fan may be blocked or faulty\n",
+			ec_cdev->fan_id);
+		qcom_ec_fan_debug_mode_off(ec_cdev);
+		mutex_unlock(&ec->io_lock);
+		return -ENODEV;
+	}
+
+	qcom_ec_fan_debug_mode_off(ec_cdev);
+	mutex_unlock(&ec->io_lock);
+
+	mutex_lock(&ec->state_lock);
+	ec_cdev->max_rpm = rpm;
+
+	for (int i = 0; i < ec->lut_cfg->max_lut_entries; i++) {
+		ec_cdev->lut_entries[i].target_rpm =
+			min_t(u32, rpm * i / (ec->lut_cfg->max_lut_entries - 1) / 100, U8_MAX);
+		ec_cdev->lut_entries[i].temp_high = ec->lut_cfg->temps[i].temp_high;
+		ec_cdev->lut_entries[i].temp_low  = ec->lut_cfg->temps[i].temp_low;
+	}
+	mutex_unlock(&ec->state_lock);
+
+	return 0;
+}
+
+static int qcom_ec_fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state)
+{
+	*state = EC_FAN_MAX_PWM;
 
 	return 0;
 }
@@ -665,6 +1097,49 @@ static void qcom_ec_soc_tj_work_fn(struct work_struct *work)
 	queue_delayed_work(system_percpu_wq, &ec->soc_tj_work, EC_SOC_TJ_TEMP_POLL_JIFFIES);
 }
 
+static void qcom_ec_fan_calib_work_fn(struct work_struct *work)
+{
+	struct qcom_ec *ec = container_of(work, struct qcom_ec, fan_calib_work);
+	struct device *dev = &ec->client->dev;
+	int i, ret;
+
+	reinit_completion(&ec->calib_abort);
+
+	mutex_lock(&ec->io_lock);
+	ec->calibrating = true;
+	mutex_unlock(&ec->io_lock);
+
+	for (i = 0; i < ec->thermal_cap.fan_cnt; i++) {
+		ret = qcom_ec_fan_calibrate(ec, &ec->ec_cdev[i]);
+		if (ret) {
+			dev_err(dev, "Failed to calibrate fan%d: %d\n", i, ret);
+			goto out;
+		}
+	}
+
+	mutex_lock(&ec->io_lock);
+	ret = qcom_ec_get_fan_profile(ec, &ec->fan_profile);
+	if (ret < 0) {
+		dev_err(dev, "Failed to read current fan profile: %d\n", ret);
+		mutex_unlock(&ec->io_lock);
+		goto out;
+	}
+
+	ec->calib_done = true;
+	ret = qcom_ec_set_lut_for_profile(ec, ec->fan_profile);
+	mutex_unlock(&ec->io_lock);
+
+	if (ret < 0)
+		dev_err(dev, "Failed to set LUT for profile 0x%x: %d\n", ec->fan_profile, ret);
+
+out:
+	mutex_lock(&ec->io_lock);
+	ec->calibrating = false;
+	mutex_unlock(&ec->io_lock);
+
+	qcom_ec_update_profile_from_power_supply(ec);
+}
+
 static int qcom_ec_resume(struct device *dev)
 {
 	struct i2c_client *client = to_i2c_client(dev);
@@ -678,9 +1153,13 @@ static int qcom_ec_resume(struct device *dev)
 	if (ret)
 		return ret;
 
-	enable_delayed_work(&ec->soc_tj_work);
-	queue_delayed_work(system_percpu_wq, &ec->soc_tj_work, EC_SOC_TJ_TEMP_POLL_JIFFIES);
+	if (ec->lut_cfg && ec->thermal_cap.fan_cnt && !ec->calib_done)
+		queue_work(system_long_wq, &ec->fan_calib_work);
 
+	if (ec->lut_cfg && ec->lut_cfg->send_soc_tj) {
+		enable_delayed_work(&ec->soc_tj_work);
+		queue_delayed_work(system_percpu_wq, &ec->soc_tj_work, EC_SOC_TJ_TEMP_POLL_JIFFIES);
+	}
 	return 0;
 }
 
@@ -690,7 +1169,16 @@ static int qcom_ec_suspend(struct device *dev)
 	struct qcom_ec *ec = i2c_get_clientdata(client);
 	int ret;
 
-	disable_delayed_work_sync(&ec->soc_tj_work);
+	if (ec->lut_cfg && ec->thermal_cap.fan_cnt) {
+		complete_all(&ec->calib_abort);
+		cancel_work_sync(&ec->fan_calib_work);
+	}
+
+	if (ec->lut_cfg)
+		cancel_work_sync(&ec->psy_work);
+
+	if (ec->lut_cfg && ec->lut_cfg->send_soc_tj)
+		disable_delayed_work_sync(&ec->soc_tj_work);
 
 	mutex_lock(&ec->io_lock);
 	ret = i2c_smbus_write_byte_data(client, EC_MODERN_STANDBY_CMD,
@@ -714,6 +1202,22 @@ static int qcom_ec_probe(struct i2c_client *client)
 	ec->client = client;
 	ec->on_ac_power = -1;
 	mutex_init(&ec->io_lock);
+	mutex_init(&ec->state_lock);
+	init_completion(&ec->calib_abort);
+	ec->lut_cfg = device_get_match_data(dev);
+
+	if (ec->lut_cfg &&
+	    (ec->lut_cfg->max_lut_entries < 2 ||
+	     ec->lut_cfg->max_lut_entries > EC_MAX_LUT_ENTRIES ||
+	     ec->lut_cfg->max_lut_entries > ec->lut_cfg->num_temps))
+		return dev_err_probe(dev, -EINVAL,
+				     "Invalid LUT config: max_lut_entries=%u, num_temps=%u, limit=%d\n",
+				     ec->lut_cfg->max_lut_entries, ec->lut_cfg->num_temps,
+				     EC_MAX_LUT_ENTRIES);
+
+	ret = devm_work_autocancel(dev, &ec->fan_calib_work, qcom_ec_fan_calib_work_fn);
+	if (ret)
+		return ret;
 
 	ret = devm_work_autocancel(dev, &ec->psy_work, qcom_ec_psy_work_fn);
 	if (ret)
@@ -742,15 +1246,18 @@ static int qcom_ec_probe(struct i2c_client *client)
 	if (ret < 0)
 		return dev_err_probe(dev, ret, "Failed to read thermal capabilities\n");
 
-	ret = qcom_ec_setup_soc_tj_zones(ec);
-	if (ret < 0)
-		return dev_err_probe(dev, ret, "Failed to setup SoC Tj thermal zones\n");
+	if (ec->lut_cfg && ec->lut_cfg->send_soc_tj) {
+		ret = qcom_ec_setup_soc_tj_zones(ec);
+		if (ret < 0)
+			return dev_err_probe(dev, ret, "Failed to setup SoC Tj thermal zones\n");
 
-	ret = devm_delayed_work_autocancel(dev, &ec->soc_tj_work, qcom_ec_soc_tj_work_fn);
-	if (ret)
-		return ret;
+		ret = devm_delayed_work_autocancel(dev, &ec->soc_tj_work,
+						   qcom_ec_soc_tj_work_fn);
+		if (ret)
+			return ret;
 
-	queue_delayed_work(system_percpu_wq, &ec->soc_tj_work, EC_SOC_TJ_TEMP_POLL_JIFFIES);
+		queue_delayed_work(system_percpu_wq, &ec->soc_tj_work, EC_SOC_TJ_TEMP_POLL_JIFFIES);
+	}
 
 	if (ec->thermal_cap.fan_cnt == 0) {
 		dev_warn(dev, FW_BUG "Failed to get fan count, firmware update required\n");
@@ -777,6 +1284,9 @@ static int qcom_ec_probe(struct i2c_client *client)
 		}
 	}
 
+	if (!ec->lut_cfg)
+		return 0;
+
 	ec->psy_nb.notifier_call = qcom_ec_psy_notifier;
 	ret = power_supply_reg_notifier(&ec->psy_nb);
 	if (ret)
@@ -788,6 +1298,7 @@ static int qcom_ec_probe(struct i2c_client *client)
 
 	qcom_ec_update_profile_from_power_supply(ec);
 
+	queue_work(system_long_wq, &ec->fan_calib_work);
 	return 0;
 }
 
@@ -795,17 +1306,28 @@ static void qcom_ec_remove(struct i2c_client *client)
 {
 	struct qcom_ec *ec = i2c_get_clientdata(client);
 
-	disable_delayed_work_sync(&ec->soc_tj_work);
+	if (ec->lut_cfg && ec->thermal_cap.fan_cnt) {
+		complete_all(&ec->calib_abort);
+		disable_work_sync(&ec->fan_calib_work);
+	}
+
+	if (ec->lut_cfg && ec->lut_cfg->send_soc_tj)
+		disable_delayed_work_sync(&ec->soc_tj_work);
 
 	for (int i = 0; i < ec->thermal_cap.fan_cnt; i++) {
 		struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i];
 
+		mutex_lock(&ec->io_lock);
 		qcom_ec_fan_debug_mode_off(ec_cdev);
+		mutex_unlock(&ec->io_lock);
 	}
 }
 
 static const struct of_device_id qcom_ec_of_match[] = {
-	{ .compatible = "qcom,hamoa-crd-ec" },
+	{ .compatible = "qcom,hamoa-iot-evk-ec", .data = &hamoa_iot_lut_config },
+	{ .compatible = "qcom,purwa-iot-evk-ec", .data = &hamoa_iot_lut_config },
+	{ .compatible = "qcom,hamoa-crd-ec",     .data = &hamoa_crd_lut_config },
+	{ .compatible = "qcom,glymur-crd-ec",    .data = &glymur_lut_config    },
 	{}
 };
 MODULE_DEVICE_TABLE(of, qcom_ec_of_match);

-- 
2.34.1