[PATCH v12 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage
Oleksij Rempel <[email protected]> Fri, 31 Jul 2026 11:59:58 +0200
| Newsgroups | dev.linux.lists.chrome-platform,org.kernel.vger.linux-kernel,org.kernel.vger.linux-pm |
|---|---|
| Message-ID | <[email protected]> |
Introduce the Power State Change Reasons Recording (PSCRR) framework: a generic mechanism to record why the last power state change (shutdown or reboot) happened - under-voltage, thermal, watchdog, software-triggered, etc. - so a postmortem reason survives a reboot or an abrupt power loss. PSCRR is built around providers. A provider is either a hardware reason source (a PMIC, SoC reset controller or watchdog exposing a reset cause) or a recorder that persists the reason across a power cycle (an NVMEM or RTC scratch cell). Each provider gets a directory under /sys/kernel/pscrr/providerN/ exposing its name, backing device, the set of observed reasons (as tokens), its capabilities, the reasons it supports and - for recorders - a record policy. The reason set is deliberately not collapsed to a single winning cause, since resets are often multi-causal. Reasons are the numeric enum psc_reason values from reboot.h, shared with the POWER_ON_REASON_* vocabulary, so they store compactly in small battery-backed cells. The current reason (get/set_psc_reason(), set by the thermal/regulator/hw_protection paths) is written to every recorder from the reboot notifier. Signed-off-by: Oleksij Rempel <[email protected]> --- changes v12: - rework into a multi-provider framework: one sysfs directory per provider instead of a single backend - add per-provider capabilities, supported_reasons and record policy - register providers through a const ops table via (devm_)pscrr_provider_register() - report reasons as parsable tokens - move the sources under drivers/power/reset/pscrr/ - add a MAINTAINERS entry; PSCRR depends on POWER_RESET - drop Reviewed-by: Matti Vaittinen; the framework was substantially reworked since it was given changes v10: - make g_pscrr static changes v8: - introduce struct pscrr_core to encapsulate backend and locking - replace global mutex and backend pointer with centralized pscrr_core - use DEFINE_GUARD() + guard(g_pscrr) for scoped mutex locking - simplify code using local backend pointer after locking - prepare code structure for future multi-backend support changes v7: - make write_reason optional - update documentation changes v6: - move enum pscr_reason to kernel reboot core - move reason storage to reboot core - add locking --- MAINTAINERS | 10 + drivers/power/reset/Kconfig | 2 + drivers/power/reset/Makefile | 1 + drivers/power/reset/pscrr/Kconfig | 33 ++ drivers/power/reset/pscrr/Makefile | 2 + drivers/power/reset/pscrr/pscrr.c | 634 +++++++++++++++++++++++++++++ include/linux/pscrr.h | 107 +++++ 7 files changed, 789 insertions(+) create mode 100644 drivers/power/reset/pscrr/Kconfig create mode 100644 drivers/power/reset/pscrr/Makefile create mode 100644 drivers/power/reset/pscrr/pscrr.c create mode 100644 include/linux/pscrr.h diff --git a/MAINTAINERS b/MAINTAINERS index 15011f5752a9..35195d9d4c4e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -21526,6 +21526,16 @@ F: Documentation/devicetree/bindings/connector/pcie-m2-e-connector.yaml F: Documentation/devicetree/bindings/connector/pcie-m2-m-connector.yaml F: drivers/power/sequencing/pwrseq-pcie-m2.c +POWER STATE CHANGE REASON RECORDING (PSCRR) +M: Oleksij Rempel <[email protected]> +R: Pengutronix Kernel Team <[email protected]> +L: [email protected] +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 <[email protected]> M: Lorenzo Pieralisi <[email protected]> diff --git a/drivers/power/reset/Kconfig b/drivers/power/reset/Kconfig index 124afb99febe..83e87794d976 100644 --- a/drivers/power/reset/Kconfig +++ b/drivers/power/reset/Kconfig @@ -365,3 +365,5 @@ config POWER_RESET_QEMU_VIRT_CTRL provides this controller, such as the m68k virt machine. endif + +source "drivers/power/reset/pscrr/Kconfig" diff --git a/drivers/power/reset/Makefile b/drivers/power/reset/Makefile index d7ae97241a83..60cf967cde21 100644 --- a/drivers/power/reset/Makefile +++ b/drivers/power/reset/Makefile @@ -36,6 +36,7 @@ obj-$(CONFIG_POWER_RESET_KEYSTONE) += keystone-reset.o obj-$(CONFIG_POWER_RESET_SYSCON) += syscon-reboot.o obj-$(CONFIG_POWER_RESET_SYSCON_POWEROFF) += syscon-poweroff.o obj-$(CONFIG_POWER_RESET_RMOBILE) += rmobile-reset.o +obj-$(CONFIG_PSCRR) += pscrr/ obj-$(CONFIG_REBOOT_MODE) += reboot-mode.o obj-$(CONFIG_SYSCON_REBOOT_MODE) += syscon-reboot-mode.o obj-$(CONFIG_POWER_RESET_SC27XX) += sc27xx-poweroff.o diff --git a/drivers/power/reset/pscrr/Kconfig b/drivers/power/reset/pscrr/Kconfig new file mode 100644 index 000000000000..72de82731b53 --- /dev/null +++ b/drivers/power/reset/pscrr/Kconfig @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig PSCRR + bool "Power State Change Reasons Recording (PSCRR) Framework" + depends on POWER_RESET + help + Enables the Power State Change Reasons Recording (PSCRR) framework. + + PSCRR records why the system last shut down or rebooted into + non-volatile storage, so the reason survives the reset and can be + read by the bootloader or early user space on the next boot. Reasons + come from software (thermal or regulator hardware-protection events, + a watchdog pretimeout, a kernel panic, a controlled reboot) or from + hardware reset-cause registers (PMIC, SoC reset controller, watchdog). + + It helps whenever the real cause would otherwise be lost or ambiguous + on the next boot, for example: + + - the reset is abrupt and block storage (eMMC/NAND) may not survive + it; on systems that can detect the event in time - such as an + imminent power loss with a short backup-capacitor window - the + reason can still be persisted first; + + - the hardware does not preserve the cause and the boot looks like a + plain power-on reset, e.g. a watchdog pretimeout that escalates + into a panic reboot would otherwise be indistinguishable from a + POR. + + The framework is extensible and exposes software-defined and + hardware-reported reasons through one interface. Sudden power cuts, + CPU freezes or other uncontrolled resets may still go unrecorded + unless hardware provides the reset cause. + + If unsure, say N. diff --git a/drivers/power/reset/pscrr/Makefile b/drivers/power/reset/pscrr/Makefile new file mode 100644 index 000000000000..e5530a858971 --- /dev/null +++ b/drivers/power/reset/pscrr/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_PSCRR) += pscrr.o diff --git a/drivers/power/reset/pscrr/pscrr.c b/drivers/power/reset/pscrr/pscrr.c new file mode 100644 index 000000000000..6f23f4c4f590 --- /dev/null +++ b/drivers/power/reset/pscrr/pscrr.c @@ -0,0 +1,634 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * pscrr.c - Core Power State Change Reason Recording + * + * PSCRR records why the last power state change (shutdown/reboot) happened. + * Reasons come from providers: hardware reset-cause registers (PMIC, SoC reset + * controller, watchdog), persistent recorders (NVMEM/RTC scratch), the + * bootloader's device-tree /chosen/reset-source (a built-in provider here), or + * test stubs. Each provider gets a directory under /sys/kernel/pscrr/ and reports + * the full set of reasons it observed - the picture is deliberately not + * collapsed to a single "winning" cause, since resets are often multi-causal. + * + * Sysfs (per provider, under /sys/kernel/pscrr/providerN/): + * name ro human label of the provider + * device symlink to the backing device (if any) + * reason the provider's reason set, as tokens; writable (record + * one reason) when the provider supports it + * caps ro non-default capabilities ("writable"); empty for a + * read-only, single-slot provider + * supported_reasons ro reasons this provider can report or record + * record_policy recorders only: keep the "first" or "last" reason + * recorded in a power cycle + * + * The kernel keeps the first (root cause) and last power-state-change reason + * (get_psc_first_reason()/get_psc_reason(), set by the thermal, regulator and + * hw_protection paths). At reboot each recorder is given the first or the last + * reason according to its record policy. + * + * Copyright (C) 2025 Pengutronix, Oleksij Rempel <[email protected]> + */ + +#define pr_fmt(fmt) "PSCRR: " fmt + +#include <linux/bitmap.h> +#include <linux/device.h> +#include <linux/idr.h> +#include <linux/kernel.h> +#include <linux/kobject.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/notifier.h> +#include <linux/of.h> +#include <linux/pscrr.h> +#include <linux/reboot.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/sysfs.h> + +static struct kobject *pscrr_root; +static DEFINE_MUTEX(pscrr_lock); +static LIST_HEAD(pscrr_dirs); +static DEFINE_IDA(pscrr_ida); + +/* + * Record policy: when several reasons are recorded in one power cycle, keep the + * first (root cause) or overwrite with the last. Global, tunable via sysfs. + */ +enum pscrr_record_policy { + PSCRR_RECORD_FIRST, + PSCRR_RECORD_LAST, +}; + +/* + * Per-provider sysfs directory. Core-owned and self-freeing on kobject_put(), + * so its lifetime is decoupled from the caller-owned struct pscrr_provider. + */ +struct pscrr_provider_dir { + struct kobject kobj; + struct pscrr_provider *provider; + struct list_head node; + int id; + enum pscrr_record_policy policy; /* single-slot recorder: keep first/last */ + bool recorded; /* a reason was recorded this power cycle */ +}; + +static inline struct pscrr_provider_dir *to_pscrr_dir(struct kobject *kobj) +{ + return container_of(kobj, struct pscrr_provider_dir, kobj); +} + +/*----------------------------------------------------------------------*/ +/* Per-provider attributes */ +/*----------------------------------------------------------------------*/ + +static ssize_t name_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", to_pscrr_dir(kobj)->provider->name); +} + +static struct kobj_attribute pscrr_name_attr = __ATTR_RO(name); + +static int pscrr_parse_reason(const char *buf, enum psc_reason *out) +{ + unsigned int val; + + /* Accept either a decimal index or a reason token. */ + if (!kstrtouint(buf, 0, &val)) { + if (val >= PSCR_REASON_COUNT) + return -ERANGE; + *out = val; + return 0; + } + + return psc_reason_from_token(buf, out); +} + +static ssize_t reason_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; + DECLARE_BITMAP(reasons, PSCR_REASON_COUNT); + ssize_t len = 0; + int bit, ret; + + bitmap_zero(reasons, PSCR_REASON_COUNT); + + ret = p->ops->read_reasons(p, reasons); + if (ret) + return ret; + + for_each_set_bit(bit, reasons, PSCR_REASON_COUNT) + len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "", + psc_reason_to_token(bit)); + len += sysfs_emit_at(buf, len, "\n"); + + return len; +} + +/* Record @reason into @dir honouring the global record policy. */ +static int pscrr_do_record(struct pscrr_provider_dir *dir, enum psc_reason reason) +{ + struct pscrr_provider *p = dir->provider; + int ret; + + if (!p->ops->write_reason) + return -EPERM; + + /* + * PSCR_UNKNOWN clears the slot and releases the latch, regardless of + * policy, so a reason recorded afterwards is taken again. + */ + if (reason == PSCR_UNKNOWN) { + ret = p->ops->write_reason(p, reason); + if (ret) + return ret; + + dir->recorded = false; + return 0; + } + + /* Reject reasons the provider does not advertise (NULL == all). */ + if (p->supported_reasons && !test_bit(reason, p->supported_reasons)) + return -EOPNOTSUPP; + + /* "first" policy: keep the first reason recorded this power cycle. */ + if (dir->policy == PSCRR_RECORD_FIRST && dir->recorded) + return 0; + + ret = p->ops->write_reason(p, reason); + if (ret) + return ret; + + dir->recorded = true; + + return 0; +} + +static ssize_t reason_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct pscrr_provider_dir *dir = to_pscrr_dir(kobj); + enum psc_reason reason; + int ret; + + if (!dir->provider->ops->write_reason) + return -EPERM; + + ret = pscrr_parse_reason(buf, &reason); + if (ret) + return ret; + + /* Serialise the record state against concurrent stores and the notifier. */ + scoped_guard(mutex, &pscrr_lock) + ret = pscrr_do_record(dir, reason); + if (ret) + return ret; + + return count; +} + +static struct kobj_attribute pscrr_reason_attr = + __ATTR(reason, 0644, reason_show, reason_store); + +static ssize_t caps_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; + ssize_t len = 0; + + /* Readable and single-slot are the defaults and not listed. */ + if (p->ops->write_reason) + len += sysfs_emit_at(buf, len, "writable"); + + return len + sysfs_emit_at(buf, len, "\n"); +} + +static struct kobj_attribute pscrr_caps_attr = __ATTR_RO(caps); + +static ssize_t supported_reasons_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; + const unsigned long *sup = p->supported_reasons; + ssize_t len = 0; + int i; + + for (i = 0; i < PSCR_REASON_COUNT; i++) { + if (sup && !test_bit(i, sup)) /* NULL means all */ + continue; + len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "", + psc_reason_to_token(i)); + } + + return len + sysfs_emit_at(buf, len, "\n"); +} + +static struct kobj_attribute pscrr_supported_attr = + __ATTR(supported_reasons, 0444, supported_reasons_show, NULL); + +static ssize_t record_policy_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct pscrr_provider_dir *dir = to_pscrr_dir(kobj); + + return sysfs_emit(buf, "%s\n", + READ_ONCE(dir->policy) == PSCRR_RECORD_FIRST ? + "first" : "last"); +} + +static ssize_t record_policy_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct pscrr_provider_dir *dir = to_pscrr_dir(kobj); + + guard(mutex)(&pscrr_lock); + + if (sysfs_streq(buf, "first")) + WRITE_ONCE(dir->policy, PSCRR_RECORD_FIRST); + else if (sysfs_streq(buf, "last")) + WRITE_ONCE(dir->policy, PSCRR_RECORD_LAST); + else + return -EINVAL; + + return count; +} + +static struct kobj_attribute pscrr_record_policy_attr = + __ATTR(record_policy, 0644, record_policy_show, record_policy_store); + +static struct attribute *pscrr_dir_attrs[] = { + &pscrr_name_attr.attr, + &pscrr_reason_attr.attr, + &pscrr_caps_attr.attr, + &pscrr_supported_attr.attr, + &pscrr_record_policy_attr.attr, + NULL, +}; + +static umode_t pscrr_dir_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; + + /* A provider without write_reason() exposes reason read-only. */ + if (attr == &pscrr_reason_attr.attr && !p->ops->write_reason) + return 0444; + + /* record_policy only applies to a (single-slot) recorder. */ + if (attr == &pscrr_record_policy_attr.attr && !p->ops->write_reason) + return 0; + + return attr->mode; +} + +static const struct attribute_group pscrr_dir_group = { + .attrs = pscrr_dir_attrs, + .is_visible = pscrr_dir_is_visible, +}; + +static const struct attribute_group *pscrr_dir_groups[] = { + &pscrr_dir_group, + NULL, +}; + +static void pscrr_dir_release(struct kobject *kobj) +{ + kfree(to_pscrr_dir(kobj)); +} + +static const struct kobj_type pscrr_dir_ktype = { + .sysfs_ops = &kobj_sysfs_ops, + .release = pscrr_dir_release, + .default_groups = pscrr_dir_groups, +}; + +/*----------------------------------------------------------------------*/ +/* Provider registration */ +/*----------------------------------------------------------------------*/ + +/** + * pscrr_provider_register - register a power state change reason provider + * @p: caller-owned provider description + * + * Creates /sys/kernel/pscrr/providerN/ with "name" and "reason" attributes + * and, when @p->dev is set, a "device" symlink. @p->reason is writable when + * @p provides write_reason(). The provider must outlive the matching + * pscrr_provider_unregister() call. + * + * Return: 0 on success or a negative errno. + */ +int pscrr_provider_register(struct pscrr_provider *p) +{ + struct pscrr_provider_dir *dir; + int ret; + + if (!p || !p->name || !p->ops || !p->ops->read_reasons) + return -EINVAL; + + dir = kzalloc_obj(*dir); + if (!dir) + return -ENOMEM; + + dir->provider = p; + dir->policy = PSCRR_RECORD_FIRST; + + scoped_guard(mutex, &pscrr_lock) { + if (!pscrr_root) { + kfree(dir); + return -ENODEV; + } + + dir->id = ida_alloc(&pscrr_ida, GFP_KERNEL); + if (dir->id < 0) { + ret = dir->id; + kfree(dir); + return ret; + } + + ret = kobject_init_and_add(&dir->kobj, &pscrr_dir_ktype, + pscrr_root, "provider%d", dir->id); + if (ret) { + /* + * kobject_init_and_add() failed: per its contract only + * kobject_put() may follow, no kobject_del(). The + * kobject is not in sysfs, so this is safe under the + * lock. + */ + ida_free(&pscrr_ida, dir->id); + kobject_put(&dir->kobj); + return ret; + } + + if (p->dev) { + ret = sysfs_create_link(&dir->kobj, &p->dev->kobj, + "device"); + if (ret) + break; + } + + list_add_tail(&dir->node, &pscrr_dirs); + return 0; + } + + /* + * sysfs_create_link() failed after the directory was created: tear it + * down outside pscrr_lock (kobject_del() drains sysfs stores that take + * the lock) and release the id only once the directory is gone. + */ + kobject_del(&dir->kobj); + ida_free(&pscrr_ida, dir->id); + kobject_put(&dir->kobj); + return ret; +} +EXPORT_SYMBOL_GPL(pscrr_provider_register); + +/** + * pscrr_provider_unregister - remove a previously registered provider + * @p: the provider passed to pscrr_provider_register() + */ +void pscrr_provider_unregister(struct pscrr_provider *p) +{ + struct pscrr_provider_dir *dir = NULL, *iter; + + scoped_guard(mutex, &pscrr_lock) { + list_for_each_entry(iter, &pscrr_dirs, node) { + if (iter->provider == p) { + dir = iter; + list_del(&dir->node); + break; + } + } + } + + if (!dir) + return; + + /* + * Tear the sysfs directory down outside pscrr_lock: kobject_del() + * drains in-flight reason/record_policy stores, which take pscrr_lock, + * so holding it here would deadlock. Release the id only once the + * directory is gone, so a concurrent register cannot reuse it and + * collide on the providerN name. + */ + if (p->dev) + sysfs_remove_link(&dir->kobj, "device"); + kobject_del(&dir->kobj); + ida_free(&pscrr_ida, dir->id); + kobject_put(&dir->kobj); +} +EXPORT_SYMBOL_GPL(pscrr_provider_unregister); + +static void pscrr_provider_devm_release(void *p) +{ + pscrr_provider_unregister(p); +} + +/** + * devm_pscrr_provider_register - device-managed reason provider registration + * @dev: device the provider belongs to; also backs the "device" symlink + * @name: human-readable provider label + * @ops: provider callback table; read_reasons() is required, write_reason() is + * optional and makes the provider a recorder + * @supported_reasons: bitmap of the reasons the provider supports, or NULL + * for all; set before the provider is exposed in sysfs + * @priv: provider private data, handed back to the @ops callbacks + * + * Allocates and registers a struct pscrr_provider and schedules its + * unregistration when @dev is unbound, so the caller keeps no reference to it. + * + * Return: the registered provider on success or an ERR_PTR() on failure. When + * CONFIG_PSCRR is disabled the call resolves to a stub returning NULL, so + * callers need no IS_ENABLED() guard. + */ +struct pscrr_provider * +devm_pscrr_provider_register(struct device *dev, const char *name, + const struct pscrr_provider_ops *ops, + const unsigned long *supported_reasons, void *priv) +{ + struct pscrr_provider *p; + int ret; + + p = devm_kzalloc(dev, sizeof(*p), GFP_KERNEL); + if (!p) + return ERR_PTR(-ENOMEM); + + p->name = name; + p->dev = dev; + p->ops = ops; + p->supported_reasons = supported_reasons; + p->priv = priv; + + ret = pscrr_provider_register(p); + if (ret) + return ERR_PTR(ret); + + ret = devm_add_action_or_reset(dev, pscrr_provider_devm_release, p); + if (ret) + return ERR_PTR(ret); + + return p; +} +EXPORT_SYMBOL_GPL(devm_pscrr_provider_register); + +/*----------------------------------------------------------------------*/ +/* Record path: reboot notifier writes the current reason to recorders */ +/*----------------------------------------------------------------------*/ + +/* + * Record the current power-state-change reason into every provider, giving + * each the first (root cause) or the last reason according to its record + * policy. The kernel keeps both, so "first" is meaningful even when later + * events overwrite the last reason. The caller holds pscrr_lock, or runs where + * the provider list is stable (panic). + */ +static void pscrr_record_current(void) +{ + enum psc_reason first = get_psc_first_reason(); + enum psc_reason last = get_psc_reason(); + struct pscrr_provider_dir *dir; + + list_for_each_entry(dir, &pscrr_dirs, node) + pscrr_do_record(dir, dir->policy == PSCRR_RECORD_FIRST ? + first : last); +} + +static int pscrr_reboot_notifier(struct notifier_block *nb, + unsigned long action, void *unused) +{ + guard(mutex)(&pscrr_lock); + + /* + * A reboot, halt or power-off that reaches here with no more specific + * reason is software-initiated by definition. Record it as such rather + * than leaving it unattributed; a real cause set earlier (thermal, + * under-voltage, ...) is already latched and left untouched. + */ + if (get_psc_reason() == PSCR_UNKNOWN) + set_psc_reason(PSCR_SOFTWARE); + + pscrr_record_current(); + + return NOTIFY_DONE; +} + +static struct notifier_block pscrr_reboot_nb = { + .notifier_call = pscrr_reboot_notifier, +}; + +/*----------------------------------------------------------------------*/ +/* Built-in provider: device-tree /chosen/reset-source */ +/*----------------------------------------------------------------------*/ + +/* + * Bootloaders such as barebox record the SoC reset cause in the standard + * device-tree /chosen/reset-source property. When it is present, surface it as + * a read-only, device-less provider so the bootloader's view of the last reset + * shows up next to any hardware or software providers - the framework just + * reads the property already there, with no dedicated node or new binding. + */ +static const struct { + const char *name; + enum psc_reason reason; +} pscrr_reset_source_map[] = { + { "POR", PSCR_POWER_ON }, + { "RST", PSCR_SOFTWARE }, + { "WDG", PSCR_WATCHDOG }, + { "THERM", PSCR_OVER_TEMPERATURE }, + { "EXT", PSCR_EXTERNAL }, + { "BROWNOUT", PSCR_UNDER_VOLTAGE }, +}; + +static const unsigned long +pscrr_reset_source_supported[BITS_TO_LONGS(PSCR_REASON_COUNT)] = { + BIT(PSCR_UNDER_VOLTAGE) | BIT(PSCR_OVER_TEMPERATURE) | + BIT(PSCR_POWER_ON) | BIT(PSCR_WATCHDOG) | BIT(PSCR_SOFTWARE) | + BIT(PSCR_EXTERNAL), +}; + +/* Parsed once at init; read back by the provider's read_reasons(). */ +static enum psc_reason pscrr_reset_source_reason = PSCR_UNKNOWN; + +static int pscrr_reset_source_read(struct pscrr_provider *p, + unsigned long *reasons) +{ + set_bit(pscrr_reset_source_reason, reasons); + + return 0; +} + +static const struct pscrr_provider_ops pscrr_reset_source_ops = { + .read_reasons = pscrr_reset_source_read, +}; + +static struct pscrr_provider pscrr_reset_source_provider = { + .name = "reset-source", + .ops = &pscrr_reset_source_ops, + .supported_reasons = pscrr_reset_source_supported, +}; + +static void __init pscrr_register_reset_source(void) +{ + const char *name; + int i, ret; + + if (!IS_ENABLED(CONFIG_OF) || !of_chosen) + return; + + if (of_property_read_string(of_chosen, "reset-source", &name)) + return; + + for (i = 0; i < ARRAY_SIZE(pscrr_reset_source_map); i++) + if (!strcmp(name, pscrr_reset_source_map[i].name)) { + pscrr_reset_source_reason = pscrr_reset_source_map[i].reason; + break; + } + + ret = pscrr_provider_register(&pscrr_reset_source_provider); + if (ret) + pr_warn("failed to register the reset-source provider: %d\n", ret); +} + +/*----------------------------------------------------------------------*/ +/* Module init/exit */ +/*----------------------------------------------------------------------*/ + +static int __init pscrr_core_init(void) +{ + int ret; + + pscrr_root = kobject_create_and_add("pscrr", kernel_kobj); + if (!pscrr_root) + return -ENOMEM; + + ret = register_reboot_notifier(&pscrr_reboot_nb); + if (ret) { + kobject_put(pscrr_root); + pscrr_root = NULL; + return ret; + } + + pscrr_register_reset_source(); + + return 0; +} + +static void __exit pscrr_core_exit(void) +{ + pscrr_provider_unregister(&pscrr_reset_source_provider); + unregister_reboot_notifier(&pscrr_reboot_nb); + kobject_put(pscrr_root); + pscrr_root = NULL; + ida_destroy(&pscrr_ida); +} + +/* Bring the core up before device drivers probe and register providers. */ +subsys_initcall(pscrr_core_init); +module_exit(pscrr_core_exit); + +MODULE_AUTHOR("Oleksij Rempel <[email protected]>"); +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 <[email protected]> + */ + +#ifndef __PSCRR_H__ +#define __PSCRR_H__ + +#include <linux/reboot.h> + +struct device; +struct pscrr_provider; + +/** + * struct pscrr_provider_ops - Provider callbacks. + * @read_reasons: Fill @reasons with the complete set this source observed. + * @reasons is a bitmap of PSCR_REASON_COUNT bits; set members + * with set_bit(PSCR_x, reasons). Return 0 on success or a + * negative errno. Required. Driver state is reached via p->priv. + * @write_reason: Persist @reason. Called from the reboot notifier for + * recorder-capable providers so the cause survives the power + * cycle. Leave NULL for read-only hardware sources. + * + * A const ops table per provider type; extend it with new callbacks without + * touching the registration signature or existing callers. + */ +struct pscrr_provider_ops { + int (*read_reasons)(struct pscrr_provider *p, unsigned long *reasons); + int (*write_reason)(struct pscrr_provider *p, enum psc_reason reason); +}; + +/** + * struct pscrr_provider - A source (and optionally recorder) of power state + * change reasons. + * + * A provider represents one place the system can learn *why* the last power + * state change happened: a hardware reset-cause register (PMIC, SoC SRC, + * watchdog), a persistent recorder (NVMEM/RTC scratch), or a test stub. Each + * registered provider gets its own directory under /sys/kernel/pscrr/, so the + * full, un-prioritised picture is visible: several providers - and several + * reasons within one provider - can be reported simultaneously. + * + * @name: Human-readable label, exported as the "name" attribute. The + * directory itself is core-indexed (providerN), so this need not + * be unique. Required. + * @dev: Backing device. When set it is exported as the "device" symlink + * in the provider directory, tying the reason to real hardware. + * May be NULL (e.g. for a test provider). + * @ops: Provider callbacks. Required. + * @supported_reasons: Bitmap of the reasons this provider can store or report, + * limited e.g. by the storage size. 0 means all reasons. + * @priv: Provider private data, passed back through the callbacks. + * + * Providers are readable and single-slot by default; only capabilities beyond + * that (currently: writable) are advertised. + */ +struct pscrr_provider { + const char *name; + struct device *dev; + const struct pscrr_provider_ops *ops; + const unsigned long *supported_reasons; + void *priv; +}; + +#if IS_ENABLED(CONFIG_PSCRR) +int pscrr_provider_register(struct pscrr_provider *p); +void pscrr_provider_unregister(struct pscrr_provider *p); + +/** + * devm_pscrr_provider_register - allocate, fill and register a provider + * @dev: device the provider belongs to (also the "device" symlink target) + * @name: provider label + * @ops: provider callbacks + * @supported_reasons: bitmap of supported reasons, or NULL for all + * @priv: driver state passed back through the callbacks + * + * The provider is unregistered automatically on device teardown. + * + * Return: the provider on success, ERR_PTR() on failure, or NULL when PSCRR + * is not built (so the caller need not guard the call). + */ +struct pscrr_provider * +devm_pscrr_provider_register(struct device *dev, const char *name, + const struct pscrr_provider_ops *ops, + const unsigned long *supported_reasons, void *priv); +#else +static inline int pscrr_provider_register(struct pscrr_provider *p) +{ + return -EOPNOTSUPP; +} + +static inline void pscrr_provider_unregister(struct pscrr_provider *p) +{ +} + +static inline struct pscrr_provider * +devm_pscrr_provider_register(struct device *dev, const char *name, + const struct pscrr_provider_ops *ops, + const unsigned long *supported_reasons, void *priv) +{ + return NULL; +} +#endif + +#endif /* __PSCRR_H__ */ -- 2.47.3