[PATCH v2 3/3] thermal: qcom: add PM8901 PMIC temperature-alarm driver
From: Herman van Hazendonk
Date: Sun May 31 2026 - 00:10:39 EST
Add a thermal-of sensor driver for the temperature-alarm block inside
the Qualcomm PM8901 PMIC. PM8901 is a secondary PMIC paired with
PM8058 on the MSM8x60 family (MSM8260/MSM8660/APQ8060). It exposes
an over-temperature alarm at SSBI offset 0x23/0x24 with three
escalating stages (105/125/145 C); the driver decodes the stage +
threshold pair into a millicelsius reading and registers two PMIC-
internal interrupts (TEMP_ALARM at block 6 bit 4, TEMP_HI_ALARM at
block 6 bit 5).
Used by board thermal-zones for the orderly_poweroff path on the HP
TouchPad.
Signed-off-by: Herman van Hazendonk <github.com@xxxxxxxxxx>
---
drivers/thermal/qcom/Kconfig | 12 +
drivers/thermal/qcom/Makefile | 1 +
drivers/thermal/qcom/qcom-pm8901-tm.c | 408 ++++++++++++++++++++++++++
3 files changed, 421 insertions(+)
create mode 100644 drivers/thermal/qcom/qcom-pm8901-tm.c
diff --git a/drivers/thermal/qcom/Kconfig b/drivers/thermal/qcom/Kconfig
index a6bb01082ec6..af099032f1e6 100644
--- a/drivers/thermal/qcom/Kconfig
+++ b/drivers/thermal/qcom/Kconfig
@@ -32,6 +32,18 @@ config QCOM_SPMI_TEMP_ALARM
real time die temperature if an ADC is present or an estimate of the
temperature based upon the over temperature stage value.
+config QCOM_PM8901_TEMP_ALARM
+ tristate "Qualcomm PM8901 PMIC Temperature Alarm"
+ depends on MFD_PM8XXX || COMPILE_TEST
+ depends on THERMAL_OF
+ help
+ This enables the thermal driver for the PM8901 PMIC over-temperature
+ alarm block. PM8901 exposes a stage-based alarm (no raw ADC) with
+ four selectable thresholds and three escalating stages. The driver
+ registers a thermal-of sensor so a board device tree can declare
+ trip points and a critical-trip action (orderly_poweroff). Used on
+ HP TouchPad (APQ8060) where PM8901 supplies the secondary PMIC die.
+
config QCOM_LMH
tristate "Qualcomm Limits Management Hardware"
depends on ARCH_QCOM || COMPILE_TEST
diff --git a/drivers/thermal/qcom/Makefile b/drivers/thermal/qcom/Makefile
index 0fa2512042e7..90dc05151e33 100644
--- a/drivers/thermal/qcom/Makefile
+++ b/drivers/thermal/qcom/Makefile
@@ -5,4 +5,5 @@ qcom_tsens-y += tsens.o tsens-v2.o tsens-v1.o tsens-v0_1.o \
tsens-8960.o
obj-$(CONFIG_QCOM_SPMI_ADC_TM5) += qcom-spmi-adc-tm5.o
obj-$(CONFIG_QCOM_SPMI_TEMP_ALARM) += qcom-spmi-temp-alarm.o
+obj-$(CONFIG_QCOM_PM8901_TEMP_ALARM) += qcom-pm8901-tm.o
obj-$(CONFIG_QCOM_LMH) += lmh.o
diff --git a/drivers/thermal/qcom/qcom-pm8901-tm.c b/drivers/thermal/qcom/qcom-pm8901-tm.c
new file mode 100644
index 000000000000..b159e9917c2c
--- /dev/null
+++ b/drivers/thermal/qcom/qcom-pm8901-tm.c
@@ -0,0 +1,408 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Qualcomm PM8901 PMIC Thermal-Alarm Driver
+ *
+ * Mainline port of the legacy 2.6.35-palm drivers/thermal/pmic8901-tm.c.
+ * PM8901 exposes a stage-based over-temperature alarm (no raw ADC) with
+ * four selectable thresholds and three escalating stages. This driver
+ * mirrors the legacy programming exactly (threshold-set 0, software
+ * override enabled, PWM gating at 8 Hz) and registers a thermal-of
+ * sensor so a board DT can declare trip points and a critical action.
+ *
+ * Copyright (c) 2010-2011, Code Aurora Forum. All rights reserved.
+ * Copyright (c) 2026 Herman van Hazendonk <github.com@xxxxxxxxxx>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/thermal.h>
+
+/* SSBI registers (offsets from the per-instance reg base) */
+#define PM8901_TM_REG_CTRL 0x00 /* CTRL/STATUS (base + 0) */
+#define PM8901_TM_REG_PWM 0x01 /* PWM gating (base + 1) */
+
+/* CTRL register fields */
+#define CTRL_ST3_SD BIT(7)
+#define CTRL_ST2_SD BIT(6)
+#define CTRL_STATUS_MASK GENMASK(5, 4)
+#define CTRL_THRESH_MASK GENMASK(3, 2)
+#define CTRL_OVRD_ST3 BIT(1)
+#define CTRL_OVRD_ST2 BIT(0)
+#define CTRL_OVRD_MASK GENMASK(1, 0)
+
+/* PWM register fields */
+#define PWM_EN BIT(7)
+#define PWM_PER_PRE_MASK GENMASK(5, 3)
+#define PWM_PER_DIV_MASK GENMASK(2, 0)
+
+/* Temperature math (from legacy pmic8901-tm.c) */
+#define PM8901_TEMP_STAGE_STEP 20000 /* 20 deg C between stages */
+#define PM8901_TEMP_STAGE_HYSTERESIS 2000 /* 2 deg C transition guard */
+#define PM8901_TEMP_THRESH_MIN 105000 /* threshold 0 base = 105 C */
+#define PM8901_TEMP_THRESH_STEP 5000 /* 5 deg C per threshold step */
+
+/*
+ * PM8901 has no real die ADC; when stage == 0 ("below threshold") we
+ * report a plausible idle estimate matching the legacy DEFAULT_NO_ADC_TEMP.
+ */
+#define PM8901_TEMP_NO_ALARM 37000
+
+struct pm8901_tm_chip {
+ struct device *dev;
+ struct regmap *map;
+ struct thermal_zone_device *tz_dev;
+ struct mutex lock;
+ unsigned int base; /* SSBI offset, from DT reg */
+ unsigned int stage;
+ unsigned int thresh;
+ int temp;
+ bool initialised;
+};
+
+static int pm8901_tm_read_ctrl(struct pm8901_tm_chip *chip, u8 *val)
+{
+ unsigned int v;
+ int ret;
+
+ ret = regmap_read(chip->map, chip->base + PM8901_TM_REG_CTRL, &v);
+ if (!ret)
+ *val = v;
+ return ret;
+}
+
+static int pm8901_tm_write_ctrl(struct pm8901_tm_chip *chip, u8 val)
+{
+ return regmap_write(chip->map, chip->base + PM8901_TM_REG_CTRL, val);
+}
+
+static int pm8901_tm_write_pwm(struct pm8901_tm_chip *chip, u8 val)
+{
+ return regmap_write(chip->map, chip->base + PM8901_TM_REG_PWM, val);
+}
+
+/*
+ * Decode the (stage, threshold) pair into a single millicelsius value.
+ * Logic matches the legacy pmic8901-tm.c hysteresis selection:
+ * - on a rising stage transition, use the lower bound of the new stage
+ * plus +HYSTERESIS so we don't bounce
+ * - on a falling stage transition, use the upper bound of the new stage
+ * minus -HYSTERESIS
+ * - on the first read after probe (initialised == false), report the
+ * lower bound of the current stage (the most conservative estimate
+ * given that the hardware only tells us "we crossed this stage's
+ * threshold"), or PM8901_TEMP_NO_ALARM when stage == 0.
+ */
+static int pm8901_tm_update_temp_locked(struct pm8901_tm_chip *chip)
+{
+ unsigned int new_stage;
+ u8 reg;
+ int ret;
+
+ ret = pm8901_tm_read_ctrl(chip, ®);
+ if (ret)
+ return ret;
+
+ new_stage = FIELD_GET(CTRL_STATUS_MASK, reg);
+ chip->thresh = FIELD_GET(CTRL_THRESH_MASK, reg);
+
+ if (!chip->initialised) {
+ if (new_stage)
+ chip->temp = PM8901_TEMP_THRESH_MIN +
+ chip->thresh * PM8901_TEMP_THRESH_STEP +
+ (new_stage - 1) * PM8901_TEMP_STAGE_STEP;
+ else
+ chip->temp = PM8901_TEMP_NO_ALARM;
+ chip->initialised = true;
+ } else if (new_stage > chip->stage) {
+ chip->temp = PM8901_TEMP_THRESH_MIN +
+ chip->thresh * PM8901_TEMP_THRESH_STEP +
+ (new_stage - 1) * PM8901_TEMP_STAGE_STEP +
+ PM8901_TEMP_STAGE_HYSTERESIS;
+ } else if (new_stage < chip->stage) {
+ chip->temp = PM8901_TEMP_THRESH_MIN +
+ chip->thresh * PM8901_TEMP_THRESH_STEP +
+ new_stage * PM8901_TEMP_STAGE_STEP -
+ PM8901_TEMP_STAGE_HYSTERESIS;
+ }
+
+ chip->stage = new_stage;
+ return 0;
+}
+
+static int pm8901_tm_get_temp(struct thermal_zone_device *tz, int *temp)
+{
+ struct pm8901_tm_chip *chip = thermal_zone_device_priv(tz);
+ int ret;
+
+ if (!temp)
+ return -EINVAL;
+
+ mutex_lock(&chip->lock);
+ ret = pm8901_tm_update_temp_locked(chip);
+ if (!ret)
+ *temp = chip->temp;
+ mutex_unlock(&chip->lock);
+
+ return ret;
+}
+
+static const struct thermal_zone_device_ops pm8901_tm_zone_ops = {
+ .get_temp = pm8901_tm_get_temp,
+};
+
+/*
+ * Shared ISR for both TEMP_ALARM (stage-transition) and TEMP_HI_ALARM
+ * (hi-temp) interrupts. Updates the cached temperature, clears any
+ * latched ST2_SD / ST3_SD shutdown bits so the next stage transition
+ * can be observed, and pokes the thermal core which then re-reads
+ * temp and walks trips (a critical-trip cross triggers orderly_poweroff
+ * via the kernel's standard machinery).
+ */
+static irqreturn_t pm8901_tm_isr(int irq, void *data)
+{
+ struct pm8901_tm_chip *chip = data;
+ u8 reg;
+ int ret;
+
+ mutex_lock(&chip->lock);
+
+ ret = pm8901_tm_update_temp_locked(chip);
+ if (ret) {
+ mutex_unlock(&chip->lock);
+ return IRQ_HANDLED;
+ }
+
+ ret = pm8901_tm_read_ctrl(chip, ®);
+ if (!ret && (reg & (CTRL_ST2_SD | CTRL_ST3_SD))) {
+ reg &= ~(CTRL_ST2_SD | CTRL_ST3_SD | CTRL_STATUS_MASK);
+ pm8901_tm_write_ctrl(chip, reg);
+ }
+
+ dev_dbg(chip->dev, "alarm irq=%d stage=%u thresh=%u temp=%d\n",
+ irq, chip->stage, chip->thresh, chip->temp);
+
+ mutex_unlock(&chip->lock);
+
+ thermal_zone_device_update(chip->tz_dev, THERMAL_EVENT_UNSPECIFIED);
+ return IRQ_HANDLED;
+}
+
+/*
+ * Program PM8901 to the legacy default: threshold-set 0 (105 / 125 / 145 C),
+ * PWM at 8 Hz (legacy "cut down on unnecessary interrupts" rate), and
+ * prime the cached temperature. This intentionally does NOT yet flip the
+ * software-override bits; HW auto-shutdown is left enabled here so the
+ * PMIC keeps protecting the part if any later probe step fails. The
+ * SW-override switch happens in pm8901_tm_enable_sw_override() and is
+ * paired with a devm action that reverts the bits if the driver unbinds.
+ */
+static int pm8901_tm_init_hw(struct pm8901_tm_chip *chip)
+{
+ int ret;
+ u8 reg;
+
+ mutex_lock(&chip->lock);
+
+ ret = pm8901_tm_read_ctrl(chip, ®);
+ if (ret)
+ goto out;
+
+ /* Clear status + threshold bits, leave OVRD bits as the HW found them. */
+ reg = reg & ~(CTRL_STATUS_MASK | CTRL_THRESH_MASK);
+ ret = pm8901_tm_write_ctrl(chip, reg);
+ if (ret)
+ goto out;
+
+ chip->thresh = 0;
+
+ /* PWM @ 8 Hz: PWM_EN | PRE=3 | DIV=3 — verbatim from legacy. */
+ reg = PWM_EN | FIELD_PREP(PWM_PER_PRE_MASK, 3) |
+ FIELD_PREP(PWM_PER_DIV_MASK, 3);
+ ret = pm8901_tm_write_pwm(chip, reg);
+ if (ret)
+ goto out;
+
+ /* Prime the cached temperature from current hardware state. */
+ chip->initialised = false;
+ ret = pm8901_tm_update_temp_locked(chip);
+
+out:
+ mutex_unlock(&chip->lock);
+ return ret;
+}
+
+/*
+ * Re-enable PM8901's hardware auto-cut on the way out, so the PMIC takes
+ * over thermal protection again once the kernel is no longer the
+ * shutdown agent. Best-effort: log on failure, do not propagate the
+ * error (there is nothing the unbind path can do about it).
+ */
+static void pm8901_tm_restore_hw_shutdown(void *data)
+{
+ struct pm8901_tm_chip *chip = data;
+ int ret;
+ u8 reg;
+
+ mutex_lock(&chip->lock);
+ ret = pm8901_tm_read_ctrl(chip, ®);
+ if (!ret) {
+ reg &= ~CTRL_OVRD_MASK;
+ ret = pm8901_tm_write_ctrl(chip, reg);
+ }
+ mutex_unlock(&chip->lock);
+
+ if (ret)
+ dev_warn(chip->dev,
+ "failed to restore PMIC HW auto-shutdown: %d\n", ret);
+}
+
+/*
+ * Hand thermal protection responsibility from the PMIC's hardware
+ * auto-cut to the kernel thermal core. This is the LAST step of probe
+ * so that, if any earlier step fails, the PMIC keeps protecting the
+ * part on its own. Once it succeeds we install a devm action that
+ * re-enables HW auto-cut if/when the driver is unbound.
+ */
+static int pm8901_tm_enable_sw_override(struct pm8901_tm_chip *chip)
+{
+ int ret;
+ u8 reg;
+
+ mutex_lock(&chip->lock);
+ ret = pm8901_tm_read_ctrl(chip, ®);
+ if (!ret) {
+ reg = (reg & ~CTRL_OVRD_MASK) | CTRL_OVRD_ST3 | CTRL_OVRD_ST2;
+ ret = pm8901_tm_write_ctrl(chip, reg);
+ }
+ mutex_unlock(&chip->lock);
+ if (ret)
+ return ret;
+
+ return devm_add_action_or_reset(chip->dev,
+ pm8901_tm_restore_hw_shutdown, chip);
+}
+
+static int pm8901_tm_probe(struct platform_device *pdev)
+{
+ struct pm8901_tm_chip *chip;
+ int ret, irq_alarm, irq_hi_alarm;
+ u32 res;
+
+ chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
+ if (!chip)
+ return -ENOMEM;
+
+ chip->dev = &pdev->dev;
+ mutex_init(&chip->lock);
+
+ chip->map = dev_get_regmap(pdev->dev.parent, NULL);
+ if (!chip->map)
+ return dev_err_probe(&pdev->dev, -ENXIO,
+ "no regmap on PM8901 parent\n");
+
+ ret = of_property_read_u32(pdev->dev.of_node, "reg", &res);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "missing reg property\n");
+ chip->base = res;
+
+ irq_alarm = platform_get_irq_byname(pdev, "alarm");
+ if (irq_alarm < 0)
+ return irq_alarm;
+ irq_hi_alarm = platform_get_irq_byname(pdev, "hi-alarm");
+ if (irq_hi_alarm < 0)
+ return irq_hi_alarm;
+
+ ret = pm8901_tm_init_hw(chip);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret, "hw init failed\n");
+
+ chip->tz_dev = devm_thermal_of_zone_register(&pdev->dev, 0, chip,
+ &pm8901_tm_zone_ops);
+ if (IS_ERR(chip->tz_dev))
+ return dev_err_probe(&pdev->dev, PTR_ERR(chip->tz_dev),
+ "thermal zone register failed\n");
+
+ ret = devm_request_threaded_irq(&pdev->dev, irq_alarm, NULL,
+ pm8901_tm_isr, IRQF_ONESHOT,
+ "pm8901-tm-alarm", chip);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "alarm IRQ request failed\n");
+
+ ret = devm_request_threaded_irq(&pdev->dev, irq_hi_alarm, NULL,
+ pm8901_tm_isr, IRQF_ONESHOT,
+ "pm8901-tm-hi-alarm", chip);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "hi-alarm IRQ request failed\n");
+
+ platform_set_drvdata(pdev, chip);
+
+ /*
+ * All resources that we need on the thermal hot path are now in
+ * place; hand thermal-shutdown responsibility from the PMIC's
+ * hardware auto-cut to the kernel thermal core. If this fails the
+ * PMIC is left with its original (post-reset) HW auto-cut intact,
+ * so we never leave the part unprotected.
+ */
+ ret = pm8901_tm_enable_sw_override(chip);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "failed to enable SW thermal override\n");
+
+ thermal_zone_device_update(chip->tz_dev, THERMAL_EVENT_UNSPECIFIED);
+
+ {
+ unsigned int stage, thresh;
+ int temp;
+
+ /*
+ * IRQs and thermal-core polling are live by now, so the
+ * cached state can be updated under chip->lock at any time.
+ * Snapshot under the lock so the boot banner is consistent.
+ */
+ mutex_lock(&chip->lock);
+ stage = chip->stage;
+ thresh = chip->thresh;
+ temp = chip->temp;
+ mutex_unlock(&chip->lock);
+
+ dev_info(&pdev->dev,
+ "PM8901 thermal alarm: base=0x%x stage=%u thresh=%u temp=%d\n",
+ chip->base, stage, thresh, temp);
+ }
+
+ return 0;
+}
+
+/*
+ * No explicit ->remove() needed: pm8901_tm_restore_hw_shutdown() is
+ * registered as a devm action in probe and re-enables the PMIC's HW
+ * auto-cut automatically on unbind.
+ */
+
+static const struct of_device_id pm8901_tm_match_table[] = {
+ { .compatible = "qcom,pm8901-temp-alarm" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, pm8901_tm_match_table);
+
+static struct platform_driver pm8901_tm_driver = {
+ .driver = {
+ .name = "pm8901-temp-alarm",
+ .of_match_table = pm8901_tm_match_table,
+ },
+ .probe = pm8901_tm_probe,
+};
+module_platform_driver(pm8901_tm_driver);
+
+MODULE_ALIAS("platform:pm8901-temp-alarm");
+MODULE_DESCRIPTION("Qualcomm PM8901 PMIC Thermal Alarm driver");
+MODULE_LICENSE("GPL v2");
--
2.43.0