mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Faruque Ansari <faruque.ansari@oss.qualcomm.com>
To: Matti Vaittinen <mazziesaccount@gmail.com>,
	Sebastian Reichel <sre@kernel.org>,
	Benson Leung <bleung@chromium.org>,
	Tzung-Bi Shih <tzungbi@kernel.org>,
	Guenter Roeck <groeck@chromium.org>,
	Liam Girdwood <lgirdwood@gmail.com>,
	Mark Brown <broonie@kernel.org>,
	"Rafael J. Wysocki" <rafael@kernel.org>,
	Daniel Lezcano <daniel.lezcano@kernel.org>,
	Zhang Rui <rui.zhang@intel.com>,
	Lukasz Luba <lukasz.luba@arm.com>,
	Oleksij Rempel <o.rempel@pengutronix.de>,
	Pengutronix Kernel Team <kernel@pengutronix.de>
Cc: linux-arm-msm@vger.kernel.org, linux-pm@vger.kernel.org,
	linux-kernel@vger.kernel.org, chrome-platform@lists.linux.dev,
	avaneesh.dwivedi@oss.qualcomm.com,
	Umang Chheda <umang.chheda@oss.qualcomm.com>,
	Francesco Valla <francesco@valla.it>
Subject: Re: [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage
Date: Wed, 23 Sep 2026 16:36:47 +0530	[thread overview]
Message-ID: <51e4cbc2-445f-4b18-a0de-64bc62ded1b0@oss.qualcomm.com> (raw)
In-Reply-To: <05f38c18-812c-4fbc-9e79-69fd3b78ba3d@gmail.com>

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@pengutronix.de>
>> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
>> Reported-by: Francesco Valla <francesco@valla.it>
>> Closes: https://lore.kernel.org/ 
>> all/178749889134.204450.5740187753642158305.b4-review@b4/
>> Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>
>>
>> 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@pengutronix.de>
>> +M:    Faruque Ansari <faruque.ansari@oss.qualcomm.com>
>> +R:    Pengutronix Kernel Team <kernel@pengutronix.de>
>> +L:    linux-pm@vger.kernel.org
>> +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@arm.com>
>>   M:    Lorenzo Pieralisi <lpieralisi@kernel.org>
>> 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@pengutronix.de>
>> + */
>> +
>> +#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@pengutronix.de>");
>> +MODULE_AUTHOR("Faruque Ansari 
>> <faruque.ansari@oss.qualcomm.com>");
>> +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@pengutronix.de>
>> + */
>> +
>> +#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@gmail.com>
> 
>> +#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__ */
>>
> 
> 


  reply	other threads:[~2026-09-23 11:06 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-21 17:44 [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
2026-09-21 17:44 ` [PATCH v14 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework Faruque Ansari
2026-09-22  6:02   ` Matti Vaittinen
2026-09-21 17:44 ` [PATCH v14 2/6] reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings Faruque Ansari
2026-09-21 17:44 ` [PATCH v14 3/6] reboot: add parsable tokens for power state change reasons Faruque Ansari
2026-09-22  7:37   ` Matti Vaittinen
2026-09-23 11:05     ` Faruque Ansari
2026-09-21 17:44 ` [PATCH v14 4/6] reboot: extend psc_reason with power-on and reset causes Faruque Ansari
2026-09-22  7:45   ` Matti Vaittinen
2026-09-23 11:05     ` Faruque Ansari
2026-09-21 17:44 ` [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Faruque Ansari
2026-09-22  9:03   ` Matti Vaittinen
2026-09-23 11:06     ` Faruque Ansari [this message]
2026-09-21 17:44 ` [PATCH v14 6/6] Documentation: Add sysfs documentation for PSCRR Faruque Ansari
2026-09-22  9:12   ` Matti Vaittinen
2026-09-22  6:33 ` [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework Bradley Morgan
2026-09-22  6:44   ` Faruque Ansari

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=51e4cbc2-445f-4b18-a0de-64bc62ded1b0@oss.qualcomm.com \
    --to=faruque.ansari@oss.qualcomm.com \
    --cc=avaneesh.dwivedi@oss.qualcomm.com \
    --cc=bleung@chromium.org \
    --cc=broonie@kernel.org \
    --cc=chrome-platform@lists.linux.dev \
    --cc=daniel.lezcano@kernel.org \
    --cc=francesco@valla.it \
    --cc=groeck@chromium.org \
    --cc=kernel@pengutronix.de \
    --cc=lgirdwood@gmail.com \
    --cc=linux-arm-msm@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-pm@vger.kernel.org \
    --cc=lukasz.luba@arm.com \
    --cc=mazziesaccount@gmail.com \
    --cc=o.rempel@pengutronix.de \
    --cc=rafael@kernel.org \
    --cc=rui.zhang@intel.com \
    --cc=sre@kernel.org \
    --cc=tzungbi@kernel.org \
    --cc=umang.chheda@oss.qualcomm.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®