[PATCH 3/3] thermal: samsung: acpm-tmu: add Exynos850 support
From: Alexey Klimov
Date: Wed Jul 08 2026 - 21:12:18 EST
Add support for the Thermal Management Unit block found on the
Exynos850 SoC.
Unlike the GS101, the Exynos850 ACPM firmware implements a smaller
set of IPC commands (primarily basic TMU initialization, suspend/resume,
and temperature reading). While the firmware supports basic TMU
initialization, suspend/resume, and temperature polling, it lacks the
ability to fully configure the hardware. The remaining functionality
is implemented via direct MMIO access:
- TMU initialization sequence,
- setting falling and rising thresholds and associated IRQs,
- helpers to convert temperature to hardware values,
- handling of IRQs,
- thermal zone control (TMU core enable/disable).
Additionally, this also reorders the sensor group assignment in
acpm_tmu_probe() to let it happen before TMU init SoC callback.
Signed-off-by: Alexey Klimov <alexey.klimov@xxxxxxxxxx>
---
drivers/thermal/samsung/acpm-tmu.c | 356 ++++++++++++++++++++++++++++++++++++-
1 file changed, 355 insertions(+), 1 deletion(-)
diff --git a/drivers/thermal/samsung/acpm-tmu.c b/drivers/thermal/samsung/acpm-tmu.c
index b3cea9890b4b..2bf880515e31 100644
--- a/drivers/thermal/samsung/acpm-tmu.c
+++ b/drivers/thermal/samsung/acpm-tmu.c
@@ -5,6 +5,7 @@
* Copyright 2026 Linaro Ltd.
*/
+#include <linux/bitfield.h>
#include <linux/cleanup.h>
#include <linux/clk.h>
#include <linux/device/devres.h>
@@ -23,9 +24,79 @@
#include "../thermal_hwmon.h"
+#define TYPE_TWO_POINT_TRIMMING 1
+
#define EXYNOS_TMU_SENSOR(i) BIT(i)
#define EXYNOS_TMU_SENSORS_MAX_COUNT 16
+#define EXYNOS850_INTPEND_RISE_MASK GENMASK(8, 0)
+#define EXYNOS850_INTPEND_FALL_MASK GENMASK(23, 16)
+
+#define EXYNOS850_GPU_SENSOR_MASK (EXYNOS_TMU_SENSOR(3))
+#define EXYNOS850_CPUCL1_SENSOR_MASK (EXYNOS_TMU_SENSOR(2))
+#define EXYNOS850_CPUCL0_SENSOR_MASK (EXYNOS_TMU_SENSOR(1))
+
+#define EXYNOS_TMU_REG_TRIMINFO (0x0)
+#define EXYNOS850_TMU_TRIMINFO_SHIFT 4
+#define EXYNOS850_TMU_TRIMINFO_OFFSET(n) \
+ (EXYNOS_TMU_REG_TRIMINFO + (n) * EXYNOS850_TMU_TRIMINFO_SHIFT)
+
+#define EXYNOS_FIRST_POINT_TRIM 25
+#define EXYNOS_SECOND_POINT_TRIM 85
+
+#define EXYNOS_TRIMINFO_CALIB_SEL_MASK BIT(23)
+#define EXYNOS_TRIMINFO_25_MASK GENMASK(8, 0)
+#define EXYNOS_TRIMINFO_85_MASK GENMASK(17, 9)
+#define EXYNOS850_TMU_BGRI_TRIM_SHIFT 20
+#define EXYNOS850_TMU_BGRI_TRIM_MASK 0xf
+#define EXYNOS850_TMU_VREF_TRIM_SHIFT 12
+#define EXYNOS850_TMU_VREF_TRIM_MASK 0xf
+#define EXYNOS850_TMU_VBEI_TRIM_SHIFT 8
+#define EXYNOS850_TMU_VBEI_TRIM_MASK 0xf
+
+#define EXYNOS850_TMU_CONTROL (0x20)
+#define EXYNOS850_TMU_CONTROL1 (0x24)
+#define EXYNOS850_TMU_COUNTER_VALUE0 (0x30)
+#define EXYNOS850_TMU_COUNTER_VALUE1 (0x34)
+#define EXYNOS850_TMU_AVG_CONTROL (0x38)
+#define EXYNOS850_TMU_TRIM0 (0x3c)
+
+#define EXYNOS_TMU_REG_INTPEND0 (0x118)
+#define EXYNOS_TMU_REG_INTPEND5 (0x318)
+#define EXYNOS_TMU_REG_INTPEND8 (0x658)
+
+#define EXYNOS_TMU_REG_INTEN0 (0x110)
+#define EXYNOS_TMU_REG_INTEN5 (0x310)
+#define EXYNOS_TMU_REG_INTEN8 (0x650)
+#define EXYNOS850_RISE_LEVEL1 BIT(1)
+#define EXYNOS850_FALL_LEVEL1 BIT(17)
+
+#define EXYNOS_TMU_REG_THD_TEMP0 (0x50)
+#define EXYNOS_TMU_REG_THD_TEMP1 (0x170)
+#define EXYNOS_TMU_REG_THD_TEMP8 (0x450)
+
+#define EXYNOS850_REG_RISE1_0_OFFSET (0xc)
+#define EXYNOS850_REG_FALL1_0_OFFSET (EXYNOS850_REG_RISE1_0_OFFSET + 0x10)
+#define EXYNOS850_THD_LEVEL1_MASK GENMASK(24, 16)
+
+#define EXYNOS850_TMU_REG_THD(p) ((p == 0) ? \
+ EXYNOS_TMU_REG_THD_TEMP0 : \
+ ((p < 8) ? \
+ (EXYNOS_TMU_REG_THD_TEMP1 + (p - 1) * 0x20) : \
+ (EXYNOS_TMU_REG_THD_TEMP8 + (p - 8) * 0x20)))
+
+#define EXYNOS850_TMU_REG_INTEN(p) ((p < 5) ? \
+ (EXYNOS_TMU_REG_INTEN0 + p * 0x10) : \
+ ((p < 8) ? \
+ (EXYNOS_TMU_REG_INTEN5 + (p - 5) * 0x10) : \
+ (EXYNOS_TMU_REG_INTEN8 + (p - 8) * 0x10)))
+
+#define EXYNOS850_REG_INTPEND(p) ((p < 5) ? \
+ (EXYNOS_TMU_REG_INTPEND0 + p * 0x10) : \
+ ((p < 8) ? \
+ (EXYNOS_TMU_REG_INTPEND5 + (p - 5) * 0x10) : \
+ (EXYNOS_TMU_REG_INTPEND8 + (p - 8) * 0x10)))
+
#define GS101_CPUCL2_SENSOR_MASK (EXYNOS_TMU_SENSOR(0) | \
EXYNOS_TMU_SENSOR(6) | \
EXYNOS_TMU_SENSOR(7) | \
@@ -69,6 +140,9 @@ struct acpm_tmu_sensor {
struct acpm_tmu_priv *priv;
struct mutex lock; /* protects sensor state */
bool enabled;
+
+ u32 efuse_value;
+ u16 temp_error1, temp_error2;
};
struct acpm_tmu_priv {
@@ -81,6 +155,7 @@ struct acpm_tmu_priv {
unsigned int mbox_chan_id;
unsigned int num_sensors;
int irq;
+ u32 cal_type;
struct acpm_tmu_sensor sensors[] __counted_by(num_sensors);
};
@@ -92,6 +167,7 @@ struct acpm_tmu_driver_data {
unsigned int mbox_chan_id;
/* SoC-specific TMU routines and values */
+ u32 efuse_value;
int (*tz_control)(struct acpm_tmu_sensor *sensor, bool on);
int (*tmu_check_and_clear_irqs)(struct acpm_tmu_sensor *sensor,
bool *pending_irq);
@@ -139,6 +215,40 @@ static const struct regmap_config gs101_regmap_config = {
.max_register = GS101_REG_INTPEND(15),
};
+static const struct acpm_tmu_sensor_group exynos850_sensor_groups[] = {
+ ACPM_TMU_SENSOR_GROUP(EXYNOS850_CPUCL0_SENSOR_MASK, 0),
+ ACPM_TMU_SENSOR_GROUP(EXYNOS850_CPUCL1_SENSOR_MASK, 1),
+ ACPM_TMU_SENSOR_GROUP(EXYNOS850_GPU_SENSOR_MASK, 2),
+};
+
+static const struct reg_field exynos850_reg_fields[REG_INTPEND_COUNT] = {
+ [P0_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(0), 0, 23),
+ [P1_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(1), 0, 23),
+ [P2_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(2), 0, 23),
+ [P3_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(3), 0, 23),
+ [P4_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(4), 0, 23),
+ [P5_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(5), 0, 23),
+ [P6_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(6), 0, 23),
+ [P7_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(7), 0, 23),
+ [P8_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(8), 0, 23),
+ [P9_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(9), 0, 23),
+ [P10_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(10), 0, 23),
+ [P11_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(11), 0, 23),
+ [P12_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(12), 0, 23),
+ [P13_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(13), 0, 23),
+ [P14_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(14), 0, 23),
+ [P15_INTPEND] = REG_FIELD(EXYNOS850_REG_INTPEND(15), 0, 23),
+};
+
+static const struct regmap_config exynos850_regmap_config = {
+ .name = "e850_tmu_regs",
+ .reg_bits = 32,
+ .reg_stride = 4,
+ .val_bits = 32,
+ .use_relaxed_mmio = true,
+ .max_register = EXYNOS850_REG_INTPEND(15),
+};
+
static int gs101_tz_control(struct acpm_tmu_sensor *sensor, bool on)
{
struct acpm_tmu_priv *priv = sensor->priv;
@@ -155,6 +265,129 @@ static int gs101_tmu_init(struct acpm_tmu_priv *priv)
return handle->ops->tmu.init(handle, priv->mbox_chan_id);
}
+static int exynos850_tz_control(struct acpm_tmu_sensor *sensor, bool on)
+{
+ struct acpm_tmu_priv *priv = sensor->priv;
+
+ regmap_update_bits(priv->regmap, EXYNOS850_TMU_CONTROL,
+ BIT(0), on ? BIT(0) : 0);
+ return 0;
+}
+
+static int exynos850_tmu_init(struct acpm_tmu_priv *priv)
+{
+ struct acpm_handle *handle = priv->handle;
+ struct regmap *regmap = priv->regmap;
+ unsigned int trim_info, trim;
+ unsigned int t_buf_vref_sel, t_buf_slope_sel, avg_mode;
+ unsigned int t_bgri_trim, t_vref_trim, t_vbei_trim;
+ u32 trim0_mask, trim0_val;
+ int ret, i;
+
+ ret = handle->ops->tmu.init(handle, priv->mbox_chan_id);
+ if (ret)
+ return ret;
+
+ /*
+ * Bootloader may partly initialise TMU or may not initialise it at all,
+ * regardless of that we do the full TMU initialisation here and
+ * collect calibration info.
+ */
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(0), &trim_info);
+ priv->cal_type = FIELD_GET(EXYNOS_TRIMINFO_CALIB_SEL_MASK, trim_info);
+
+ for (i = 0; i < priv->num_sensors; i++) {
+ struct acpm_tmu_sensor *sensor = &priv->sensors[i];
+ unsigned long mask = sensor->group->mask;
+ int t;
+ u16 temp_error1, temp_error2;
+
+ for_each_set_bit(t, &mask, EXYNOS_TMU_SENSORS_MAX_COUNT) {
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(t), &trim_info);
+
+ temp_error1 = FIELD_GET(EXYNOS_TRIMINFO_25_MASK, trim_info);
+ temp_error2 = FIELD_GET(EXYNOS_TRIMINFO_85_MASK, trim_info);
+
+ if (temp_error1)
+ sensor->temp_error1 = temp_error1;
+ else
+ sensor->temp_error1 = FIELD_GET(EXYNOS_TRIMINFO_25_MASK,
+ priv->data->efuse_value);
+
+ if (priv->cal_type == TYPE_TWO_POINT_TRIMMING) {
+ if (temp_error1)
+ sensor->temp_error2 = temp_error2;
+ else
+ sensor->temp_error2 = FIELD_GET(EXYNOS_TRIMINFO_85_MASK,
+ priv->data->efuse_value);
+ }
+ }
+ }
+
+ /* Read TRIMINFO 0, 1, 2 settings */
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(0), &trim);
+ t_buf_vref_sel = FIELD_GET(GENMASK(22, 18), trim);
+
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(1), &trim);
+ t_buf_slope_sel = FIELD_GET(GENMASK(21, 18), trim);
+
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(2), &trim);
+ avg_mode = FIELD_GET(GENMASK(20, 18), trim);
+
+ /*
+ * Set BUF_VREF_SEL and BUF_SLOPE_SEL.
+ * Clear bit 12 -- disable thermal tripping for now.
+ * Clear bit 0 -- TMU core off
+ */
+ regmap_update_bits(regmap, EXYNOS850_TMU_CONTROL,
+ GENMASK(28, 24) | BIT(12) | GENMASK(11, 8) | BIT(0),
+ FIELD_PREP(GENMASK(28, 24), t_buf_vref_sel) |
+ FIELD_PREP(GENMASK(11, 8), t_buf_slope_sel));
+
+ /*
+ * Set NUM_PROBE equal to number of sensors.
+ * Disable LPI by clearing bit 10.
+ */
+ regmap_update_bits(regmap, EXYNOS850_TMU_CONTROL1,
+ GENMASK(19, 16) | BIT(10),
+ FIELD_PREP(GENMASK(19, 16), priv->num_sensors));
+
+ /* Set AVG_MODE (If avg_mode > 0, set bit 4 and insert avg_mode into bits [2:0]) */
+ regmap_update_bits(regmap, EXYNOS850_TMU_AVG_CONTROL,
+ BIT(4) | GENMASK(2, 0),
+ avg_mode ? (BIT(4) | FIELD_PREP(GENMASK(2, 0), avg_mode)) : 0);
+
+ /* Set COUNTER_VALUE 0 & 1 */
+ regmap_update_bits(regmap, EXYNOS850_TMU_COUNTER_VALUE0,
+ GENMASK(15, 0), FIELD_PREP(GENMASK(15, 0), 0x028A));
+
+ regmap_update_bits(regmap, EXYNOS850_TMU_COUNTER_VALUE1,
+ GENMASK(31, 16), FIELD_PREP(GENMASK(31, 16), 0x028A));
+
+ /* Read TRIMINFO 3, 4, 5 for BGR_I/VREF/VBE_I */
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(3), &trim);
+ t_bgri_trim = FIELD_GET(GENMASK(21, 18), trim);
+
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(4), &trim);
+ t_vref_trim = FIELD_GET(GENMASK(21, 18), trim);
+
+ regmap_read(regmap, EXYNOS850_TMU_TRIMINFO_OFFSET(5), &trim);
+ t_vbei_trim = FIELD_GET(GENMASK(21, 18), trim);
+
+ /* Set TRIM0 */
+ trim0_mask = (EXYNOS850_TMU_BGRI_TRIM_MASK << EXYNOS850_TMU_BGRI_TRIM_SHIFT) |
+ (EXYNOS850_TMU_VREF_TRIM_MASK << EXYNOS850_TMU_VREF_TRIM_SHIFT) |
+ (EXYNOS850_TMU_VBEI_TRIM_MASK << EXYNOS850_TMU_VBEI_TRIM_SHIFT);
+
+ trim0_val = (t_bgri_trim << EXYNOS850_TMU_BGRI_TRIM_SHIFT) |
+ (t_vref_trim << EXYNOS850_TMU_VREF_TRIM_SHIFT) |
+ (t_vbei_trim << EXYNOS850_TMU_VBEI_TRIM_SHIFT);
+
+ regmap_update_bits(regmap, EXYNOS850_TMU_TRIM0, trim0_mask, trim0_val);
+
+ return 0;
+}
+
static int acpm_tmu_op_tz_control(struct acpm_tmu_sensor *sensor, bool on)
{
struct acpm_tmu_priv *priv = sensor->priv;
@@ -268,6 +501,74 @@ static int acpm_tmu_get_temp(struct thermal_zone_device *tz, int *temp)
return 0;
}
+static int temp_to_code(struct acpm_tmu_sensor *sensor, u8 temp)
+{
+ struct acpm_tmu_priv *priv = sensor->priv;
+
+ if (priv->cal_type == TYPE_TWO_POINT_TRIMMING)
+ return (temp - EXYNOS_FIRST_POINT_TRIM) *
+ (sensor->temp_error2 - sensor->temp_error1) /
+ (EXYNOS_SECOND_POINT_TRIM - EXYNOS_FIRST_POINT_TRIM) +
+ sensor->temp_error1;
+
+ return temp + sensor->temp_error1 - EXYNOS_FIRST_POINT_TRIM;
+}
+
+static int exynos850_tmu_update_thresholds(struct acpm_tmu_sensor *sensor,
+ u8 thresholds[2], u8 inten)
+{
+ struct acpm_tmu_priv *priv = sensor->priv;
+ unsigned long mask = sensor->group->mask;
+ bool was_enabled;
+ int ret, restore_ret, i, trip_code, trip_code_low;
+ u32 addr;
+
+ guard(mutex)(&sensor->lock);
+
+ was_enabled = sensor->enabled;
+
+ if (was_enabled) {
+ ret = acpm_tmu_op_tz_control(sensor, false);
+ if (ret)
+ return ret;
+ }
+
+ trip_code = temp_to_code(sensor, thresholds[1]);
+ trip_code_low = temp_to_code(sensor, thresholds[0]);
+
+ for_each_set_bit(i, &mask, EXYNOS_TMU_SENSORS_MAX_COUNT) {
+ u32 irq_en = 0;
+
+ if (inten & BIT(0)) {
+ addr = EXYNOS850_TMU_REG_THD(i) + EXYNOS850_REG_FALL1_0_OFFSET;
+ regmap_update_bits(priv->regmap, addr, EXYNOS850_THD_LEVEL1_MASK,
+ FIELD_PREP(EXYNOS850_THD_LEVEL1_MASK, trip_code_low));
+ irq_en = EXYNOS850_FALL_LEVEL1;
+ }
+
+ if (inten & BIT(1)) {
+ addr = EXYNOS850_TMU_REG_THD(i) + EXYNOS850_REG_RISE1_0_OFFSET;
+ regmap_update_bits(priv->regmap, addr, EXYNOS850_THD_LEVEL1_MASK,
+ FIELD_PREP(EXYNOS850_THD_LEVEL1_MASK, trip_code));
+ irq_en |= EXYNOS850_RISE_LEVEL1;
+ }
+
+ regmap_update_bits(priv->regmap, EXYNOS850_TMU_REG_INTEN(i),
+ (EXYNOS850_FALL_LEVEL1 | EXYNOS850_RISE_LEVEL1), irq_en);
+ }
+
+ if (was_enabled) {
+ restore_ret = acpm_tmu_op_tz_control(sensor, true);
+ if (restore_ret)
+ dev_err(priv->dev, "Failed to restore sensor state: %d\n",
+ restore_ret);
+ if (!ret)
+ ret = restore_ret;
+ }
+
+ return 0;
+}
+
static int gs101_tmu_update_thresholds(struct acpm_tmu_sensor *sensor,
u8 thresholds[2], u8 inten)
{
@@ -376,6 +677,36 @@ static int gs101_handle_irqs(struct acpm_tmu_sensor *sensor, bool *pending_irq)
return ret;
}
+static int exynos850_handle_irqs(struct acpm_tmu_sensor *sensor,
+ bool *pending_irq)
+{
+ struct acpm_tmu_priv *priv = sensor->priv;
+ unsigned long mask = sensor->group->mask;
+ int i, ret;
+ u32 val, clear_val;
+
+ guard(mutex)(&sensor->lock);
+
+ for_each_set_bit(i, &mask, EXYNOS_TMU_SENSORS_MAX_COUNT) {
+ ret = regmap_field_read(priv->regmap_fields[i], &val);
+ if (ret)
+ return ret;
+
+ if (val) {
+ clear_val = val &
+ (EXYNOS850_INTPEND_RISE_MASK | EXYNOS850_INTPEND_FALL_MASK);
+ ret = regmap_field_force_write(priv->regmap_fields[i], clear_val);
+ if (ret)
+ dev_err(priv->dev,
+ "sensor %d: failed to clear IRQ (%d)\n", i, ret);
+ *pending_irq = true;
+ break;
+ }
+ }
+
+ return 0;
+}
+
static irqreturn_t acpm_tmu_thread_fn(int irq, void *id)
{
struct acpm_tmu_priv *priv = id;
@@ -423,11 +754,28 @@ static const struct acpm_tmu_driver_data acpm_tmu_gs101 = {
.tmu_update_thresholds = gs101_tmu_update_thresholds,
};
+static const struct acpm_tmu_driver_data acpm_tmu_exynos850 = {
+ .reg_fields = exynos850_reg_fields,
+ .regmap_config = &exynos850_regmap_config,
+ .sensor_groups = exynos850_sensor_groups,
+ .num_sensor_groups = ARRAY_SIZE(exynos850_sensor_groups),
+ .mbox_chan_id = 9,
+ .efuse_value = 55,
+ .tz_control = exynos850_tz_control,
+ .tmu_check_and_clear_irqs = exynos850_handle_irqs,
+ .tmu_init = exynos850_tmu_init,
+ .tmu_update_thresholds = exynos850_tmu_update_thresholds,
+};
+
static const struct of_device_id acpm_tmu_match[] = {
{
.compatible = "google,gs101-tmu-top",
.data = &acpm_tmu_gs101
},
+ {
+ .compatible = "samsung,exynos850-tmu-top",
+ .data = &acpm_tmu_exynos850,
+ },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, acpm_tmu_match);
@@ -501,6 +849,13 @@ static int acpm_tmu_probe(struct platform_device *pdev)
if (ret < 0)
return dev_err_probe(dev, ret, "Failed to resume device\n");
+ /* Sensor groups should be assigned before ->tmu_init() for some SoCs */
+ for (i = 0; i < priv->num_sensors; i++) {
+ struct acpm_tmu_sensor *sensor = &priv->sensors[i];
+
+ sensor->group = &data->sensor_groups[i];
+ }
+
if (priv->data->tmu_init) {
ret = priv->data->tmu_init(priv);
if (ret) {
@@ -513,7 +868,6 @@ static int acpm_tmu_probe(struct platform_device *pdev)
struct acpm_tmu_sensor *sensor = &priv->sensors[i];
mutex_init(&sensor->lock);
- sensor->group = &data->sensor_groups[i];
sensor->priv = priv;
sensor->tzd = devm_thermal_of_zone_register(dev, i, sensor,
--
2.51.0