Add the thermal driver for the Google GS101 SoC. The GS101 TMU utilizes a hybrid management model shared between the Application Processor (AP) and the ACPM (Alive Clock and Power Manager) firmware. The driver maintains direct memory-mapped access to the TMU interrupt pending registers to identify thermal events, while delegating functional tasks - such as sensor initialization, threshold configuration, and temperature acquisition - to the ACPM firmware via the ACPM IPC protocol.
Signed-off-by: Tudor Ambarus <[email protected]> --- drivers/thermal/samsung/Kconfig | 16 + drivers/thermal/samsung/Makefile | 2 + drivers/thermal/samsung/acpm-tmu.c | 643 +++++++++++++++++++++++++++++++++++++ 3 files changed, 661 insertions(+) diff --git a/drivers/thermal/samsung/Kconfig b/drivers/thermal/samsung/Kconfig index f4eff5a41a84ce02b12abb85d6a0f8818031d0dc..5679dfa85f4079c7d40317ac231bd6a1af93c7e7 100644 --- a/drivers/thermal/samsung/Kconfig +++ b/drivers/thermal/samsung/Kconfig @@ -9,3 +9,19 @@ config EXYNOS_THERMAL the TMU, reports temperature and handles cooling action if defined. This driver uses the Exynos core thermal APIs and TMU configuration data from the supported SoCs. + +config EXYNOS_ACPM_THERMAL + tristate "Exynos ACPM thermal management unit driver" + depends on THERMAL_OF + depends on EXYNOS_ACPM_PROTOCOL || (COMPILE_TEST && !EXYNOS_ACPM_PROTOCOL) + help + Support for the Thermal Management Unit (TMU) on Google GS101 SoC. + + The TMU on GS101 is managed through a hybrid architecture. This driver + handles direct register access for thermal interrupt status monitoring + and communicates with the Alive Clock and Power Manager (ACPM) + firmware via the ACPM IPC protocol for functional sensor control and + configuration. + + Select this if you want to monitor device temperature and enable + thermal mitigation on GS101 based devices. diff --git a/drivers/thermal/samsung/Makefile b/drivers/thermal/samsung/Makefile index f139407150d26940fc9ffcf000505cea4866223f..daed80647c349ba4f937ed8fbe6be9df06c34aac 100644 --- a/drivers/thermal/samsung/Makefile +++ b/drivers/thermal/samsung/Makefile @@ -4,3 +4,5 @@ # obj-$(CONFIG_EXYNOS_THERMAL) += exynos_thermal.o exynos_thermal-y := exynos_tmu.o +obj-$(CONFIG_EXYNOS_ACPM_THERMAL) += exynos_acpm_thermal.o +exynos_acpm_thermal-y := acpm-tmu.o diff --git a/drivers/thermal/samsung/acpm-tmu.c b/drivers/thermal/samsung/acpm-tmu.c new file mode 100644 index 0000000000000000000000000000000000000000..9fbac174e554284ff0b8c0b188a44dcceee4c440 --- /dev/null +++ b/drivers/thermal/samsung/acpm-tmu.c @@ -0,0 +1,643 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2019 Samsung Electronics Co., Ltd. + * Copyright 2025 Google LLC. + * Copyright 2026 Linaro Ltd. + */ + +#include <linux/cleanup.h> +#include <linux/clk.h> +#include <linux/device/devres.h> +#include <linux/err.h> +#include <linux/firmware/samsung/exynos-acpm-protocol.h> +#include <linux/interrupt.h> +#include <linux/mod_devicetable.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/regmap.h> +#include <linux/thermal.h> +#include <linux/units.h> + +#include "../thermal_hwmon.h" + +#define EXYNOS_TMU_SENSOR(i) BIT(i) +#define EXYNOS_TMU_SENSORS_MAX_COUNT 16 + +#define GS101_CPUCL2_SENSOR_MASK (EXYNOS_TMU_SENSOR(0) | \ + EXYNOS_TMU_SENSOR(6) | \ + EXYNOS_TMU_SENSOR(7) | \ + EXYNOS_TMU_SENSOR(8) | \ + EXYNOS_TMU_SENSOR(9)) +#define GS101_CPUCL1_SENSOR_MASK (EXYNOS_TMU_SENSOR(4) | \ + EXYNOS_TMU_SENSOR(5)) +#define GS101_CPUCL0_SENSOR_MASK (EXYNOS_TMU_SENSOR(1) | \ + EXYNOS_TMU_SENSOR(2)) + +#define GS101_REG_INTPEND(i) ((i) * 0x50 + 0xf8) + +enum { + P0_INTPEND, + P1_INTPEND, + P2_INTPEND, + P3_INTPEND, + P4_INTPEND, + P5_INTPEND, + P6_INTPEND, + P7_INTPEND, + P8_INTPEND, + P9_INTPEND, + P10_INTPEND, + P11_INTPEND, + P12_INTPEND, + P13_INTPEND, + P14_INTPEND, + P15_INTPEND, + REG_INTPEND_COUNT, +}; + +struct acpm_tmu_sensor_group { + u16 mask; + u8 id; +}; + +struct acpm_tmu_sensor { + const struct acpm_tmu_sensor_group *group; + struct thermal_zone_device *tzd; + struct acpm_tmu_priv *priv; + struct mutex lock; /* protects sensor state */ + bool enabled; +}; + +struct acpm_tmu_priv { + struct regmap_field *regmap_fields[REG_INTPEND_COUNT]; + const struct acpm_handle *handle; + struct device *dev; + struct clk *clk; + unsigned int mbox_chan_id; + unsigned int num_sensors; + int irq; + struct acpm_tmu_sensor sensors[] __counted_by(num_sensors); +}; + +struct acpm_tmu_driver_data { + const struct reg_field *reg_fields; + const struct acpm_tmu_sensor_group *sensor_groups; + unsigned int num_sensor_groups; + unsigned int mbox_chan_id; +}; + +struct acpm_tmu_initialize_tripwalkdata { + unsigned char threshold[8]; + unsigned char hysteresis[8]; + unsigned char inten; + int i; +}; + +struct acpm_tmu_trip_temp_tripwalkdata { + const struct thermal_trip *trip; + unsigned char threshold[8]; + int temperature; + int i; +}; + +#define ACPM_TMU_SENSOR_GROUP(_mask, _id) \ + { \ + .mask = _mask, \ + .id = _id, \ + } + +static const struct acpm_tmu_sensor_group gs101_sensor_groups[] = { + ACPM_TMU_SENSOR_GROUP(GS101_CPUCL2_SENSOR_MASK, 0), + ACPM_TMU_SENSOR_GROUP(GS101_CPUCL1_SENSOR_MASK, 1), + ACPM_TMU_SENSOR_GROUP(GS101_CPUCL0_SENSOR_MASK, 2), +}; + +static const struct reg_field gs101_reg_fields[REG_INTPEND_COUNT] = { + [P0_INTPEND] = REG_FIELD(GS101_REG_INTPEND(0), 0, 31), + [P1_INTPEND] = REG_FIELD(GS101_REG_INTPEND(1), 0, 31), + [P2_INTPEND] = REG_FIELD(GS101_REG_INTPEND(2), 0, 31), + [P3_INTPEND] = REG_FIELD(GS101_REG_INTPEND(3), 0, 31), + [P4_INTPEND] = REG_FIELD(GS101_REG_INTPEND(4), 0, 31), + [P5_INTPEND] = REG_FIELD(GS101_REG_INTPEND(5), 0, 31), + [P6_INTPEND] = REG_FIELD(GS101_REG_INTPEND(6), 0, 31), + [P7_INTPEND] = REG_FIELD(GS101_REG_INTPEND(7), 0, 31), + [P8_INTPEND] = REG_FIELD(GS101_REG_INTPEND(8), 0, 31), + [P9_INTPEND] = REG_FIELD(GS101_REG_INTPEND(9), 0, 31), + [P10_INTPEND] = REG_FIELD(GS101_REG_INTPEND(10), 0, 31), + [P11_INTPEND] = REG_FIELD(GS101_REG_INTPEND(11), 0, 31), + [P12_INTPEND] = REG_FIELD(GS101_REG_INTPEND(12), 0, 31), + [P13_INTPEND] = REG_FIELD(GS101_REG_INTPEND(13), 0, 31), + [P14_INTPEND] = REG_FIELD(GS101_REG_INTPEND(14), 0, 31), + [P15_INTPEND] = REG_FIELD(GS101_REG_INTPEND(15), 0, 31), +}; + +static const struct regmap_config gs101_regmap_config = { + .reg_bits = 32, + .reg_stride = 4, + .val_bits = 32, + .use_relaxed_mmio = true, + .max_register = GS101_REG_INTPEND(15), +}; + +static const struct acpm_tmu_driver_data acpm_tmu_gs101 = { + .reg_fields = gs101_reg_fields, + .sensor_groups = gs101_sensor_groups, + .num_sensor_groups = ARRAY_SIZE(gs101_sensor_groups), + .mbox_chan_id = 9, +}; + +static int acpm_tmu_op_tz_control(struct acpm_tmu_sensor *sensor, bool on) +{ + struct acpm_tmu_priv *priv = sensor->priv; + const struct acpm_handle *handle = priv->handle; + const struct acpm_tmu_ops *ops = &handle->ops.tmu; + int ret; + + ret = ops->tz_control(handle, priv->mbox_chan_id, sensor->group->id, + on); + if (ret) + return ret; + + sensor->enabled = on; + + return 0; +} + +static int acpm_tmu_control(struct acpm_tmu_priv *priv, bool on) +{ + struct device *dev = priv->dev; + int i, ret; + + ret = pm_runtime_resume_and_get(dev); + if (ret < 0) + return ret; + + for (i = 0; i < priv->num_sensors; i++) { + struct acpm_tmu_sensor *sensor = &priv->sensors[i]; + + /* Skip sensors that weren't found in DT */ + if (!sensor->tzd) + continue; + + scoped_guard(mutex, &sensor->lock) { + ret = acpm_tmu_op_tz_control(sensor, on); + } + + if (ret) + goto out; + } + +out: + pm_runtime_put_autosuspend(dev); + return ret; +} + +static int acpm_tmu_get_temp(struct thermal_zone_device *tz, int *temp) +{ + struct acpm_tmu_sensor *sensor = thermal_zone_device_priv(tz); + struct acpm_tmu_priv *priv = sensor->priv; + const struct acpm_handle *handle = priv->handle; + const struct acpm_tmu_ops *ops = &handle->ops.tmu; + struct device *dev = priv->dev; + int acpm_temp, ret; + + if (!sensor->enabled) + return -EAGAIN; + + ret = pm_runtime_resume_and_get(dev); + if (ret < 0) + return ret; + + scoped_guard(mutex, &sensor->lock) { + ret = ops->read_temp(handle, priv->mbox_chan_id, + sensor->group->id, &acpm_temp); + } + + pm_runtime_put_autosuspend(dev); + + if (ret) + return ret; + + *temp = acpm_temp * MILLIDEGREE_PER_DEGREE; + + return 0; +} + +static int acpm_tmu_trip_temp_walk_cb(struct thermal_trip *trip, void *data) +{ + struct acpm_tmu_trip_temp_tripwalkdata * const twd = data; + int temperature; + + if (twd->i >= ARRAY_SIZE(twd->threshold)) + return -ERANGE; + + if (trip->type == THERMAL_TRIP_PASSIVE) + goto out; + + temperature = (trip == twd->trip) ? twd->temperature : trip->temperature; + + twd->threshold[twd->i] = temperature / MILLIDEGREE_PER_DEGREE; + +out: + ++twd->i; + return 0; +} + +static int acpm_tmu_set_trip_temp(struct thermal_zone_device *tz, + const struct thermal_trip *trip, int temperature) +{ + struct acpm_tmu_sensor *sensor = thermal_zone_device_priv(tz); + struct acpm_tmu_priv *priv = sensor->priv; + const struct acpm_handle *handle = priv->handle; + const struct acpm_tmu_ops *ops = &handle->ops.tmu; + struct device *dev = priv->dev; + struct acpm_tmu_trip_temp_tripwalkdata twd = { + .trip = trip, + .temperature = temperature, + }; + int ret; + + if (trip->type == THERMAL_TRIP_PASSIVE) + return 0; + + for_each_thermal_trip(tz, acpm_tmu_trip_temp_walk_cb, &twd); + + ret = pm_runtime_resume_and_get(dev); + if (ret) + return ret; + + guard(mutex)(&sensor->lock); + + if (!sensor->enabled) { + ret = ops->set_threshold(handle, priv->mbox_chan_id, + sensor->group->id, twd.threshold, + ARRAY_SIZE(twd.threshold)); + goto out; + } + + ret = acpm_tmu_op_tz_control(sensor, false); + if (ret) + goto out; + + ret = ops->set_threshold(handle, priv->mbox_chan_id, sensor->group->id, + twd.threshold, ARRAY_SIZE(twd.threshold)); + if (ret) + goto out; + + ret = acpm_tmu_op_tz_control(sensor, true); + +out: + pm_runtime_put_autosuspend(dev); + return ret; +} + +static const struct thermal_zone_device_ops acpm_tmu_sensor_ops = { + .get_temp = acpm_tmu_get_temp, + .set_trip_temp = acpm_tmu_set_trip_temp, +}; + +static int acpm_tmu_has_pending_irq(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; + + 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) { + *pending_irq = true; + break; + } + } + + return 0; +} + +static irqreturn_t acpm_tmu_thread_fn(int irq, void *id) +{ + struct acpm_tmu_priv *priv = id; + const struct acpm_handle *handle = priv->handle; + const struct acpm_tmu_ops *ops = &handle->ops.tmu; + struct device *dev = priv->dev; + int i, ret; + + ret = pm_runtime_resume_and_get(dev); + if (ret) { + dev_err(dev, "Failed to resume: %d\n", ret); + return IRQ_NONE; + } + + for (i = 0; i < priv->num_sensors; i++) { + struct acpm_tmu_sensor *sensor = &priv->sensors[i]; + bool pending_irq = false; + + if (!sensor->tzd) + continue; + + ret = acpm_tmu_has_pending_irq(sensor, &pending_irq); + if (ret || !pending_irq) + continue; + + thermal_zone_device_update(sensor->tzd, + THERMAL_EVENT_UNSPECIFIED); + + scoped_guard(mutex, &sensor->lock) { + ret = ops->clear_tz_irq(handle, priv->mbox_chan_id, + sensor->group->id); + if (ret) + dev_err(priv->dev, "Sensor %d: failed to clear IRQ (%d)\n", + i, ret); + } + } + + pm_runtime_put_autosuspend(dev); + + return IRQ_HANDLED; +} + +static int acpm_tmu_init_sensor(struct acpm_tmu_sensor *sensor, + const struct acpm_tmu_initialize_tripwalkdata *twd) +{ + struct acpm_tmu_priv *priv = sensor->priv; + const struct acpm_handle *handle = priv->handle; + const struct acpm_tmu_ops *ops = &handle->ops.tmu; + unsigned int mbox_chan_id = priv->mbox_chan_id; + u8 acpm_sensor_id = sensor->group->id; + int ret; + + guard(mutex)(&sensor->lock); + + ret = ops->set_threshold(handle, mbox_chan_id, acpm_sensor_id, + twd->threshold, + ARRAY_SIZE(twd->threshold)); + if (ret) + return ret; + + ret = ops->set_hysteresis(handle, mbox_chan_id, acpm_sensor_id, + twd->hysteresis, + ARRAY_SIZE(twd->threshold)); + if (ret) + return ret; + + ret = ops->set_interrupt_enable(handle, mbox_chan_id, acpm_sensor_id, + twd->inten); + return ret; +} + +static int acpm_tmu_initialize_walk_cb(struct thermal_trip *trip, void *data) +{ + struct acpm_tmu_initialize_tripwalkdata * const twd = data; + int i; + + if (twd->i >= ARRAY_SIZE(twd->threshold)) + return -ERANGE; + + if (trip->type == THERMAL_TRIP_PASSIVE) + goto out; + + i = twd->i; + + twd->threshold[i] = trip->temperature / MILLIDEGREE_PER_DEGREE; + twd->hysteresis[i] = trip->hysteresis / MILLIDEGREE_PER_DEGREE; + + twd->inten |= BIT(i); + +out: + ++twd->i; + return 0; +} + +static int acpm_tmu_init_sensors(struct acpm_tmu_priv *priv) +{ + int i, ret; + + for (i = 0; i < priv->num_sensors; i++) { + struct acpm_tmu_sensor *sensor = &priv->sensors[i]; + struct acpm_tmu_initialize_tripwalkdata twd = {}; + + /* Skip sensors that weren't found in DT */ + if (!sensor->tzd) + continue; + + thermal_zone_for_each_trip(sensor->tzd, + acpm_tmu_initialize_walk_cb, &twd); + + ret = acpm_tmu_init_sensor(sensor, &twd); + if (ret) + return ret; + } + + return 0; +} + +static const struct of_device_id acpm_tmu_match[] = { + { .compatible = "google,gs101-tmu-top" }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, acpm_tmu_match); + +static int acpm_tmu_probe(struct platform_device *pdev) +{ + const struct acpm_tmu_driver_data *data = &acpm_tmu_gs101; + const struct acpm_handle *acpm_handle; + struct device *dev = &pdev->dev; + struct acpm_tmu_priv *priv; + struct regmap *regmap; + void __iomem *base; + int i, ret; + + acpm_handle = devm_acpm_get_by_phandle(dev); + if (IS_ERR(acpm_handle)) + return dev_err_probe(dev, PTR_ERR(acpm_handle), + "Failed to get ACPM handle\n"); + + priv = devm_kzalloc(dev, + struct_size(priv, sensors, data->num_sensor_groups), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = dev; + priv->handle = acpm_handle; + priv->mbox_chan_id = data->mbox_chan_id; + priv->num_sensors = data->num_sensor_groups; + + platform_set_drvdata(pdev, priv); + + base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(base)) + return dev_err_probe(dev, PTR_ERR(base), "Failed to ioremap resource\n"); + + regmap = devm_regmap_init_mmio(dev, base, &gs101_regmap_config); + if (IS_ERR(regmap)) + return dev_err_probe(dev, PTR_ERR(regmap), "Failed to init regmap\n"); + + ret = devm_regmap_field_bulk_alloc(dev, regmap, priv->regmap_fields, + data->reg_fields, REG_INTPEND_COUNT); + if (ret) + return dev_err_probe(dev, ret, + "Unable to map syscon registers\n"); + + priv->clk = devm_clk_get(dev, NULL); + if (IS_ERR(priv->clk)) + return dev_err_probe(dev, PTR_ERR(priv->clk), + "Failed to get the clock\n"); + + priv->irq = platform_get_irq(pdev, 0); + if (priv->irq < 0) + return dev_err_probe(dev, priv->irq, "Failed to get irq\n"); + + ret = devm_request_threaded_irq(dev, priv->irq, NULL, + acpm_tmu_thread_fn, IRQF_ONESHOT, + dev_name(dev), priv); + if (ret) + return dev_err_probe(dev, ret, "Failed to request irq\n"); + + pm_runtime_set_autosuspend_delay(dev, 100); + pm_runtime_use_autosuspend(dev); + + ret = devm_pm_runtime_enable(dev); + if (ret) + return dev_err_probe(dev, ret, "Failed to enable runtime PM\n"); + + ret = pm_runtime_resume_and_get(dev); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to resume device\n"); + + ret = acpm_handle->ops.tmu.init(acpm_handle, priv->mbox_chan_id); + if (ret) { + ret = dev_err_probe(dev, ret, "Failed to init TMU\n"); + goto err_pm_put; + } + + for (i = 0; i < priv->num_sensors; i++) { + 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, + &acpm_tmu_sensor_ops); + if (IS_ERR(sensor->tzd)) { + ret = PTR_ERR(sensor->tzd); + if (ret == -ENODEV) { + sensor->tzd = NULL; + dev_dbg(dev, "Sensor %d not used in DT, skipping\n", i); + continue; + } + + ret = dev_err_probe(dev, ret, "Failed to register sensor %d\n", i); + goto err_pm_put; + } + + ret = devm_thermal_add_hwmon_sysfs(dev, sensor->tzd); + if (ret) + dev_warn(dev, "Failed to add hwmon sysfs!\n"); + } + + ret = acpm_tmu_init_sensors(priv); + if (ret) { + ret = dev_err_probe(dev, ret, "Failed to init sensors\n"); + goto err_pm_put; + } + + ret = acpm_tmu_control(priv, true); + if (ret) { + ret = dev_err_probe(dev, ret, "Failed to enable TMU\n"); + goto err_pm_put; + } + + pm_runtime_put_autosuspend(&pdev->dev); + + return 0; + +err_pm_put: + pm_runtime_put_sync(dev); + return ret; +} + +static void acpm_tmu_remove(struct platform_device *pdev) +{ + struct acpm_tmu_priv *priv = platform_get_drvdata(pdev); + + /* Stop IRQ first to prevent race with thread_fn */ + disable_irq(priv->irq); + + acpm_tmu_control(priv, false); +} + +static int acpm_tmu_suspend(struct device *dev) +{ + struct acpm_tmu_priv *priv = dev_get_drvdata(dev); + const struct acpm_handle *handle = priv->handle; + const struct acpm_tmu_ops *ops = &handle->ops.tmu; + int ret; + + ret = acpm_tmu_control(priv, false); + if (ret) + return ret; + + /* APB clock not required for this specific msg */ + return ops->suspend(handle, priv->mbox_chan_id); +} + +static int acpm_tmu_resume(struct device *dev) +{ + struct acpm_tmu_priv *priv = dev_get_drvdata(dev); + const struct acpm_handle *handle = priv->handle; + const struct acpm_tmu_ops *ops = &handle->ops.tmu; + int ret; + + /* APB clock not required for this specific msg */ + ret = ops->resume(handle, priv->mbox_chan_id); + if (ret) + return ret; + + return acpm_tmu_control(priv, true); +} + +static int acpm_tmu_runtime_suspend(struct device *dev) +{ + struct acpm_tmu_priv *priv = dev_get_drvdata(dev); + + clk_disable_unprepare(priv->clk); + + return 0; +} + +static int acpm_tmu_runtime_resume(struct device *dev) +{ + struct acpm_tmu_priv *priv = dev_get_drvdata(dev); + + return clk_prepare_enable(priv->clk); +} + +static const struct dev_pm_ops acpm_tmu_pm_ops = { + SYSTEM_SLEEP_PM_OPS(acpm_tmu_suspend, acpm_tmu_resume) + RUNTIME_PM_OPS(acpm_tmu_runtime_suspend, acpm_tmu_runtime_resume, NULL) +}; + +static struct platform_driver acpm_tmu_driver = { + .driver = { + .name = "gs-tmu", + .pm = pm_ptr(&acpm_tmu_pm_ops), + .of_match_table = acpm_tmu_match, + }, + .probe = acpm_tmu_probe, + .remove = acpm_tmu_remove, +}; +module_platform_driver(acpm_tmu_driver); + +MODULE_AUTHOR("Tudor Ambarus <[email protected]>"); +MODULE_DESCRIPTION("Samsung Exynos ACPM TMU Driver"); +MODULE_LICENSE("GPL"); -- 2.52.0.457.g6b5491de43-goog
