* [PATCH v2 1/3] platform: arm64:: lenovo-thinkpad-t14s-ec: Add hwmon support for temperatures
2026-06-30 14:53 [PATCH v2 0/3] Lenovo ThinkPad T14s EC thermal monitoring and thermal zone integration Daniel Lezcano
@ 2026-06-30 14:53 ` Daniel Lezcano
2026-06-30 15:08 ` Ilpo Järvinen
2026-06-30 14:53 ` [PATCH v2 2/3] platform: arm64: lenovo-thinkpad-t14s: Wire EC thermal events to hwmon Daniel Lezcano
2026-06-30 14:53 ` [PATCH v2 3/3] arm64: dts: qcom: x1e78100-t14s: Add thermal zones for keyboard skin and charging sensors Daniel Lezcano
2 siblings, 1 reply; 6+ messages in thread
From: Daniel Lezcano @ 2026-06-30 14:53 UTC (permalink / raw)
To: sre, hansg, ilpo.jarvinen, linux, andersson, konradybcio, robh,
krzk+dt, conor+dt
Cc: bryan.odonoghue, platform-driver-x86, linux-kernel, linux-hwmon,
linux-arm-msm, devicetree
Expose the Lenovo ThinkPad T14s EC environmental sensors through
the hwmon subsystem.
The driver now registers a hwmon device providing access to six EC
temperature sensors corresponding to the SoC, keyboard area, base
cover, PMIC/charging circuitry, QTM module and SSD. Sensor labels
are exported to allow user space to identify each measurement.
This allows standard monitoring tools such as lm-sensors to report
platform temperatures.
Signed-off-by: Daniel Lezcano daniel.lezcano@oss.qualcomm.com
---
drivers/platform/arm64/lenovo-thinkpad-t14s.c | 128 ++++++++++++++++++
1 file changed, 128 insertions(+)
diff --git a/drivers/platform/arm64/lenovo-thinkpad-t14s.c b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
index 5590302a5694..35a6f8b0cb6b 100644
--- a/drivers/platform/arm64/lenovo-thinkpad-t14s.c
+++ b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
@@ -11,6 +11,7 @@
#include <linux/delay.h>
#include <linux/dev_printk.h>
#include <linux/err.h>
+#include <linux/hwmon.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
@@ -67,6 +68,13 @@
#define T14S_EC_EVT_KEY_FN_F11 0x7a
#define T14S_EC_EVT_KEY_FN_G 0x7e
+#define T14S_EC_SYS_THERM0 0x78 /* SoC (CPU+GPU) */
+#define T14S_EC_SYS_THERM1 0x79 /* Keyboard */
+#define T14S_EC_SYS_THERM2 0x7a /* Back cover */
+#define T14S_EC_SYS_THERM3 0x7b /* Charger / PMIC */
+#define T14S_EC_SYS_THERM6 0x7c /* QTM West */
+#define T14S_EC_SYS_THERM7 0x7d /* SSD */
+
/* Hardware LED blink rate is 1 Hz (500ms off, 500ms on) */
#define T14S_EC_BLINK_RATE_ON_OFF_MS 500
@@ -93,9 +101,19 @@ struct t14s_ec_led_classdev {
struct t14s_ec *ec;
};
+struct t14s_ec_hwmon_sys_thermx {
+ const char *label;
+ int reg;
+};
+
+struct t14s_ec_hwmon {
+ struct t14s_ec_hwmon_sys_thermx *sys_thermx;
+};
+
struct t14s_ec {
struct regmap *regmap;
struct device *dev;
+ struct t14s_ec_hwmon ec_hwmon;
struct t14s_ec_led_classdev led_pwr_btn;
struct t14s_ec_led_classdev led_chrg_orange;
struct t14s_ec_led_classdev led_chrg_white;
@@ -555,6 +573,112 @@ static irqreturn_t t14s_ec_irq_handler(int irq, void *data)
return IRQ_HANDLED;
}
+static umode_t t14s_ec_hwmon_is_visible(const void *drvdata,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ switch (type) {
+ case hwmon_temp:
+ if (attr == hwmon_temp_input ||
+ attr == hwmon_temp_label)
+ return 0444;
+ break;
+ default:
+ return 0;
+ }
+
+ return 0;
+}
+
+static int t14s_ec_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ struct t14s_ec *ec = dev_get_drvdata(dev);
+ switch (type) {
+ case hwmon_temp:
+ if (attr == hwmon_temp_label) {
+ *str = ec->ec_hwmon.sys_thermx[channel].label;
+ return 0;
+ }
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int t14s_ec_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *val)
+{
+ struct t14s_ec *ec = dev_get_drvdata(dev);
+ unsigned int value;
+ int ret;
+
+ switch (type) {
+ case hwmon_temp:
+ if (attr == hwmon_temp_input) {
+ ret = t14s_ec_read(ec, ec->ec_hwmon.sys_thermx[channel].reg, &value);
+ if (ret)
+ return ret;
+ *val = value * 1000;
+
+ return 0;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static const struct hwmon_ops t14s_ec_hwmon_ops = {
+ .is_visible = t14s_ec_hwmon_is_visible,
+ .read = t14s_ec_hwmon_read,
+ .read_string = t14s_ec_hwmon_read_string,
+};
+
+static const struct hwmon_channel_info *t14s_ec_hwmon_info[] = {
+ HWMON_CHANNEL_INFO(temp,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL),
+ NULL
+};
+
+static const struct hwmon_chip_info t14s_ec_chip_info = {
+ .ops = &t14s_ec_hwmon_ops,
+ .info = t14s_ec_hwmon_info,
+};
+
+static int t14s_ec_hwmon_probe(struct t14s_ec *ec)
+{
+ struct device *dev;
+ struct t14s_ec_hwmon_sys_thermx sys_thermx[] = {
+ { .label = "soc", .reg = T14S_EC_SYS_THERM0 },
+ { .label = "keyboard", .reg = T14S_EC_SYS_THERM1 },
+ { .label = "base", .reg = T14S_EC_SYS_THERM2 },
+ { .label = "charging", .reg = T14S_EC_SYS_THERM3 },
+ { .label = "qtm", .reg = T14S_EC_SYS_THERM6 },
+ { .label = "ssd", .reg = T14S_EC_SYS_THERM7 },
+ };
+
+ ec->ec_hwmon.sys_thermx = devm_kmemdup_array(ec->dev, sys_thermx,
+ ARRAY_SIZE(sys_thermx),
+ sizeof(sys_thermx[0]), GFP_KERNEL);
+ if (!ec->ec_hwmon.sys_thermx)
+ return -ENOMEM;
+
+ dev = devm_hwmon_device_register_with_info(ec->dev, "t14s_ec", ec,
+ &t14s_ec_chip_info, NULL);
+
+ return PTR_ERR_OR_ZERO(dev);
+}
+
static int t14s_ec_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
@@ -590,6 +714,10 @@ static int t14s_ec_probe(struct i2c_client *client)
if (ret < 0)
return ret;
+ ret = t14s_ec_hwmon_probe(ec);
+ if (ret < 0)
+ return ret;
+
ret = devm_request_threaded_irq(dev, client->irq, NULL,
t14s_ec_irq_handler,
IRQF_ONESHOT, dev_name(dev), ec);
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [PATCH v2 1/3] platform: arm64:: lenovo-thinkpad-t14s-ec: Add hwmon support for temperatures
2026-06-30 14:53 ` [PATCH v2 1/3] platform: arm64:: lenovo-thinkpad-t14s-ec: Add hwmon support for temperatures Daniel Lezcano
@ 2026-06-30 15:08 ` Ilpo Järvinen
0 siblings, 0 replies; 6+ messages in thread
From: Ilpo Järvinen @ 2026-06-30 15:08 UTC (permalink / raw)
To: Daniel Lezcano
Cc: sre, Hans de Goede, linux, andersson, konradybcio, robh, krzk+dt,
conor+dt, bryan.odonoghue, platform-driver-x86, LKML, linux-hwmon,
linux-arm-msm, devicetree
On Tue, 30 Jun 2026, Daniel Lezcano wrote:
> Expose the Lenovo ThinkPad T14s EC environmental sensors through
> the hwmon subsystem.
>
> The driver now registers a hwmon device providing access to six EC
> temperature sensors corresponding to the SoC, keyboard area, base
> cover, PMIC/charging circuitry, QTM module and SSD. Sensor labels
> are exported to allow user space to identify each measurement.
>
> This allows standard monitoring tools such as lm-sensors to report
> platform temperatures.
>
> Signed-off-by: Daniel Lezcano daniel.lezcano@oss.qualcomm.com
> ---
> drivers/platform/arm64/lenovo-thinkpad-t14s.c | 128 ++++++++++++++++++
> 1 file changed, 128 insertions(+)
>
> diff --git a/drivers/platform/arm64/lenovo-thinkpad-t14s.c b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
> index 5590302a5694..35a6f8b0cb6b 100644
> --- a/drivers/platform/arm64/lenovo-thinkpad-t14s.c
> +++ b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
> @@ -11,6 +11,7 @@
> #include <linux/delay.h>
> #include <linux/dev_printk.h>
> #include <linux/err.h>
> +#include <linux/hwmon.h>
> #include <linux/i2c.h>
> #include <linux/input.h>
> #include <linux/input/sparse-keymap.h>
> @@ -67,6 +68,13 @@
> #define T14S_EC_EVT_KEY_FN_F11 0x7a
> #define T14S_EC_EVT_KEY_FN_G 0x7e
>
> +#define T14S_EC_SYS_THERM0 0x78 /* SoC (CPU+GPU) */
> +#define T14S_EC_SYS_THERM1 0x79 /* Keyboard */
> +#define T14S_EC_SYS_THERM2 0x7a /* Back cover */
> +#define T14S_EC_SYS_THERM3 0x7b /* Charger / PMIC */
> +#define T14S_EC_SYS_THERM6 0x7c /* QTM West */
> +#define T14S_EC_SYS_THERM7 0x7d /* SSD */
> +
> /* Hardware LED blink rate is 1 Hz (500ms off, 500ms on) */
> #define T14S_EC_BLINK_RATE_ON_OFF_MS 500
>
> @@ -93,9 +101,19 @@ struct t14s_ec_led_classdev {
> struct t14s_ec *ec;
> };
>
> +struct t14s_ec_hwmon_sys_thermx {
> + const char *label;
> + int reg;
> +};
> +
> +struct t14s_ec_hwmon {
> + struct t14s_ec_hwmon_sys_thermx *sys_thermx;
> +};
> +
> struct t14s_ec {
> struct regmap *regmap;
> struct device *dev;
> + struct t14s_ec_hwmon ec_hwmon;
> struct t14s_ec_led_classdev led_pwr_btn;
> struct t14s_ec_led_classdev led_chrg_orange;
> struct t14s_ec_led_classdev led_chrg_white;
> @@ -555,6 +573,112 @@ static irqreturn_t t14s_ec_irq_handler(int irq, void *data)
> return IRQ_HANDLED;
> }
>
> +static umode_t t14s_ec_hwmon_is_visible(const void *drvdata,
> + enum hwmon_sensor_types type,
> + u32 attr, int channel)
> +{
> + switch (type) {
> + case hwmon_temp:
> + if (attr == hwmon_temp_input ||
> + attr == hwmon_temp_label)
> + return 0444;
> + break;
> + default:
> + return 0;
> + }
> +
> + return 0;
> +}
> +
> +static int t14s_ec_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type,
> + u32 attr, int channel, const char **str)
> +{
> + struct t14s_ec *ec = dev_get_drvdata(dev);
Add an empty line.
> + switch (type) {
> + case hwmon_temp:
> + if (attr == hwmon_temp_label) {
> + *str = ec->ec_hwmon.sys_thermx[channel].label;
> + return 0;
> + }
> + break;
> + default:
> + return -EOPNOTSUPP;
> + }
> +
> + return -EOPNOTSUPP;
> +}
> +
> +static int t14s_ec_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
> + u32 attr, int channel, long *val)
> +{
> + struct t14s_ec *ec = dev_get_drvdata(dev);
> + unsigned int value;
> + int ret;
> +
> + switch (type) {
> + case hwmon_temp:
> + if (attr == hwmon_temp_input) {
> + ret = t14s_ec_read(ec, ec->ec_hwmon.sys_thermx[channel].reg, &value);
> + if (ret)
> + return ret;
> + *val = value * 1000;
Does this literal relate to something from units.h?
> +
> + return 0;
> + }
> + break;
> + default:
> + break;
Nit, style is inconsistent with the one above.
> + }
> +
> + return -EOPNOTSUPP;
> +}
> +
> +static const struct hwmon_ops t14s_ec_hwmon_ops = {
> + .is_visible = t14s_ec_hwmon_is_visible,
> + .read = t14s_ec_hwmon_read,
> + .read_string = t14s_ec_hwmon_read_string,
> +};
> +
> +static const struct hwmon_channel_info *t14s_ec_hwmon_info[] = {
> + HWMON_CHANNEL_INFO(temp,
> + HWMON_T_INPUT | HWMON_T_LABEL,
> + HWMON_T_INPUT | HWMON_T_LABEL,
> + HWMON_T_INPUT | HWMON_T_LABEL,
> + HWMON_T_INPUT | HWMON_T_LABEL,
> + HWMON_T_INPUT | HWMON_T_LABEL,
> + HWMON_T_INPUT | HWMON_T_LABEL),
> + NULL
> +};
> +
> +static const struct hwmon_chip_info t14s_ec_chip_info = {
> + .ops = &t14s_ec_hwmon_ops,
> + .info = t14s_ec_hwmon_info,
> +};
> +
> +static int t14s_ec_hwmon_probe(struct t14s_ec *ec)
> +{
> + struct device *dev;
> + struct t14s_ec_hwmon_sys_thermx sys_thermx[] = {
> + { .label = "soc", .reg = T14S_EC_SYS_THERM0 },
> + { .label = "keyboard", .reg = T14S_EC_SYS_THERM1 },
> + { .label = "base", .reg = T14S_EC_SYS_THERM2 },
> + { .label = "charging", .reg = T14S_EC_SYS_THERM3 },
> + { .label = "qtm", .reg = T14S_EC_SYS_THERM6 },
> + { .label = "ssd", .reg = T14S_EC_SYS_THERM7 },
> + };
> +
> + ec->ec_hwmon.sys_thermx = devm_kmemdup_array(ec->dev, sys_thermx,
> + ARRAY_SIZE(sys_thermx),
> + sizeof(sys_thermx[0]), GFP_KERNEL);
> + if (!ec->ec_hwmon.sys_thermx)
> + return -ENOMEM;
> +
> + dev = devm_hwmon_device_register_with_info(ec->dev, "t14s_ec", ec,
> + &t14s_ec_chip_info, NULL);
> +
> + return PTR_ERR_OR_ZERO(dev);
> +}
> +
> static int t14s_ec_probe(struct i2c_client *client)
> {
> struct device *dev = &client->dev;
> @@ -590,6 +714,10 @@ static int t14s_ec_probe(struct i2c_client *client)
> if (ret < 0)
> return ret;
>
> + ret = t14s_ec_hwmon_probe(ec);
> + if (ret < 0)
> + return ret;
> +
> ret = devm_request_threaded_irq(dev, client->irq, NULL,
> t14s_ec_irq_handler,
> IRQF_ONESHOT, dev_name(dev), ec);
>
--
i.
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v2 2/3] platform: arm64: lenovo-thinkpad-t14s: Wire EC thermal events to hwmon
2026-06-30 14:53 [PATCH v2 0/3] Lenovo ThinkPad T14s EC thermal monitoring and thermal zone integration Daniel Lezcano
2026-06-30 14:53 ` [PATCH v2 1/3] platform: arm64:: lenovo-thinkpad-t14s-ec: Add hwmon support for temperatures Daniel Lezcano
@ 2026-06-30 14:53 ` Daniel Lezcano
2026-06-30 15:03 ` Ilpo Järvinen
2026-06-30 14:53 ` [PATCH v2 3/3] arm64: dts: qcom: x1e78100-t14s: Add thermal zones for keyboard skin and charging sensors Daniel Lezcano
2 siblings, 1 reply; 6+ messages in thread
From: Daniel Lezcano @ 2026-06-30 14:53 UTC (permalink / raw)
To: sre, hansg, ilpo.jarvinen, linux, andersson, konradybcio, robh,
krzk+dt, conor+dt
Cc: bryan.odonoghue, platform-driver-x86, linux-kernel, linux-hwmon,
linux-arm-msm, devicetree
The EC generates thermal zone status change notifications for a subset
of the exposed temperature sensors. Wire these EC events to the hwmon
notification framework so userspace can be informed when a thermal alarm
state changes.
Associate each hwmon temperature channel with its corresponding EC
thermal event and emit hwmon_temp_alarm notifications through
hwmon_notify_event() when the EC reports a thermal zone status change.
Also register thermal zones in the hwmon chip capabilities and keep a
reference to the hwmon device to allow event propagation from the IRQ
handler.
This allows userspace monitoring tools to receive thermal alarm
updates without polling the sensors and gives the opportuniy to the
kernel to cool them down.
Signed-off-by: Daniel Lezcano <daniel.lezcano@oss.qualcomm.com>
---
drivers/platform/arm64/lenovo-thinkpad-t14s.c | 78 ++++++++++++++-----
1 file changed, 57 insertions(+), 21 deletions(-)
diff --git a/drivers/platform/arm64/lenovo-thinkpad-t14s.c b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
index 35a6f8b0cb6b..5fafb01a2b33 100644
--- a/drivers/platform/arm64/lenovo-thinkpad-t14s.c
+++ b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
@@ -104,10 +104,13 @@ struct t14s_ec_led_classdev {
struct t14s_ec_hwmon_sys_thermx {
const char *label;
int reg;
+ u8 event;
};
struct t14s_ec_hwmon {
+ struct device *dev;
struct t14s_ec_hwmon_sys_thermx *sys_thermx;
+ size_t num_sys_thermx;
};
struct t14s_ec {
@@ -490,6 +493,20 @@ static int t14s_input_probe(struct t14s_ec *ec)
return input_register_device(ec->inputdev);
}
+static void t14s_ec_hwmon_notify_event(struct t14s_ec *ec, u8 event)
+{
+ for (int i = 0; i < ec->ec_hwmon.num_sys_thermx; i++) {
+ if (ec->ec_hwmon.sys_thermx[i].event != event)
+ continue;
+
+ hwmon_notify_event(ec->ec_hwmon.dev, hwmon_temp,
+ hwmon_temp_alarm, i);
+
+ dev_dbg(ec->dev, "Thermal Zone (%s) Status Change Event\n",
+ ec->ec_hwmon.sys_thermx[i].label);
+ }
+}
+
static irqreturn_t t14s_ec_irq_handler(int irq, void *data)
{
struct t14s_ec *ec = data;
@@ -539,13 +556,9 @@ static irqreturn_t t14s_ec_irq_handler(int irq, void *data)
dev_dbg(ec->dev, "LID closed\n");
break;
case T14S_EC_EVT_THERMAL_TZ40:
- dev_dbg(ec->dev, "Thermal Zone 40 Status Change Event (CPU/GPU)\n");
- break;
case T14S_EC_EVT_THERMAL_TZ42:
- dev_dbg(ec->dev, "Thermal Zone 42 Status Change Event (Battery)\n");
- break;
case T14S_EC_EVT_THERMAL_TZ39:
- dev_dbg(ec->dev, "Thermal Zone 39 Status Change Event (CPU/GPU)\n");
+ t14s_ec_hwmon_notify_event(ec, val);
break;
case T14S_EC_EVT_KEY_FN_G:
dev_dbg(ec->dev, "FN + G - toggle double-tapping\n");
@@ -640,13 +653,14 @@ static const struct hwmon_ops t14s_ec_hwmon_ops = {
};
static const struct hwmon_channel_info *t14s_ec_hwmon_info[] = {
+ HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ),
HWMON_CHANNEL_INFO(temp,
- HWMON_T_INPUT | HWMON_T_LABEL,
- HWMON_T_INPUT | HWMON_T_LABEL,
- HWMON_T_INPUT | HWMON_T_LABEL,
- HWMON_T_INPUT | HWMON_T_LABEL,
- HWMON_T_INPUT | HWMON_T_LABEL,
- HWMON_T_INPUT | HWMON_T_LABEL),
+ HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
+ HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
+ HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
+ HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
+ HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
+ HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM),
NULL
};
@@ -657,14 +671,34 @@ static const struct hwmon_chip_info t14s_ec_chip_info = {
static int t14s_ec_hwmon_probe(struct t14s_ec *ec)
{
- struct device *dev;
struct t14s_ec_hwmon_sys_thermx sys_thermx[] = {
- { .label = "soc", .reg = T14S_EC_SYS_THERM0 },
- { .label = "keyboard", .reg = T14S_EC_SYS_THERM1 },
- { .label = "base", .reg = T14S_EC_SYS_THERM2 },
- { .label = "charging", .reg = T14S_EC_SYS_THERM3 },
- { .label = "qtm", .reg = T14S_EC_SYS_THERM6 },
- { .label = "ssd", .reg = T14S_EC_SYS_THERM7 },
+ {
+ .label = "soc",
+ .reg = T14S_EC_SYS_THERM0,
+ .event = T14S_EC_EVT_THERMAL_TZ39
+ },
+ {
+ .label = "keyboard",
+ .reg = T14S_EC_SYS_THERM1,
+ .event = T14S_EC_EVT_THERMAL_TZ40
+ },
+ {
+ .label = "base",
+ .reg = T14S_EC_SYS_THERM2,
+ },
+ {
+ .label = "charging",
+ .reg = T14S_EC_SYS_THERM3,
+ .event = T14S_EC_EVT_THERMAL_TZ42
+ },
+ {
+ .label = "qtm",
+ .reg = T14S_EC_SYS_THERM6
+ },
+ {
+ .label = "ssd",
+ .reg = T14S_EC_SYS_THERM7
+ },
};
ec->ec_hwmon.sys_thermx = devm_kmemdup_array(ec->dev, sys_thermx,
@@ -673,10 +707,12 @@ static int t14s_ec_hwmon_probe(struct t14s_ec *ec)
if (!ec->ec_hwmon.sys_thermx)
return -ENOMEM;
- dev = devm_hwmon_device_register_with_info(ec->dev, "t14s_ec", ec,
- &t14s_ec_chip_info, NULL);
+ ec->ec_hwmon.num_sys_thermx = ARRAY_SIZE(sys_thermx);
+
+ ec->ec_hwmon.dev = devm_hwmon_device_register_with_info(ec->dev, "t14s_ec", ec,
+ &t14s_ec_chip_info, NULL);
- return PTR_ERR_OR_ZERO(dev);
+ return PTR_ERR_OR_ZERO(ec->ec_hwmon.dev);
}
static int t14s_ec_probe(struct i2c_client *client)
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [PATCH v2 2/3] platform: arm64: lenovo-thinkpad-t14s: Wire EC thermal events to hwmon
2026-06-30 14:53 ` [PATCH v2 2/3] platform: arm64: lenovo-thinkpad-t14s: Wire EC thermal events to hwmon Daniel Lezcano
@ 2026-06-30 15:03 ` Ilpo Järvinen
0 siblings, 0 replies; 6+ messages in thread
From: Ilpo Järvinen @ 2026-06-30 15:03 UTC (permalink / raw)
To: Daniel Lezcano
Cc: sre, Hans de Goede, linux, andersson, konradybcio, robh, krzk+dt,
conor+dt, bryan.odonoghue, platform-driver-x86, LKML, linux-hwmon,
linux-arm-msm, devicetree
On Tue, 30 Jun 2026, Daniel Lezcano wrote:
> The EC generates thermal zone status change notifications for a subset
> of the exposed temperature sensors. Wire these EC events to the hwmon
> notification framework so userspace can be informed when a thermal alarm
> state changes.
>
> Associate each hwmon temperature channel with its corresponding EC
> thermal event and emit hwmon_temp_alarm notifications through
> hwmon_notify_event() when the EC reports a thermal zone status change.
>
> Also register thermal zones in the hwmon chip capabilities and keep a
> reference to the hwmon device to allow event propagation from the IRQ
> handler.
>
> This allows userspace monitoring tools to receive thermal alarm
> updates without polling the sensors and gives the opportuniy to the
opportuniy -> opportunity
> kernel to cool them down.
>
> Signed-off-by: Daniel Lezcano <daniel.lezcano@oss.qualcomm.com>
> ---
> drivers/platform/arm64/lenovo-thinkpad-t14s.c | 78 ++++++++++++++-----
> 1 file changed, 57 insertions(+), 21 deletions(-)
>
> diff --git a/drivers/platform/arm64/lenovo-thinkpad-t14s.c b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
> index 35a6f8b0cb6b..5fafb01a2b33 100644
> --- a/drivers/platform/arm64/lenovo-thinkpad-t14s.c
> +++ b/drivers/platform/arm64/lenovo-thinkpad-t14s.c
> @@ -104,10 +104,13 @@ struct t14s_ec_led_classdev {
> struct t14s_ec_hwmon_sys_thermx {
> const char *label;
> int reg;
> + u8 event;
> };
>
> struct t14s_ec_hwmon {
> + struct device *dev;
> struct t14s_ec_hwmon_sys_thermx *sys_thermx;
> + size_t num_sys_thermx;
> };
>
> struct t14s_ec {
> @@ -490,6 +493,20 @@ static int t14s_input_probe(struct t14s_ec *ec)
> return input_register_device(ec->inputdev);
> }
>
> +static void t14s_ec_hwmon_notify_event(struct t14s_ec *ec, u8 event)
> +{
> + for (int i = 0; i < ec->ec_hwmon.num_sys_thermx; i++) {
> + if (ec->ec_hwmon.sys_thermx[i].event != event)
> + continue;
> +
> + hwmon_notify_event(ec->ec_hwmon.dev, hwmon_temp,
> + hwmon_temp_alarm, i);
> +
> + dev_dbg(ec->dev, "Thermal Zone (%s) Status Change Event\n",
> + ec->ec_hwmon.sys_thermx[i].label);
> + }
> +}
> +
> static irqreturn_t t14s_ec_irq_handler(int irq, void *data)
> {
> struct t14s_ec *ec = data;
> @@ -539,13 +556,9 @@ static irqreturn_t t14s_ec_irq_handler(int irq, void *data)
> dev_dbg(ec->dev, "LID closed\n");
> break;
> case T14S_EC_EVT_THERMAL_TZ40:
> - dev_dbg(ec->dev, "Thermal Zone 40 Status Change Event (CPU/GPU)\n");
> - break;
> case T14S_EC_EVT_THERMAL_TZ42:
> - dev_dbg(ec->dev, "Thermal Zone 42 Status Change Event (Battery)\n");
> - break;
> case T14S_EC_EVT_THERMAL_TZ39:
> - dev_dbg(ec->dev, "Thermal Zone 39 Status Change Event (CPU/GPU)\n");
> + t14s_ec_hwmon_notify_event(ec, val);
> break;
> case T14S_EC_EVT_KEY_FN_G:
> dev_dbg(ec->dev, "FN + G - toggle double-tapping\n");
> @@ -640,13 +653,14 @@ static const struct hwmon_ops t14s_ec_hwmon_ops = {
> };
>
> static const struct hwmon_channel_info *t14s_ec_hwmon_info[] = {
> + HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ),
> HWMON_CHANNEL_INFO(temp,
> - HWMON_T_INPUT | HWMON_T_LABEL,
> - HWMON_T_INPUT | HWMON_T_LABEL,
> - HWMON_T_INPUT | HWMON_T_LABEL,
> - HWMON_T_INPUT | HWMON_T_LABEL,
> - HWMON_T_INPUT | HWMON_T_LABEL,
> - HWMON_T_INPUT | HWMON_T_LABEL),
> + HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
> + HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
> + HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
> + HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
> + HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM,
> + HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_ALARM),
> NULL
> };
>
> @@ -657,14 +671,34 @@ static const struct hwmon_chip_info t14s_ec_chip_info = {
>
> static int t14s_ec_hwmon_probe(struct t14s_ec *ec)
> {
> - struct device *dev;
> struct t14s_ec_hwmon_sys_thermx sys_thermx[] = {
> - { .label = "soc", .reg = T14S_EC_SYS_THERM0 },
> - { .label = "keyboard", .reg = T14S_EC_SYS_THERM1 },
> - { .label = "base", .reg = T14S_EC_SYS_THERM2 },
> - { .label = "charging", .reg = T14S_EC_SYS_THERM3 },
> - { .label = "qtm", .reg = T14S_EC_SYS_THERM6 },
> - { .label = "ssd", .reg = T14S_EC_SYS_THERM7 },
> + {
> + .label = "soc",
> + .reg = T14S_EC_SYS_THERM0,
> + .event = T14S_EC_EVT_THERMAL_TZ39
> + },
> + {
> + .label = "keyboard",
> + .reg = T14S_EC_SYS_THERM1,
> + .event = T14S_EC_EVT_THERMAL_TZ40
> + },
> + {
> + .label = "base",
> + .reg = T14S_EC_SYS_THERM2,
> + },
> + {
> + .label = "charging",
> + .reg = T14S_EC_SYS_THERM3,
> + .event = T14S_EC_EVT_THERMAL_TZ42
> + },
> + {
> + .label = "qtm",
> + .reg = T14S_EC_SYS_THERM6
> + },
> + {
> + .label = "ssd",
> + .reg = T14S_EC_SYS_THERM7
> + },
> };
>
> ec->ec_hwmon.sys_thermx = devm_kmemdup_array(ec->dev, sys_thermx,
> @@ -673,10 +707,12 @@ static int t14s_ec_hwmon_probe(struct t14s_ec *ec)
> if (!ec->ec_hwmon.sys_thermx)
> return -ENOMEM;
>
> - dev = devm_hwmon_device_register_with_info(ec->dev, "t14s_ec", ec,
> - &t14s_ec_chip_info, NULL);
> + ec->ec_hwmon.num_sys_thermx = ARRAY_SIZE(sys_thermx);
Please add include.
> +
> + ec->ec_hwmon.dev = devm_hwmon_device_register_with_info(ec->dev, "t14s_ec", ec,
> + &t14s_ec_chip_info, NULL);
>
> - return PTR_ERR_OR_ZERO(dev);
> + return PTR_ERR_OR_ZERO(ec->ec_hwmon.dev);
> }
>
> static int t14s_ec_probe(struct i2c_client *client)
>
--
i.
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v2 3/3] arm64: dts: qcom: x1e78100-t14s: Add thermal zones for keyboard skin and charging sensors
2026-06-30 14:53 [PATCH v2 0/3] Lenovo ThinkPad T14s EC thermal monitoring and thermal zone integration Daniel Lezcano
2026-06-30 14:53 ` [PATCH v2 1/3] platform: arm64:: lenovo-thinkpad-t14s-ec: Add hwmon support for temperatures Daniel Lezcano
2026-06-30 14:53 ` [PATCH v2 2/3] platform: arm64: lenovo-thinkpad-t14s: Wire EC thermal events to hwmon Daniel Lezcano
@ 2026-06-30 14:53 ` Daniel Lezcano
2 siblings, 0 replies; 6+ messages in thread
From: Daniel Lezcano @ 2026-06-30 14:53 UTC (permalink / raw)
To: sre, hansg, ilpo.jarvinen, linux, andersson, konradybcio, robh,
krzk+dt, conor+dt
Cc: bryan.odonoghue, platform-driver-x86, linux-kernel, linux-hwmon,
linux-arm-msm, devicetree
The Lenovo ThinkPad T14s embedded controller exposes several platform
temperature sensors that are already used by the firmware for thermal
management.
Expose the EC as a thermal sensor provider and describe the keyboard
skin and charging circuitry sensors as thermal zones in the device
tree.
The keyboard thermal zone defines passive and hot trip points, while
the charging thermal zone also associates a cooling map with the CPU
clusters, allowing the generic thermal framework to apply CPU
throttling when the charging circuitry temperature exceeds the passive
threshold.
This integrates the EC temperature sensors with the Linux thermal
framework and enables platform thermal management using standard
thermal zone definitions.
The EC protocol currently does not provide a mechanism to program trip
points from Linux. Consequently, the thermal zones rely on periodic
polling to detect threshold crossings.
Using the charging circuitry temperature for thermal mitigation provides
a conservative approximation of the platform thermal state and prevents
the platform from reaching critical temperatures under sustained heavy
CPU load.
Without this change the platform reaches a critical thermal condition
and resets under heavy load.
Signed-off-by: Daniel Lezcano <daniel.lezcano@oss.qualcomm.com>
---
.../qcom/x1e78100-lenovo-thinkpad-t14s.dtsi | 68 ++++++++++++++++++-
1 file changed, 67 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/boot/dts/qcom/x1e78100-lenovo-thinkpad-t14s.dtsi b/arch/arm64/boot/dts/qcom/x1e78100-lenovo-thinkpad-t14s.dtsi
index 5d49df41be02..a19a363da9ed 100644
--- a/arch/arm64/boot/dts/qcom/x1e78100-lenovo-thinkpad-t14s.dtsi
+++ b/arch/arm64/boot/dts/qcom/x1e78100-lenovo-thinkpad-t14s.dtsi
@@ -979,7 +979,7 @@ &i2c6 {
status = "okay";
- embedded-controller@28 {
+ ec: embedded-controller@28 {
compatible = "lenovo,thinkpad-t14s-ec";
reg = <0x28>;
@@ -988,6 +988,8 @@ embedded-controller@28 {
pinctrl-0 = <&ec_int_n_default>;
pinctrl-names = "default";
+ #thermal-sensor-cells = <1>;
+
wakeup-source;
};
};
@@ -1729,3 +1731,67 @@ &usb_mp_qmpphy1 {
status = "okay";
};
+
+&thermal_zones {
+ ec-keyboard-thermal {
+ polling-delay = <5000>;
+ polling-delay-passive = <1000>;
+
+ thermal-sensors = <&ec 1>;
+
+ trips {
+ trip-point0 {
+ temperature = <55000>;
+ hysteresis = <2000>;
+ type = "passive";
+ };
+
+ trip-point1 {
+ temperature = <62000>;
+ hysteresis = <0>;
+ type = "hot";
+ };
+ };
+ };
+
+ ec-charging-thermal {
+ /* EC trip points cannot yet be programmed. */
+ polling-delay = <5000>;
+ polling-delay-passive = <2000>;
+
+ thermal-sensors = <&ec 3>;
+
+ trips {
+ ec_charging_psv0: trip-point0 {
+ temperature = <55000>;
+ hysteresis = <0>;
+ type = "passive";
+ };
+
+ ec_charging_alrt0: trip-point1 {
+ temperature = <63000>;
+ hysteresis = <0>;
+ type = "hot";
+ };
+ };
+
+ cooling-maps {
+ map0 {
+ trip = <&ec_charging_psv0>;
+ cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu2 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu3 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu4 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu5 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu6 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu7 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu8 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu9 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu10 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>,
+ <&cpu11 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>;
+ };
+ };
+
+ };
+};
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread