[PATCH v2 2/7] firmware: samsung: acpm: Add TMU protocol support

Tudor Ambarus
2026-01-19T12:08:56Z

The Thermal Management Unit (TMU) on the Google GS101 SoC is managed
through a hybrid model shared between the kernel and the Alive Clock
and Power Manager (ACPM) firmware.

Add the protocol helpers required to communicate with the ACPM for
thermal operations, including initialization, threshold configuration,
temperature reading, and system suspend/resume handshakes.

Signed-off-by: Tudor Ambarus <tudor.ambarus@linaro.org>
---
drivers/firmware/samsung/Makefile | 1 +
drivers/firmware/samsung/exynos-acpm-tmu.c | 212 +++++++++++++++++++++
drivers/firmware/samsung/exynos-acpm-tmu.h | 33 ++++
drivers/firmware/samsung/exynos-acpm.c | 12 ++
.../linux/firmware/samsung/exynos-acpm-protocol.h | 24 +++
5 files changed, 282 insertions(+)

diff --git a/drivers/firmware/samsung/Makefile b/drivers/firmware/samsung/Makefile
index 80d4f89b33a9558b68c9083da675c70ec3d05f19..5a6f72bececfd98ba5af37d1d65fed48a3d8f912 100644
--- a/drivers/firmware/samsung/Makefile
+++ b/drivers/firmware/samsung/Makefile
@@ -3,4 +3,5 @@
acpm-protocol-objs := exynos-acpm.o
acpm-protocol-objs += exynos-acpm-pmic.o
acpm-protocol-objs += exynos-acpm-dvfs.o
+acpm-protocol-objs += exynos-acpm-tmu.o
obj-$(CONFIG_EXYNOS_ACPM_PROTOCOL) += acpm-protocol.o
diff --git a/drivers/firmware/samsung/exynos-acpm-tmu.c b/drivers/firmware/samsung/exynos-acpm-tmu.c
new file mode 100644
index 0000000000000000000000000000000000000000..7ec4b48074eb8b4e569b39d4bb5963d887aa9521
--- /dev/null
+++ b/drivers/firmware/samsung/exynos-acpm-tmu.c
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2020 Samsung Electronics Co., Ltd.
+ * Copyright 2020 Google LLC.
+ * Copyright 2026 Linaro Ltd.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/firmware/samsung/exynos-acpm-protocol.h>
+#include <linux/ktime.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include "exynos-acpm.h"
+#include "exynos-acpm-tmu.h"
+
+/* IPC Request Types */
+#define ACPM_TMU_INIT 0x01
+#define ACPM_TMU_READ_TEMP 0x02
+#define ACPM_TMU_SUSPEND 0x04
+#define ACPM_TMU_RESUME 0x10
+#define ACPM_TMU_THRESHOLD 0x11
+#define ACPM_TMU_INTEN 0x12
+#define ACPM_TMU_CONTROL 0x13
+#define ACPM_TMU_IRQ_CLEAR 0x14
+#define ACPM_TMU_HYSTERESIS 0x16
+
+#define ACPM_TMU_TX_DATA_LEN 8
+#define ACPM_TMU_RX_DATA_LEN 7
+
+struct acpm_tmu_tx {
+ u16 ctx;
+ u16 fw_use;
+ u8 type;
+ u8 rsvd0;
+ u8 tzid;
+ u8 rsvd1;
+ u8 data[ACPM_TMU_TX_DATA_LEN];
+} __packed;
+
+struct acpm_tmu_rx {
+ u16 ctx;
+ u16 fw_use;
+ u8 type;
+ s8 ret;
+ u8 tzid;
+ s8 temp;
+ u8 rsvd;
+ u8 data[ACPM_TMU_RX_DATA_LEN];
+} __packed;
+
+union acpm_tmu_msg {
+ u32 data[4];
+ struct acpm_tmu_tx tx;
+ struct acpm_tmu_rx rx;
+} __packed;
+
+static void acpm_tmu_set_xfer(struct acpm_xfer *xfer, u32 *cmd, size_t cmdlen,
+ unsigned int acpm_chan_id)
+{
+ xfer->acpm_chan_id = acpm_chan_id;
+ xfer->txd = cmd;
+ xfer->txlen = cmdlen;
+ xfer->rxd = cmd;
+ xfer->rxlen = cmdlen;
+}
+
+int acpm_tmu_init(const struct acpm_handle *handle, unsigned int acpm_chan_id)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+
+ msg.tx.type = ACPM_TMU_INIT;
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
+
+int acpm_tmu_read_temp(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, int *temp)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+ int ret;
+
+ msg.tx.type = ACPM_TMU_READ_TEMP;
+ msg.tx.tzid = tz;
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ ret = acpm_do_xfer(handle, &xfer);
+ if (ret)
+ return ret;
+
+ *temp = msg.rx.temp;
+
+ return 0;
+}
+
+int acpm_tmu_set_threshold(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz,
+ const u8 temperature[8], size_t tlen)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+ int i;
+
+ if (tlen > ACPM_TMU_TX_DATA_LEN)
+ return -EINVAL;
+
+ msg.tx.type = ACPM_TMU_THRESHOLD;
+ msg.tx.tzid = tz;
+
+ for (i = 0; i < tlen; i++)
+ msg.tx.data[i] = temperature[i];
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
+
+int acpm_tmu_set_hysteresis(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz,
+ const u8 hysteresis[8], size_t hlen)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+ int i;
+
+ if (hlen > ACPM_TMU_TX_DATA_LEN)
+ return -EINVAL;
+
+ msg.tx.type = ACPM_TMU_HYSTERESIS;
+ msg.tx.tzid = tz;
+
+ for (i = 0; i < hlen; i++)
+ msg.tx.data[i] = hysteresis[i];
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
+
+int acpm_tmu_set_interrupt_enable(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, u8 inten)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+
+ msg.tx.type = ACPM_TMU_INTEN;
+ msg.tx.tzid = tz;
+ msg.tx.data[0] = inten;
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
+
+int acpm_tmu_tz_control(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, bool enable)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+
+ msg.tx.type = ACPM_TMU_CONTROL;
+ msg.tx.tzid = tz;
+ msg.tx.data[0] = enable ? 1 : 0;
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
+
+int acpm_tmu_clear_tz_irq(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+
+ msg.tx.type = ACPM_TMU_IRQ_CLEAR;
+ msg.tx.tzid = tz;
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
+
+int acpm_tmu_suspend(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+
+ msg.tx.type = ACPM_TMU_SUSPEND;
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
+
+int acpm_tmu_resume(const struct acpm_handle *handle, unsigned int acpm_chan_id)
+{
+ union acpm_tmu_msg msg = {0};
+ struct acpm_xfer xfer;
+
+ msg.tx.type = ACPM_TMU_RESUME;
+
+ acpm_tmu_set_xfer(&xfer, msg.data, sizeof(msg.data), acpm_chan_id);
+
+ return acpm_do_xfer(handle, &xfer);
+}
diff --git a/drivers/firmware/samsung/exynos-acpm-tmu.h b/drivers/firmware/samsung/exynos-acpm-tmu.h
new file mode 100644
index 0000000000000000000000000000000000000000..f1a1ac21736d52bea0ad2a7cb3b280201fa74ffe
--- /dev/null
+++ b/drivers/firmware/samsung/exynos-acpm-tmu.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright 2020 Samsung Electronics Co., Ltd.
+ * Copyright 2020 Google LLC.
+ * Copyright 2026 Linaro Ltd.
+ */
+#ifndef __EXYNOS_ACPM_TMU_H__
+#define __EXYNOS_ACPM_TMU_H__
+
+#include <linux/types.h>
+
+struct acpm_handle;
+
+int acpm_tmu_init(const struct acpm_handle *handle, unsigned int acpm_chan_id);
+int acpm_tmu_read_temp(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, int *temp);
+int acpm_tmu_set_threshold(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz,
+ const u8 temperature[8], size_t tlen);
+int acpm_tmu_set_hysteresis(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz,
+ const u8 hysteresis[8], size_t hlen);
+int acpm_tmu_set_interrupt_enable(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, u8 inten);
+int acpm_tmu_tz_control(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, bool enable);
+int acpm_tmu_clear_tz_irq(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz);
+int acpm_tmu_suspend(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id);
+int acpm_tmu_resume(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id);
+#endif /* __EXYNOS_ACPM_TMU_H__ */
diff --git a/drivers/firmware/samsung/exynos-acpm.c b/drivers/firmware/samsung/exynos-acpm.c
index 0cb269c7046015d4c5fe5731ba0d61d48dcaeee1..cc045370f4b0dc6ccea99e3c2d6f86a43b2e9671 100644
--- a/drivers/firmware/samsung/exynos-acpm.c
+++ b/drivers/firmware/samsung/exynos-acpm.c
@@ -31,6 +31,7 @@
#include "exynos-acpm.h"
#include "exynos-acpm-dvfs.h"
#include "exynos-acpm-pmic.h"
+#include "exynos-acpm-tmu.h"

#define ACPM_PROTOCOL_SEQNUM GENMASK(21, 16)

@@ -595,6 +596,7 @@ static void acpm_setup_ops(struct acpm_info *acpm)
{
struct acpm_dvfs_ops *dvfs_ops = &acpm->handle.ops.dvfs_ops;
struct acpm_pmic_ops *pmic_ops = &acpm->handle.ops.pmic_ops;
+ struct acpm_tmu_ops *tmu_ops = &acpm->handle.ops.tmu;

dvfs_ops->set_rate = acpm_dvfs_set_rate;
dvfs_ops->get_rate = acpm_dvfs_get_rate;
@@ -604,6 +606,16 @@ static void acpm_setup_ops(struct acpm_info *acpm)
pmic_ops->write_reg = acpm_pmic_write_reg;
pmic_ops->bulk_write = acpm_pmic_bulk_write;
pmic_ops->update_reg = acpm_pmic_update_reg;
+
+ tmu_ops->init = acpm_tmu_init;
+ tmu_ops->read_temp = acpm_tmu_read_temp;
+ tmu_ops->set_threshold = acpm_tmu_set_threshold;
+ tmu_ops->set_hysteresis = acpm_tmu_set_hysteresis;
+ tmu_ops->set_interrupt_enable = acpm_tmu_set_interrupt_enable;
+ tmu_ops->tz_control = acpm_tmu_tz_control;
+ tmu_ops->clear_tz_irq = acpm_tmu_clear_tz_irq;
+ tmu_ops->suspend = acpm_tmu_suspend;
+ tmu_ops->resume = acpm_tmu_resume;
}

static void acpm_clk_pdev_unregister(void *data)
diff --git a/include/linux/firmware/samsung/exynos-acpm-protocol.h b/include/linux/firmware/samsung/exynos-acpm-protocol.h
index 2091da965a5ad238b5e16c567a72fe88fafe6095..43d41e11ad2eb985e27a918ce3f9e9ac15a194ee 100644
--- a/include/linux/firmware/samsung/exynos-acpm-protocol.h
+++ b/include/linux/firmware/samsung/exynos-acpm-protocol.h
@@ -40,9 +40,33 @@ struct acpm_pmic_ops {
u8 value, u8 mask);
};

+struct acpm_tmu_ops {
+ int (*init)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id);
+ int (*read_temp)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, int *temp);
+ int (*set_threshold)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz,
+ const u8 temperature[8], size_t tlen);
+ int (*set_hysteresis)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz,
+ const u8 hysteresis[8], size_t hlen);
+ int (*set_interrupt_enable)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, u8 inten);
+ int (*tz_control)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz, bool enable);
+ int (*clear_tz_irq)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id, u8 tz);
+ int (*suspend)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id);
+ int (*resume)(const struct acpm_handle *handle,
+ unsigned int acpm_chan_id);
+};
+
struct acpm_ops {
struct acpm_dvfs_ops dvfs_ops;
struct acpm_pmic_ops pmic_ops;
+ struct acpm_tmu_ops tmu;
};

/**

--
2.52.0.457.g6b5491de43-goog