mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Matti Vaittinen <mazziesaccount@gmail.com>
To: Faruque Ansari <faruque.ansari@oss.qualcomm.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: Tue, 22 Sep 2026 12:03:10 +0300	[thread overview]
Message-ID: <05f38c18-812c-4fbc-9e79-69fd3b78ba3d@gmail.com> (raw)
In-Reply-To: <20260921-pscrr-framework-v14-5-a16290003b7c@oss.qualcomm.com>

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 :)

> + */
> +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).

> +
> +	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__ */
> 


-- 
Matti Vaittinen
Linux kernel developer at ROHM Semiconductors
Oulu Finland

~~ When things go utterly wrong vim users can always type :help! ~~

  reply	other threads:[~2026-09-22  9:03 UTC|newest]

Thread overview: 25+ 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 [this message]
2026-09-23 11:06     ` Faruque Ansari
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
2026-09-24  7:08 ` [PATCH v15 " Faruque Ansari
2026-09-24  7:08   ` [PATCH v15 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework Faruque Ansari
2026-09-24  7:08   ` [PATCH v15 2/6] reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings Faruque Ansari
2026-09-24  7:08   ` [PATCH v15 3/6] reboot: add parsable tokens for power state change reasons Faruque Ansari
2026-09-24  7:08   ` [PATCH v15 4/6] reboot: extend psc_reason with power-on and reset causes Faruque Ansari
2026-09-24 12:14     ` Matti Vaittinen
2026-09-24  7:08   ` [PATCH v15 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Faruque Ansari
2026-09-24  7:08   ` [PATCH v15 6/6] Documentation: Add sysfs documentation for PSCRR 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=05f38c18-812c-4fbc-9e79-69fd3b78ba3d@gmail.com \
    --to=mazziesaccount@gmail.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=faruque.ansari@oss.qualcomm.com \
    --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=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®