[PATCH 2/2] rtc: mt6397: expose the spare bytes of the alarm registers as nvmem

From: Ryan Brue

Date: Fri Sep 18 2026 - 00:58:51 EST


Each alarm field lives in the low bits of its register and the driver
masks its writes accordingly, so the high byte of four of them is storage
the RTC never touches. MediaTek names these RTC_NEW_SPARE0 to
RTC_NEW_SPARE3 and gives the first to a fuel gauge, which is how its PMIC
battery drivers carry a state of charge over a reboot.

Offer all four as a battery-backed nvmem provider, so that a consumer does
not have to reach into this block behind the driver's back. Doing it here
is what makes it safe: a write lands under the same lock the alarm paths
take, so it can neither be lost inside mtk_rtc_set_alarm()'s
read-modify-write nor fire the write trigger in the middle of one.

The nvmem core does not range check a cell against the provider size, so
the callbacks check the offset themselves.

Tested on an MT6397; MediaTek's spare map for mt6323 matches, and the
alarm field masks are common to every compatible this driver binds.

Assisted-by: LLM
Signed-off-by: Ryan Brue <ryanbrue.dev@xxxxxxxxx>
---
drivers/rtc/rtc-mt6397.c | 91 +++++++++++++++++++++++++++++++++++++++++-
include/linux/mfd/mt6397/rtc.h | 7 ++++
2 files changed, 97 insertions(+), 1 deletion(-)

diff --git a/drivers/rtc/rtc-mt6397.c b/drivers/rtc/rtc-mt6397.c
index 3d857681f760..d6e156516aa6 100644
--- a/drivers/rtc/rtc-mt6397.c
+++ b/drivers/rtc/rtc-mt6397.c
@@ -4,6 +4,8 @@
* Author: Tianping.Fang <tianping.fang@xxxxxxxxxxxx>
*/

+#include <linux/array_size.h>
+#include <linux/bitfield.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/mfd/mt6397/core.h>
@@ -243,10 +245,91 @@ static const struct rtc_class_ops mtk_rtc_ops = {
.set_alarm = mtk_rtc_set_alarm,
};

+/*
+ * The spare byte of each of these registers, in the order a board addresses
+ * them as RTC_NEW_SPARE0 to RTC_NEW_SPARE3.
+ */
+static const u32 mtk_rtc_spare_reg[] = {
+ RTC_AL_HOU, RTC_AL_DOM, RTC_AL_DOW, RTC_AL_MTH,
+};
+
+static int mtk_rtc_nvram_read(void *priv, unsigned int offset, void *val,
+ size_t bytes)
+{
+ struct mt6397_rtc *rtc = priv;
+ u8 *buf = val;
+ u32 data;
+ int ret = 0;
+
+ if (offset >= ARRAY_SIZE(mtk_rtc_spare_reg) ||
+ bytes > ARRAY_SIZE(mtk_rtc_spare_reg) - offset)
+ return -EINVAL;
+
+ mutex_lock(&rtc->lock);
+
+ while (bytes--) {
+ ret = regmap_read(rtc->regmap,
+ rtc->addr_base + mtk_rtc_spare_reg[offset++],
+ &data);
+ if (ret)
+ break;
+
+ *buf++ = FIELD_GET(RTC_SPARE_MASK, data);
+ }
+
+ mutex_unlock(&rtc->lock);
+
+ return ret;
+}
+
+static int mtk_rtc_nvram_write(void *priv, unsigned int offset, void *val,
+ size_t bytes)
+{
+ struct mt6397_rtc *rtc = priv;
+ u8 *buf = val;
+ int ret = 0;
+
+ if (offset >= ARRAY_SIZE(mtk_rtc_spare_reg) ||
+ bytes > ARRAY_SIZE(mtk_rtc_spare_reg) - offset)
+ return -EINVAL;
+
+ mutex_lock(&rtc->lock);
+
+ while (bytes--) {
+ ret = regmap_update_bits(rtc->regmap,
+ rtc->addr_base + mtk_rtc_spare_reg[offset++],
+ RTC_SPARE_MASK,
+ FIELD_PREP(RTC_SPARE_MASK, *buf++));
+ if (ret)
+ goto out;
+ }
+
+ /*
+ * None of it reaches the always-on domain until the write trigger,
+ * which commits every pending alarm register at once -- so this runs
+ * under the same lock the alarm paths take, rather than landing in
+ * the middle of one of them.
+ */
+ ret = mtk_rtc_write_trigger(rtc);
+out:
+ mutex_unlock(&rtc->lock);
+
+ return ret;
+}
+
static int mtk_rtc_probe(struct platform_device *pdev)
{
struct resource *res;
struct mt6397_chip *mt6397_chip = dev_get_drvdata(pdev->dev.parent);
+ struct nvmem_config nvmem_cfg = {
+ .name = "mt6397_rtc_spare",
+ .word_size = 1,
+ .stride = 1,
+ .size = ARRAY_SIZE(mtk_rtc_spare_reg),
+ .type = NVMEM_TYPE_BATTERY_BACKED,
+ .reg_read = mtk_rtc_nvram_read,
+ .reg_write = mtk_rtc_nvram_write,
+ };
struct mt6397_rtc *rtc;
int ret;

@@ -293,7 +376,13 @@ static int mtk_rtc_probe(struct platform_device *pdev)
rtc->rtc_dev->start_secs = mktime64(1968, 1, 2, 0, 0, 0);
rtc->rtc_dev->set_start_time = true;

- return devm_rtc_register_device(rtc->rtc_dev);
+ ret = devm_rtc_register_device(rtc->rtc_dev);
+ if (ret)
+ return ret;
+
+ nvmem_cfg.priv = rtc;
+
+ return devm_rtc_nvmem_register(rtc->rtc_dev, &nvmem_cfg);
}

#ifdef CONFIG_PM_SLEEP
diff --git a/include/linux/mfd/mt6397/rtc.h b/include/linux/mfd/mt6397/rtc.h
index 27883af44f87..f4da579ec638 100644
--- a/include/linux/mfd/mt6397/rtc.h
+++ b/include/linux/mfd/mt6397/rtc.h
@@ -49,6 +49,13 @@

#define RTC_AL_SEC 0x0018

+/* The high byte of four of the alarms is spare, always-on storage */
+#define RTC_AL_HOU 0x001c
+#define RTC_AL_DOM 0x001e
+#define RTC_AL_DOW 0x0020
+#define RTC_AL_MTH 0x0022
+#define RTC_SPARE_MASK GENMASK(15, 8)
+
#define RTC_AL_SEC_MASK 0x003f
#define RTC_AL_MIN_MASK 0x003f
#define RTC_AL_HOU_MASK 0x001f

--
2.55.0