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 + ===== ================================================= 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 b14b6c24469f3..54458b9bfbdd4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20550,6 +20550,19 @@ 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: 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/ + POWER STATE COORDINATION INTERFACE (PSCI) M: Mark Rutland M: Lorenzo Pieralisi 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>; + }; + }; }; }; 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"); 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/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..8fb795af2cc5c --- /dev/null +++ b/drivers/power/reset/pscrr/pscrr.c @@ -0,0 +1,670 @@ +// 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 +#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, +}; + +/* + * 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) +{ + + /* + * 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); + + pscrr_record_current(); + + return NOTIFY_DONE; +} + +static struct notifier_block pscrr_panic_nb = { + .notifier_call = pscrr_panic_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; + } + + atomic_notifier_chain_register(&panic_notifier_list, &pscrr_panic_nb); + + pscrr_register_reset_source(); + + return 0; +} + +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; + 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/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/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"); } diff --git a/include/linux/power/power_on_reason.h b/include/linux/power/power_on_reason.h index 95a1ec0c403c6..8e3889fa43ef9 100644 --- a/include/linux/power/power_on_reason.h +++ b/include/linux/power/power_on_reason.h @@ -15,5 +15,12 @@ #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" +#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/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__ */ diff --git a/include/linux/reboot.h b/include/linux/reboot.h index aa08c3bbbf59a..f313e326b2a65 100644 --- a/include/linux/reboot.h +++ b/include/linux/reboot.h @@ -178,6 +178,110 @@ 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_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_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. + * + * The reasons mirror the POWER_ON_REASON_* strings in + * ; keep the two in sync when extending. + */ +enum psc_reason { + PSCR_UNKNOWN, + PSCR_UNDER_VOLTAGE, + PSCR_OVER_CURRENT, + PSCR_REGULATOR_FAILURE, + PSCR_OVER_TEMPERATURE, + PSCR_EC_PANIC, + /* Append new reasons here; the numeric order is ABI. */ + PSCR_POWER_ON, + PSCR_WATCHDOG, + PSCR_SOFTWARE, + PSCR_EXTERNAL, + PSCR_RTC, + PSCR_RESET_BUTTON, + PSCR_CPU_CLK_FAIL, + PSCR_XTAL_FAIL, + PSCR_KERNEL_PANIC, + PSCR_WATCHDOG_PRETIMEOUT, + + /* Number of reasons. Must remain the last member. */ + PSCR_REASON_COUNT +}; + +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 * @@ -191,13 +295,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 +310,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..0474d5a307994 100644 --- a/kernel/reboot.c +++ b/kernel/reboot.c @@ -7,12 +7,15 @@ #define pr_fmt(fmt) "reboot: " fmt +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -50,6 +53,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 +1016,165 @@ 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); + +/** + * 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 }, + [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 }, + [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"); + +/** + * psc_reason_to_str - Human-readable label for a power state change reason. + * @reason: The `psc_reason` value to convert. + * + * 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 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 * - * @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 +1186,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 +1194,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))