[PATCH v2 2/4] power: reset: add PSCRR NVMEM recorder
Faruque Ansari <[email protected]>
| Newsgroups | org.kernel.vger.linux-watchdog,org.kernel.vger.linux-arm-msm,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel,org.kernel.vger.linux-pm |
|---|---|
| Message-ID | <[email protected]> |
Record the power state change reason into a small NVMEM cell (an RTC scratch register or EEPROM) and read it back on the next boot, so the cause survives a power cycle. The cell is a plain nvmem consumer reference and the reason value is stored directly. Co-developed-by: Oleksij Rempel <[email protected]> Signed-off-by: Oleksij Rempel <[email protected]> Signed-off-by: Faruque Ansari <[email protected]> --- drivers/power/reset/pscrr/Kconfig | 14 +++ drivers/power/reset/pscrr/Makefile | 1 + drivers/power/reset/pscrr/pscrr-nvmem.c | 154 ++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) diff --git a/drivers/power/reset/pscrr/Kconfig b/drivers/power/reset/pscrr/Kconfig index 72de82731b53..bfcf04b0c8f5 100644 --- a/drivers/power/reset/pscrr/Kconfig +++ b/drivers/power/reset/pscrr/Kconfig @@ -31,3 +31,17 @@ menuconfig PSCRR unless hardware provides the reset cause. If unsure, say N. + +if PSCRR + +config PSCRR_NVMEM + tristate "PSCRR NVMEM recorder provider" + depends on NVMEM + 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. + +endif # PSCRR diff --git a/drivers/power/reset/pscrr/Makefile b/drivers/power/reset/pscrr/Makefile index e5530a858971..95c80c80c7da 100644 --- a/drivers/power/reset/pscrr/Makefile +++ b/drivers/power/reset/pscrr/Makefile @@ -1,2 +1,3 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_PSCRR) += pscrr.o +obj-$(CONFIG_PSCRR_NVMEM) += pscrr-nvmem.o diff --git a/drivers/power/reset/pscrr/pscrr-nvmem.c b/drivers/power/reset/pscrr/pscrr-nvmem.c new file mode 100644 index 000000000000..1c3145c0d058 --- /dev/null +++ b/drivers/power/reset/pscrr/pscrr-nvmem.c @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * pscrr-nvmem.c - NVMEM recorder provider for PSCRR + * + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * Copyright (C) 2025 Pengutronix, Oleksij Rempel <[email protected]> + */ + +#include <linux/module.h> +#include <linux/nvmem-consumer.h> +#include <linux/platform_device.h> +#include <linux/pscrr.h> +#include <linux/reboot.h> +#include <linux/slab.h> + +struct pscrr_nvmem { + struct nvmem_cell *cell; + enum psc_reason reason; +}; + +static int pscrr_nvmem_cell_read(struct pscrr_nvmem *priv, u8 *out) +{ + size_t len; + void *buf; + + buf = nvmem_cell_read(priv->cell, &len); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + *out = len ? *(u8 *)buf : 0; + kfree(buf); + + return 0; +} + +static int pscrr_nvmem_cell_write(struct pscrr_nvmem *priv, u8 val) +{ + int ret; + + /* nvmem_cell_write() returns the byte count on success, not 0. */ + ret = nvmem_cell_write(priv->cell, &val, sizeof(val)); + + return ret < 0 ? ret : 0; +} + +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 int pscrr_nvmem_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct pscrr_nvmem *priv; + size_t bytes, bits; + u8 val; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->cell = devm_nvmem_cell_get(dev, "pscr"); + if (IS_ERR(priv->cell)) + return dev_err_probe(dev, PTR_ERR(priv->cell), + "failed to get the pscr nvmem cell\n"); + + ret = nvmem_cell_get_size(priv->cell, &bytes, &bits); + if (ret) + return ret; + + /* The reason is a single byte; a bit cell still occupies one. */ + if ((bytes ?: DIV_ROUND_UP(bits, 8)) != sizeof(val)) + return dev_err_probe(dev, -EINVAL, + "unsupported pscr nvmem cell size\n"); + + /* + * The cell survives resets, so at boot it still holds whatever the + * previous session recorded before it went down. Latch that as this + * boot's reason, then clear the cell back to PSCR_UNKNOWN: if this + * session is later cut short by an abrupt reset that never runs the + * recorder (watchdog power-cycle, sudden power loss), the next boot + * reads "unknown" instead of a stale reason left over from an earlier + * cycle. + */ + ret = pscrr_nvmem_cell_read(priv, &val); + if (ret) + return dev_err_probe(dev, ret, + "failed to read the pscr nvmem cell\n"); + + priv->reason = val < PSCR_REASON_COUNT ? val : PSCR_UNKNOWN; + + ret = pscrr_nvmem_cell_write(priv, PSCR_UNKNOWN); + if (ret) + return dev_err_probe(dev, ret, + "failed to clear the pscr nvmem cell\n"); + + return PTR_ERR_OR_ZERO(devm_pscrr_provider_register(dev, "nvmem", + &pscrr_nvmem_ops, NULL, + priv)); +} + +static const struct of_device_id pscrr_nvmem_of_match[] = { + { .compatible = "pscrr-nvmem" }, + { } +}; +MODULE_DEVICE_TABLE(of, pscrr_nvmem_of_match); + +static struct platform_driver pscrr_nvmem_driver = { + .probe = pscrr_nvmem_probe, + .driver = { + .name = "pscrr-nvmem", + .of_match_table = pscrr_nvmem_of_match, + }, +}; +module_platform_driver(pscrr_nvmem_driver); + +MODULE_AUTHOR("Oleksij Rempel <[email protected]>"); +MODULE_AUTHOR("Faruque Ansari <[email protected]>"); +MODULE_DESCRIPTION("NVMEM recorder provider for PSCRR"); +MODULE_LICENSE("GPL"); -- 2.34.1