[PATCH 4/8] platform: arm64: qcom-hamoa-ec: Add fan RPM query and LUT calibration
From: Anvesh Jain P
Date: Tue Jul 28 2026 - 15:10:59 EST
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 <anvesh.p@xxxxxxxxxxxxxxxx>
---
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