From 374a184ab1d36c4bb30b9873b0cf39a7ebb85062 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 12:38:42 +0530 Subject: [PATCH 01/12] FROMLIST: power: Extend power_on_reason.h for upcoming PSCRR framework 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 Signed-off-by: Oleksij Rempel Reviewed-by: Sebastian Reichel Reviewed-by: Matti Vaittinen Reviewed-by: Daniel Lezcano Signed-off-by: Faruque Ansari Link: https://lore.kernel.org/r/20261019-pscrr-framework-v17-1-69ea6cdf4731@oss.qualcomm.com --- 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 95a1ec0c403c6..13e61ace14f5b 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 */ From 137a940516ab4af3e310d2808fb3068a35a9b403 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 12:38:43 +0530 Subject: [PATCH 02/12] FROMLIST: reboot: hw_protection_trigger: use standardized numeric shutdown/reboot reasons instead of strings 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 : () Co-developed-by: Oleksij Rempel Signed-off-by: Oleksij Rempel Reviewed-by: Matti Vaittinen Acked-by: Mark Brown Acked-by: Tzung-Bi Shih Acked-by: Daniel Lezcano Reviewed-by: Bradley Morgan Reviewed-by: Daniel Lezcano Signed-off-by: Faruque Ansari Link: https://lore.kernel.org/r/20261019-pscrr-framework-v17-2-69ea6cdf4731@oss.qualcomm.com --- 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 | 80 +++++++++++++++++- kernel/reboot.c | 112 +++++++++++++++++++++++++- 6 files changed, 200 insertions(+), 22 deletions(-) diff --git a/drivers/platform/chrome/cros_ec_lpc.c b/drivers/platform/chrome/cros_ec_lpc.c index 78cfff80cdeaf..00e98d12ddbcf 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 eae756c82c262..1df707c7593b3 100644 --- a/drivers/regulator/core.c +++ b/drivers/regulator/core.c @@ -5338,26 +5338,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 5742faee8071d..25f5ba3a3a6ac 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 1310e76c0fa64..4ebbba981ab25 100644 --- a/drivers/thermal/thermal_core.c +++ b/drivers/thermal/thermal_core.c @@ -379,11 +379,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 aa08c3bbbf59a..546411d354185 100644 --- a/include/linux/reboot.h +++ b/include/linux/reboot.h @@ -178,6 +178,75 @@ 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. The EC reports the panic itself (e.g. the ACPI + * notify value ACPI_NOTIFY_CROS_EC_PANIC on ChromeOS EC hardware); the + * kernel only relays that report, see cros_ec_lpc_acpi_notify(). + * + * @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 +260,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 +275,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 61924a0560c42..29c4ee6f8cc12 100644 --- a/kernel/reboot.c +++ b/kernel/reboot.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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)) From 614bef3c1b2ad625d007d073bf701188aca40986 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 12:38:44 +0530 Subject: [PATCH 03/12] FROMLIST: reboot: add parsable tokens for power state change reasons 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 Signed-off-by: Oleksij Rempel Reviewed-by: Matti Vaittinen Reviewed-by: Bradley Morgan Reviewed-by: Daniel Lezcano Signed-off-by: Faruque Ansari Link: https://lore.kernel.org/r/20261019-pscrr-framework-v17-3-69ea6cdf4731@oss.qualcomm.com --- include/linux/reboot.h | 2 ++ kernel/reboot.c | 79 +++++++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/include/linux/reboot.h b/include/linux/reboot.h index 546411d354185..c2b3f1076e0a1 100644 --- a/include/linux/reboot.h +++ b/include/linux/reboot.h @@ -246,6 +246,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 29c4ee6f8cc12..848f43017c13a 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 reason" 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 "Invalid 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 * From e6623a7bd4ddcadf44fc15e81ed2dc253486ae0c Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 12:38:45 +0530 Subject: [PATCH 04/12] FROMLIST: reboot: extend psc_reason with power-on and reset causes 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 Signed-off-by: Oleksij Rempel Reviewed-by: Matti Vaittinen Reviewed-by: Bradley Morgan Signed-off-by: Faruque Ansari Link: https://lore.kernel.org/r/20261019-pscrr-framework-v17-4-69ea6cdf4731@oss.qualcomm.com --- include/linux/reboot.h | 36 +++++++++++++++++++++++++++++++----- kernel/reboot.c | 12 ++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/include/linux/reboot.h b/include/linux/reboot.h index c2b3f1076e0a1..a3ca139f48c15 100644 --- a/include/linux/reboot.h +++ b/include/linux/reboot.h @@ -227,9 +227,28 @@ extern void orderly_reboot(void); * notify value ACPI_NOTIFY_CROS_EC_PANIC on ChromeOS EC hardware); the * kernel only relays that report, see cros_ec_lpc_acpi_notify(). * + * @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 + * ; keep the two in sync when extending. */ enum psc_reason { PSCR_UNKNOWN, @@ -238,13 +257,20 @@ enum psc_reason { PSCR_REGULATOR_FAILURE, PSCR_OVER_TEMPERATURE, PSCR_EC_PANIC, - - /* Number of reasons */ - PSCR_REASON_COUNT, + /* 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. Must remain the last member. */ + PSCR_REASON_COUNT }; -#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); diff --git a/kernel/reboot.c b/kernel/reboot.c index 848f43017c13a..20ad76cd7ca4c 100644 --- a/kernel/reboot.c +++ b/kernel/reboot.c @@ -7,7 +7,9 @@ #define pr_fmt(fmt) "reboot: " fmt +#include #include +#include #include #include #include @@ -1102,7 +1104,17 @@ 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 }, }; +static_assert(ARRAY_SIZE(psc_reason_descs) == PSCR_REASON_COUNT, + "psc_reason_descs[] must have an entry for every psc_reason"); /** * psc_reason_to_str - Human-readable label for a power state change reason. From 733a52b1a2c9162557215fa53c712efa5cf70577 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 12:38:46 +0530 Subject: [PATCH 05/12] FROMLIST: power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage 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 Signed-off-by: Oleksij Rempel Reported-by: Francesco Valla Closes: https://lore.kernel.org/all/178749889134.204450.5740187753642158305.b4-review@b4/ Reviewed-by: Matti Vaittinen Reviewed-by: Daniel Lezcano Reviewed-by: Bradley Morgan Signed-off-by: Faruque Ansari Link: https://lore.kernel.org/r/20261019-pscrr-framework-v17-5-69ea6cdf4731@oss.qualcomm.com --- 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 | 640 +++++++++++++++++++++++++++++ include/linux/pscrr.h | 107 +++++ 7 files changed, 796 insertions(+) create mode 100644 drivers/power/reset/pscrr/Kconfig create mode 100644 drivers/power/reset/pscrr/Makefile create mode 100644 drivers/power/reset/pscrr/pscrr.c create mode 100644 include/linux/pscrr.h diff --git a/MAINTAINERS b/MAINTAINERS index b14b6c24469f3..0fedd6c89ec7e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20550,6 +20550,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 +M: Faruque Ansari +R: Pengutronix Kernel Team +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 M: Lorenzo Pieralisi diff --git a/drivers/power/reset/Kconfig b/drivers/power/reset/Kconfig index 8248895ca9038..a918c572114bd 100644 --- a/drivers/power/reset/Kconfig +++ b/drivers/power/reset/Kconfig @@ -346,3 +346,5 @@ config POWER_MLXBF This driver supports reset or low power mode handling for Mellanox BlueField. endif + +source "drivers/power/reset/pscrr/Kconfig" diff --git a/drivers/power/reset/Makefile b/drivers/power/reset/Makefile index 51da87e05ce76..9129c222a8e74 100644 --- a/drivers/power/reset/Makefile +++ b/drivers/power/reset/Makefile @@ -35,6 +35,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 0000000000000..72de82731b531 --- /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 0000000000000..e5530a8589713 --- /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 0000000000000..f7c1478a50c4b --- /dev/null +++ b/drivers/power/reset/pscrr/pscrr.c @@ -0,0 +1,640 @@ +// 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 + * Copyright (c) 2025 Qualcomm Innovation Center, Inc. All rights reserved. + */ + +#define pr_fmt(fmt) "PSCRR: " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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. + * + * The sysfs show/store callbacks below dereference ->provider without + * pscrr_lock: kobject_del() in pscrr_provider_unregister() blocks until + * every in-flight callback on this kobject returns before the directory + * (and thus ->provider) can be torn down, so a callback that is running + * always sees a live 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 -EOPNOTSUPP; + + /* + * 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 -EINVAL; + + /* "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 -EOPNOTSUPP; + + 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 "); +MODULE_AUTHOR("Faruque Ansari "); +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 0000000000000..330d6ac191bd2 --- /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 + */ + +#ifndef __PSCRR_H__ +#define __PSCRR_H__ + +#include + +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__ */ From 90d86ab7965a6f8c3cc68975040cb259eb850640 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 12:38:47 +0530 Subject: [PATCH 06/12] FROMLIST: Documentation: Add sysfs documentation for PSCRR 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 Signed-off-by: Oleksij Rempel Reviewed-by: Matti Vaittinen Reviewed-by: Daniel Lezcano Signed-off-by: Faruque Ansari Link: https://lore.kernel.org/r/20261019-pscrr-framework-v17-6-69ea6cdf4731@oss.qualcomm.com --- Documentation/ABI/testing/sysfs-kernel-pscrr | 108 +++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 Documentation/ABI/testing/sysfs-kernel-pscrr diff --git a/Documentation/ABI/testing/sysfs-kernel-pscrr b/Documentation/ABI/testing/sysfs-kernel-pscrr new file mode 100644 index 0000000000000..63aa411b7362e --- /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 +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 +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 +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 +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 +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 +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 +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 + ===== ================================================= From 48cced9daf9d701799d397c673329b9bce94f51d Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 16:56:01 +0530 Subject: [PATCH 07/12] FROMLIST: power: reset: pscrr: add kernel panic reason tracking Kernel panic resets are not recorded in NVMEM, causing subsequent boots to report PSCR_UNKNOWN and making post-mortem analysis more difficult. Register a panic notifier to preserve the shutdown reason across panic -triggered resets. Add PSCR_KERNEL_PANIC as a dedicated reset reason code and its corresponding reason string to identify kernel panic resets on subsequent boots. Link: https://lore.kernel.org/r/20260805-pscrr-reboot-reason-v4-1-54ef517084cc@oss.qualcomm.com Signed-off-by: Faruque Ansari --- drivers/power/reset/pscrr/pscrr.c | 27 +++++++++++++++++++++++++++ include/linux/power/power_on_reason.h | 1 + include/linux/reboot.h | 3 +++ kernel/reboot.c | 1 + 4 files changed, 32 insertions(+) diff --git a/drivers/power/reset/pscrr/pscrr.c b/drivers/power/reset/pscrr/pscrr.c index f7c1478a50c4b..0be293c14575a 100644 --- a/drivers/power/reset/pscrr/pscrr.c +++ b/drivers/power/reset/pscrr/pscrr.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -524,6 +525,29 @@ static struct notifier_block pscrr_reboot_nb = { .notifier_call = pscrr_reboot_notifier, }; +/* + * Panic notifier: record that the machine went down through a kernel panic, so + * the cause is visible on the next boot. Runs in atomic panic context, so the + * provider list is walked without pscrr_lock - providers no longer come or go + * once the machine is going down. + */ +static int pscrr_panic_notifier(struct notifier_block *nb, + unsigned long action, void *unused) +{ + struct pscrr_provider_dir *dir; + + set_psc_reason(PSCR_KERNEL_PANIC); + + list_for_each_entry(dir, &pscrr_dirs, node) + pscrr_do_record(dir, get_psc_reason()); + + return NOTIFY_DONE; +} + +static struct notifier_block pscrr_panic_nb = { + .notifier_call = pscrr_panic_notifier, +}; + /*----------------------------------------------------------------------*/ /* Built-in provider: device-tree /chosen/reset-source */ /*----------------------------------------------------------------------*/ @@ -616,6 +640,8 @@ static int __init pscrr_core_init(void) return ret; } + atomic_notifier_chain_register(&panic_notifier_list, &pscrr_panic_nb); + pscrr_register_reset_source(); return 0; @@ -624,6 +650,7 @@ static int __init pscrr_core_init(void) static void __exit pscrr_core_exit(void) { pscrr_provider_unregister(&pscrr_reset_source_provider); + atomic_notifier_chain_unregister(&panic_notifier_list, &pscrr_panic_nb); unregister_reboot_notifier(&pscrr_reboot_nb); kobject_put(pscrr_root); pscrr_root = NULL; diff --git a/include/linux/power/power_on_reason.h b/include/linux/power/power_on_reason.h index 13e61ace14f5b..8c99bdd750215 100644 --- a/include/linux/power/power_on_reason.h +++ b/include/linux/power/power_on_reason.h @@ -20,5 +20,6 @@ #define POWER_ON_REASON_OVER_TEMPERATURE "over temperature" #define POWER_ON_REASON_EC_PANIC "EC panic" #define POWER_ON_REASON_EXTERNAL "external reset" +#define POWER_ON_REASON_KERNEL_PANIC "kernel panic" #endif /* POWER_ON_REASON_H */ diff --git a/include/linux/reboot.h b/include/linux/reboot.h index a3ca139f48c15..7c8d405a156d7 100644 --- a/include/linux/reboot.h +++ b/include/linux/reboot.h @@ -243,6 +243,8 @@ extern void orderly_reboot(void); * * @PSCR_XTAL_FAIL: Reset caused by a crystal oscillator failure. * + * @PSCR_KERNEL_PANIC: Reset that followed a kernel panic. + * * @PSCR_REASON_COUNT: Number of defined power state change reasons. This * value is useful for range checking and potential future extensions * while maintaining compatibility. @@ -266,6 +268,7 @@ enum psc_reason { PSCR_RESET_BUTTON, PSCR_CPU_CLK_FAIL, PSCR_XTAL_FAIL, + PSCR_KERNEL_PANIC, /* Number of reasons. Must remain the last member. */ PSCR_REASON_COUNT diff --git a/kernel/reboot.c b/kernel/reboot.c index 20ad76cd7ca4c..00db99cad7fe9 100644 --- a/kernel/reboot.c +++ b/kernel/reboot.c @@ -1112,6 +1112,7 @@ static const struct psc_reason_desc psc_reason_descs[] = { [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 }, + [PSCR_KERNEL_PANIC] = { "kernel-panic", POWER_ON_REASON_KERNEL_PANIC }, }; static_assert(ARRAY_SIZE(psc_reason_descs) == PSCR_REASON_COUNT, "psc_reason_descs[] must have an entry for every psc_reason"); From 5b07360a70a787e545bbfa4b1743fcde3abb313a Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 16:56:18 +0530 Subject: [PATCH 08/12] FROMLIST: power: reset: pscrr: add watchdog pretimeout reason tracking Watchdog pretimeout resets are not recorded with a dedicated reason, causing subsequent boots to report PSCR_UNKNOWN and making it difficult to distinguish them from other unexpected resets. Add PSCR_WATCHDOG_PRETIMEOUT as a dedicated reason code and prevent the panic notifier from overwriting a watchdog pretimeout reason with PSCR_KERNEL_PANIC when the pretimeout governor triggers a panic. Link: https://lore.kernel.org/r/20260805-pscrr-reboot-reason-v4-2-54ef517084cc@oss.qualcomm.com Signed-off-by: Faruque Ansari --- drivers/power/reset/pscrr/pscrr.c | 11 +++++++---- include/linux/power/power_on_reason.h | 1 + include/linux/reboot.h | 4 ++++ kernel/reboot.c | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/drivers/power/reset/pscrr/pscrr.c b/drivers/power/reset/pscrr/pscrr.c index 0be293c14575a..8fb795af2cc5c 100644 --- a/drivers/power/reset/pscrr/pscrr.c +++ b/drivers/power/reset/pscrr/pscrr.c @@ -534,12 +534,15 @@ static struct notifier_block pscrr_reboot_nb = { static int pscrr_panic_notifier(struct notifier_block *nb, unsigned long action, void *unused) { - struct pscrr_provider_dir *dir; - set_psc_reason(PSCR_KERNEL_PANIC); + /* + * Do not overwrite a watchdog pretimeout reason already set by the + * pretimeout path before it triggered this panic. + */ + if (get_psc_reason() != PSCR_WATCHDOG_PRETIMEOUT) + set_psc_reason(PSCR_KERNEL_PANIC); - list_for_each_entry(dir, &pscrr_dirs, node) - pscrr_do_record(dir, get_psc_reason()); + pscrr_record_current(); return NOTIFY_DONE; } diff --git a/include/linux/power/power_on_reason.h b/include/linux/power/power_on_reason.h index 8c99bdd750215..8e3889fa43ef9 100644 --- a/include/linux/power/power_on_reason.h +++ b/include/linux/power/power_on_reason.h @@ -21,5 +21,6 @@ #define POWER_ON_REASON_EC_PANIC "EC panic" #define POWER_ON_REASON_EXTERNAL "external reset" #define POWER_ON_REASON_KERNEL_PANIC "kernel panic" +#define POWER_ON_REASON_WATCHDOG_PRETIMEOUT "watchdog pretimeout" #endif /* POWER_ON_REASON_H */ diff --git a/include/linux/reboot.h b/include/linux/reboot.h index 7c8d405a156d7..f313e326b2a65 100644 --- a/include/linux/reboot.h +++ b/include/linux/reboot.h @@ -245,6 +245,9 @@ extern void orderly_reboot(void); * * @PSCR_KERNEL_PANIC: Reset that followed a kernel panic. * + * @PSCR_WATCHDOG_PRETIMEOUT: Recorded when a watchdog pretimeout fires, before + * the pretimeout governor brings the system down. + * * @PSCR_REASON_COUNT: Number of defined power state change reasons. This * value is useful for range checking and potential future extensions * while maintaining compatibility. @@ -269,6 +272,7 @@ enum psc_reason { PSCR_CPU_CLK_FAIL, PSCR_XTAL_FAIL, PSCR_KERNEL_PANIC, + PSCR_WATCHDOG_PRETIMEOUT, /* Number of reasons. Must remain the last member. */ PSCR_REASON_COUNT diff --git a/kernel/reboot.c b/kernel/reboot.c index 00db99cad7fe9..0474d5a307994 100644 --- a/kernel/reboot.c +++ b/kernel/reboot.c @@ -1113,6 +1113,7 @@ static const struct psc_reason_desc psc_reason_descs[] = { [PSCR_CPU_CLK_FAIL] = { "cpu-clock-failure", POWER_ON_REASON_CPU_CLK_FAIL }, [PSCR_XTAL_FAIL] = { "crystal-failure", POWER_ON_REASON_XTAL_FAIL }, [PSCR_KERNEL_PANIC] = { "kernel-panic", POWER_ON_REASON_KERNEL_PANIC }, + [PSCR_WATCHDOG_PRETIMEOUT] = { "watchdog-pretimeout", POWER_ON_REASON_WATCHDOG_PRETIMEOUT }, }; static_assert(ARRAY_SIZE(psc_reason_descs) == PSCR_REASON_COUNT, "psc_reason_descs[] must have an entry for every psc_reason"); From 3be48f6296d6aa51d43737aeade669fa408dcb9b Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Thu, 24 Sep 2026 16:56:32 +0530 Subject: [PATCH 09/12] FROMLIST: watchdog: pretimeout: update PSC reason in panic pretimeout governor Update the PSC reset reason by invoking set_psc_reason(PSCR_WATCHDOG_PRETIMEOUT) from the panic governor pretimeout handler before calling panic(), so the reset cause is committed to persistent storage before the system goes down. Link: https://lore.kernel.org/r/20260805-pscrr-reboot-reason-v4-3-54ef517084cc@oss.qualcomm.com Acked-by: Guenter Roeck Signed-off-by: Faruque Ansari --- drivers/watchdog/pretimeout_panic.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/watchdog/pretimeout_panic.c b/drivers/watchdog/pretimeout_panic.c index 8c3ac674dc456..819487b5e6fee 100644 --- a/drivers/watchdog/pretimeout_panic.c +++ b/drivers/watchdog/pretimeout_panic.c @@ -5,6 +5,7 @@ #include #include +#include #include #include "watchdog_pretimeout.h" @@ -17,6 +18,7 @@ */ static void pretimeout_panic(struct watchdog_device *wdd) { + set_psc_reason(PSCR_WATCHDOG_PRETIMEOUT); panic("watchdog pretimeout event\n"); } From efe448e25bf81697e7ff36eaaa2712c01dd96ae8 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Mon, 14 Sep 2026 10:08:45 +0530 Subject: [PATCH 10/12] FROMLIST: dt-bindings: nvmem: layouts: Add PSCRR NVMEM layout Add a schema for the PSCRR NVMEM layout, which persists the power state change reason in a single NVMEM cell so it survives a power cycle and can be read back on the next boot. Link: https://lore.kernel.org/r/20261023-pscrr-provider-v4-1-e70fabb7e927@oss.qualcomm.com Reviewed-by: Rob Herring (Arm) Signed-off-by: Faruque Ansari --- .../bindings/nvmem/layouts/nvmem-layout.yaml | 1 + .../bindings/nvmem/layouts/pscrr-nvmem.yaml | 87 +++++++++++++++++++ MAINTAINERS | 1 + 3 files changed, 89 insertions(+) create mode 100644 Documentation/devicetree/bindings/nvmem/layouts/pscrr-nvmem.yaml diff --git a/Documentation/devicetree/bindings/nvmem/layouts/nvmem-layout.yaml b/Documentation/devicetree/bindings/nvmem/layouts/nvmem-layout.yaml index 3825070606511..24545db614167 100644 --- a/Documentation/devicetree/bindings/nvmem/layouts/nvmem-layout.yaml +++ b/Documentation/devicetree/bindings/nvmem/layouts/nvmem-layout.yaml @@ -21,6 +21,7 @@ oneOf: - $ref: fixed-layout.yaml - $ref: kontron,sl28-vpd.yaml - $ref: onie,tlv-layout.yaml + - $ref: pscrr-nvmem.yaml - $ref: u-boot,env.yaml properties: diff --git a/Documentation/devicetree/bindings/nvmem/layouts/pscrr-nvmem.yaml b/Documentation/devicetree/bindings/nvmem/layouts/pscrr-nvmem.yaml new file mode 100644 index 0000000000000..118669d33f5c2 --- /dev/null +++ b/Documentation/devicetree/bindings/nvmem/layouts/pscrr-nvmem.yaml @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/nvmem/layouts/pscrr-nvmem.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: NVMEM layout for the Power State Change Reason recorder (PSCRR) + +maintainers: + - Faruque Ansari + - Oleksij Rempel + +description: + An NVMEM layout for a Power State Change Reason (PSCRR) recorder. The + reboot or shutdown cause is persisted into a single small NVMEM cell, + such as an RTC scratch register, a PMIC SDAM (Scratch Data Access Memory) + byte, or an EEPROM byte, so the reason survives a power cycle and can be + read back on the next boot. + +select: false + +properties: + compatible: + const: pscrr-nvmem + + "#address-cells": + const: 1 + + "#size-cells": + const: 1 + +patternProperties: + "^.*@[0-9a-f]+$": + type: object + description: + The single NVMEM cell used to persist the power state change reason. + + properties: + reg: + items: + - items: + - description: Offset, in bytes, of the cell within the + parent NVMEM device. + - const: 1 + description: + Offset and size, in bytes, of the cell within the parent NVMEM + device. The reason value occupies a single byte, so the size + must be 1. + + bits: + $ref: /schemas/types.yaml#/definitions/uint32-array + items: + - minimum: 0 + maximum: 7 + description: + Bit offset within the byte addressed by reg. + - minimum: 1 + maximum: 8 + description: + Number of bits used to store the reason. Must be wide enough + to hold every value of enum psc_reason (currently 4 bits or + more). + description: + Optional. When absent, the whole byte addressed by reg is used. + + required: + - reg + + additionalProperties: false + +required: + - compatible + +additionalProperties: false + +examples: + - | + nvmem-layout { + compatible = "pscrr-nvmem"; + #address-cells = <1>; + #size-cells = <1>; + + reboot-reason-persist@49 { + reg = <0x49 0x1>; + bits = <0 4>; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 0fedd6c89ec7e..f92f229a92c75 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20557,6 +20557,7 @@ R: Pengutronix Kernel Team L: linux-pm@vger.kernel.org S: Maintained F: Documentation/ABI/testing/sysfs-kernel-pscrr +F: Documentation/devicetree/bindings/nvmem/layouts/pscrr-nvmem.yaml F: drivers/power/reset/pscrr/ F: include/linux/pscrr.h F: tools/testing/selftests/pscrr/ From 4d9778dc834f68865c3585cd5ba8e6105ce39524 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Mon, 14 Sep 2026 10:08:46 +0530 Subject: [PATCH 11/12] FROMLIST: power: reset: Add PSCRR NVMEM recorder Record the power state change reason into a NVMEM cell (a PMIC SDAM byte, RTC scratch register, or EEPROM) and read it back on the next boot, so the cause survives a power cycle. The recorder is implemented as an NVMEM layout driver: it owns the single cell described by its layout binding and adds it to the parent NVMEM device, rather than acting as a standalone consumer of someone else's cell. Link: https://lore.kernel.org/r/20261023-pscrr-provider-v4-2-e70fabb7e927@oss.qualcomm.com Co-developed-by: Oleksij Rempel Signed-off-by: Oleksij Rempel Signed-off-by: Faruque Ansari --- MAINTAINERS | 1 + drivers/nvmem/layouts/Kconfig | 10 ++ drivers/nvmem/layouts/Makefile | 1 + drivers/nvmem/layouts/pscrr-nvmem.c | 270 ++++++++++++++++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 drivers/nvmem/layouts/pscrr-nvmem.c diff --git a/MAINTAINERS b/MAINTAINERS index f92f229a92c75..54458b9bfbdd4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20558,6 +20558,7 @@ L: linux-pm@vger.kernel.org S: Maintained F: Documentation/ABI/testing/sysfs-kernel-pscrr F: Documentation/devicetree/bindings/nvmem/layouts/pscrr-nvmem.yaml +F: drivers/nvmem/layouts/pscrr-nvmem.c F: drivers/power/reset/pscrr/ F: include/linux/pscrr.h F: tools/testing/selftests/pscrr/ diff --git a/drivers/nvmem/layouts/Kconfig b/drivers/nvmem/layouts/Kconfig index 5e586dfebe47b..fcd234b8ad885 100644 --- a/drivers/nvmem/layouts/Kconfig +++ b/drivers/nvmem/layouts/Kconfig @@ -37,6 +37,16 @@ config NVMEM_LAYOUT_U_BOOT_ENV If unsure, say N. +config NVMEM_LAYOUT_PSCRR + tristate "PSCRR NVMEM recorder provider" + depends on PSCRR + help + PSCRR recorder that stores the power state change reason in a + small NVMEM cell (such as an RTC scratch register) and reads it + back on the next boot, so the cause survives a power cycle. + + If unsure, say N. + endmenu endif diff --git a/drivers/nvmem/layouts/Makefile b/drivers/nvmem/layouts/Makefile index dd6c6c70b1a92..0079e935c4adb 100644 --- a/drivers/nvmem/layouts/Makefile +++ b/drivers/nvmem/layouts/Makefile @@ -7,3 +7,4 @@ obj-$(CONFIG_NVMEM_LAYOUTS) += fixed-layout.o obj-$(CONFIG_NVMEM_LAYOUT_SL28_VPD) += sl28vpd.o obj-$(CONFIG_NVMEM_LAYOUT_ONIE_TLV) += onie-tlv.o obj-$(CONFIG_NVMEM_LAYOUT_U_BOOT_ENV) += u-boot-env.o +obj-$(CONFIG_NVMEM_LAYOUT_PSCRR) += pscrr-nvmem.o diff --git a/drivers/nvmem/layouts/pscrr-nvmem.c b/drivers/nvmem/layouts/pscrr-nvmem.c new file mode 100644 index 0000000000000..ba501f126117c --- /dev/null +++ b/drivers/nvmem/layouts/pscrr-nvmem.c @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * pscrr-nvmem.c - NVMEM layout driver for PSCRR + * + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * Copyright (C) 2025 Pengutronix, Oleksij Rempel + */ + +#include +#include +#include +#include +#include +#include +#include + +struct pscrr_nvmem { + struct nvmem_device *nvmem; + u32 offset; /* byte offset of the cell, from reg[0] */ + u32 bit_offset; /* 0 when the cell has no "bits" property */ + u32 nbits; /* BITS_PER_BYTE when the cell has no "bits" property */ + enum psc_reason reason; +}; + +static inline u8 pscrr_nvmem_mask(struct pscrr_nvmem *priv) +{ + return (u8)GENMASK(priv->bit_offset + priv->nbits - 1, priv->bit_offset); +} + +static int pscrr_nvmem_raw_read(struct pscrr_nvmem *priv, u8 *raw) +{ + int ret; + + ret = nvmem_device_read(priv->nvmem, priv->offset, sizeof(*raw), raw); + + return ret < 0 ? ret : 0; +} + +static int pscrr_nvmem_raw_write(struct pscrr_nvmem *priv, u8 raw) +{ + int ret; + + ret = nvmem_device_write(priv->nvmem, priv->offset, sizeof(raw), &raw); + + return ret < 0 ? ret : 0; +} + +static int pscrr_nvmem_cell_read(struct pscrr_nvmem *priv, u8 *out) +{ + u8 raw, mask = pscrr_nvmem_mask(priv); + int ret; + + ret = pscrr_nvmem_raw_read(priv, &raw); + if (ret) + return ret; + + *out = (raw & mask) >> priv->bit_offset; + + return 0; +} + +static int pscrr_nvmem_cell_write(struct pscrr_nvmem *priv, u8 val) +{ + u8 raw, mask = pscrr_nvmem_mask(priv); + int ret; + + /* Sub-byte cell: preserve the neighbouring bits, read-modify-write. */ + if (priv->nbits < BITS_PER_BYTE) { + ret = pscrr_nvmem_raw_read(priv, &raw); + if (ret) + return ret; + } else { + raw = 0; + } + + raw = (raw & ~mask) | ((val << priv->bit_offset) & mask); + + return pscrr_nvmem_raw_write(priv, raw); +} + +static int pscrr_nvmem_read_reasons(struct pscrr_provider *p, + unsigned long *reasons) +{ + struct pscrr_nvmem *priv = p->priv; + + /* + * Report the reason latched at probe (or overwritten by a later + * record); the cell itself was cleared at probe, so it is not read + * live here. + */ + set_bit(READ_ONCE(priv->reason), reasons); + + return 0; +} + +static int pscrr_nvmem_write_reason(struct pscrr_provider *p, + enum psc_reason reason) +{ + struct pscrr_nvmem *priv = p->priv; + int ret; + + if (reason >= PSCR_REASON_COUNT) + return -EINVAL; + + ret = pscrr_nvmem_cell_write(priv, reason); + if (ret) + return ret; + + WRITE_ONCE(priv->reason, reason); + + return 0; +} + +static const struct pscrr_provider_ops pscrr_nvmem_ops = { + .read_reasons = pscrr_nvmem_read_reasons, + .write_reason = pscrr_nvmem_write_reason, +}; + +static struct device_node *pscrr_nvmem_get_cell(struct device *dev, + struct nvmem_device *nvmem, + u32 reg[2]) +{ + struct device_node *layout_np, *cell_np = NULL; + + layout_np = of_nvmem_layout_get_container(nvmem); + if (!layout_np) + return ERR_PTR(-ENOENT); + + for_each_child_of_node_scoped(layout_np, child) { + if (of_property_read_u32_array(child, "reg", reg, 2)) + continue; + + /* + * The binding describes a single reason cell. Reject a layout + * that lists more than one rather than silently using the first. + */ + if (cell_np) { + of_node_put(cell_np); + of_node_put(layout_np); + return ERR_PTR(dev_err_probe(dev, -EINVAL, + "expected a single pscr nvmem cell\n")); + } + + cell_np = of_node_get(child); + } + of_node_put(layout_np); + + return cell_np ?: ERR_PTR(dev_err_probe(dev, -ENOENT, + "missing pscr nvmem cell\n")); +} + +static int pscrr_nvmem_add_cells(struct nvmem_layout *layout) +{ + struct device *dev = &layout->dev; + struct pscrr_nvmem *priv; + struct device_node *cell_np; + struct nvmem_cell_info info = { }; + u32 reg[2], bits[2]; + u8 val; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->nvmem = layout->nvmem; + + cell_np = pscrr_nvmem_get_cell(dev, priv->nvmem, reg); + if (IS_ERR(cell_np)) + return PTR_ERR(cell_np); + + /* The reason is a single byte; a bit cell still occupies one. */ + if (reg[1] != sizeof(val)) { + ret = dev_err_probe(dev, -EINVAL, + "unsupported pscr nvmem cell size\n"); + goto err_put; + } + priv->offset = reg[0]; + + if (!of_property_read_u32_array(cell_np, "bits", bits, ARRAY_SIZE(bits))) { + priv->bit_offset = bits[0]; + priv->nbits = bits[1]; + } else { + priv->bit_offset = 0; + priv->nbits = BITS_PER_BYTE; + } + + if (priv->bit_offset + priv->nbits > BITS_PER_BYTE * reg[1]) { + ret = dev_err_probe(dev, -EINVAL, "bits exceed cell size\n"); + goto err_put; + } + + /* PSCR_REASON_COUNT must fit in the allotted bits. */ + if (PSCR_REASON_COUNT > (1U << priv->nbits)) { + ret = dev_err_probe(dev, -EINVAL, + "too few bits for the reason range\n"); + goto err_put; + } + + ret = pscrr_nvmem_cell_read(priv, &val); + if (ret) { + dev_err_probe(dev, ret, "failed to read the pscr nvmem cell\n"); + goto err_put; + } + + priv->reason = val < PSCR_REASON_COUNT ? val : PSCR_UNKNOWN; + + ret = pscrr_nvmem_cell_write(priv, PSCR_UNKNOWN); + if (ret) { + dev_err_probe(dev, ret, "failed to clear the pscr nvmem cell\n"); + goto err_put; + } + + info.name = kasprintf(GFP_KERNEL, "%pOFn", cell_np); + if (!info.name) { + ret = -ENOMEM; + goto err_put; + } + info.offset = reg[0]; + info.bytes = reg[1]; + info.bit_offset = priv->bit_offset; + info.nbits = priv->nbits; + info.np = cell_np; + + ret = nvmem_add_one_cell(priv->nvmem, &info); + kfree(info.name); + if (ret) + goto err_put; + + return PTR_ERR_OR_ZERO(devm_pscrr_provider_register(dev, "nvmem", + &pscrr_nvmem_ops, NULL, + priv)); + +err_put: + of_node_put(cell_np); + return ret; +} + +static int pscrr_nvmem_probe(struct nvmem_layout *layout) +{ + layout->add_cells = pscrr_nvmem_add_cells; + + return nvmem_layout_register(layout); +} + +static void pscrr_nvmem_remove(struct nvmem_layout *layout) +{ + nvmem_layout_unregister(layout); +} + +static const struct of_device_id pscrr_nvmem_of_match[] = { + { .compatible = "pscrr-nvmem" }, + { } +}; +MODULE_DEVICE_TABLE(of, pscrr_nvmem_of_match); + +static struct nvmem_layout_driver pscrr_nvmem_layout = { + .driver = { + .name = "pscrr-nvmem", + .of_match_table = pscrr_nvmem_of_match, + }, + .probe = pscrr_nvmem_probe, + .remove = pscrr_nvmem_remove, +}; +module_nvmem_layout_driver(pscrr_nvmem_layout); + +MODULE_AUTHOR("Oleksij Rempel "); +MODULE_AUTHOR("Faruque Ansari "); +MODULE_DESCRIPTION("NVMEM recorder provider for PSCRR"); +MODULE_LICENSE("GPL"); From fd57c6093752039e2d6ec2604ee8e4c89b1aff62 Mon Sep 17 00:00:00 2001 From: Faruque Ansari Date: Mon, 14 Sep 2026 10:08:47 +0530 Subject: [PATCH 12/12] FROMLIST: arm64: dts: qcom: lemans: Add PSCRR NVMEM recorder node The PSCRR NVMEM recorder needs a persistent storage cell to save the power state change reason before shutdown or reset. Without a described NVMEM cell, the recorder has no platform storage where it can preserve that reason for the next boot. Add a pscrr-nvmem layout under the PMIC SDAM and describe a one-byte cell at offset 0x49. The recorder uses the lower four bits of this cell to persist the power state change reason across resets. Link: https://lore.kernel.org/r/20261023-pscrr-provider-v4-3-e70fabb7e927@oss.qualcomm.com Reviewed-by: Abel Vesa Signed-off-by: Faruque Ansari --- arch/arm64/boot/dts/qcom/lemans-pmics.dtsi | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/lemans-pmics.dtsi b/arch/arm64/boot/dts/qcom/lemans-pmics.dtsi index 6caec3e4df4bb..db71c51b48b46 100644 --- a/arch/arm64/boot/dts/qcom/lemans-pmics.dtsi +++ b/arch/arm64/boot/dts/qcom/lemans-pmics.dtsi @@ -186,6 +186,17 @@ reg = <0x48 0x1>; bits = <1 7>; }; + + nvmem-layout { + compatible = "pscrr-nvmem"; + #address-cells = <1>; + #size-cells = <1>; + + reboot_reason_persist: reboot-reason-persist@49 { + reg = <0x49 0x1>; + bits = <0 6>; + }; + }; }; };