mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework
@ 2026-09-21 17:44 Faruque Ansari
  2026-09-21 17:44 ` [PATCH v14 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework Faruque Ansari
                   ` (6 more replies)
  0 siblings, 7 replies; 14+ messages in thread
From: Faruque Ansari @ 2026-09-21 17:44 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Guenter Roeck,
	Liam Girdwood, Mark Brown, Matti Vaittinen, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Faruque Ansari,
	Sebastian Reichel, Francesco Valla, Daniel Lezcano

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 pick its storage is still
deadlocked, and several projects already need the framework, so this v13
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

Changes in v14:
- Ignore v13, it was mistakenly posted as a reply to Oleksij's v12
  patch series.

Changes in v13:
- Rework pscrr_provider_register() based on review feedback from Francesco Valla:
  move the pscrr_root check before allocation and narrow pscrr_lock scope.
- Carry the series forward with Faruque Ansari as author and Oleksij
  Rempel as co-developer, and vice versa where required.
- Link to v12: https://lore.kernel.org/r/20260731095959.296056-1-o.rempel@pengutronix.de

Changes in 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 in v11:
- add missing break reported by kernel test robot <lkp@intel.com>

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

Changes in 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 in 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 in v7:
- document expected values in sysfs documentation
- make write support optional

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

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

Changes in v4:
- fix compile with CONFIG_PSCRR=n

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

Changes in 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

To: Sebastian Reichel <sre@kernel.org>
To: Benson Leung <bleung@chromium.org>
To: Tzung-Bi Shih <tzungbi@kernel.org>
To: Guenter Roeck <groeck@chromium.org>
To: Liam Girdwood <lgirdwood@gmail.com>
To: Mark Brown <broonie@kernel.org>
To: Matti Vaittinen <mazziesaccount@gmail.com>
To: Rafael J. Wysocki <rafael@kernel.org>
To: Daniel Lezcano <daniel.lezcano@kernel.org>
To: Zhang Rui <rui.zhang@intel.com>
To: Lukasz Luba <lukasz.luba@arm.com>
To: Oleksij Rempel <o.rempel@pengutronix.de>
To: Pengutronix Kernel Team <kernel@pengutronix.de>
Cc: linux-arm-msm@vger.kernel.org
Cc: linux-pm@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: chrome-platform@lists.linux.dev
Cc: avaneesh.dwivedi@oss.qualcomm.com
Cc: Umang Chheda <umang.chheda@oss.qualcomm.com>

Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>
---
Faruque Ansari (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                                  |  11 +
 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            | 633 +++++++++++++++++++++++++++
 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(-)
---
base-commit: 5c4d4169604b335c38bbc79bc1fc03042981fc6f
change-id: 20260921-pscrr-framework-25f48818efe8

Best regards,
-- 
Faruque Ansari <faruque.ansari@oss.qualcomm.com>


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

* [PATCH v14 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework
  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 ` 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
                   ` (5 subsequent siblings)
  6 siblings, 1 reply; 14+ messages in thread
From: Faruque Ansari @ 2026-09-21 17:44 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Guenter Roeck,
	Liam Girdwood, Mark Brown, Matti Vaittinen, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Faruque Ansari,
	Sebastian Reichel

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.

Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Reviewed-by: Sebastian Reichel <sebastian.reichel@collabora.com>
Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.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.34.1


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

* [PATCH v14 2/6] reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings
  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-21 17:44 ` Faruque Ansari
  2026-09-21 17:44 ` [PATCH v14 3/6] reboot: add parsable tokens for power state change reasons Faruque Ansari
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 14+ messages in thread
From: Faruque Ansari @ 2026-09-21 17:44 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Guenter Roeck,
	Liam Girdwood, Mark Brown, Matti Vaittinen, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Faruque Ansari, Daniel Lezcano

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

Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
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>
Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>

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 631b55142809..90c406a4a045 100644
--- a/drivers/regulator/core.c
+++ b/drivers/regulator/core.c
@@ -5551,26 +5551,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 ac928c199829..3618986c1cab 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 d177d89fcc33..d5a45db82c3d 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/rcupdate.h>
 #include <linux/reboot.h>
 #include <linux/sched/signal.h>
@@ -50,6 +51,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;
@@ -1011,10 +1014,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.
@@ -1026,7 +1127,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);
@@ -1034,8 +1135,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.34.1


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

* [PATCH v14 3/6] reboot: add parsable tokens for power state change reasons
  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-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 ` Faruque Ansari
  2026-09-22  7:37   ` Matti Vaittinen
  2026-09-21 17:44 ` [PATCH v14 4/6] reboot: extend psc_reason with power-on and reset causes Faruque Ansari
                   ` (3 subsequent siblings)
  6 siblings, 1 reply; 14+ messages in thread
From: Faruque Ansari @ 2026-09-21 17:44 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Guenter Roeck,
	Liam Girdwood, Mark Brown, Matti Vaittinen, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Faruque Ansari

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.

Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>

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 d5a45db82c3d..e53fb31bad06 100644
--- a/kernel/reboot.c
+++ b/kernel/reboot.c
@@ -1084,34 +1084,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.34.1


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

* [PATCH v14 4/6] reboot: extend psc_reason with power-on and reset causes
  2026-09-21 17:44 [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
                   ` (2 preceding siblings ...)
  2026-09-21 17:44 ` [PATCH v14 3/6] reboot: add parsable tokens for power state change reasons Faruque Ansari
@ 2026-09-21 17:44 ` Faruque Ansari
  2026-09-22  7:45   ` Matti Vaittinen
  2026-09-21 17:44 ` [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Faruque Ansari
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 14+ messages in thread
From: Faruque Ansari @ 2026-09-21 17:44 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Guenter Roeck,
	Liam Girdwood, Mark Brown, Matti Vaittinen, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Faruque Ansari

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.

Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>

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 e53fb31bad06..5d4ecc82b35e 100644
--- a/kernel/reboot.c
+++ b/kernel/reboot.c
@@ -1102,6 +1102,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.34.1


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

* [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage
  2026-09-21 17:44 [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
                   ` (3 preceding siblings ...)
  2026-09-21 17:44 ` [PATCH v14 4/6] reboot: extend psc_reason with power-on and reset causes Faruque Ansari
@ 2026-09-21 17:44 ` Faruque Ansari
  2026-09-22  9:03   ` Matti Vaittinen
  2026-09-21 17:44 ` [PATCH v14 6/6] Documentation: Add sysfs documentation for PSCRR Faruque Ansari
  2026-09-22  6:33 ` [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework Bradley Morgan
  6 siblings, 1 reply; 14+ messages in thread
From: Faruque Ansari @ 2026-09-21 17:44 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Guenter Roeck,
	Liam Girdwood, Mark Brown, Matti Vaittinen, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Faruque Ansari, Francesco Valla

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


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

* [PATCH v14 6/6] Documentation: Add sysfs documentation for PSCRR
  2026-09-21 17:44 [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
                   ` (4 preceding siblings ...)
  2026-09-21 17:44 ` [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Faruque Ansari
@ 2026-09-21 17:44 ` 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
  6 siblings, 1 reply; 14+ messages in thread
From: Faruque Ansari @ 2026-09-21 17:44 UTC (permalink / raw)
  To: Sebastian Reichel, Benson Leung, Tzung-Bi Shih, Guenter Roeck,
	Liam Girdwood, Mark Brown, Matti Vaittinen, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Faruque Ansari

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.

Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>

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(+)

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.34.1


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

* Re: [PATCH v14 1/6] power: Extend power_on_reason.h for upcoming PSCRR framework
  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
  0 siblings, 0 replies; 14+ messages in thread
From: Matti Vaittinen @ 2026-09-22  6:02 UTC (permalink / raw)
  To: Faruque Ansari, Sebastian Reichel, Benson Leung, Tzung-Bi Shih,
	Guenter Roeck, Liam Girdwood, Mark Brown, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Sebastian Reichel

On 21/09/2026 20:44, Faruque Ansari wrote:
> 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.
> 
> Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Reviewed-by: Sebastian Reichel <sebastian.reichel@collabora.com>
> Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>

Reviewed-by: Matti Vaittinen <mazziesaccount@gmail.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 */
> 


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

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

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

* Re: [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework
  2026-09-21 17:44 [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework Faruque Ansari
                   ` (5 preceding siblings ...)
  2026-09-21 17:44 ` [PATCH v14 6/6] Documentation: Add sysfs documentation for PSCRR Faruque Ansari
@ 2026-09-22  6:33 ` Bradley Morgan
  2026-09-22  6:44   ` Faruque Ansari
  6 siblings, 1 reply; 14+ messages in thread
From: Bradley Morgan @ 2026-09-22  6:33 UTC (permalink / raw)
  To: faruque.ansari
  Cc: avaneesh.dwivedi, bleung, broonie, chrome-platform,
	daniel.lezcano, francesco, groeck, kernel, lgirdwood,
	linux-arm-msm, linux-kernel, linux-pm, lukasz.luba,
	mazziesaccount, o.rempel, rafael, rui.zhang, sebastian.reichel,
	sre, tzungbi, umang.chheda

On 21 September 2026 18:44:15 BST, Faruque Ansari
<faruque.ansari@oss.qualcomm.com> wrote:
>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 pick its storage is still
>deadlocked, and several projects already need the framework, so this v13
>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
>
>Changes in v14:
>- Ignore v13, it was mistakenly posted as a reply to Oleksij's v12
>  patch series.
>
>Changes in v13:
>- Rework pscrr_provider_register() based on review feedback from Francesco Valla:
>  move the pscrr_root check before allocation and narrow pscrr_lock scope.
>- Carry the series forward with Faruque Ansari as author and Oleksij
>  Rempel as co-developer, and vice versa where required.
>- Link to v12: https://lore.kernel.org/r/20260731095959.296056-1-o.rempel@pengutronix.de
>
>Changes in 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 in v11:
>- add missing break reported by kernel test robot <lkp@intel.com>
>
>Changes in v10:
>- add some add Reviewed-by tags
>- regulator_handle_critical: set pscr = PSCR_UNKNOWN for default case
>- make g_pscrr static
>
>Changes in 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 in 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 in v7:
>- document expected values in sysfs documentation
>- make write support optional
>
>Changes in v6:
>- add sysfs documentation
>- push core part of the reset reason into kernel/reboot.c
>
>Changes in v5:
>- fix compile with NVMEM=n and potential issues with NVMEM=m
>
>Changes in v4:
>- fix compile with CONFIG_PSCRR=n
>
>Changes in v3:
>- rework to remove devicetree dependencies
>- extend NVMEM to search devices and cells by names
>
>Changes in 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

Hey, I dont see the use to PSCRR, are you planning to ship any providers?

>
>To: Sebastian Reichel <sre@kernel.org>
>To: Benson Leung <bleung@chromium.org>
>To: Tzung-Bi Shih <tzungbi@kernel.org>
>To: Guenter Roeck <groeck@chromium.org>
>To: Liam Girdwood <lgirdwood@gmail.com>
>To: Mark Brown <broonie@kernel.org>
>To: Matti Vaittinen <mazziesaccount@gmail.com>
>To: Rafael J. Wysocki <rafael@kernel.org>
>To: Daniel Lezcano <daniel.lezcano@kernel.org>
>To: Zhang Rui <rui.zhang@intel.com>
>To: Lukasz Luba <lukasz.luba@arm.com>
>To: Oleksij Rempel <o.rempel@pengutronix.de>
>To: Pengutronix Kernel Team <kernel@pengutronix.de>
>Cc: linux-arm-msm@vger.kernel.org
>Cc: linux-pm@vger.kernel.org
>Cc: linux-kernel@vger.kernel.org
>Cc: chrome-platform@lists.linux.dev
>Cc: avaneesh.dwivedi@oss.qualcomm.com
>Cc: Umang Chheda <umang.chheda@oss.qualcomm.com>
>
>Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>
>---
>Faruque Ansari (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                                  |  11 +
> 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            | 633
> +++++++++++++++++++++++++++
> 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(-)
>---
>base-commit: 5c4d4169604b335c38bbc79bc1fc03042981fc6f
>change-id: 20260921-pscrr-framework-25f48818efe8
>
>Best regards,
>

--- Thanks!
"I'm not a very positive person" - Linus torvalds

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

* Re: [PATCH v14 0/6] power: reset: Introduce the Power State Change Reasons Recording (PSCRR) framework
  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
  0 siblings, 0 replies; 14+ messages in thread
From: Faruque Ansari @ 2026-09-22  6:44 UTC (permalink / raw)
  To: Bradley Morgan
  Cc: avaneesh.dwivedi, bleung, broonie, chrome-platform,
	daniel.lezcano, francesco, groeck, kernel, lgirdwood,
	linux-arm-msm, linux-kernel, linux-pm, lukasz.luba,
	mazziesaccount, o.rempel, rafael, rui.zhang, sebastian.reichel,
	sre, tzungbi, umang.chheda

Hi Bradley Morgan,

On 22-Sep-26 12:03 PM, Bradley Morgan wrote:
> On 21 September 2026 18:44:15 BST, Faruque Ansari
> <faruque.ansari@oss.qualcomm.com> wrote:
>> 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 pick its storage is still
>> deadlocked, and several projects already need the framework, so this v13
>> 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
>>
>> Changes in v14:
>> - Ignore v13, it was mistakenly posted as a reply to Oleksij's v12
>>   patch series.
>>
>> Changes in v13:
>> - Rework pscrr_provider_register() based on review feedback from Francesco Valla:
>>   move the pscrr_root check before allocation and narrow pscrr_lock scope.
>> - Carry the series forward with Faruque Ansari as author and Oleksij
>>   Rempel as co-developer, and vice versa where required.
>> - Link to v12: https://lore.kernel.org/r/20260731095959.296056-1-o.rempel@pengutronix.de
>>
>> Changes in 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 in v11:
>> - add missing break reported by kernel test robot <lkp@intel.com>
>>
>> Changes in v10:
>> - add some add Reviewed-by tags
>> - regulator_handle_critical: set pscr = PSCR_UNKNOWN for default case
>> - make g_pscrr static
>>
>> Changes in 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 in 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 in v7:
>> - document expected values in sysfs documentation
>> - make write support optional
>>
>> Changes in v6:
>> - add sysfs documentation
>> - push core part of the reset reason into kernel/reboot.c
>>
>> Changes in v5:
>> - fix compile with NVMEM=n and potential issues with NVMEM=m
>>
>> Changes in v4:
>> - fix compile with CONFIG_PSCRR=n
>>
>> Changes in v3:
>> - rework to remove devicetree dependencies
>> - extend NVMEM to search devices and cells by names
>>
>> Changes in 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
> 
> Hey, I dont see the use to PSCRR, are you planning to ship any providers?

I have already posted the NVMEM cell provider support as a 
separate email thread.

https://lore.kernel.org/all/20260914-pscrr-provider-v3-0-c45cb16542e8@oss.qualcomm.com/

Thanks,
Faruque Ansari


>>
>> Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>
>> ---
>> Faruque Ansari (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                                  |  11 +
>> 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            | 633
>> +++++++++++++++++++++++++++
>> 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(-)
>> ---
>> base-commit: 5c4d4169604b335c38bbc79bc1fc03042981fc6f
>> change-id: 20260921-pscrr-framework-25f48818efe8
>>
>> Best regards,
>>
> 
> --- Thanks!
> "I'm not a very positive person" - Linus torvalds


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

* Re: [PATCH v14 3/6] reboot: add parsable tokens for power state change reasons
  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
  0 siblings, 0 replies; 14+ messages in thread
From: Matti Vaittinen @ 2026-09-22  7:37 UTC (permalink / raw)
  To: Faruque Ansari, Sebastian Reichel, Benson Leung, Tzung-Bi Shih,
	Guenter Roeck, Liam Girdwood, Mark Brown, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda

On 21/09/2026 20:44, Faruque Ansari wrote:
> 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.
> 
> Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>
> 
> 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 d5a45db82c3d..e53fb31bad06 100644
> --- a/kernel/reboot.c
> +++ b/kernel/reboot.c
> @@ -1084,34 +1084,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 },
>   };

nit:
Is there a reason for having the label as a define while token is a 
plain string? It may be just me, but I would have expected the userland 
contract to be defined in a header. (It may be just my false expectation 
though!).

>   
>   /**
> - * 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;

nit:
I would feel a tad safer if the psc_reason_descs[] was always guaranteed 
to be initialized up-to PSCR_REASON_COUNT. Having the enum and 
PSCR_REASON_COUNT defined in one place, and psc_reason_descs[] 
initialized here, adds a window for: "I added new enum value, forgot to 
update the psc_reason_descs[]". Can we add a NULL check or are we on a 
very performance critical path here?

>   }
>   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;

Please, see my comment above.

> +}
> +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)) {

I suppose my comment applies here as well.

> +			*reason = i;
> +			return 0;
> +		}
> +	}
> +
> +	return -EINVAL;
> +}
> +EXPORT_SYMBOL_GPL(psc_reason_from_token);
> +
>   /**
>    * __hw_protection_trigger - Trigger an emergency system shutdown or reboot
>    *

After all these years in the business - it's hard to be as confident as 
I used to. ;) So, I won't say [aloud] that my preferred way is the only 
correct way. Please weigh my comments and decide as you see best, this 
is "good enough" for me if you don't agree with my suggestions.

Reviewed-by: Matti Vaittinen <mazziesaccount@gmail.com>

Yours,
   -- Matti

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

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

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

* Re: [PATCH v14 4/6] reboot: extend psc_reason with power-on and reset causes
  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
  0 siblings, 0 replies; 14+ messages in thread
From: Matti Vaittinen @ 2026-09-22  7:45 UTC (permalink / raw)
  To: Faruque Ansari, Sebastian Reichel, Benson Leung, Tzung-Bi Shih,
	Guenter Roeck, Liam Girdwood, Mark Brown, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda

On 21/09/2026 20:44, Faruque Ansari wrote:
> 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.
> 
> Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>
> 
> 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.

(still a nit)
We should now also ensure the psc_reason_desc[] in reboot.c stays in 
sync... (See my nits for 3/6). I have a nagging feeling this is not 
optimal :)

Is there a reason for having this as a separate patch and not merged 
with 3/6? If yes:

Reviewed-by: Matti Vaittinen <mazziesaccount@gmail.com>

>    */
>   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 e53fb31bad06..5d4ecc82b35e 100644
> --- a/kernel/reboot.c
> +++ b/kernel/reboot.c
> @@ -1102,6 +1102,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 },
>   };
>   
>   /**
> 


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

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

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

* Re: [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage
  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
  0 siblings, 0 replies; 14+ messages in thread
From: Matti Vaittinen @ 2026-09-22  9:03 UTC (permalink / raw)
  To: Faruque Ansari, Sebastian Reichel, Benson Leung, Tzung-Bi Shih,
	Guenter Roeck, Liam Girdwood, Mark Brown, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda, Francesco Valla

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! ~~

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

* Re: [PATCH v14 6/6] Documentation: Add sysfs documentation for PSCRR
  2026-09-21 17:44 ` [PATCH v14 6/6] Documentation: Add sysfs documentation for PSCRR Faruque Ansari
@ 2026-09-22  9:12   ` Matti Vaittinen
  0 siblings, 0 replies; 14+ messages in thread
From: Matti Vaittinen @ 2026-09-22  9:12 UTC (permalink / raw)
  To: Faruque Ansari, Sebastian Reichel, Benson Leung, Tzung-Bi Shih,
	Guenter Roeck, Liam Girdwood, Mark Brown, Rafael J. Wysocki,
	Daniel Lezcano, Zhang Rui, Lukasz Luba, Oleksij Rempel,
	Pengutronix Kernel Team
  Cc: linux-arm-msm, linux-pm, linux-kernel, chrome-platform,
	avaneesh.dwivedi, Umang Chheda

On 21/09/2026 20:44, Faruque Ansari wrote:
> 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.
> 
> Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Signed-off-by: Faruque Ansari <faruque.ansari@oss.qualcomm.com>

Good stuff.

Reviewed-by: Matti Vaittinen <mazziesaccount@gmail.com>


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

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

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

end of thread, other threads:[~2026-09-22  9:12 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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-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-21 17:44 ` [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage Faruque Ansari
2026-09-22  9:03   ` Matti Vaittinen
2026-09-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

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®