The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework
@ 2026-07-31  9:59 Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework Oleksij Rempel
                   ` (6 more replies)
  0 siblings, 7 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-07-31  9:59 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano
  Cc: Oleksij Rempel, kernel, linux-kernel, Liam Girdwood, Mark Brown,
	Rafael J. Wysocki, Zhang Rui, Lukasz Luba, linux-pm,
	Søren Andersen, Guenter Roeck, Matti Vaittinen, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Faruque Ansari, Francesco Valla,
	Greg Kroah-Hartman, chrome-platform

changes v12:
- Drop all provider drivers and their tests; post the framework alone. The
  NVMEM-cell binding a recorder needs to pick its storage is still
  deadlocked, and several projects already need the framework - so unblock
  the core now and let the providers (NVMEM, PMIC, ...) follow separately.
- Rework into a multi-provider design (per-provider /sys/kernel/pscrr/
  directories); add reason tokens and a built-in /chosen/reset-source
  provider.

changes v11:
- add missing break reported by kernel test robot <lkp@intel.com>

changes v10:
- add some add Reviewed-by tags
- regulator_handle_critical: set pscr = PSCR_UNKNOWN for default case
- make g_pscrr static

changes v9:
- Remove redundant pr_crit() messages before hw_protection_trigger()
- Replace psc_reason_to_str() switch with static const string array
- Mark psc_last_reason as static

changes v8:
- Use DEFINE_GUARD() and guard(g_pscrr) for scoped locking of the global
  pscrr_core struct
- Replace manual mutex_lock/unlock with automatic cleanup-based guard() usage
- Centralize backend and locking state in struct pscrr_core
- Prepare for future multi-backend support with clean encapsulation
- Improve sysfs documentation

changes v7:
- document expected values in sysfs documentation
- make write support optional

changes v6:
- add sysfs documentation
- push core part of the reset reason into kernel/reboot.c

changes v5:
- fix compile with NVMEM=n and potential issues with NVMEM=m

changes v4:
- fix compile with CONFIG_PSCRR=n

changes v3:
- rework to remove devicetree dependencies
- extend NVMEM to search devices and cells by names

changes v2:
- rename the framework from PSCR to PSCRR (last R is for Recorder)
- extend the power-on reason header and reuse it for the detected reason
- rebase on top of v6.8-rc1

Hello all,

A system power-state transition - a shutdown or a reboot - rarely has a
single, unambiguous cause, and no one component sees the whole picture: the
PMIC, the watchdog, the SoC reset registers, the bootloader and software each
only know their own part, so there is no single source of truth.

PSCRR gives these sources one central place to report side by side. Every
provider keeps its own view - several providers, and several reasons within a
provider, can be reported at once and are deliberately not collapsed into a
single "winning" cause (a bit of a minority report). On top of that it adds a
software-backed recorder, so software-detected reasons - under-voltage,
over-temperature, a watchdog pretimeout, a panic, a controlled reboot - are
captured too and survive into the next boot.

The NVMEM-cell binding a recorder needs to nominate its storage is still
deadlocked, and several projects already need the framework, so this v12
posts the framework, its reboot/reason infrastructure and the sysfs
documentation only; the provider drivers (NVMEM, PMIC, ...) and the tests
follow separately once the binding is resolved. One built-in provider,
reporting the bootloader's device-tree /chosen/reset-source, is kept so the
framework is useful on its own without a new binding.

For followup drivers, see:
 https://github.com/olerem/linux-2.6/tree/v7.2/topic/pscr-v12

Oleksij Rempel (6):
  power: Extend power_on_reason.h for upcoming PSCRR framework
  reboot: hw_protection_trigger: use standardized numeric
    shutdown/reboot reasons instead of strings
  reboot: add parsable tokens for power state change reasons
  reboot: extend psc_reason with power-on and reset causes
  power: reset: Introduce PSCR Recording Framework for Non-Volatile
    Storage
  Documentation: Add sysfs documentation for PSCRR

 Documentation/ABI/testing/sysfs-kernel-pscrr | 108 ++++
 MAINTAINERS                                  |  10 +
 drivers/platform/chrome/cros_ec_lpc.c        |   2 +-
 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            | 634 +++++++++++++++++++
 drivers/regulator/core.c                     |  16 +-
 drivers/regulator/irq_helpers.c              |   9 +-
 drivers/thermal/thermal_core.c               |   3 +-
 include/linux/power/power_on_reason.h        |   5 +
 include/linux/pscrr.h                        | 107 ++++
 include/linux/reboot.h                       | 108 +++-
 kernel/reboot.c                              | 165 ++++-
 15 files changed, 1183 insertions(+), 22 deletions(-)
 create mode 100644 Documentation/ABI/testing/sysfs-kernel-pscrr
 create mode 100644 drivers/power/reset/pscrr/Kconfig
 create mode 100644 drivers/power/reset/pscrr/Makefile
 create mode 100644 drivers/power/reset/pscrr/pscrr.c
 create mode 100644 include/linux/pscrr.h

--
2.47.3


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v12 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework
  2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
@ 2026-07-31  9:59 ` Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 2/6] reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings Oleksij Rempel
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-07-31  9:59 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano
  Cc: Oleksij Rempel, Sebastian Reichel, kernel, linux-kernel,
	Liam Girdwood, Mark Brown, Rafael J. Wysocki, Zhang Rui,
	Lukasz Luba, linux-pm, Søren Andersen, Guenter Roeck,
	Matti Vaittinen, Ahmad Fatoum, Andrew Morton, Kees Cook,
	Faruque Ansari, Francesco Valla, Greg Kroah-Hartman,
	chrome-platform

Prepare for the introduction of the Power State Change Reason Recorder
(PSCRR) framework by expanding the power_on_reason.h header. This
extension includes new power-on reasons:
- POWER_ON_REASON_OVER_CURRENT for over-current conditions.
- POWER_ON_REASON_REGULATOR_FAILURE for regulator failures.
- POWER_ON_REASON_OVER_TEMPERATURE for over temperature situations.
- POWER_ON_REASON_EC_PANIC for EC panics.
- POWER_ON_REASON_EXTERNAL for an external reset source.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Reviewed-by: Sebastian Reichel <sebastian.reichel@collabora.com>
---
changes v12:
- add POWER_ON_REASON_EXTERNAL
changes v10:
- add Reviewed-by: Sebastian Reichel ...
changes v6:
- add POWER_ON_REASON_EC_PANIC
- s/POWER_ON_REASON_OVERTEMPERATURE/POWER_ON_REASON_OVER_TEMPERATURE
(cherry picked from commit a6fb08a81864e2ed17cebba9f249af3d44e42c3f)
---
 include/linux/power/power_on_reason.h | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/include/linux/power/power_on_reason.h b/include/linux/power/power_on_reason.h
index 95a1ec0c403c..13e61ace14f5 100644
--- a/include/linux/power/power_on_reason.h
+++ b/include/linux/power/power_on_reason.h
@@ -15,5 +15,10 @@
 #define POWER_ON_REASON_XTAL_FAIL "crystal oscillator failure"
 #define POWER_ON_REASON_BROWN_OUT "brown-out reset"
 #define POWER_ON_REASON_UNKNOWN "unknown reason"
+#define POWER_ON_REASON_OVER_CURRENT "over current"
+#define POWER_ON_REASON_REGULATOR_FAILURE "regulator failure"
+#define POWER_ON_REASON_OVER_TEMPERATURE "over temperature"
+#define POWER_ON_REASON_EC_PANIC "EC panic"
+#define POWER_ON_REASON_EXTERNAL "external reset"
 
 #endif /* POWER_ON_REASON_H */
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v12 2/6] reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings
  2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework Oleksij Rempel
@ 2026-07-31  9:59 ` Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 3/6] reboot: add parsable tokens for power state change reasons Oleksij Rempel
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-07-31  9:59 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano
  Cc: Oleksij Rempel, Matti Vaittinen, Mark Brown, kernel, linux-kernel,
	Liam Girdwood, Rafael J. Wysocki, Zhang Rui, Lukasz Luba,
	linux-pm, Søren Andersen, Guenter Roeck, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Faruque Ansari, Francesco Valla,
	Greg Kroah-Hartman, chrome-platform

Prepares the kernel for the Power State Change Reason (PSCR) recorder,
which will store shutdown and reboot reasons in persistent storage.

Instead of using string-based reason descriptions, which are often too
large to fit within limited storage spaces (e.g., RTC clocks with only 8
bits of battery-backed storage), we introduce `enum psc_reason`. This
enumerates predefined reasons for power state changes, making it
efficient to store and retrieve shutdown causes.

Key changes:
- Introduced `enum psc_reason`, defining structured reasons for power state
  changes.
- Replaced string-based shutdown reasons with `psc_reason` identifiers.
- Implemented `get_psc_reason()` and `set_psc_reason()` for tracking the
  last shutdown cause.
- Added `psc_reason_to_str()` to map enum values to human-readable strings.
- Updated `hw_protection_trigger()` to use `psc_reason` instead of string
  parameters.
- Updated all consumers of `hw_protection_trigger()` to pass an appropriate
  `psc_reason` value instead of a string.
- All structured logs now go through a single `pr_emerg()` in
  `__hw_protection_trigger()`, providing consistent output:
    HARDWARE PROTECTION <action>: <reason-code> (<reason-string>)

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Reviewed-by: Matti Vaittinen <mazziesaccount@gmail.com>
Acked-by: Mark Brown <broonie@kernel.org>
Acked-by: Tzung-Bi Shih <tzungbi@kernel.org>
Acked-by: Daniel Lezcano <daniel.lezcano@linaro.org>
---
changes v12:
- drop Tested-by: Faruque Ansari and Francesco Valla; the end-to-end
  recording flow they tested has been reworked (multi-provider) and the
  recorder driver is no longer part of the series - re-testing is welcome
changes v10
- regulator_handle_critical: set pscr = PSCR_UNKNOWN for default case
- add Acked-by: Daniel Lezcano ..
changes v9:
- Remove redundant pr_crit() messages before hw_protection_trigger()
- Replace psc_reason_to_str() switch with static const string array
- Mark psc_last_reason as static
changes v8:
- add Acked/Reviewed-by.
changes v6:
- added in this version
(cherry picked from commit 29591d88486c392e2264de73c4e90106f181b33c)
---
 drivers/platform/chrome/cros_ec_lpc.c |   2 +-
 drivers/regulator/core.c              |  16 ++--
 drivers/regulator/irq_helpers.c       |   9 ++-
 drivers/thermal/thermal_core.c        |   3 +-
 include/linux/reboot.h                |  78 +++++++++++++++++-
 kernel/reboot.c                       | 112 +++++++++++++++++++++++++-
 6 files changed, 198 insertions(+), 22 deletions(-)

diff --git a/drivers/platform/chrome/cros_ec_lpc.c b/drivers/platform/chrome/cros_ec_lpc.c
index 78cfff80cdea..00e98d12ddbc 100644
--- a/drivers/platform/chrome/cros_ec_lpc.c
+++ b/drivers/platform/chrome/cros_ec_lpc.c
@@ -455,7 +455,7 @@ static void cros_ec_lpc_acpi_notify(acpi_handle device, u32 value, void *data)
 		blocking_notifier_call_chain(&ec_dev->panic_notifier, 0, ec_dev);
 		kobject_uevent_env(&ec_dev->dev->kobj, KOBJ_CHANGE, (char **)env);
 		/* Begin orderly shutdown. EC will force reset after a short period. */
-		__hw_protection_trigger("CrOS EC Panic", -1, HWPROT_ACT_SHUTDOWN);
+		__hw_protection_trigger(PSCR_EC_PANIC, -1, HWPROT_ACT_SHUTDOWN);
 		/* Do not query for other events after a panic is reported */
 		return;
 	}
diff --git a/drivers/regulator/core.c b/drivers/regulator/core.c
index dc5d67767336..2075cf838bd4 100644
--- a/drivers/regulator/core.c
+++ b/drivers/regulator/core.c
@@ -5532,26 +5532,26 @@ EXPORT_SYMBOL_GPL(regulator_bulk_free);
 static void regulator_handle_critical(struct regulator_dev *rdev,
 				      unsigned long event)
 {
-	const char *reason = NULL;
+	enum psc_reason pscr;
 
 	if (!rdev->constraints->system_critical)
 		return;
 
 	switch (event) {
 	case REGULATOR_EVENT_UNDER_VOLTAGE:
-		reason = "System critical regulator: voltage drop detected";
+		pscr = PSCR_UNDER_VOLTAGE;
 		break;
 	case REGULATOR_EVENT_OVER_CURRENT:
-		reason = "System critical regulator: over-current detected";
+		pscr = PSCR_OVER_CURRENT;
 		break;
 	case REGULATOR_EVENT_FAIL:
-		reason = "System critical regulator: unknown error";
+		pscr = PSCR_REGULATOR_FAILURE;
+		break;
+	default:
+		pscr = PSCR_UNKNOWN;
 	}
 
-	if (!reason)
-		return;
-
-	hw_protection_trigger(reason,
+	hw_protection_trigger(pscr,
 			      rdev->constraints->uv_less_critical_window_ms);
 }
 
diff --git a/drivers/regulator/irq_helpers.c b/drivers/regulator/irq_helpers.c
index 5b3cfac28667..712e0ce840e7 100644
--- a/drivers/regulator/irq_helpers.c
+++ b/drivers/regulator/irq_helpers.c
@@ -64,15 +64,16 @@ static void regulator_notifier_isr_work(struct work_struct *work)
 reread:
 	if (d->fatal_cnt && h->retry_cnt > d->fatal_cnt) {
 		if (!d->die)
-			return hw_protection_trigger("Regulator HW failure? - no IC recovery",
+			return hw_protection_trigger(PSCR_REGULATOR_FAILURE,
 						     REGULATOR_FORCED_SAFETY_SHUTDOWN_WAIT_MS);
+
 		ret = d->die(rid);
 		/*
 		 * If the 'last resort' IC recovery failed we will have
 		 * nothing else left to do...
 		 */
 		if (ret)
-			return hw_protection_trigger("Regulator HW failure. IC recovery failed",
+			return hw_protection_trigger(PSCR_REGULATOR_FAILURE,
 						     REGULATOR_FORCED_SAFETY_SHUTDOWN_WAIT_MS);
 
 		/*
@@ -263,13 +264,13 @@ static irqreturn_t regulator_notifier_isr(int irq, void *data)
 	if (d->fatal_cnt && h->retry_cnt > d->fatal_cnt) {
 		/* If we have no recovery, just try shut down straight away */
 		if (!d->die) {
-			hw_protection_trigger("Regulator failure. Retry count exceeded",
+			hw_protection_trigger(PSCR_REGULATOR_FAILURE,
 					      REGULATOR_FORCED_SAFETY_SHUTDOWN_WAIT_MS);
 		} else {
 			ret = d->die(rid);
 			/* If die() failed shut down as a last attempt to save the HW */
 			if (ret)
-				hw_protection_trigger("Regulator failure. Recovery failed",
+				hw_protection_trigger(PSCR_REGULATOR_FAILURE,
 						      REGULATOR_FORCED_SAFETY_SHUTDOWN_WAIT_MS);
 		}
 	}
diff --git a/drivers/thermal/thermal_core.c b/drivers/thermal/thermal_core.c
index 28a20d4b475c..df8fa11f78dc 100644
--- a/drivers/thermal/thermal_core.c
+++ b/drivers/thermal/thermal_core.c
@@ -319,11 +319,10 @@ static void thermal_zone_device_halt(struct thermal_zone_device *tz,
 	 * Its a must for forced_emergency_poweroff_work to be scheduled.
 	 */
 	int poweroff_delay_ms = CONFIG_THERMAL_EMERGENCY_POWEROFF_DELAY_MS;
-	const char *msg = "Temperature too high";
 
 	dev_emerg(&tz->device, "%s: critical temperature reached\n", tz->type);
 
-	__hw_protection_trigger(msg, poweroff_delay_ms, action);
+	__hw_protection_trigger(PSCR_OVER_TEMPERATURE, poweroff_delay_ms, action);
 }
 
 void thermal_zone_device_critical(struct thermal_zone_device *tz)
diff --git a/include/linux/reboot.h b/include/linux/reboot.h
index aa08c3bbbf59..08a7549bbc40 100644
--- a/include/linux/reboot.h
+++ b/include/linux/reboot.h
@@ -178,6 +178,73 @@ void ctrl_alt_del(void);
 extern void orderly_poweroff(bool force);
 extern void orderly_reboot(void);
 
+
+/**
+ * enum psc_reason - Enumerates reasons for power state changes.
+ *
+ * This enum defines various reasons why a system might transition into a
+ * shutdown, reboot, or kexec state. While originally intended for hardware
+ * protection events, `psc_reason` can be extended to track other system
+ * transitions, such as controlled reboots triggered by software or
+ * maintenance operations.
+ *
+ * The values in this enumeration provide structured and standardized
+ * identifiers that replace free-form string descriptions. They are designed
+ * to be stored efficiently, making them suitable for use in environments
+ * with limited storage, such as battery-backed RTC registers, non-volatile
+ * memory, or bootloader communication mechanisms.
+ *
+ * Importantly, the order of these values **must remain stable**, as
+ * bootloaders, user-space tools, or post-mortem investigation utilities
+ * may rely on their numerical representation for consistent behavior.
+ *
+ * @PSCR_UNKNOWN: Unknown or unspecified reason for the power state change.
+ *	This value serves as a default when no explicit cause is recorded.
+ *
+ * @PSCR_UNDER_VOLTAGE: Shutdown or reboot triggered due to supply voltage
+ *      dropping below a safe threshold. This helps prevent instability or
+ *      corruption caused by insufficient power.
+ *
+ * @PSCR_OVER_CURRENT: System shutdown or reboot due to excessive current draw,
+ *      which may indicate a short circuit, an overloaded power rail, or other
+ *      hardware faults requiring immediate action.
+ *
+ * @PSCR_REGULATOR_FAILURE: A critical failure in a voltage regulator, causing
+ *      improper power delivery. This may be due to internal component failure,
+ *      transient conditions, or external load issues requiring mitigation.
+ *
+ * @PSCR_OVER_TEMPERATURE: System shutdown or reboot due to excessive thermal
+ *	conditions. This attempts to prevent hardware damage when temperature
+ *	sensors detect unsafe levels, often impacting CPUs, GPUs, or power
+ *	components.
+ *
+ * @PSCR_EC_PANIC: Shutdown or reboot triggered by an Embedded Controller (EC)
+ *	panic. The EC is a microcontroller responsible for low-level system
+ *	management, including power sequencing, thermal control, and battery
+ *	management. An EC panic may indicate critical firmware issues, power
+ *	management errors, or an unrecoverable hardware fault requiring
+ *	immediate response.
+ *
+ * @PSCR_REASON_COUNT: Number of defined power state change reasons. This
+ *	value is useful for range checking and potential future extensions
+ *	while maintaining compatibility.
+ */
+enum psc_reason {
+	PSCR_UNKNOWN,
+	PSCR_UNDER_VOLTAGE,
+	PSCR_OVER_CURRENT,
+	PSCR_REGULATOR_FAILURE,
+	PSCR_OVER_TEMPERATURE,
+	PSCR_EC_PANIC,
+
+	/* Number of reasons */
+	PSCR_REASON_COUNT,
+};
+
+#define PSCR_MAX_REASON	(PSCR_REASON_COUNT - 1)
+
+const char *psc_reason_to_str(enum psc_reason reason);
+
 /**
  * enum hw_protection_action - Hardware protection action
  *
@@ -191,13 +258,13 @@ extern void orderly_reboot(void);
  */
 enum hw_protection_action { HWPROT_ACT_DEFAULT, HWPROT_ACT_SHUTDOWN, HWPROT_ACT_REBOOT };
 
-void __hw_protection_trigger(const char *reason, int ms_until_forced,
+void __hw_protection_trigger(enum psc_reason reason, int ms_until_forced,
 			     enum hw_protection_action action);
 
 /**
  * hw_protection_trigger - Trigger default emergency system hardware protection action
  *
- * @reason:		Reason of emergency shutdown or reboot to be printed.
+ * @reason:		Reason of emergency shutdown or reboot.
  * @ms_until_forced:	Time to wait for orderly shutdown or reboot before
  *			triggering it. Negative value disables the forced
  *			shutdown or reboot.
@@ -206,11 +273,16 @@ void __hw_protection_trigger(const char *reason, int ms_until_forced,
  * hardware from further damage. The exact action taken is controllable at
  * runtime and defaults to shutdown.
  */
-static inline void hw_protection_trigger(const char *reason, int ms_until_forced)
+static inline void hw_protection_trigger(enum psc_reason reason,
+					 int ms_until_forced)
 {
 	__hw_protection_trigger(reason, ms_until_forced, HWPROT_ACT_DEFAULT);
 }
 
+enum psc_reason get_psc_reason(void);
+enum psc_reason get_psc_first_reason(void);
+void set_psc_reason(enum psc_reason reason);
+
 /*
  * Emergency restart, callable from an interrupt handler.
  */
diff --git a/kernel/reboot.c b/kernel/reboot.c
index 695c33e75efd..29c68441fa53 100644
--- a/kernel/reboot.c
+++ b/kernel/reboot.c
@@ -13,6 +13,7 @@
 #include <linux/kexec.h>
 #include <linux/kmod.h>
 #include <linux/kmsg_dump.h>
+#include <linux/power/power_on_reason.h>
 #include <linux/reboot.h>
 #include <linux/suspend.h>
 #include <linux/syscalls.h>
@@ -49,6 +50,8 @@ int reboot_default = 1;
 int reboot_cpu;
 enum reboot_type reboot_type = BOOT_ACPI;
 int reboot_force;
+static enum psc_reason psc_last_reason = PSCR_UNKNOWN;
+static enum psc_reason psc_first_reason = PSCR_UNKNOWN;
 
 struct sys_off_handler {
 	struct notifier_block nb;
@@ -1010,10 +1013,108 @@ static void hw_failure_emergency_schedule(enum hw_protection_action action,
 			      msecs_to_jiffies(action_delay_ms));
 }
 
+/**
+ * get_psc_reason - Retrieve the last recorded power state change reason.
+ *
+ * This function returns the most recent power state change reason stored
+ * in `psc_last_reason`. The value is set using `set_psc_reason()` when a
+ * shutdown, reboot, or kexec event occurs.
+ *
+ * The reason can be used for system diagnostics, post-mortem analysis, or
+ * debugging unexpected power state changes. Bootloaders or user-space tools
+ * may retrieve this value to determine why the system last transitioned to
+ * a new power state.
+ *
+ * Return: A value from `enum psc_reason`, indicating the last known power
+ * state change reason.
+ */
+enum psc_reason get_psc_reason(void)
+{
+	return READ_ONCE(psc_last_reason);
+}
+EXPORT_SYMBOL_GPL(get_psc_reason);
+
+/**
+ * get_psc_first_reason - Retrieve the first power state change reason.
+ *
+ * Returns the first meaningful (non-PSCR_UNKNOWN) reason recorded during this
+ * boot, i.e. the root cause, as opposed to get_psc_reason() which returns the
+ * most recent one. A later event (a watchdog pretimeout, a kernel panic, ...)
+ * updates the last reason but leaves this one untouched, so a recorder using
+ * the "first" record policy can preserve the original cause.
+ *
+ * Return: A value from `enum psc_reason`.
+ */
+enum psc_reason get_psc_first_reason(void)
+{
+	return READ_ONCE(psc_first_reason);
+}
+EXPORT_SYMBOL_GPL(get_psc_first_reason);
+
+/**
+ * set_psc_reason - Set the reason for the last power state change.
+ *
+ * @reason: A value from `enum psc_reason` indicating the cause of the power
+ *          state change.
+ *
+ * This function records the reason for a shutdown, reboot, or kexec event
+ * by storing it in `psc_last_reason`. It ensures that the value remains
+ * consistent within the running system, allowing retrieval via
+ * `get_psc_reason()` for diagnostics, logging, or post-mortem analysis.
+ *
+ * Persistence Consideration:
+ * - This function **does not persist** the recorded reason across power cycles.
+ * - After a system reset or complete power loss, the recorded reason is lost.
+ * - To store power state change reasons persistently, additional tools such as
+ *   the Power State Change Reason Recorder (PSCRR) framework should be used.
+ */
+void set_psc_reason(enum psc_reason reason)
+{
+	WRITE_ONCE(psc_last_reason, reason);
+
+	/*
+	 * Latch the first meaningful reason of this boot as the root cause, so
+	 * that a later event overwriting the last reason does not hide it from
+	 * a recorder using the "first" record policy.
+	 */
+	if (reason != PSCR_UNKNOWN &&
+	    READ_ONCE(psc_first_reason) == PSCR_UNKNOWN)
+		WRITE_ONCE(psc_first_reason, reason);
+}
+EXPORT_SYMBOL_GPL(set_psc_reason);
+
+static const char * const pscr_reason_strs[] = {
+	[PSCR_UNKNOWN]            = POWER_ON_REASON_UNKNOWN,
+	[PSCR_UNDER_VOLTAGE]      = POWER_ON_REASON_BROWN_OUT,
+	[PSCR_OVER_CURRENT]       = POWER_ON_REASON_OVER_CURRENT,
+	[PSCR_REGULATOR_FAILURE]  = POWER_ON_REASON_REGULATOR_FAILURE,
+	[PSCR_OVER_TEMPERATURE]   = POWER_ON_REASON_OVER_TEMPERATURE,
+	[PSCR_EC_PANIC]           = POWER_ON_REASON_EC_PANIC,
+};
+
+/**
+ * psc_reason_to_str - Converts a power state change reason enum to a string.
+ * @reason: The `psc_reason` enum value to be converted.
+ *
+ * This function provides a human-readable string representation of the power
+ * state change reason, making it easier to interpret logs and debug messages.
+ *
+ * Return:
+ * - A string corresponding to the given `psc_reason` value.
+ * - `"Invalid"` if the value is not recognized.
+ */
+const char *psc_reason_to_str(enum psc_reason reason)
+{
+	if (reason < 0 || reason >= PSCR_REASON_COUNT)
+		return "Invalid";
+	return pscr_reason_strs[reason];
+}
+EXPORT_SYMBOL_GPL(psc_reason_to_str);
+
 /**
  * __hw_protection_trigger - Trigger an emergency system shutdown or reboot
  *
- * @reason:		Reason of emergency shutdown or reboot to be printed.
+ * @reason:		Reason of emergency shutdown or reboot.
  * @ms_until_forced:	Time to wait for orderly shutdown or reboot before
  *			triggering it. Negative value disables the forced
  *			shutdown or reboot.
@@ -1025,7 +1126,7 @@ static void hw_failure_emergency_schedule(enum hw_protection_action action,
  * pending even if the previous request has given a large timeout for forced
  * shutdown/reboot.
  */
-void __hw_protection_trigger(const char *reason, int ms_until_forced,
+void __hw_protection_trigger(enum psc_reason reason, int ms_until_forced,
 			     enum hw_protection_action action)
 {
 	static atomic_t allow_proceed = ATOMIC_INIT(1);
@@ -1033,8 +1134,11 @@ void __hw_protection_trigger(const char *reason, int ms_until_forced,
 	if (action == HWPROT_ACT_DEFAULT)
 		action = hw_protection_action;
 
-	pr_emerg("HARDWARE PROTECTION %s (%s)\n",
-		 hw_protection_action_str(action), reason);
+	set_psc_reason(reason);
+
+	pr_emerg("HARDWARE PROTECTION %s: %i (%s)\n",
+		 hw_protection_action_str(action), reason,
+		 psc_reason_to_str(reason));
 
 	/* Shutdown should be initiated only once. */
 	if (!atomic_dec_and_test(&allow_proceed))
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v12 3/6] reboot: add parsable tokens for power state change reasons
  2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 2/6] reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings Oleksij Rempel
@ 2026-07-31  9:59 ` Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 4/6] reboot: extend psc_reason with power-on and reset causes Oleksij Rempel
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-07-31  9:59 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano
  Cc: Oleksij Rempel, kernel, linux-kernel, Liam Girdwood, Mark Brown,
	Rafael J. Wysocki, Zhang Rui, Lukasz Luba, linux-pm,
	Søren Andersen, Guenter Roeck, Matti Vaittinen, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Faruque Ansari, Francesco Valla,
	Greg Kroah-Hartman, chrome-platform

psc_reason_to_str() returns human-readable labels that contain spaces
(e.g. "over temperature"). Those are fine for logs but unusable as
values in a space-separated sysfs list or as a write target.

Extend the single reason descriptor table with a stable, space-free
token next to the existing label, and add psc_reason_to_token() and
psc_reason_from_token() so consumers can emit and parse reasons without
inventing their own string table.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>

---
changes v12:
- new patch
---
 include/linux/reboot.h |  2 ++
 kernel/reboot.c        | 77 +++++++++++++++++++++++++++++++++---------
 2 files changed, 63 insertions(+), 16 deletions(-)

diff --git a/include/linux/reboot.h b/include/linux/reboot.h
index 08a7549bbc40..4c5327dd7645 100644
--- a/include/linux/reboot.h
+++ b/include/linux/reboot.h
@@ -244,6 +244,8 @@ enum psc_reason {
 #define PSCR_MAX_REASON	(PSCR_REASON_COUNT - 1)
 
 const char *psc_reason_to_str(enum psc_reason reason);
+const char *psc_reason_to_token(enum psc_reason reason);
+int psc_reason_from_token(const char *token, enum psc_reason *reason);
 
 /**
  * enum hw_protection_action - Hardware protection action
diff --git a/kernel/reboot.c b/kernel/reboot.c
index 29c68441fa53..929496be20c0 100644
--- a/kernel/reboot.c
+++ b/kernel/reboot.c
@@ -1083,34 +1083,79 @@ void set_psc_reason(enum psc_reason reason)
 }
 EXPORT_SYMBOL_GPL(set_psc_reason);
 
-static const char * const pscr_reason_strs[] = {
-	[PSCR_UNKNOWN]            = POWER_ON_REASON_UNKNOWN,
-	[PSCR_UNDER_VOLTAGE]      = POWER_ON_REASON_BROWN_OUT,
-	[PSCR_OVER_CURRENT]       = POWER_ON_REASON_OVER_CURRENT,
-	[PSCR_REGULATOR_FAILURE]  = POWER_ON_REASON_REGULATOR_FAILURE,
-	[PSCR_OVER_TEMPERATURE]   = POWER_ON_REASON_OVER_TEMPERATURE,
-	[PSCR_EC_PANIC]           = POWER_ON_REASON_EC_PANIC,
+/**
+ * struct psc_reason_desc - Descriptor for a power state change reason.
+ * @token: Stable, parsable identifier without spaces (e.g. "over-temperature").
+ *	   Suitable for use in sysfs values and as a user/kernel contract.
+ * @label: Human-readable description (e.g. "over temperature"), for logs.
+ */
+struct psc_reason_desc {
+	const char *token;
+	const char *label;
+};
+
+static const struct psc_reason_desc psc_reason_descs[] = {
+	[PSCR_UNKNOWN]		 = { "unknown",		  POWER_ON_REASON_UNKNOWN },
+	[PSCR_UNDER_VOLTAGE]	 = { "under-voltage",	  POWER_ON_REASON_BROWN_OUT },
+	[PSCR_OVER_CURRENT]	 = { "over-current",	  POWER_ON_REASON_OVER_CURRENT },
+	[PSCR_REGULATOR_FAILURE] = { "regulator-failure", POWER_ON_REASON_REGULATOR_FAILURE },
+	[PSCR_OVER_TEMPERATURE]	 = { "over-temperature",  POWER_ON_REASON_OVER_TEMPERATURE },
+	[PSCR_EC_PANIC]		 = { "ec-panic",	  POWER_ON_REASON_EC_PANIC },
 };
 
 /**
- * psc_reason_to_str - Converts a power state change reason enum to a string.
- * @reason: The `psc_reason` enum value to be converted.
- *
- * This function provides a human-readable string representation of the power
- * state change reason, making it easier to interpret logs and debug messages.
+ * psc_reason_to_str - Human-readable label for a power state change reason.
+ * @reason: The `psc_reason` value to convert.
  *
- * Return:
- * - A string corresponding to the given `psc_reason` value.
- * - `"Invalid"` if the value is not recognized.
+ * Return: The label string, or "Invalid" if @reason is out of range. For a
+ * stable, parsable form use psc_reason_to_token() instead.
  */
 const char *psc_reason_to_str(enum psc_reason reason)
 {
 	if (reason < 0 || reason >= PSCR_REASON_COUNT)
 		return "Invalid";
-	return pscr_reason_strs[reason];
+	return psc_reason_descs[reason].label;
 }
 EXPORT_SYMBOL_GPL(psc_reason_to_str);
 
+/**
+ * psc_reason_to_token - Stable, parsable token for a power state change reason.
+ * @reason: The `psc_reason` value to convert.
+ *
+ * Return: The token string (no spaces), or "invalid" if @reason is out of
+ * range. Round-trips with psc_reason_from_token().
+ */
+const char *psc_reason_to_token(enum psc_reason reason)
+{
+	if (reason < 0 || reason >= PSCR_REASON_COUNT)
+		return "invalid";
+	return psc_reason_descs[reason].token;
+}
+EXPORT_SYMBOL_GPL(psc_reason_to_token);
+
+/**
+ * psc_reason_from_token - Parse a reason token into a `psc_reason` value.
+ * @token: A token as returned by psc_reason_to_token(). A trailing newline is
+ *	   tolerated.
+ * @reason: Output; set on success.
+ *
+ * Return: 0 on success or -EINVAL if @token matches no known reason.
+ */
+int psc_reason_from_token(const char *token, enum psc_reason *reason)
+{
+	int i;
+
+	for (i = 0; i < PSCR_REASON_COUNT; i++) {
+		if (sysfs_streq(token, psc_reason_descs[i].token)) {
+			*reason = i;
+			return 0;
+		}
+	}
+
+	return -EINVAL;
+}
+EXPORT_SYMBOL_GPL(psc_reason_from_token);
+
 /**
  * __hw_protection_trigger - Trigger an emergency system shutdown or reboot
  *
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v12 4/6] reboot: extend psc_reason with power-on and reset causes
  2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
                   ` (2 preceding siblings ...)
  2026-07-31  9:59 ` [PATCH v12 3/6] reboot: add parsable tokens for power state change reasons Oleksij Rempel
@ 2026-07-31  9:59 ` Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Oleksij Rempel
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-07-31  9:59 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano
  Cc: Oleksij Rempel, kernel, linux-kernel, Liam Girdwood, Mark Brown,
	Rafael J. Wysocki, Zhang Rui, Lukasz Luba, linux-pm,
	Søren Andersen, Guenter Roeck, Matti Vaittinen, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Faruque Ansari, Francesco Valla,
	Greg Kroah-Hartman, chrome-platform

psc_reason covered only the protection reasons. Extend it to mirror the
full POWER_ON_REASON_* vocabulary - power-on, watchdog, software,
external, RTC, reset button, CPU clock and crystal failures - so a
provider can report why the system last powered on, not only why it was
shut down. New values are appended to keep the existing numeric ABI.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>

---
changes v12:
- new patch
---
 include/linux/reboot.h | 28 ++++++++++++++++++++++++++++
 kernel/reboot.c        |  8 ++++++++
 2 files changed, 36 insertions(+)

diff --git a/include/linux/reboot.h b/include/linux/reboot.h
index 4c5327dd7645..13f004ad1066 100644
--- a/include/linux/reboot.h
+++ b/include/linux/reboot.h
@@ -225,9 +225,28 @@ extern void orderly_reboot(void);
  *	management errors, or an unrecoverable hardware fault requiring
  *	immediate response.
  *
+ * @PSCR_POWER_ON: Regular cold power-on (e.g. via a PMIC power-on request).
+ *
+ * @PSCR_WATCHDOG: Reset caused by a watchdog timeout.
+ *
+ * @PSCR_SOFTWARE: Software-initiated reset or reboot.
+ *
+ * @PSCR_EXTERNAL: Reset asserted through an external pin or reset input.
+ *
+ * @PSCR_RTC: Wake-up or power-on triggered by the RTC.
+ *
+ * @PSCR_RESET_BUTTON: Reset triggered by a user reset button.
+ *
+ * @PSCR_CPU_CLK_FAIL: Reset caused by a CPU clock failure.
+ *
+ * @PSCR_XTAL_FAIL: Reset caused by a crystal oscillator failure.
+ *
  * @PSCR_REASON_COUNT: Number of defined power state change reasons. This
  *	value is useful for range checking and potential future extensions
  *	while maintaining compatibility.
+ *
+ * The reasons mirror the POWER_ON_REASON_* strings in
+ * <linux/power/power_on_reason.h>; keep the two in sync when extending.
  */
 enum psc_reason {
 	PSCR_UNKNOWN,
@@ -236,6 +255,15 @@ enum psc_reason {
 	PSCR_REGULATOR_FAILURE,
 	PSCR_OVER_TEMPERATURE,
 	PSCR_EC_PANIC,
+	/* Append new reasons here; the numeric order is ABI. */
+	PSCR_POWER_ON,
+	PSCR_WATCHDOG,
+	PSCR_SOFTWARE,
+	PSCR_EXTERNAL,
+	PSCR_RTC,
+	PSCR_RESET_BUTTON,
+	PSCR_CPU_CLK_FAIL,
+	PSCR_XTAL_FAIL,
 
 	/* Number of reasons */
 	PSCR_REASON_COUNT,
diff --git a/kernel/reboot.c b/kernel/reboot.c
index 929496be20c0..3e262c4450c5 100644
--- a/kernel/reboot.c
+++ b/kernel/reboot.c
@@ -1101,6 +1101,14 @@ static const struct psc_reason_desc psc_reason_descs[] = {
 	[PSCR_REGULATOR_FAILURE] = { "regulator-failure", POWER_ON_REASON_REGULATOR_FAILURE },
 	[PSCR_OVER_TEMPERATURE]	 = { "over-temperature",  POWER_ON_REASON_OVER_TEMPERATURE },
 	[PSCR_EC_PANIC]		 = { "ec-panic",	  POWER_ON_REASON_EC_PANIC },
+	[PSCR_POWER_ON]		 = { "power-on",	  POWER_ON_REASON_REGULAR },
+	[PSCR_WATCHDOG]		 = { "watchdog",	  POWER_ON_REASON_WATCHDOG },
+	[PSCR_SOFTWARE]		 = { "software",	  POWER_ON_REASON_SOFTWARE },
+	[PSCR_EXTERNAL]		 = { "external",	  POWER_ON_REASON_EXTERNAL },
+	[PSCR_RTC]		 = { "rtc",		  POWER_ON_REASON_RTC },
+	[PSCR_RESET_BUTTON]	 = { "reset-button",	  POWER_ON_REASON_RST_BTN },
+	[PSCR_CPU_CLK_FAIL]	 = { "cpu-clock-failure", POWER_ON_REASON_CPU_CLK_FAIL },
+	[PSCR_XTAL_FAIL]	 = { "crystal-failure",	  POWER_ON_REASON_XTAL_FAIL },
 };
 
 /**
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v12 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage
  2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
                   ` (3 preceding siblings ...)
  2026-07-31  9:59 ` [PATCH v12 4/6] reboot: extend psc_reason with power-on and reset causes Oleksij Rempel
@ 2026-07-31  9:59 ` Oleksij Rempel
  2026-07-31  9:59 ` [PATCH v12 6/6] Documentation: Add sysfs documentation for PSCRR Oleksij Rempel
  2026-08-05  9:34 ` [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
  6 siblings, 0 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-07-31  9:59 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano
  Cc: Oleksij Rempel, kernel, linux-kernel, Liam Girdwood, Mark Brown,
	Rafael J. Wysocki, Zhang Rui, Lukasz Luba, linux-pm,
	Søren Andersen, Guenter Roeck, Matti Vaittinen, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Faruque Ansari, Francesco Valla,
	Greg Kroah-Hartman, chrome-platform

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.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
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                        |  10 +
 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  | 634 +++++++++++++++++++++++++++++
 include/linux/pscrr.h              | 107 +++++
 7 files changed, 789 insertions(+)
 create mode 100644 drivers/power/reset/pscrr/Kconfig
 create mode 100644 drivers/power/reset/pscrr/Makefile
 create mode 100644 drivers/power/reset/pscrr/pscrr.c
 create mode 100644 include/linux/pscrr.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 15011f5752a9..35195d9d4c4e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21526,6 +21526,16 @@ 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>
+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 124afb99febe..83e87794d976 100644
--- a/drivers/power/reset/Kconfig
+++ b/drivers/power/reset/Kconfig
@@ -365,3 +365,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 d7ae97241a83..60cf967cde21 100644
--- a/drivers/power/reset/Makefile
+++ b/drivers/power/reset/Makefile
@@ -36,6 +36,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..6f23f4c4f590
--- /dev/null
+++ b/drivers/power/reset/pscrr/pscrr.c
@@ -0,0 +1,634 @@
+// 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.
+ */
+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;
+
+	/*
+	 * 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;
+
+	/* "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;
+
+	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;
+
+	dir = kzalloc_obj(*dir);
+	if (!dir)
+		return -ENOMEM;
+
+	dir->provider = p;
+	dir->policy = PSCRR_RECORD_FIRST;
+
+	scoped_guard(mutex, &pscrr_lock) {
+		if (!pscrr_root) {
+			kfree(dir);
+			return -ENODEV;
+		}
+
+		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(). The
+			 * kobject is not in sysfs, so this is safe under the
+			 * lock.
+			 */
+			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)
+				break;
+		}
+
+		list_add_tail(&dir->node, &pscrr_dirs);
+		return 0;
+	}
+
+	/*
+	 * sysfs_create_link() failed after the directory was created: tear it
+	 * down outside pscrr_lock (kobject_del() drains sysfs stores that take
+	 * the lock) and release the id only once the directory is gone.
+	 */
+	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_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);
+#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__ */
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v12 6/6] Documentation: Add sysfs documentation for PSCRR
  2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
                   ` (4 preceding siblings ...)
  2026-07-31  9:59 ` [PATCH v12 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Oleksij Rempel
@ 2026-07-31  9:59 ` Oleksij Rempel
  2026-08-05  9:34 ` [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
  6 siblings, 0 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-07-31  9:59 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano
  Cc: Oleksij Rempel, kernel, linux-kernel, Liam Girdwood, Mark Brown,
	Rafael J. Wysocki, Zhang Rui, Lukasz Luba, linux-pm,
	Søren Andersen, Guenter Roeck, Matti Vaittinen, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Faruque Ansari, Francesco Valla,
	Greg Kroah-Hartman, chrome-platform

Document the Power State Change Reasons Recording (PSCRR) sysfs interface
under /sys/kernel/pscrr/: the per-provider directories and their name,
device, reason, caps, supported_reasons and record_policy attributes,
including the stable reason token values.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
changes v12:
- rewrite for the per-provider interface (providerN/ directories with
  caps, supported_reasons and record_policy)
- rename the file to sysfs-kernel-pscrr to match the sysfs path
- refresh KernelVersion/Date
- drop Reviewed-by: Matti Vaittinen; the documentation was rewritten
changes v8:
- simplify and clarify example sysfs value comments
- add note that not all values are meaningful on every system
changes v7:
- document expected values
---
 Documentation/ABI/testing/sysfs-kernel-pscrr | 108 +++++++++++++++++++
 1 file changed, 108 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-kernel-pscrr

diff --git a/Documentation/ABI/testing/sysfs-kernel-pscrr b/Documentation/ABI/testing/sysfs-kernel-pscrr
new file mode 100644
index 000000000000..63aa411b7362
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-kernel-pscrr
@@ -0,0 +1,108 @@
+What:		/sys/kernel/pscrr/
+Date:		July 2026
+KernelVersion:	7.2
+Contact:	Oleksij Rempel <o.rempel@pengutronix.de>
+Description:
+		Root directory of the Power State Change Reason Recording
+		(PSCRR) framework. It contains one subdirectory per registered
+		reason provider, named providerN (N is an arbitrary, stable
+		index assigned at registration).
+
+		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 current reason across a power cycle
+		(e.g. an NVMEM or RTC scratch cell). The set of reasons is
+		deliberately not collapsed to a single "winning" cause, since
+		resets are often multi-causal.
+
+What:		/sys/kernel/pscrr/providerN/name
+Date:		July 2026
+KernelVersion:	7.2
+Contact:	Oleksij Rempel <o.rempel@pengutronix.de>
+Description:
+		(RO) Human-readable label identifying the provider, e.g.
+		"pca9450" or "nvmem".
+
+What:		/sys/kernel/pscrr/providerN/device
+Date:		July 2026
+KernelVersion:	7.2
+Contact:	Oleksij Rempel <o.rempel@pengutronix.de>
+Description:
+		Symbolic link to the backing struct device of the provider.
+		Present only for providers that are bound to a device.
+
+What:		/sys/kernel/pscrr/providerN/reason
+Date:		July 2026
+KernelVersion:	7.2
+Contact:	Oleksij Rempel <o.rempel@pengutronix.de>
+Description:
+		The set of power state change reasons observed by this
+		provider, as a space-separated list of reason tokens (an
+		empty line means no reason is recorded).
+
+		The attribute is writable only for providers that can record
+		a reason; for a pure hardware source it is read-only. A write
+		records one reason and accepts either a reason token or its
+		decimal index. The tokens and their stable numeric values are:
+
+		==  =================  ============================================
+		0   unknown            Unknown or unspecified reason
+		1   under-voltage      Supply voltage dropped below a safe level
+		2   over-current       Excessive current draw / possible short
+		3   regulator-failure  Voltage regulator failure
+		4   over-temperature   Unsafe temperature detected
+		5   ec-panic           Embedded controller (EC) panic
+		6   power-on           Regular cold power-on
+		7   watchdog           Watchdog timeout
+		8   software           Software-initiated reset or reboot
+		9   external           External reset input asserted
+		10  rtc                RTC-triggered wake-up or power-on
+		11  reset-button       User reset button
+		12  cpu-clock-failure  CPU clock failure
+		13  crystal-failure    Crystal oscillator failure
+		==  =================  ============================================
+
+		The numeric order is stable ABI: new reasons are only ever
+		appended. A provider may support only a subset of these; see
+		"supported_reasons".
+
+What:		/sys/kernel/pscrr/providerN/caps
+Date:		July 2026
+KernelVersion:	7.2
+Contact:	Oleksij Rempel <o.rempel@pengutronix.de>
+Description:
+		(RO) Space-separated list of the provider's non-default
+		capabilities. Being readable and storing a single reason are
+		the defaults and are not listed. Currently defined:
+
+		========  ==============================================
+		writable  the provider can record a reason (see "reason"
+		          and "record_policy")
+		========  ==============================================
+
+		An empty line therefore denotes a read-only, single-slot
+		provider.
+
+What:		/sys/kernel/pscrr/providerN/supported_reasons
+Date:		July 2026
+KernelVersion:	7.2
+Contact:	Oleksij Rempel <o.rempel@pengutronix.de>
+Description:
+		(RO) Space-separated list of the reason tokens (see "reason")
+		this provider is able to report or record. A provider that
+		supports every reason lists them all.
+
+What:		/sys/kernel/pscrr/providerN/record_policy
+Date:		July 2026
+KernelVersion:	7.2
+Contact:	Oleksij Rempel <o.rempel@pengutronix.de>
+Description:
+		(RW) Policy used when more than one reason is recorded during a
+		single power cycle. Present only for providers that can record
+		(see "caps"). Valid values are:
+
+		=====  =================================================
+		first  keep the first reason recorded this cycle (the
+		       root cause); this is the default
+		last   overwrite with the most recently recorded reason
+		=====  =================================================
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework
  2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
                   ` (5 preceding siblings ...)
  2026-07-31  9:59 ` [PATCH v12 6/6] Documentation: Add sysfs documentation for PSCRR Oleksij Rempel
@ 2026-08-05  9:34 ` Faruque Ansari
  2026-08-05 11:07   ` Oleksij Rempel
  6 siblings, 1 reply; 9+ messages in thread
From: Faruque Ansari @ 2026-08-05  9:34 UTC (permalink / raw)
  To: Oleksij Rempel, Sebastian Reichel, Benson Leung, Tzung-Bi Shih,
	Daniel Lezcano
  Cc: kernel, linux-kernel, Liam Girdwood, Mark Brown,
	Rafael J. Wysocki, Zhang Rui, Lukasz Luba, linux-pm,
	Søren Andersen, Guenter Roeck, Matti Vaittinen, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Francesco Valla, Greg Kroah-Hartman,
	chrome-platform

Hi Oleksij,

On 31-Jul-26 3:29 PM, Oleksij Rempel wrote:
> changes v12:
> - Drop all provider drivers and their tests; post the framework alone. The
>    NVMEM-cell binding a recorder needs to pick its storage is still
>    deadlocked, and several projects already need the framework - so unblock
>    the core now and let the providers (NVMEM, PMIC, ...) follow separately.
> - Rework into a multi-provider design (per-provider /sys/kernel/pscrr/
>    directories); add reason tokens and a built-in /chosen/reset-source
>    provider.
> 

When do you plan to post the provider series (NVMEM, PMIC, etc.) 
separately to the mailing list?

Thanks
Faruque Ansari

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework
  2026-08-05  9:34 ` [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
@ 2026-08-05 11:07   ` Oleksij Rempel
  0 siblings, 0 replies; 9+ messages in thread
From: Oleksij Rempel @ 2026-08-05 11:07 UTC (permalink / raw)
  To: Faruque Ansari
  Cc: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Daniel Lezcano,
	kernel, linux-kernel, Liam Girdwood, Mark Brown,
	Rafael J. Wysocki, Zhang Rui, Lukasz Luba, linux-pm,
	Søren Andersen, Guenter Roeck, Matti Vaittinen, Ahmad Fatoum,
	Andrew Morton, Kees Cook, Francesco Valla, Greg Kroah-Hartman,
	chrome-platform

Hi Faruque,

On Wed, Aug 05, 2026 at 03:04:42PM +0530, Faruque Ansari wrote:
> Hi Oleksij,
> 
> On 31-Jul-26 3:29 PM, Oleksij Rempel wrote:
> > changes v12:
> > - Drop all provider drivers and their tests; post the framework alone. The
> >    NVMEM-cell binding a recorder needs to pick its storage is still
> >    deadlocked, and several projects already need the framework - so unblock
> >    the core now and let the providers (NVMEM, PMIC, ...) follow separately.
> > - Rework into a multi-provider design (per-provider /sys/kernel/pscrr/
> >    directories); add reason tokens and a built-in /chosen/reset-source
> >    provider.
> > 
> 
> When do you plan to post the provider series (NVMEM, PMIC, etc.) separately
> to the mailing list?

After this patch stack is merged.

Currently I do not have capacity to continue. If you can, it would be
good it if you can take over next upstreaming rounds for this core. All
related follow-up patches are in my git repo.

Best Regards,
Oleksij
-- 
Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | http://www.pengutronix.de/  |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-5555 |

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-08-05 11:07 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31  9:59 [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Oleksij Rempel
2026-07-31  9:59 ` [PATCH v12 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework Oleksij Rempel
2026-07-31  9:59 ` [PATCH v12 2/6] reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings Oleksij Rempel
2026-07-31  9:59 ` [PATCH v12 3/6] reboot: add parsable tokens for power state change reasons Oleksij Rempel
2026-07-31  9:59 ` [PATCH v12 4/6] reboot: extend psc_reason with power-on and reset causes Oleksij Rempel
2026-07-31  9:59 ` [PATCH v12 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Oleksij Rempel
2026-07-31  9:59 ` [PATCH v12 6/6] Documentation: Add sysfs documentation for PSCRR Oleksij Rempel
2026-08-05  9:34 ` [PATCH v12 0/6] Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
2026-08-05 11:07   ` Oleksij Rempel

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox