Re: [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage

From: Faruque Ansari

Date: Wed Sep 23 2026 - 07:15:53 EST


Hi Matti Vaittinen,

On 22-Sep-26 2:33 PM, Matti Vaittinen wrote:
On 21/09/2026 20:44, Faruque Ansari wrote:
Introduce the Power State Change Reasons Recording (PSCRR) framework: a
generic mechanism to record why the last power state change (shutdown or
reboot) happened - under-voltage, thermal, watchdog, software- triggered,
etc. - so a postmortem reason survives a reboot or an abrupt power loss.

PSCRR is built around providers. A provider is either a hardware reason
source (a PMIC, SoC reset controller or watchdog exposing a reset cause)
or a recorder that persists the reason across a power cycle (an NVMEM or
RTC scratch cell). Each provider gets a directory under
/sys/kernel/pscrr/providerN/ exposing its name, backing device, the set
of observed reasons (as tokens), its capabilities, the reasons it
supports and - for recorders - a record policy. The reason set is
deliberately not collapsed to a single winning cause, since resets are
often multi-causal.

Reasons are the numeric enum psc_reason values from reboot.h, shared with
the POWER_ON_REASON_* vocabulary, so they store compactly in small
battery-backed cells. The current reason (get/set_psc_reason(), set by
the thermal/regulator/hw_protection paths) is written to every recorder
from the reboot notifier.

Co-developed-by: Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>
Signed-off-by: Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>
Reported-by: Francesco Valla <francesco@xxxxxxxx>
Closes: https://lore.kernel.org/ all/178749889134.204450.5740187753642158305.b4-review@b4/
Signed-off-by: Faruque Ansari <faruque.ansari@xxxxxxxxxxxxxxxx>

changes v13:
- Narrow pscrr_lock usage in pscrr_provider_register() to only protect
   pscrr_dirs list updates.
- Check pscrr_root before allocation and limit locking around
   ida_alloc() and kobject/sysfs setup.
changes v12:
- rework into a multi-provider framework: one sysfs directory per
   provider instead of a single backend
- add per-provider capabilities, supported_reasons and record policy
- register providers through a const ops table via
   (devm_)pscrr_provider_register()
- report reasons as parsable tokens
- move the sources under drivers/power/reset/pscrr/
- add a MAINTAINERS entry; PSCRR depends on POWER_RESET
- drop Reviewed-by: Matti Vaittinen; the framework was substantially
   reworked since it was given
changes v10:
- make g_pscrr static
changes v8:
- introduce struct pscrr_core to encapsulate backend and locking
- replace global mutex and backend pointer with centralized pscrr_core
- use DEFINE_GUARD() + guard(g_pscrr) for scoped mutex locking
- simplify code using local backend pointer after locking
- prepare code structure for future multi-backend support
changes v7:
- make write_reason optional
- update documentation
changes v6:
- move enum pscr_reason to kernel reboot core
- move reason storage to reboot core
- add locking
---
  MAINTAINERS                        |  11 +
  drivers/power/reset/Kconfig        |   2 +
  drivers/power/reset/Makefile       |   1 +
  drivers/power/reset/pscrr/Kconfig  |  33 ++
  drivers/power/reset/pscrr/Makefile |   2 +
  drivers/power/reset/pscrr/pscrr.c  | 633 ++++++++++++++++++++ +++++++++++++++++
  include/linux/pscrr.h              | 107 +++++++
  7 files changed, 789 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 05215fcc18a5..a7145dbb6eb7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21967,6 +21967,17 @@ F:    Documentation/devicetree/ bindings/connector/pcie-m2-e-connector.yaml
  F:    Documentation/devicetree/bindings/connector/pcie-m2-m- connector.yaml
  F:    drivers/power/sequencing/pwrseq-pcie-m2.c
+POWER STATE CHANGE REASON RECORDING (PSCRR)
+M:    Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>
+M:    Faruque Ansari <faruque.ansari@xxxxxxxxxxxxxxxx>
+R:    Pengutronix Kernel Team <kernel@xxxxxxxxxxxxxx>
+L:    linux-pm@xxxxxxxxxxxxxxx
+S:    Maintained
+F:    Documentation/ABI/testing/sysfs-kernel-pscrr
+F:    drivers/power/reset/pscrr/
+F:    include/linux/pscrr.h
+F:    tools/testing/selftests/pscrr/
+
  POWER STATE COORDINATION INTERFACE (PSCI)
  M:    Mark Rutland <mark.rutland@xxxxxxx>
  M:    Lorenzo Pieralisi <lpieralisi@xxxxxxxxxx>
diff --git a/drivers/power/reset/Kconfig b/drivers/power/reset/ Kconfig
index bce996bbef28..f24ff89dd94e 100644
--- a/drivers/power/reset/Kconfig
+++ b/drivers/power/reset/Kconfig
@@ -377,3 +377,5 @@ config POWER_RESET_QEMU_VIRT_CTRL
        provides this controller, such as the m68k virt machine.
  endif
+
+source "drivers/power/reset/pscrr/Kconfig"
diff --git a/drivers/power/reset/Makefile b/drivers/power/ reset/Makefile
index e31cab4ba78e..c73dc747e36c 100644
--- a/drivers/power/reset/Makefile
+++ b/drivers/power/reset/Makefile
@@ -37,6 +37,7 @@ obj-$(CONFIG_POWER_RESET_KEYSTONE) += keystone-reset.o
  obj-$(CONFIG_POWER_RESET_SYSCON) += syscon-reboot.o
  obj-$(CONFIG_POWER_RESET_SYSCON_POWEROFF) += syscon-poweroff.o
  obj-$(CONFIG_POWER_RESET_RMOBILE) += rmobile-reset.o
+obj-$(CONFIG_PSCRR) += pscrr/
  obj-$(CONFIG_REBOOT_MODE) += reboot-mode.o
  obj-$(CONFIG_SYSCON_REBOOT_MODE) += syscon-reboot-mode.o
  obj-$(CONFIG_POWER_RESET_SC27XX) += sc27xx-poweroff.o
diff --git a/drivers/power/reset/pscrr/Kconfig b/drivers/power/ reset/pscrr/Kconfig
new file mode 100644
index 000000000000..72de82731b53
--- /dev/null
+++ b/drivers/power/reset/pscrr/Kconfig
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: GPL-2.0-only
+menuconfig PSCRR
+    bool "Power State Change Reasons Recording (PSCRR) Framework"
+    depends on POWER_RESET
+    help
+      Enables the Power State Change Reasons Recording (PSCRR) framework.
+
+      PSCRR records why the system last shut down or rebooted into
+      non-volatile storage, so the reason survives the reset and can be
+      read by the bootloader or early user space on the next boot. Reasons
+      come from software (thermal or regulator hardware- protection events,
+      a watchdog pretimeout, a kernel panic, a controlled reboot) or from
+      hardware reset-cause registers (PMIC, SoC reset controller, watchdog).
+
+      It helps whenever the real cause would otherwise be lost or ambiguous
+      on the next boot, for example:
+
+      - the reset is abrupt and block storage (eMMC/NAND) may not survive
+        it; on systems that can detect the event in time - such as an
+        imminent power loss with a short backup-capacitor window - the
+        reason can still be persisted first;
+
+      - the hardware does not preserve the cause and the boot looks like a
+        plain power-on reset, e.g. a watchdog pretimeout that escalates
+        into a panic reboot would otherwise be indistinguishable from a
+        POR.
+
+      The framework is extensible and exposes software-defined and
+      hardware-reported reasons through one interface. Sudden power cuts,
+      CPU freezes or other uncontrolled resets may still go unrecorded
+      unless hardware provides the reset cause.
+
+      If unsure, say N.
diff --git a/drivers/power/reset/pscrr/Makefile b/drivers/ power/reset/pscrr/Makefile
new file mode 100644
index 000000000000..e5530a858971
--- /dev/null
+++ b/drivers/power/reset/pscrr/Makefile
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_PSCRR) += pscrr.o
diff --git a/drivers/power/reset/pscrr/pscrr.c b/drivers/power/ reset/pscrr/pscrr.c
new file mode 100644
index 000000000000..ab0974d0423e
--- /dev/null
+++ b/drivers/power/reset/pscrr/pscrr.c
@@ -0,0 +1,633 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pscrr.c - Core Power State Change Reason Recording
+ *
+ * PSCRR records why the last power state change (shutdown/ reboot) happened.
+ * Reasons come from providers: hardware reset-cause registers (PMIC, SoC reset
+ * controller, watchdog), persistent recorders (NVMEM/RTC scratch), the
+ * bootloader's device-tree /chosen/reset-source (a built-in provider here), or
+ * test stubs. Each provider gets a directory under /sys/ kernel/pscrr/ and reports
+ * the full set of reasons it observed - the picture is deliberately not
+ * collapsed to a single "winning" cause, since resets are often multi-causal.
+ *
+ * Sysfs (per provider, under /sys/kernel/pscrr/providerN/):
+ *   name               ro  human label of the provider
+ *   device             symlink to the backing device (if any)
+ *   reason             the provider's reason set, as tokens; writable (record
+ *                      one reason) when the provider supports it
+ *   caps               ro  non-default capabilities ("writable"); empty for a
+ *                      read-only, single-slot provider
+ *   supported_reasons  ro  reasons this provider can report or record
+ *   record_policy      recorders only: keep the "first" or "last" reason
+ *                      recorded in a power cycle
+ *
+ * The kernel keeps the first (root cause) and last power- state-change reason
+ * (get_psc_first_reason()/get_psc_reason(), set by the thermal, regulator and
+ * hw_protection paths). At reboot each recorder is given the first or the last
+ * reason according to its record policy.
+ *
+ * Copyright (C) 2025 Pengutronix, Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>
+ */
+
+#define pr_fmt(fmt) "PSCRR: " fmt
+
+#include <linux/bitmap.h>
+#include <linux/device.h>
+#include <linux/idr.h>
+#include <linux/kernel.h>
+#include <linux/kobject.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/notifier.h>
+#include <linux/of.h>
+#include <linux/pscrr.h>
+#include <linux/reboot.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/sysfs.h>
+
+static struct kobject *pscrr_root;
+static DEFINE_MUTEX(pscrr_lock);
+static LIST_HEAD(pscrr_dirs);
+static DEFINE_IDA(pscrr_ida);
+
+/*
+ * Record policy: when several reasons are recorded in one power cycle, keep the
+ * first (root cause) or overwrite with the last. Global, tunable via sysfs.
+ */
+enum pscrr_record_policy {
+    PSCRR_RECORD_FIRST,
+    PSCRR_RECORD_LAST,
+};
+
+/*
+ * Per-provider sysfs directory. Core-owned and self-freeing on kobject_put(),
+ * so its lifetime is decoupled from the caller-owned struct pscrr_provider.

nit: Seeing this comment and then seeing the seemingly unprotected:
to_pscrr_dir(kobj)->provider->XXX in sysfs operations directly below, required me to go through the tear-down stuff. Is it possible to add a word as to why the direct access to provider pointer is still Ok, so others don't have to do the same :)


Thanks for the review.
I agree with the feedback and will update the comments in the next version.

+ */
+struct pscrr_provider_dir {
+    struct kobject kobj;
+    struct pscrr_provider *provider;
+    struct list_head node;
+    int id;
+    enum pscrr_record_policy policy;    /* single-slot recorder: keep first/last */
+    bool recorded;        /* a reason was recorded this power cycle */
+};
+
+static inline struct pscrr_provider_dir *to_pscrr_dir(struct kobject *kobj)
+{
+    return container_of(kobj, struct pscrr_provider_dir, kobj);
+}
+
+/ *----------------------------------------------------------------------*/
+/* Per-provider attributes */
+/ *----------------------------------------------------------------------*/
+
+static ssize_t name_show(struct kobject *kobj, struct kobj_attribute *attr,
+             char *buf)
+{
+    return sysfs_emit(buf, "%s\n", to_pscrr_dir(kobj)- >provider->name);
+}
+
+static struct kobj_attribute pscrr_name_attr = __ATTR_RO(name);
+
+static int pscrr_parse_reason(const char *buf, enum psc_reason *out)
+{
+    unsigned int val;
+
+    /* Accept either a decimal index or a reason token. */
+    if (!kstrtouint(buf, 0, &val)) {
+        if (val >= PSCR_REASON_COUNT)
+            return -ERANGE;
+        *out = val;
+        return 0;
+    }
+
+    return psc_reason_from_token(buf, out);
+}
+
+static ssize_t reason_show(struct kobject *kobj, struct kobj_attribute *attr,
+               char *buf)
+{
+    struct pscrr_provider *p = to_pscrr_dir(kobj)->provider;
+    DECLARE_BITMAP(reasons, PSCR_REASON_COUNT);
+    ssize_t len = 0;
+    int bit, ret;
+
+    bitmap_zero(reasons, PSCR_REASON_COUNT);
+
+    ret = p->ops->read_reasons(p, reasons);
+    if (ret)
+        return ret;
+
+    for_each_set_bit(bit, reasons, PSCR_REASON_COUNT)
+        len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "",
+                     psc_reason_to_token(bit));
+    len += sysfs_emit_at(buf, len, "\n");
+
+    return len;
+}
+
+/* Record @reason into @dir honouring the global record policy. */
+static int pscrr_do_record(struct pscrr_provider_dir *dir, enum psc_reason reason)
+{
+    struct pscrr_provider *p = dir->provider;
+    int ret;
+
+    if (!p->ops->write_reason)
+        return -EPERM;

nit: To me this is more of -EOPNOTSUPP, see below.

+
+    /*
+     * PSCR_UNKNOWN clears the slot and releases the latch, regardless of
+     * policy, so a reason recorded afterwards is taken again.
+     */
+    if (reason == PSCR_UNKNOWN) {
+        ret = p->ops->write_reason(p, reason);
+        if (ret)
+            return ret;
+
+        dir->recorded = false;
+        return 0;
+    }
+
+    /* Reject reasons the provider does not advertise (NULL == all). */
+    if (p->supported_reasons && !test_bit(reason, p- >supported_reasons))
+        return -EOPNOTSUPP;

nit: I'd use -EINVAL here, see below.

+
+    /* "first" policy: keep the first reason recorded this power cycle. */
+    if (dir->policy == PSCRR_RECORD_FIRST && dir->recorded)
+        return 0;
+
+    ret = p->ops->write_reason(p, reason);
+    if (ret)
+        return ret;
+
+    dir->recorded = true;
+
+    return 0;
+}
+
+static ssize_t reason_store(struct kobject *kobj, struct kobj_attribute *attr,
+                const char *buf, size_t count)
+{
+    struct pscrr_provider_dir *dir = to_pscrr_dir(kobj);
+    enum psc_reason reason;
+    int ret;
+
+    if (!dir->provider->ops->write_reason)
+        return -EPERM;

nit: Why not the "not supported" instead? EPERM would make me to doubt the user privileges, not capablility of the backing device. (Oh, I just noticed the -EOPNOTSUPP when specific reason is not supported - I'd say this is more of -EOPNOTSUPP, while giving unsupported reason as argument is more of -EINVAL to me).


Good catch, thanks for pointing that out. I agree. I'll address this in the next version.

Thanks,
Faruque Ansari

+
+    ret = pscrr_parse_reason(buf, &reason);
+    if (ret)
+        return ret;
+
+    /* Serialise the record state against concurrent stores and the notifier. */
+    scoped_guard(mutex, &pscrr_lock)
+        ret = pscrr_do_record(dir, reason);
+    if (ret)
+        return ret;
+
+    return count;
+}
+
+static struct kobj_attribute pscrr_reason_attr =
+    __ATTR(reason, 0644, reason_show, reason_store);
+
+static ssize_t caps_show(struct kobject *kobj, struct kobj_attribute *attr,
+             char *buf)
+{
+    struct pscrr_provider *p = to_pscrr_dir(kobj)->provider;
+    ssize_t len = 0;
+
+    /* Readable and single-slot are the defaults and not listed. */
+    if (p->ops->write_reason)
+        len += sysfs_emit_at(buf, len, "writable");
+
+    return len + sysfs_emit_at(buf, len, "\n");
+}
+
+static struct kobj_attribute pscrr_caps_attr = __ATTR_RO(caps);
+
+static ssize_t supported_reasons_show(struct kobject *kobj,
+                      struct kobj_attribute *attr, char *buf)
+{
+    struct pscrr_provider *p = to_pscrr_dir(kobj)->provider;
+    const unsigned long *sup = p->supported_reasons;
+    ssize_t len = 0;
+    int i;
+
+    for (i = 0; i < PSCR_REASON_COUNT; i++) {
+        if (sup && !test_bit(i, sup))    /* NULL means all */
+            continue;
+        len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "",
+                     psc_reason_to_token(i));
+    }
+
+    return len + sysfs_emit_at(buf, len, "\n");
+}
+
+static struct kobj_attribute pscrr_supported_attr =
+    __ATTR(supported_reasons, 0444, supported_reasons_show, NULL);
+
+static ssize_t record_policy_show(struct kobject *kobj,
+                  struct kobj_attribute *attr, char *buf)
+{
+    struct pscrr_provider_dir *dir = to_pscrr_dir(kobj);
+
+    return sysfs_emit(buf, "%s\n",
+              READ_ONCE(dir->policy) == PSCRR_RECORD_FIRST ?
+              "first" : "last");
+}
+
+static ssize_t record_policy_store(struct kobject *kobj,
+                   struct kobj_attribute *attr,
+                   const char *buf, size_t count)
+{
+    struct pscrr_provider_dir *dir = to_pscrr_dir(kobj);
+
+    guard(mutex)(&pscrr_lock);
+
+    if (sysfs_streq(buf, "first"))
+        WRITE_ONCE(dir->policy, PSCRR_RECORD_FIRST);
+    else if (sysfs_streq(buf, "last"))
+        WRITE_ONCE(dir->policy, PSCRR_RECORD_LAST);
+    else
+        return -EINVAL;
+
+    return count;
+}
+
+static struct kobj_attribute pscrr_record_policy_attr =
+    __ATTR(record_policy, 0644, record_policy_show, record_policy_store);
+
+static struct attribute *pscrr_dir_attrs[] = {
+    &pscrr_name_attr.attr,
+    &pscrr_reason_attr.attr,
+    &pscrr_caps_attr.attr,
+    &pscrr_supported_attr.attr,
+    &pscrr_record_policy_attr.attr,
+    NULL,
+};
+
+static umode_t pscrr_dir_is_visible(struct kobject *kobj, struct attribute *attr,
+                    int n)
+{
+    struct pscrr_provider *p = to_pscrr_dir(kobj)->provider;
+
+    /* A provider without write_reason() exposes reason read- only. */
+    if (attr == &pscrr_reason_attr.attr && !p->ops->write_reason)
+        return 0444;
+
+    /* record_policy only applies to a (single-slot) recorder. */
+    if (attr == &pscrr_record_policy_attr.attr && !p->ops- >write_reason)
+        return 0;
+
+    return attr->mode;
+}
+
+static const struct attribute_group pscrr_dir_group = {
+    .attrs        = pscrr_dir_attrs,
+    .is_visible    = pscrr_dir_is_visible,
+};
+
+static const struct attribute_group *pscrr_dir_groups[] = {
+    &pscrr_dir_group,
+    NULL,
+};
+
+static void pscrr_dir_release(struct kobject *kobj)
+{
+    kfree(to_pscrr_dir(kobj));
+}
+
+static const struct kobj_type pscrr_dir_ktype = {
+    .sysfs_ops    = &kobj_sysfs_ops,
+    .release    = pscrr_dir_release,
+    .default_groups    = pscrr_dir_groups,
+};
+
+/ *----------------------------------------------------------------------*/
+/* Provider registration */
+/ *----------------------------------------------------------------------*/
+
+/**
+ * pscrr_provider_register - register a power state change reason provider
+ * @p: caller-owned provider description
+ *
+ * Creates /sys/kernel/pscrr/providerN/ with "name" and "reason" attributes
+ * and, when @p->dev is set, a "device" symlink. @p->reason is writable when
+ * @p provides write_reason(). The provider must outlive the matching
+ * pscrr_provider_unregister() call.
+ *
+ * Return: 0 on success or a negative errno.
+ */
+int pscrr_provider_register(struct pscrr_provider *p)
+{
+    struct pscrr_provider_dir *dir;
+    int ret;
+
+    if (!p || !p->name || !p->ops || !p->ops->read_reasons)
+        return -EINVAL;
+
+    /*
+     * pscrr_root is set once at core init and cleared at core exit;
+     * neither transition is serialised against this function by
+     * pscrr_lock, so check it up front rather than under the lock below.
+     */
+    if (!pscrr_root)
+        return -ENODEV;
+
+    dir = kzalloc_obj(*dir);
+    if (!dir)
+        return -ENOMEM;
+
+    dir->provider = p;
+    dir->policy = PSCRR_RECORD_FIRST;
+
+    /* ida_alloc() and the kobject/sysfs calls below are individually thread-safe. */
+    dir->id = ida_alloc(&pscrr_ida, GFP_KERNEL);
+    if (dir->id < 0) {
+        ret = dir->id;
+        kfree(dir);
+        return ret;
+    }
+
+    ret = kobject_init_and_add(&dir->kobj, &pscrr_dir_ktype, pscrr_root,
+                   "provider%d", dir->id);
+    if (ret) {
+        /*
+         * kobject_init_and_add() failed: per its contract only
+         * kobject_put() may follow, no kobject_del().
+         */
+        ida_free(&pscrr_ida, dir->id);
+        kobject_put(&dir->kobj);
+        return ret;
+    }
+
+    if (p->dev) {
+        ret = sysfs_create_link(&dir->kobj, &p->dev->kobj, "device");
+        if (ret)
+            goto err_del;
+    }
+
+    /* pscrr_lock only serialises pscrr_dirs against concurrent (un)registration. */
+    scoped_guard(mutex, &pscrr_lock)
+        list_add_tail(&dir->node, &pscrr_dirs);
+
+    return 0;
+
+err_del:
+    kobject_del(&dir->kobj);
+    ida_free(&pscrr_ida, dir->id);
+    kobject_put(&dir->kobj);
+    return ret;
+}
+EXPORT_SYMBOL_GPL(pscrr_provider_register);
+
+/**
+ * pscrr_provider_unregister - remove a previously registered provider
+ * @p: the provider passed to pscrr_provider_register()
+ */
+void pscrr_provider_unregister(struct pscrr_provider *p)
+{
+    struct pscrr_provider_dir *dir = NULL, *iter;
+
+    scoped_guard(mutex, &pscrr_lock) {
+        list_for_each_entry(iter, &pscrr_dirs, node) {
+            if (iter->provider == p) {
+                dir = iter;
+                list_del(&dir->node);
+                break;
+            }
+        }
+    }
+
+    if (!dir)
+        return;
+
+    /*
+     * Tear the sysfs directory down outside pscrr_lock: kobject_del()
+     * drains in-flight reason/record_policy stores, which take pscrr_lock,
+     * so holding it here would deadlock. Release the id only once the
+     * directory is gone, so a concurrent register cannot reuse it and
+     * collide on the providerN name.
+     */
+    if (p->dev)
+        sysfs_remove_link(&dir->kobj, "device");
+    kobject_del(&dir->kobj);
+    ida_free(&pscrr_ida, dir->id);
+    kobject_put(&dir->kobj);
+}
+EXPORT_SYMBOL_GPL(pscrr_provider_unregister);
+
+static void pscrr_provider_devm_release(void *p)
+{
+    pscrr_provider_unregister(p);
+}
+
+/**
+ * devm_pscrr_provider_register - device-managed reason provider registration
+ * @dev: device the provider belongs to; also backs the "device" symlink
+ * @name: human-readable provider label
+ * @ops: provider callback table; read_reasons() is required, write_reason() is
+ *     optional and makes the provider a recorder
+ * @supported_reasons: bitmap of the reasons the provider supports, or NULL
+ *     for all; set before the provider is exposed in sysfs
+ * @priv: provider private data, handed back to the @ops callbacks
+ *
+ * Allocates and registers a struct pscrr_provider and schedules its
+ * unregistration when @dev is unbound, so the caller keeps no reference to it.
+ *
+ * Return: the registered provider on success or an ERR_PTR() on failure. When
+ * CONFIG_PSCRR is disabled the call resolves to a stub returning NULL, so
+ * callers need no IS_ENABLED() guard.
+ */
+struct pscrr_provider *
+devm_pscrr_provider_register(struct device *dev, const char *name,
+                 const struct pscrr_provider_ops *ops,
+                 const unsigned long *supported_reasons, void *priv)
+{
+    struct pscrr_provider *p;
+    int ret;
+
+    p = devm_kzalloc(dev, sizeof(*p), GFP_KERNEL);
+    if (!p)
+        return ERR_PTR(-ENOMEM);
+
+    p->name = name;
+    p->dev = dev;
+    p->ops = ops;
+    p->supported_reasons = supported_reasons;
+    p->priv = priv;
+
+    ret = pscrr_provider_register(p);
+    if (ret)
+        return ERR_PTR(ret);
+
+    ret = devm_add_action_or_reset(dev, pscrr_provider_devm_release, p);
+    if (ret)
+        return ERR_PTR(ret);
+
+    return p;
+}
+EXPORT_SYMBOL_GPL(devm_pscrr_provider_register);
+
+/ *----------------------------------------------------------------------*/
+/* Record path: reboot notifier writes the current reason to recorders */
+/ *----------------------------------------------------------------------*/
+
+/*
+ * Record the current power-state-change reason into every provider, giving
+ * each the first (root cause) or the last reason according to its record
+ * policy. The kernel keeps both, so "first" is meaningful even when later
+ * events overwrite the last reason. The caller holds pscrr_lock, or runs where
+ * the provider list is stable (panic).
+ */
+static void pscrr_record_current(void)
+{
+    enum psc_reason first = get_psc_first_reason();
+    enum psc_reason last = get_psc_reason();
+    struct pscrr_provider_dir *dir;
+
+    list_for_each_entry(dir, &pscrr_dirs, node)
+        pscrr_do_record(dir, dir->policy == PSCRR_RECORD_FIRST ?
+                         first : last);
+}
+
+static int pscrr_reboot_notifier(struct notifier_block *nb,
+                 unsigned long action, void *unused)
+{
+    guard(mutex)(&pscrr_lock);
+
+    /*
+     * A reboot, halt or power-off that reaches here with no more specific
+     * reason is software-initiated by definition. Record it as such rather
+     * than leaving it unattributed; a real cause set earlier (thermal,
+     * under-voltage, ...) is already latched and left untouched.
+     */
+    if (get_psc_reason() == PSCR_UNKNOWN)
+        set_psc_reason(PSCR_SOFTWARE);
+
+    pscrr_record_current();
+
+    return NOTIFY_DONE;
+}
+
+static struct notifier_block pscrr_reboot_nb = {
+    .notifier_call = pscrr_reboot_notifier,
+};
+
+/ *----------------------------------------------------------------------*/
+/* Built-in provider: device-tree /chosen/reset- source                  */
+/ *----------------------------------------------------------------------*/
+
+/*
+ * Bootloaders such as barebox record the SoC reset cause in the standard
+ * device-tree /chosen/reset-source property. When it is present, surface it as
+ * a read-only, device-less provider so the bootloader's view of the last reset
+ * shows up next to any hardware or software providers - the framework just
+ * reads the property already there, with no dedicated node or new binding.
+ */
+static const struct {
+    const char *name;
+    enum psc_reason reason;
+} pscrr_reset_source_map[] = {
+    { "POR",      PSCR_POWER_ON },
+    { "RST",      PSCR_SOFTWARE },
+    { "WDG",      PSCR_WATCHDOG },
+    { "THERM",    PSCR_OVER_TEMPERATURE },
+    { "EXT",      PSCR_EXTERNAL },
+    { "BROWNOUT", PSCR_UNDER_VOLTAGE },
+};
+
+static const unsigned long
+pscrr_reset_source_supported[BITS_TO_LONGS(PSCR_REASON_COUNT)] = {
+    BIT(PSCR_UNDER_VOLTAGE) | BIT(PSCR_OVER_TEMPERATURE) |
+    BIT(PSCR_POWER_ON) | BIT(PSCR_WATCHDOG) | BIT(PSCR_SOFTWARE) |
+    BIT(PSCR_EXTERNAL),
+};
+
+/* Parsed once at init; read back by the provider's read_reasons(). */
+static enum psc_reason pscrr_reset_source_reason = PSCR_UNKNOWN;
+
+static int pscrr_reset_source_read(struct pscrr_provider *p,
+                   unsigned long *reasons)
+{
+    set_bit(pscrr_reset_source_reason, reasons);
+
+    return 0;
+}
+
+static const struct pscrr_provider_ops pscrr_reset_source_ops = {
+    .read_reasons = pscrr_reset_source_read,
+};
+
+static struct pscrr_provider pscrr_reset_source_provider = {
+    .name            = "reset-source",
+    .ops            = &pscrr_reset_source_ops,
+    .supported_reasons    = pscrr_reset_source_supported,
+};
+
+static void __init pscrr_register_reset_source(void)
+{
+    const char *name;
+    int i, ret;
+
+    if (!IS_ENABLED(CONFIG_OF) || !of_chosen)
+        return;
+
+    if (of_property_read_string(of_chosen, "reset-source", &name))
+        return;
+
+    for (i = 0; i < ARRAY_SIZE(pscrr_reset_source_map); i++)
+        if (!strcmp(name, pscrr_reset_source_map[i].name)) {
+            pscrr_reset_source_reason = pscrr_reset_source_map[i].reason;
+            break;
+        }
+
+    ret = pscrr_provider_register(&pscrr_reset_source_provider);
+    if (ret)
+        pr_warn("failed to register the reset-source provider: %d\n", ret);
+}
+
+/ *----------------------------------------------------------------------*/
+/* Module init/exit */
+/ *----------------------------------------------------------------------*/
+
+static int __init pscrr_core_init(void)
+{
+    int ret;
+
+    pscrr_root = kobject_create_and_add("pscrr", kernel_kobj);
+    if (!pscrr_root)
+        return -ENOMEM;
+
+    ret = register_reboot_notifier(&pscrr_reboot_nb);
+    if (ret) {
+        kobject_put(pscrr_root);
+        pscrr_root = NULL;
+        return ret;
+    }
+
+    pscrr_register_reset_source();
+
+    return 0;
+}
+
+static void __exit pscrr_core_exit(void)
+{
+    pscrr_provider_unregister(&pscrr_reset_source_provider);
+    unregister_reboot_notifier(&pscrr_reboot_nb);
+    kobject_put(pscrr_root);
+    pscrr_root = NULL;
+    ida_destroy(&pscrr_ida);
+}
+
+/* Bring the core up before device drivers probe and register providers. */
+subsys_initcall(pscrr_core_init);
+module_exit(pscrr_core_exit);
+
+MODULE_AUTHOR("Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>");
+MODULE_AUTHOR("Faruque Ansari <faruque.ansari@xxxxxxxxxxxxxxxx>");
+MODULE_DESCRIPTION("Power State Change Reason Recording (PSCRR) core");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/pscrr.h b/include/linux/pscrr.h
new file mode 100644
index 000000000000..330d6ac191bd
--- /dev/null
+++ b/include/linux/pscrr.h
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * pscrr.h - Public header for Power State Change Reason Recording (PSCRR).
+ *
+ * Copyright (C) 2025 Pengutronix, Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>
+ */
+
+#ifndef __PSCRR_H__
+#define __PSCRR_H__
+
+#include <linux/reboot.h>
+
+struct device;
+struct pscrr_provider;
+
+/**
+ * struct pscrr_provider_ops - Provider callbacks.
+ * @read_reasons: Fill @reasons with the complete set this source observed.
+ *        @reasons is a bitmap of PSCR_REASON_COUNT bits; set members
+ *        with set_bit(PSCR_x, reasons). Return 0 on success or a
+ *        negative errno. Required. Driver state is reached via p->priv.
+ * @write_reason: Persist @reason. Called from the reboot notifier for
+ *        recorder-capable providers so the cause survives the power
+ *        cycle. Leave NULL for read-only hardware sources.
+ *
+ * A const ops table per provider type; extend it with new callbacks without
+ * touching the registration signature or existing callers.
+ */
+struct pscrr_provider_ops {
+    int (*read_reasons)(struct pscrr_provider *p, unsigned long *reasons);
+    int (*write_reason)(struct pscrr_provider *p, enum psc_reason reason);
+};
+
+/**
+ * struct pscrr_provider - A source (and optionally recorder) of power state
+ *               change reasons.
+ *
+ * A provider represents one place the system can learn *why* the last power
+ * state change happened: a hardware reset-cause register (PMIC, SoC SRC,
+ * watchdog), a persistent recorder (NVMEM/RTC scratch), or a test stub. Each
+ * registered provider gets its own directory under /sys/ kernel/pscrr/, so the
+ * full, un-prioritised picture is visible: several providers - and several
+ * reasons within one provider - can be reported simultaneously.
+ *
+ * @name:    Human-readable label, exported as the "name" attribute. The
+ *        directory itself is core-indexed (providerN), so this need not
+ *        be unique. Required.
+ * @dev:    Backing device. When set it is exported as the "device" symlink
+ *        in the provider directory, tying the reason to real hardware.
+ *        May be NULL (e.g. for a test provider).
+ * @ops:    Provider callbacks. Required.
+ * @supported_reasons: Bitmap of the reasons this provider can store or report,
+ *        limited e.g. by the storage size. 0 means all reasons.
+ * @priv:    Provider private data, passed back through the callbacks.
+ *
+ * Providers are readable and single-slot by default; only capabilities beyond
+ * that (currently: writable) are advertised.
+ */
+struct pscrr_provider {
+    const char *name;
+    struct device *dev;
+    const struct pscrr_provider_ops *ops;
+    const unsigned long *supported_reasons;
+    void *priv;
+};
+
+#if IS_ENABLED(CONFIG_PSCRR)
+int pscrr_provider_register(struct pscrr_provider *p);
+void pscrr_provider_unregister(struct pscrr_provider *p);
+
+/**
+ * devm_pscrr_provider_register - allocate, fill and register a provider
+ * @dev: device the provider belongs to (also the "device" symlink target)
+ * @name: provider label
+ * @ops: provider callbacks
+ * @supported_reasons: bitmap of supported reasons, or NULL for all
+ * @priv: driver state passed back through the callbacks
+ *
+ * The provider is unregistered automatically on device teardown.
+ *
+ * Return: the provider on success, ERR_PTR() on failure, or NULL when PSCRR
+ * is not built (so the caller need not guard the call).
+ */
+struct pscrr_provider *
+devm_pscrr_provider_register(struct device *dev, const char *name,
+                 const struct pscrr_provider_ops *ops,
+                 const unsigned long *supported_reasons, void *priv);

I really like the stuff in this header! After a glance, this looks like something even I might be able to add out PMIC drivers! Nice!

Reviewed-by: Matti Vaittinen <mazziesaccount@xxxxxxxxx>

+#else
+static inline int pscrr_provider_register(struct pscrr_provider *p)
+{
+    return -EOPNOTSUPP;
+}
+
+static inline void pscrr_provider_unregister(struct pscrr_provider *p)
+{
+}
+
+static inline struct pscrr_provider *
+devm_pscrr_provider_register(struct device *dev, const char *name,
+                 const struct pscrr_provider_ops *ops,
+                 const unsigned long *supported_reasons, void *priv)
+{
+    return NULL;
+}
+#endif
+
+#endif /* __PSCRR_H__ */