* [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 +
| 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
--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],
+ ®0_fld0) ||
+ regmap_field_read(priv->fields[K3_DDR_TEMP_REG0_FIELD1],
+ ®0_fld1) ||
+ regmap_field_read(priv->fields[K3_DDR_TEMP_REG1_FIELD0],
+ ®1_fld0) ||
+ regmap_field_read(priv->fields[K3_DDR_TEMP_REG1_FIELD1],
+ ®1_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