Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH 06/22] memory: ti-ddrss: Add MR4 temperature-driven refresh rate driver
@ 2026-07-14 12:55 MANNURU VENKATESWARLU
  2026-07-14 21:29 ` Uwe Kleine-König
  0 siblings, 1 reply; 2+ messages in thread
From: MANNURU VENKATESWARLU @ 2026-07-14 12:55 UTC (permalink / raw)
  To: krzk; +Cc: linux-arm-kernel, linux-kernel, n-francis, s-k6, bb, v-mannuru

From: Santhosh Kumar K <s-k6@ti.com>

Add a driver to monitor LPDDR4 temperature via the MR4 register
and adjust DDR refresh timing (tRAS_MAX, tREF) in the kernel on
every TUF interrupt following JEDEC recommendations. Exposes
refresh_status (RO) and auto_adjust (RW) sysfs attributes.

Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
Signed-off-by: Gandhar Deshpande <g-deshpande@ti.com>
Signed-off-by: Neha Malcom Francis <n-francis@ti.com>
Signed-off-by: MANNURU VENKATESWARLU <v-mannuru@ti.com>
---
 drivers/memory/Kconfig        |  19 ++
 drivers/memory/Makefile       |   1 +
 drivers/memory/ti-ddrss-mr4.c | 322 ++++++++++++++++++++++++++++++++++
 3 files changed, 342 insertions(+)
 create mode 100644 drivers/memory/ti-ddrss-mr4.c

diff --git a/drivers/memory/Kconfig b/drivers/memory/Kconfig
index e5527020ff337..259654a12650a 100644
--- a/drivers/memory/Kconfig
+++ b/drivers/memory/Kconfig
@@ -231,6 +231,25 @@ config STM32_OMM
 	    - chip select selection override.
 	    - the time between 2 transactions in multiplexed mode.
 
+config K3_DDR_MR4
+	tristate "Texas Instruments K3 DDR MR4 refresh rate driver"
+	depends on ARCH_K3 || COMPILE_TEST
+	depends on MFD_TI_DDRSS
+	help
+	  Say Y here to enable automatic DDR refresh rate adjustment for TI K3
+	  SoCs (AM62x, AM62Ax, AM62Px, AM64x, J721E, J7200, J721S2, J784S4).
+
+	  On every TUF interrupt the driver reads the MR4 temperature sensor,
+	  takes the maximum across all channels, and updates tRAS_MAX and tREF
+	  for all three Frequency Set Points.  The auto_adjust sysfs attribute
+	  allows userspace to disable kernel adjustment and observe the MR4
+	  state via refresh_status.
+
+	  This extends the LPDDR MR4 derating pattern from drivers/memory/emif.c
+	  (LPDDR2) to LPDDR4 on Cadence controllers.
+
+	  To compile as a module: the module will be called ti-ddrss-mr4.
+
 source "drivers/memory/samsung/Kconfig"
 source "drivers/memory/tegra/Kconfig"
 
diff --git a/drivers/memory/Makefile b/drivers/memory/Makefile
index 3ee883c8759a9..18556ba8bb2a9 100644
--- a/drivers/memory/Makefile
+++ b/drivers/memory/Makefile
@@ -29,6 +29,7 @@ obj-$(CONFIG_SAMSUNG_MC)	+= samsung/
 obj-$(CONFIG_TEGRA_MC)		+= tegra/
 obj-$(CONFIG_TI_EMIF_SRAM)	+= ti-emif-sram.o
 obj-$(CONFIG_FPGA_DFL_EMIF)	+= dfl-emif.o
+obj-$(CONFIG_K3_DDR_MR4)	+= ti-ddrss-mr4.o
 
 ti-emif-sram-objs		:= ti-emif-pm.o ti-emif-sram-pm.o
 
diff --git a/drivers/memory/ti-ddrss-mr4.c b/drivers/memory/ti-ddrss-mr4.c
new file mode 100644
index 0000000000000..e37b791694987
--- /dev/null
+++ b/drivers/memory/ti-ddrss-mr4.c
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ti-ddrss-mr4.c -- TI K3 DDR MR4 temperature-driven refresh rate driver
+ *
+ * Copyright (C) 2026 Texas Instruments Incorporated - https://ti.com
+ */
+
+#include <linux/devm-helpers.h>
+#include <linux/interrupt.h>
+#include <linux/kstrtox.h>
+#include <linux/mfd/ti-ddrss.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/sysfs.h>
+#include <linux/workqueue.h>
+
+#define K3_DDR_NUM_FSP        3
+
+/*
+ * MR4 OP[2:0] -> left-shift mapping (JEDEC).
+ * Boot programs base = 0.25x interval.  Shifting left relaxes
+ * the interval; shifting right tightens it toward the boot-safe value.
+ */
+static const u8 k3_ddr_mr4_shift[8] = {
+	[0] = 2,    /* cold:     1x nominal  */
+	[1] = 2,    /* cold:     1x nominal  */
+	[2] = 2,    /* cool:     1x nominal  */
+	[3] = 2,    /* normal:   1x nominal  */
+	[4] = 1,    /* warm:     0.5x        */
+	[5] = 0,    /* hot:      0.25x (boot)*/
+	[6] = 0,    /* very hot: 0.25x (boot)*/
+	[7] = 0,    /* critical: 0.25x (JEDEC high-temp limit exceeded ) */
+};
+
+static const char * const k3_ddr_mr4_status[] = {
+	[0] = "low temperature",
+	[1] = "4x refresh interval",
+	[2] = "2x refresh interval",
+	[3] = "1x refresh interval",
+	[4] = "0.5x refresh interval",
+	[5] = "0.25x refresh interval",
+	[6] = "0.25x refresh interval with derating",
+	[7] = "high temperature",
+};
+
+/**
+ * struct k3_ddr_mr4 - per-instance private data
+ * @fields:      regmap_field pointers for all register fields
+ * @dev:         device pointer
+ * @mr4_cached:  last MR4 reading, updated by the IRQ thread
+ * @auto_adjust: when true, IRQ thread writes tRAS_MAX/tREF on every TUF
+ * @tras_nom:    boot-time tRAS_MAX base values for FSP 0, 1, 2
+ * @tref_nom:    boot-time tREF base values for FSP 0, 1, 2
+ * @cfg:         SoC-specific register configuration from the MFD parent
+ * @notify_work: deferred work to push sysfs uevent after each TUF
+ */
+struct k3_ddr_mr4 {
+	struct regmap_field		*fields[K3_DDR_MAX_FIELDS];
+	struct device			*dev;
+	u8				mr4_cached;
+	bool				auto_adjust;
+	u32				tras_nom[K3_DDR_NUM_FSP];
+	u32				tref_nom[K3_DDR_NUM_FSP];
+	const struct k3_ddr_cfg		*cfg;
+	struct work_struct		notify_work;
+};
+
+static ssize_t refresh_status_show(struct device *dev,
+				   struct device_attribute *attr,
+				   char *buf)
+{
+	struct k3_ddr_mr4 *priv = dev_get_drvdata(dev);
+	u8 mr4 = READ_ONCE(priv->mr4_cached);
+
+	if (mr4 >= ARRAY_SIZE(k3_ddr_mr4_status))
+		return sysfs_emit(buf, "unknown\n");
+
+	return sysfs_emit(buf, "%s\n", k3_ddr_mr4_status[mr4]);
+}
+static DEVICE_ATTR_RO(refresh_status);
+
+static ssize_t auto_adjust_show(struct device *dev,
+				struct device_attribute *attr,
+				char *buf)
+{
+	struct k3_ddr_mr4 *priv = dev_get_drvdata(dev);
+
+	return sysfs_emit(buf, "%u\n", READ_ONCE(priv->auto_adjust) ? 1 : 0);
+}
+
+static ssize_t auto_adjust_store(struct device *dev,
+				 struct device_attribute *attr,
+				 const char *buf, size_t count)
+{
+	struct k3_ddr_mr4 *priv = dev_get_drvdata(dev);
+	bool val;
+	int ret;
+
+	ret = kstrtobool(buf, &val);
+	if (ret)
+		return ret;
+
+	WRITE_ONCE(priv->auto_adjust, val);
+	return count;
+}
+static DEVICE_ATTR_RW(auto_adjust);
+
+static struct attribute *k3_ddr_mr4_attrs[] = {
+	&dev_attr_refresh_status.attr,
+	&dev_attr_auto_adjust.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(k3_ddr_mr4);
+
+static void k3_ddr_mr4_notify_work(struct work_struct *work)
+{
+	struct k3_ddr_mr4 *priv =
+		container_of(work, struct k3_ddr_mr4, notify_work);
+
+	sysfs_notify(&priv->dev->kobj, NULL, "refresh_status");
+	kobject_uevent(&priv->dev->kobj, KOBJ_CHANGE);
+}
+
+static irqreturn_t k3_ddr_mr4_irq_thread(int irq, void *data)
+{
+	struct k3_ddr_mr4 *priv = data;
+	unsigned int reg0_fld0, reg0_fld1, reg1_fld0, reg1_fld1;
+	unsigned int stat_group = 0, stat_tuf = 0;
+	u8 temp0, temp1, max_mr4;
+	int i;
+
+	/* AM62/AM62A: check group interrupt status before anything else */
+	if (priv->cfg->has_intr_group) {
+		if (regmap_field_read(priv->fields[K3_DDR_INT_STAT_MASTER],
+				      &stat_group))
+			return IRQ_NONE;
+		if (!stat_group)
+			return IRQ_NONE;
+	}
+
+	/* Verify TUF is the source before reading MR4 */
+	if (regmap_field_read(priv->fields[K3_DDR_INT_STAT_TUF], &stat_tuf))
+		return IRQ_NONE;
+	if (!stat_tuf)
+		return IRQ_NONE;
+
+	if (regmap_field_read(priv->fields[K3_DDR_TEMP_REG0_FIELD0],
+			      &reg0_fld0) ||
+	    regmap_field_read(priv->fields[K3_DDR_TEMP_REG0_FIELD1],
+			      &reg0_fld1) ||
+	    regmap_field_read(priv->fields[K3_DDR_TEMP_REG1_FIELD0],
+			      &reg1_fld0) ||
+	    regmap_field_read(priv->fields[K3_DDR_TEMP_REG1_FIELD1],
+			      &reg1_fld1))
+		return IRQ_NONE;
+
+	temp0 = max(reg0_fld0, reg0_fld1);
+	temp1 = max(reg1_fld0, reg1_fld1);
+	max_mr4 = max(temp0, temp1);
+
+	WRITE_ONCE(priv->mr4_cached, max_mr4);
+
+	dev_dbg(priv->dev, "TUF: MR4=%u (%s)\n", max_mr4,
+		max_mr4 < ARRAY_SIZE(k3_ddr_mr4_status) ?
+		k3_ddr_mr4_status[max_mr4] : "unknown");
+
+/* ACK unconditionally - even when auto_adjust is off - to prevent IRQ storm. */
+	regmap_field_write(priv->fields[K3_DDR_INT_ACK_TUF], 1);
+
+	schedule_work(&priv->notify_work);
+
+	if (!READ_ONCE(priv->auto_adjust))
+		return IRQ_HANDLED;
+
+	for (i = 0; i < K3_DDR_NUM_FSP; i++) {
+		regmap_field_write(priv->fields[K3_DDR_TRAS_MAX_F0 + i],
+				   priv->tras_nom[i] <<
+				   k3_ddr_mr4_shift[max_mr4]);
+		regmap_field_write(priv->fields[K3_DDR_TREF_F0 + i],
+				   priv->tref_nom[i] <<
+				   k3_ddr_mr4_shift[max_mr4]);
+	}
+
+	return IRQ_HANDLED;
+}
+
+static int k3_ddr_mr4_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct ti_ddrss_dev *ddrss;
+	struct k3_ddr_mr4 *priv;
+	unsigned int v;
+	int i, ret;
+
+	ddrss = dev_get_drvdata(dev->parent);
+	if (!ddrss)
+		return dev_err_probe(dev, -ENODEV,
+				     "Failed to get parent drvdata\n");
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->dev = dev;
+	priv->cfg = ddrss->cfg;
+	priv->auto_adjust = true;
+
+	for (i = 0; i < K3_DDR_MAX_FIELDS; i++) {
+		priv->fields[i] = devm_regmap_field_alloc(dev, ddrss->regmap,
+							  ddrss->cfg->cfg_fields[i]);
+		if (IS_ERR(priv->fields[i]))
+			return dev_err_probe(dev, PTR_ERR(priv->fields[i]),
+					     "Failed to alloc regmap field %d\n",
+					     i);
+	}
+
+	/*
+	 * Save boot-time tRAS_MAX and tREF (boot programs 0.25x interval).
+	 * All run-time adjustments shift from these base values.
+	 */
+	for (i = 0; i < K3_DDR_NUM_FSP; i++) {
+		ret = regmap_field_read(priv->fields[K3_DDR_TRAS_MAX_F0 + i],
+					&v);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "Failed to read tRAS_MAX FSP%d\n",
+					     i);
+		priv->tras_nom[i] = v;
+
+		ret = regmap_field_read(priv->fields[K3_DDR_TREF_F0 + i], &v);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "Failed to read tREF FSP%d\n", i);
+		priv->tref_nom[i] = v;
+	}
+
+	/* Read initial MR4 so refresh_status is valid before first TUF. */
+	{
+		unsigned int r0f0, r0f1, r1f0, r1f1;
+		u8 t0, t1, init_mr4;
+
+		if (regmap_field_read(priv->fields[K3_DDR_TEMP_REG0_FIELD0],
+				      &r0f0) ||
+		    regmap_field_read(priv->fields[K3_DDR_TEMP_REG0_FIELD1],
+				      &r0f1) ||
+		    regmap_field_read(priv->fields[K3_DDR_TEMP_REG1_FIELD0],
+				      &r1f0) ||
+		    regmap_field_read(priv->fields[K3_DDR_TEMP_REG1_FIELD1],
+				      &r1f1)) {
+			init_mr4 = 3;
+		} else {
+			t0 = max(r0f0, r0f1);
+			t1 = max(r1f0, r1f1);
+			init_mr4 = max(t0, t1);
+			if (!init_mr4)
+				init_mr4 = 3; /* MR4=0 is invalid; fall back to nominal */
+		}
+		priv->mr4_cached = init_mr4;
+	}
+
+	ret = devm_work_autocancel(dev, &priv->notify_work,
+				   k3_ddr_mr4_notify_work);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to init notify work\n");
+
+	ret = devm_request_threaded_irq(dev, ddrss->irq,
+					NULL, k3_ddr_mr4_irq_thread,
+					IRQF_ONESHOT | IRQF_NO_SUSPEND,
+					dev_name(dev), priv);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "Failed to request threaded IRQ\n");
+
+	/*
+	 * Unmask after IRQ handler is registered to avoid losing early events.
+	 * J7: single bit.  AM62/AM62A: three levels (MISC group, global, TUF).
+	 */
+	ret = regmap_field_write(priv->fields[K3_DDR_INT_MASK_MASTER_MISC],
+				 0U);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to unmask interrupt\n");
+
+	if (priv->cfg->has_mask_misc) {
+		ret = regmap_field_write(priv->fields[K3_DDR_INT_MASK_MASTER_GLOBAL],
+					 0U);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "Failed to unmask global interrupt\n");
+
+		ret = regmap_field_write(priv->fields[K3_DDR_INT_MASK_TUF],
+					 0U);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "Failed to unmask TUF interrupt\n");
+	}
+
+	platform_set_drvdata(pdev, priv);
+
+	return 0;
+}
+
+static const struct platform_device_id k3_ddr_mr4_id[] = {
+	{ "ti-ddrss-mr4", 0 },
+	{}
+};
+MODULE_DEVICE_TABLE(platform, k3_ddr_mr4_id);
+
+static struct platform_driver k3_ddr_mr4_driver = {
+	.driver = {
+		.name		= "ti-ddrss-mr4",
+		.dev_groups	= k3_ddr_mr4_groups,
+	},
+	.probe		= k3_ddr_mr4_probe,
+	.id_table	= k3_ddr_mr4_id,
+};
+module_platform_driver(k3_ddr_mr4_driver);
+
+MODULE_DESCRIPTION("TI K3 DDR MR4 temperature-driven refresh rate driver");
+MODULE_AUTHOR("Texas Instruments Inc");
+MODULE_LICENSE("GPL");
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [RFC PATCH 06/22] memory: ti-ddrss: Add MR4 temperature-driven refresh rate driver
  2026-07-14 12:55 [RFC PATCH 06/22] memory: ti-ddrss: Add MR4 temperature-driven refresh rate driver MANNURU VENKATESWARLU
@ 2026-07-14 21:29 ` Uwe Kleine-König
  0 siblings, 0 replies; 2+ messages in thread
From: Uwe Kleine-König @ 2026-07-14 21:29 UTC (permalink / raw)
  To: MANNURU VENKATESWARLU
  Cc: krzk, linux-arm-kernel, linux-kernel, n-francis, s-k6, bb

[-- Attachment #1: Type: text/plain, Size: 1079 bytes --]

Hello,

On Tue, Jul 14, 2026 at 06:25:57PM +0530, MANNURU VENKATESWARLU wrote:
> +#include <linux/mfd/ti-ddrss.h>
> +#include <linux/mod_devicetable.h>

Please drop this header. platform_device_id is provided by
<linux/platform_device.h>, so the explicit include isn't needed.

The mod_devicetable.h header will go away soon.

> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> [...]
> +static const struct platform_device_id k3_ddr_mr4_id[] = {
> +	{ "ti-ddrss-mr4", 0 },
> +	{}
> +};

Please make this:

static const struct platform_device_id k3_ddr_mr4_id[] = {
	{ .name = "ti-ddrss-mr4" },
	{ }
};

> +MODULE_DEVICE_TABLE(platform, k3_ddr_mr4_id);
> +
> +static struct platform_driver k3_ddr_mr4_driver = {
> +	.driver = {
> +		.name		= "ti-ddrss-mr4",
> +		.dev_groups	= k3_ddr_mr4_groups,
> +	},
> +	.probe		= k3_ddr_mr4_probe,
> +	.id_table	= k3_ddr_mr4_id,

I'm not a fan of aligned `=`. These tend to diverge over time. And
sometimes they are not even aligned from the start ...
If you ask me, just use a single space before the `=`.

Best regards
Uwe

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-07-14 21:36 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14 12:55 [RFC PATCH 06/22] memory: ti-ddrss: Add MR4 temperature-driven refresh rate driver MANNURU VENKATESWARLU
2026-07-14 21:29 ` Uwe Kleine-König

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox