From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-lr2-f35.google.com (mail-lr2-f35.google.com [74.125.230.99]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5DD9A52F29A for ; Tue, 22 Sep 2026 09:03:18 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.230.99 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790067801; cv=none; b=GLSgStAwhhnijMmK1GLTrRPqVViyM7PD0mLVdVpsofr5I+l5giF2TYTvIj1i+GnZA1CEX3D3t5VOKzeKnW1TRqL58hwJYZLwVBDt4tAF2m0m6NuGodN4jb5PImenWIUKtaLS5pBbRpO8kyrQSjQLa4PGsf7l7kaAKxXfi6PTGnM= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790067801; c=relaxed/simple; bh=aNcxpY3D1SjkWmBwp2b8JMEFHtv/cWQv7afXq3zYIuA=; h=Message-ID:Date:MIME-Version:Subject:To:Cc:References:From: In-Reply-To:Content-Type; b=qC4/vfCHi2CgE2xVQj5msRY0hRtk8jDyrbqptePNFjYXLwFOqTxlzVjMKuVAJaoV5tbfZc1bNLI/Ll/q5koV7dusoXynFvIGPinqkfZpWyqWVIksSY/SxXo3WndbEGL2HKOO0GkLhdPiogglFYOAyZqBxss+8Gc+eXMal+eSQLY= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=VCHF+RFv; arc=none smtp.client-ip=74.125.230.99 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="VCHF+RFv" Received: by mail-lr2-f35.google.com with SMTP id 38308e7fff4ca-3a61240473fso21346331fa.3 for ; Tue, 22 Sep 2026 02:03:18 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1790067796; x=1790672596; darn=vger.kernel.org; h=content-transfer-encoding:content-type:in-reply-to:from :content-language:references:cc:to:subject:user-agent:mime-version :date:message-id:from:to:cc:subject:date:message-id:reply-to :content-type; bh=Q7UmCTgqSBqSSaYs28J+kUnJ2yN7IHpzAh6F5k1POSE=; b=VCHF+RFv0NEtHLnWwht+MuxD3xGnzpiPmLytuxHaeXLmz07SBlp5Nb2UTXLPOI5PPp LLmaIUG+vnDU2Hz/J/09mNXnozZ8CO+7HQvVMOM3wm6QBuI0hIfqIAD0BE4zieC5/aIX L4xG5ZlNBf+WkYWxzJMDsdy7+pudVkzWECp5F9BnZSbjXmDg/hS1WqLV3n0Bd54UzQ5u lgvNm7JawvTyYtcBqPZT3IXA1Dt/FVPl8tJCboNZ3RXdv3lu29kjrVNNqsJe2s1oEwqO 1mbvNyrCGou+D/lNgZlaT+rY3qR+SZYqVui1II3+N9x8TV7Z0Wdb+SI1F3TWIIgfFApt /S9w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1790067796; x=1790672596; h=content-transfer-encoding:content-type:in-reply-to:from :content-language:references:cc:to:subject:user-agent:mime-version :date:message-id:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to:content-type; bh=Q7UmCTgqSBqSSaYs28J+kUnJ2yN7IHpzAh6F5k1POSE=; b=nBLr0MSW3hsailKcTbP5/CbT+20tYbInxQ3UHioMDUBgfxxN9rkhKhmJRBs4xDXh8Q 7oxBartvu1/G23Ul83gV80A8t9wYur7o36X4UxRHPr6VlU0tPx8+JgfIicrj15g7ywWa 9g09/Xd6M1oyTemE+PmVx49VnSX25cBcKH5og46ZiuZ4rLXHlzmPMGEWNUARpLMeepQP ZEQ7UaXwt5LRuAiybkPN/yh7ZmMK47X7ojg75AL6C07olv3eLWle9a1TnSa50LW/SqiR cI0tx/OYSJCe2kMfiHqZczZRGTw0yp4jfmSt3u0FA66mZOm3dMRHd5ZExJnAcsTakfib 3hEQ== X-Forwarded-Encrypted: i=1; AKwUvByOmqbCej+K/uRick84JQGhDB6jzu3uQ/Rm/J22ZdUxNPox/Y5KmsGnQ0k9Mgo5CXb7rAH8vv1MeQrv9Js=@vger.kernel.org X-Gm-Message-State: AFuF++n1zAWY2oi/ypIb3nfFt0ZIY4Di+CzNdPMolfcGM9AlFw90wa5z 0fiC6gwb/FTDB53JfWNggMKPQHfO7FdyWxf7Sf9FqgoWCFImdIJL9pbJ X-Gm-Gg: AYBFou0nx4CAjkIzH+NxGYziCj75wyw24+2Oj2dnmJOALSPrsNojJecRc0ajaokJriX jCERw7mCeLhZW3gcXbuaQ0zP5W6dNiG9P7PlTUHCvSxyvq11nZKQR/f93EDE0sNiapA23W1MhHQ rYND6dgMWRpkkijW1L42DCz8wIFnscVT62cOk62htF3wklVjiphq3WCD8DPFpUJG6wSEdhwEQ3Q LWJRguvJeAU/bWLIhQ2SbpbmkdOSsPPUz7F5YJQnSRIdmr6PpZOFsOPR6LB1/m0j45NfWd0VCkd elsgtnJRKS56umW8b2/BwsAamD6qErAA6OzfrQk/K3yk8A6O1iu6YXmo1IqVSgw7T87YV5qJn7U GJWnevTJUEZZBerG/Xod4vMUV3a52RLM07gTJ+BW3hZvD/yLsf4W6OKcW/+ZifLJdZPHfGIgcJk xBqn2FtXcZXrtK86Pv6VP60jvE/OuOsg4nACHNE8wPEBnsk8qJ991JDwUD1Iqv6eGm4gte2IT2k qhtUG7EyZLBUx8ww4m9k/g29wTs8xq4Qx6XhFvd3MZxXBOBlj+lm/Y= X-Received: by 2002:a05:651c:f13:b0:3a5:f911:12bf with SMTP id 38308e7fff4ca-3a5fbe80dbdmr26600381fa.4.1790067794337; Tue, 22 Sep 2026 02:03:14 -0700 (PDT) Received: from ?IPV6:2a10:a5c0:800d:dd00:8fdf:935a:2c85:d703? ([2a10:a5c0:800d:dd00:8fdf:935a:2c85:d703]) by smtp.gmail.com with ESMTPSA id 38308e7fff4ca-3a627b7b96asm3661001fa.3.2026.09.22.02.03.10 (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Tue, 22 Sep 2026 02:03:12 -0700 (PDT) Message-ID: <05f38c18-812c-4fbc-9e79-69fd3b78ba3d@gmail.com> Date: Tue, 22 Sep 2026 12:03:10 +0300 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH v14 5/6] power: reset: Introduce PSCR Recording Framework for Non-Volatile Storage To: Faruque Ansari , Sebastian Reichel , Benson Leung , Tzung-Bi Shih , Guenter Roeck , Liam Girdwood , Mark Brown , "Rafael J. Wysocki" , Daniel Lezcano , Zhang Rui , Lukasz Luba , Oleksij Rempel , Pengutronix Kernel Team Cc: linux-arm-msm@vger.kernel.org, linux-pm@vger.kernel.org, linux-kernel@vger.kernel.org, chrome-platform@lists.linux.dev, avaneesh.dwivedi@oss.qualcomm.com, Umang Chheda , Francesco Valla References: <20260921-pscrr-framework-v14-0-a16290003b7c@oss.qualcomm.com> <20260921-pscrr-framework-v14-5-a16290003b7c@oss.qualcomm.com> Content-Language: en-US, en-AU, en-GB, en-BW From: Matti Vaittinen In-Reply-To: <20260921-pscrr-framework-v14-5-a16290003b7c@oss.qualcomm.com> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit On 21/09/2026 20:44, Faruque Ansari wrote: > Introduce the Power State Change Reasons Recording (PSCRR) framework: a > generic mechanism to record why the last power state change (shutdown or > reboot) happened - under-voltage, thermal, watchdog, software-triggered, > etc. - so a postmortem reason survives a reboot or an abrupt power loss. > > PSCRR is built around providers. A provider is either a hardware reason > source (a PMIC, SoC reset controller or watchdog exposing a reset cause) > or a recorder that persists the reason across a power cycle (an NVMEM or > RTC scratch cell). Each provider gets a directory under > /sys/kernel/pscrr/providerN/ exposing its name, backing device, the set > of observed reasons (as tokens), its capabilities, the reasons it > supports and - for recorders - a record policy. The reason set is > deliberately not collapsed to a single winning cause, since resets are > often multi-causal. > > Reasons are the numeric enum psc_reason values from reboot.h, shared with > the POWER_ON_REASON_* vocabulary, so they store compactly in small > battery-backed cells. The current reason (get/set_psc_reason(), set by > the thermal/regulator/hw_protection paths) is written to every recorder > from the reboot notifier. > > Co-developed-by: Oleksij Rempel > Signed-off-by: Oleksij Rempel > Reported-by: Francesco Valla > Closes: https://lore.kernel.org/all/178749889134.204450.5740187753642158305.b4-review@b4/ > Signed-off-by: Faruque Ansari > > changes v13: > - Narrow pscrr_lock usage in pscrr_provider_register() to only protect > pscrr_dirs list updates. > - Check pscrr_root before allocation and limit locking around > ida_alloc() and kobject/sysfs setup. > changes v12: > - rework into a multi-provider framework: one sysfs directory per > provider instead of a single backend > - add per-provider capabilities, supported_reasons and record policy > - register providers through a const ops table via > (devm_)pscrr_provider_register() > - report reasons as parsable tokens > - move the sources under drivers/power/reset/pscrr/ > - add a MAINTAINERS entry; PSCRR depends on POWER_RESET > - drop Reviewed-by: Matti Vaittinen; the framework was substantially > reworked since it was given > changes v10: > - make g_pscrr static > changes v8: > - introduce struct pscrr_core to encapsulate backend and locking > - replace global mutex and backend pointer with centralized pscrr_core > - use DEFINE_GUARD() + guard(g_pscrr) for scoped mutex locking > - simplify code using local backend pointer after locking > - prepare code structure for future multi-backend support > changes v7: > - make write_reason optional > - update documentation > changes v6: > - move enum pscr_reason to kernel reboot core > - move reason storage to reboot core > - add locking > --- > MAINTAINERS | 11 + > drivers/power/reset/Kconfig | 2 + > drivers/power/reset/Makefile | 1 + > drivers/power/reset/pscrr/Kconfig | 33 ++ > drivers/power/reset/pscrr/Makefile | 2 + > drivers/power/reset/pscrr/pscrr.c | 633 +++++++++++++++++++++++++++++++++++++ > include/linux/pscrr.h | 107 +++++++ > 7 files changed, 789 insertions(+) > > diff --git a/MAINTAINERS b/MAINTAINERS > index 05215fcc18a5..a7145dbb6eb7 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -21967,6 +21967,17 @@ F: Documentation/devicetree/bindings/connector/pcie-m2-e-connector.yaml > F: Documentation/devicetree/bindings/connector/pcie-m2-m-connector.yaml > F: drivers/power/sequencing/pwrseq-pcie-m2.c > > +POWER STATE CHANGE REASON RECORDING (PSCRR) > +M: Oleksij Rempel > +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 bce996bbef28..f24ff89dd94e 100644 > --- a/drivers/power/reset/Kconfig > +++ b/drivers/power/reset/Kconfig > @@ -377,3 +377,5 @@ config POWER_RESET_QEMU_VIRT_CTRL > provides this controller, such as the m68k virt machine. > > endif > + > +source "drivers/power/reset/pscrr/Kconfig" > diff --git a/drivers/power/reset/Makefile b/drivers/power/reset/Makefile > index e31cab4ba78e..c73dc747e36c 100644 > --- a/drivers/power/reset/Makefile > +++ b/drivers/power/reset/Makefile > @@ -37,6 +37,7 @@ obj-$(CONFIG_POWER_RESET_KEYSTONE) += keystone-reset.o > obj-$(CONFIG_POWER_RESET_SYSCON) += syscon-reboot.o > obj-$(CONFIG_POWER_RESET_SYSCON_POWEROFF) += syscon-poweroff.o > obj-$(CONFIG_POWER_RESET_RMOBILE) += rmobile-reset.o > +obj-$(CONFIG_PSCRR) += pscrr/ > obj-$(CONFIG_REBOOT_MODE) += reboot-mode.o > obj-$(CONFIG_SYSCON_REBOOT_MODE) += syscon-reboot-mode.o > obj-$(CONFIG_POWER_RESET_SC27XX) += sc27xx-poweroff.o > diff --git a/drivers/power/reset/pscrr/Kconfig b/drivers/power/reset/pscrr/Kconfig > new file mode 100644 > index 000000000000..72de82731b53 > --- /dev/null > +++ b/drivers/power/reset/pscrr/Kconfig > @@ -0,0 +1,33 @@ > +# SPDX-License-Identifier: GPL-2.0-only > +menuconfig PSCRR > + bool "Power State Change Reasons Recording (PSCRR) Framework" > + depends on POWER_RESET > + help > + Enables the Power State Change Reasons Recording (PSCRR) framework. > + > + PSCRR records why the system last shut down or rebooted into > + non-volatile storage, so the reason survives the reset and can be > + read by the bootloader or early user space on the next boot. Reasons > + come from software (thermal or regulator hardware-protection events, > + a watchdog pretimeout, a kernel panic, a controlled reboot) or from > + hardware reset-cause registers (PMIC, SoC reset controller, watchdog). > + > + It helps whenever the real cause would otherwise be lost or ambiguous > + on the next boot, for example: > + > + - the reset is abrupt and block storage (eMMC/NAND) may not survive > + it; on systems that can detect the event in time - such as an > + imminent power loss with a short backup-capacitor window - the > + reason can still be persisted first; > + > + - the hardware does not preserve the cause and the boot looks like a > + plain power-on reset, e.g. a watchdog pretimeout that escalates > + into a panic reboot would otherwise be indistinguishable from a > + POR. > + > + The framework is extensible and exposes software-defined and > + hardware-reported reasons through one interface. Sudden power cuts, > + CPU freezes or other uncontrolled resets may still go unrecorded > + unless hardware provides the reset cause. > + > + If unsure, say N. > diff --git a/drivers/power/reset/pscrr/Makefile b/drivers/power/reset/pscrr/Makefile > new file mode 100644 > index 000000000000..e5530a858971 > --- /dev/null > +++ b/drivers/power/reset/pscrr/Makefile > @@ -0,0 +1,2 @@ > +# SPDX-License-Identifier: GPL-2.0 > +obj-$(CONFIG_PSCRR) += pscrr.o > diff --git a/drivers/power/reset/pscrr/pscrr.c b/drivers/power/reset/pscrr/pscrr.c > new file mode 100644 > index 000000000000..ab0974d0423e > --- /dev/null > +++ b/drivers/power/reset/pscrr/pscrr.c > @@ -0,0 +1,633 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * pscrr.c - Core Power State Change Reason Recording > + * > + * PSCRR records why the last power state change (shutdown/reboot) happened. > + * Reasons come from providers: hardware reset-cause registers (PMIC, SoC reset > + * controller, watchdog), persistent recorders (NVMEM/RTC scratch), the > + * bootloader's device-tree /chosen/reset-source (a built-in provider here), or > + * test stubs. Each provider gets a directory under /sys/kernel/pscrr/ and reports > + * the full set of reasons it observed - the picture is deliberately not > + * collapsed to a single "winning" cause, since resets are often multi-causal. > + * > + * Sysfs (per provider, under /sys/kernel/pscrr/providerN/): > + * name ro human label of the provider > + * device symlink to the backing device (if any) > + * reason the provider's reason set, as tokens; writable (record > + * one reason) when the provider supports it > + * caps ro non-default capabilities ("writable"); empty for a > + * read-only, single-slot provider > + * supported_reasons ro reasons this provider can report or record > + * record_policy recorders only: keep the "first" or "last" reason > + * recorded in a power cycle > + * > + * The kernel keeps the first (root cause) and last power-state-change reason > + * (get_psc_first_reason()/get_psc_reason(), set by the thermal, regulator and > + * hw_protection paths). At reboot each recorder is given the first or the last > + * reason according to its record policy. > + * > + * Copyright (C) 2025 Pengutronix, Oleksij Rempel > + */ > + > +#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. nit: Seeing this comment and then seeing the seemingly unprotected: to_pscrr_dir(kobj)->provider->XXX in sysfs operations directly below, required me to go through the tear-down stuff. Is it possible to add a word as to why the direct access to provider pointer is still Ok, so others don't have to do the same :) > + */ > +struct pscrr_provider_dir { > + struct kobject kobj; > + struct pscrr_provider *provider; > + struct list_head node; > + int id; > + enum pscrr_record_policy policy; /* single-slot recorder: keep first/last */ > + bool recorded; /* a reason was recorded this power cycle */ > +}; > + > +static inline struct pscrr_provider_dir *to_pscrr_dir(struct kobject *kobj) > +{ > + return container_of(kobj, struct pscrr_provider_dir, kobj); > +} > + > +/*----------------------------------------------------------------------*/ > +/* Per-provider attributes */ > +/*----------------------------------------------------------------------*/ > + > +static ssize_t name_show(struct kobject *kobj, struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "%s\n", to_pscrr_dir(kobj)->provider->name); > +} > + > +static struct kobj_attribute pscrr_name_attr = __ATTR_RO(name); > + > +static int pscrr_parse_reason(const char *buf, enum psc_reason *out) > +{ > + unsigned int val; > + > + /* Accept either a decimal index or a reason token. */ > + if (!kstrtouint(buf, 0, &val)) { > + if (val >= PSCR_REASON_COUNT) > + return -ERANGE; > + *out = val; > + return 0; > + } > + > + return psc_reason_from_token(buf, out); > +} > + > +static ssize_t reason_show(struct kobject *kobj, struct kobj_attribute *attr, > + char *buf) > +{ > + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; > + DECLARE_BITMAP(reasons, PSCR_REASON_COUNT); > + ssize_t len = 0; > + int bit, ret; > + > + bitmap_zero(reasons, PSCR_REASON_COUNT); > + > + ret = p->ops->read_reasons(p, reasons); > + if (ret) > + return ret; > + > + for_each_set_bit(bit, reasons, PSCR_REASON_COUNT) > + len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "", > + psc_reason_to_token(bit)); > + len += sysfs_emit_at(buf, len, "\n"); > + > + return len; > +} > + > +/* Record @reason into @dir honouring the global record policy. */ > +static int pscrr_do_record(struct pscrr_provider_dir *dir, enum psc_reason reason) > +{ > + struct pscrr_provider *p = dir->provider; > + int ret; > + > + if (!p->ops->write_reason) > + return -EPERM; nit: To me this is more of -EOPNOTSUPP, see below. > + > + /* > + * PSCR_UNKNOWN clears the slot and releases the latch, regardless of > + * policy, so a reason recorded afterwards is taken again. > + */ > + if (reason == PSCR_UNKNOWN) { > + ret = p->ops->write_reason(p, reason); > + if (ret) > + return ret; > + > + dir->recorded = false; > + return 0; > + } > + > + /* Reject reasons the provider does not advertise (NULL == all). */ > + if (p->supported_reasons && !test_bit(reason, p->supported_reasons)) > + return -EOPNOTSUPP; nit: I'd use -EINVAL here, see below. > + > + /* "first" policy: keep the first reason recorded this power cycle. */ > + if (dir->policy == PSCRR_RECORD_FIRST && dir->recorded) > + return 0; > + > + ret = p->ops->write_reason(p, reason); > + if (ret) > + return ret; > + > + dir->recorded = true; > + > + return 0; > +} > + > +static ssize_t reason_store(struct kobject *kobj, struct kobj_attribute *attr, > + const char *buf, size_t count) > +{ > + struct pscrr_provider_dir *dir = to_pscrr_dir(kobj); > + enum psc_reason reason; > + int ret; > + > + if (!dir->provider->ops->write_reason) > + return -EPERM; nit: Why not the "not supported" instead? EPERM would make me to doubt the user privileges, not capablility of the backing device. (Oh, I just noticed the -EOPNOTSUPP when specific reason is not supported - I'd say this is more of -EOPNOTSUPP, while giving unsupported reason as argument is more of -EINVAL to me). > + > + ret = pscrr_parse_reason(buf, &reason); > + if (ret) > + return ret; > + > + /* Serialise the record state against concurrent stores and the notifier. */ > + scoped_guard(mutex, &pscrr_lock) > + ret = pscrr_do_record(dir, reason); > + if (ret) > + return ret; > + > + return count; > +} > + > +static struct kobj_attribute pscrr_reason_attr = > + __ATTR(reason, 0644, reason_show, reason_store); > + > +static ssize_t caps_show(struct kobject *kobj, struct kobj_attribute *attr, > + char *buf) > +{ > + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; > + ssize_t len = 0; > + > + /* Readable and single-slot are the defaults and not listed. */ > + if (p->ops->write_reason) > + len += sysfs_emit_at(buf, len, "writable"); > + > + return len + sysfs_emit_at(buf, len, "\n"); > +} > + > +static struct kobj_attribute pscrr_caps_attr = __ATTR_RO(caps); > + > +static ssize_t supported_reasons_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; > + const unsigned long *sup = p->supported_reasons; > + ssize_t len = 0; > + int i; > + > + for (i = 0; i < PSCR_REASON_COUNT; i++) { > + if (sup && !test_bit(i, sup)) /* NULL means all */ > + continue; > + len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "", > + psc_reason_to_token(i)); > + } > + > + return len + sysfs_emit_at(buf, len, "\n"); > +} > + > +static struct kobj_attribute pscrr_supported_attr = > + __ATTR(supported_reasons, 0444, supported_reasons_show, NULL); > + > +static ssize_t record_policy_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct pscrr_provider_dir *dir = to_pscrr_dir(kobj); > + > + return sysfs_emit(buf, "%s\n", > + READ_ONCE(dir->policy) == PSCRR_RECORD_FIRST ? > + "first" : "last"); > +} > + > +static ssize_t record_policy_store(struct kobject *kobj, > + struct kobj_attribute *attr, > + const char *buf, size_t count) > +{ > + struct pscrr_provider_dir *dir = to_pscrr_dir(kobj); > + > + guard(mutex)(&pscrr_lock); > + > + if (sysfs_streq(buf, "first")) > + WRITE_ONCE(dir->policy, PSCRR_RECORD_FIRST); > + else if (sysfs_streq(buf, "last")) > + WRITE_ONCE(dir->policy, PSCRR_RECORD_LAST); > + else > + return -EINVAL; > + > + return count; > +} > + > +static struct kobj_attribute pscrr_record_policy_attr = > + __ATTR(record_policy, 0644, record_policy_show, record_policy_store); > + > +static struct attribute *pscrr_dir_attrs[] = { > + &pscrr_name_attr.attr, > + &pscrr_reason_attr.attr, > + &pscrr_caps_attr.attr, > + &pscrr_supported_attr.attr, > + &pscrr_record_policy_attr.attr, > + NULL, > +}; > + > +static umode_t pscrr_dir_is_visible(struct kobject *kobj, struct attribute *attr, > + int n) > +{ > + struct pscrr_provider *p = to_pscrr_dir(kobj)->provider; > + > + /* A provider without write_reason() exposes reason read-only. */ > + if (attr == &pscrr_reason_attr.attr && !p->ops->write_reason) > + return 0444; > + > + /* record_policy only applies to a (single-slot) recorder. */ > + if (attr == &pscrr_record_policy_attr.attr && !p->ops->write_reason) > + return 0; > + > + return attr->mode; > +} > + > +static const struct attribute_group pscrr_dir_group = { > + .attrs = pscrr_dir_attrs, > + .is_visible = pscrr_dir_is_visible, > +}; > + > +static const struct attribute_group *pscrr_dir_groups[] = { > + &pscrr_dir_group, > + NULL, > +}; > + > +static void pscrr_dir_release(struct kobject *kobj) > +{ > + kfree(to_pscrr_dir(kobj)); > +} > + > +static const struct kobj_type pscrr_dir_ktype = { > + .sysfs_ops = &kobj_sysfs_ops, > + .release = pscrr_dir_release, > + .default_groups = pscrr_dir_groups, > +}; > + > +/*----------------------------------------------------------------------*/ > +/* Provider registration */ > +/*----------------------------------------------------------------------*/ > + > +/** > + * pscrr_provider_register - register a power state change reason provider > + * @p: caller-owned provider description > + * > + * Creates /sys/kernel/pscrr/providerN/ with "name" and "reason" attributes > + * and, when @p->dev is set, a "device" symlink. @p->reason is writable when > + * @p provides write_reason(). The provider must outlive the matching > + * pscrr_provider_unregister() call. > + * > + * Return: 0 on success or a negative errno. > + */ > +int pscrr_provider_register(struct pscrr_provider *p) > +{ > + struct pscrr_provider_dir *dir; > + int ret; > + > + if (!p || !p->name || !p->ops || !p->ops->read_reasons) > + return -EINVAL; > + > + /* > + * pscrr_root is set once at core init and cleared at core exit; > + * neither transition is serialised against this function by > + * pscrr_lock, so check it up front rather than under the lock below. > + */ > + if (!pscrr_root) > + return -ENODEV; > + > + dir = kzalloc_obj(*dir); > + if (!dir) > + return -ENOMEM; > + > + dir->provider = p; > + dir->policy = PSCRR_RECORD_FIRST; > + > + /* ida_alloc() and the kobject/sysfs calls below are individually thread-safe. */ > + dir->id = ida_alloc(&pscrr_ida, GFP_KERNEL); > + if (dir->id < 0) { > + ret = dir->id; > + kfree(dir); > + return ret; > + } > + > + ret = kobject_init_and_add(&dir->kobj, &pscrr_dir_ktype, pscrr_root, > + "provider%d", dir->id); > + if (ret) { > + /* > + * kobject_init_and_add() failed: per its contract only > + * kobject_put() may follow, no kobject_del(). > + */ > + ida_free(&pscrr_ida, dir->id); > + kobject_put(&dir->kobj); > + return ret; > + } > + > + if (p->dev) { > + ret = sysfs_create_link(&dir->kobj, &p->dev->kobj, "device"); > + if (ret) > + goto err_del; > + } > + > + /* pscrr_lock only serialises pscrr_dirs against concurrent (un)registration. */ > + scoped_guard(mutex, &pscrr_lock) > + list_add_tail(&dir->node, &pscrr_dirs); > + > + return 0; > + > +err_del: > + kobject_del(&dir->kobj); > + ida_free(&pscrr_ida, dir->id); > + kobject_put(&dir->kobj); > + return ret; > +} > +EXPORT_SYMBOL_GPL(pscrr_provider_register); > + > +/** > + * pscrr_provider_unregister - remove a previously registered provider > + * @p: the provider passed to pscrr_provider_register() > + */ > +void pscrr_provider_unregister(struct pscrr_provider *p) > +{ > + struct pscrr_provider_dir *dir = NULL, *iter; > + > + scoped_guard(mutex, &pscrr_lock) { > + list_for_each_entry(iter, &pscrr_dirs, node) { > + if (iter->provider == p) { > + dir = iter; > + list_del(&dir->node); > + break; > + } > + } > + } > + > + if (!dir) > + return; > + > + /* > + * Tear the sysfs directory down outside pscrr_lock: kobject_del() > + * drains in-flight reason/record_policy stores, which take pscrr_lock, > + * so holding it here would deadlock. Release the id only once the > + * directory is gone, so a concurrent register cannot reuse it and > + * collide on the providerN name. > + */ > + if (p->dev) > + sysfs_remove_link(&dir->kobj, "device"); > + kobject_del(&dir->kobj); > + ida_free(&pscrr_ida, dir->id); > + kobject_put(&dir->kobj); > +} > +EXPORT_SYMBOL_GPL(pscrr_provider_unregister); > + > +static void pscrr_provider_devm_release(void *p) > +{ > + pscrr_provider_unregister(p); > +} > + > +/** > + * devm_pscrr_provider_register - device-managed reason provider registration > + * @dev: device the provider belongs to; also backs the "device" symlink > + * @name: human-readable provider label > + * @ops: provider callback table; read_reasons() is required, write_reason() is > + * optional and makes the provider a recorder > + * @supported_reasons: bitmap of the reasons the provider supports, or NULL > + * for all; set before the provider is exposed in sysfs > + * @priv: provider private data, handed back to the @ops callbacks > + * > + * Allocates and registers a struct pscrr_provider and schedules its > + * unregistration when @dev is unbound, so the caller keeps no reference to it. > + * > + * Return: the registered provider on success or an ERR_PTR() on failure. When > + * CONFIG_PSCRR is disabled the call resolves to a stub returning NULL, so > + * callers need no IS_ENABLED() guard. > + */ > +struct pscrr_provider * > +devm_pscrr_provider_register(struct device *dev, const char *name, > + const struct pscrr_provider_ops *ops, > + const unsigned long *supported_reasons, void *priv) > +{ > + struct pscrr_provider *p; > + int ret; > + > + p = devm_kzalloc(dev, sizeof(*p), GFP_KERNEL); > + if (!p) > + return ERR_PTR(-ENOMEM); > + > + p->name = name; > + p->dev = dev; > + p->ops = ops; > + p->supported_reasons = supported_reasons; > + p->priv = priv; > + > + ret = pscrr_provider_register(p); > + if (ret) > + return ERR_PTR(ret); > + > + ret = devm_add_action_or_reset(dev, pscrr_provider_devm_release, p); > + if (ret) > + return ERR_PTR(ret); > + > + return p; > +} > +EXPORT_SYMBOL_GPL(devm_pscrr_provider_register); > + > +/*----------------------------------------------------------------------*/ > +/* Record path: reboot notifier writes the current reason to recorders */ > +/*----------------------------------------------------------------------*/ > + > +/* > + * Record the current power-state-change reason into every provider, giving > + * each the first (root cause) or the last reason according to its record > + * policy. The kernel keeps both, so "first" is meaningful even when later > + * events overwrite the last reason. The caller holds pscrr_lock, or runs where > + * the provider list is stable (panic). > + */ > +static void pscrr_record_current(void) > +{ > + enum psc_reason first = get_psc_first_reason(); > + enum psc_reason last = get_psc_reason(); > + struct pscrr_provider_dir *dir; > + > + list_for_each_entry(dir, &pscrr_dirs, node) > + pscrr_do_record(dir, dir->policy == PSCRR_RECORD_FIRST ? > + first : last); > +} > + > +static int pscrr_reboot_notifier(struct notifier_block *nb, > + unsigned long action, void *unused) > +{ > + guard(mutex)(&pscrr_lock); > + > + /* > + * A reboot, halt or power-off that reaches here with no more specific > + * reason is software-initiated by definition. Record it as such rather > + * than leaving it unattributed; a real cause set earlier (thermal, > + * under-voltage, ...) is already latched and left untouched. > + */ > + if (get_psc_reason() == PSCR_UNKNOWN) > + set_psc_reason(PSCR_SOFTWARE); > + > + pscrr_record_current(); > + > + return NOTIFY_DONE; > +} > + > +static struct notifier_block pscrr_reboot_nb = { > + .notifier_call = pscrr_reboot_notifier, > +}; > + > +/*----------------------------------------------------------------------*/ > +/* Built-in provider: device-tree /chosen/reset-source */ > +/*----------------------------------------------------------------------*/ > + > +/* > + * Bootloaders such as barebox record the SoC reset cause in the standard > + * device-tree /chosen/reset-source property. When it is present, surface it as > + * a read-only, device-less provider so the bootloader's view of the last reset > + * shows up next to any hardware or software providers - the framework just > + * reads the property already there, with no dedicated node or new binding. > + */ > +static const struct { > + const char *name; > + enum psc_reason reason; > +} pscrr_reset_source_map[] = { > + { "POR", PSCR_POWER_ON }, > + { "RST", PSCR_SOFTWARE }, > + { "WDG", PSCR_WATCHDOG }, > + { "THERM", PSCR_OVER_TEMPERATURE }, > + { "EXT", PSCR_EXTERNAL }, > + { "BROWNOUT", PSCR_UNDER_VOLTAGE }, > +}; > + > +static const unsigned long > +pscrr_reset_source_supported[BITS_TO_LONGS(PSCR_REASON_COUNT)] = { > + BIT(PSCR_UNDER_VOLTAGE) | BIT(PSCR_OVER_TEMPERATURE) | > + BIT(PSCR_POWER_ON) | BIT(PSCR_WATCHDOG) | BIT(PSCR_SOFTWARE) | > + BIT(PSCR_EXTERNAL), > +}; > + > +/* Parsed once at init; read back by the provider's read_reasons(). */ > +static enum psc_reason pscrr_reset_source_reason = PSCR_UNKNOWN; > + > +static int pscrr_reset_source_read(struct pscrr_provider *p, > + unsigned long *reasons) > +{ > + set_bit(pscrr_reset_source_reason, reasons); > + > + return 0; > +} > + > +static const struct pscrr_provider_ops pscrr_reset_source_ops = { > + .read_reasons = pscrr_reset_source_read, > +}; > + > +static struct pscrr_provider pscrr_reset_source_provider = { > + .name = "reset-source", > + .ops = &pscrr_reset_source_ops, > + .supported_reasons = pscrr_reset_source_supported, > +}; > + > +static void __init pscrr_register_reset_source(void) > +{ > + const char *name; > + int i, ret; > + > + if (!IS_ENABLED(CONFIG_OF) || !of_chosen) > + return; > + > + if (of_property_read_string(of_chosen, "reset-source", &name)) > + return; > + > + for (i = 0; i < ARRAY_SIZE(pscrr_reset_source_map); i++) > + if (!strcmp(name, pscrr_reset_source_map[i].name)) { > + pscrr_reset_source_reason = pscrr_reset_source_map[i].reason; > + break; > + } > + > + ret = pscrr_provider_register(&pscrr_reset_source_provider); > + if (ret) > + pr_warn("failed to register the reset-source provider: %d\n", ret); > +} > + > +/*----------------------------------------------------------------------*/ > +/* Module init/exit */ > +/*----------------------------------------------------------------------*/ > + > +static int __init pscrr_core_init(void) > +{ > + int ret; > + > + pscrr_root = kobject_create_and_add("pscrr", kernel_kobj); > + if (!pscrr_root) > + return -ENOMEM; > + > + ret = register_reboot_notifier(&pscrr_reboot_nb); > + if (ret) { > + kobject_put(pscrr_root); > + pscrr_root = NULL; > + return ret; > + } > + > + pscrr_register_reset_source(); > + > + return 0; > +} > + > +static void __exit pscrr_core_exit(void) > +{ > + pscrr_provider_unregister(&pscrr_reset_source_provider); > + unregister_reboot_notifier(&pscrr_reboot_nb); > + kobject_put(pscrr_root); > + pscrr_root = NULL; > + ida_destroy(&pscrr_ida); > +} > + > +/* Bring the core up before device drivers probe and register providers. */ > +subsys_initcall(pscrr_core_init); > +module_exit(pscrr_core_exit); > + > +MODULE_AUTHOR("Oleksij Rempel "); > +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 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 > + */ > + > +#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); I really like the stuff in this header! After a glance, this looks like something even I might be able to add out PMIC drivers! Nice! Reviewed-by: Matti Vaittinen > +#else > +static inline int pscrr_provider_register(struct pscrr_provider *p) > +{ > + return -EOPNOTSUPP; > +} > + > +static inline void pscrr_provider_unregister(struct pscrr_provider *p) > +{ > +} > + > +static inline struct pscrr_provider * > +devm_pscrr_provider_register(struct device *dev, const char *name, > + const struct pscrr_provider_ops *ops, > + const unsigned long *supported_reasons, void *priv) > +{ > + return NULL; > +} > +#endif > + > +#endif /* __PSCRR_H__ */ > -- Matti Vaittinen Linux kernel developer at ROHM Semiconductors Oulu Finland ~~ When things go utterly wrong vim users can always type :help! ~~